Compare commits
8 Commits
1.1a
..
119ae757b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
119ae757b9
|
|||
|
c3d380ddb4
|
|||
|
a8ba00269b
|
|||
|
756a37de23
|
|||
|
5f7266013d
|
|||
|
e98dc4a86b
|
|||
|
afb14ff7b9
|
|||
|
1bee1dabbe
|
@@ -1,7 +1,7 @@
|
||||
<!--suppress ALL -->
|
||||
<div align="center">
|
||||
<h1>Cowspent</h1>
|
||||
<img width="100px" src="./metadata/en-US/images/icon.webp" alt="Logo">
|
||||
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
|
||||
|
||||
<p>
|
||||
|
||||
|
||||
+14
-4
@@ -82,11 +82,18 @@ android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles (for Google Play)
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.compose.foundation:foundation:1.11.2'
|
||||
implementation 'androidx.compose.runtime:runtime:1.11.2'
|
||||
implementation 'androidx.compose.foundation:foundation:1.11.3'
|
||||
implementation 'androidx.compose.runtime:runtime:1.11.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
@@ -99,7 +106,7 @@ dependencies {
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:1.1.0'
|
||||
implementation 'com.opencsv:opencsv:5.12.0'
|
||||
|
||||
implementation platform('androidx.compose:compose-bom:2026.05.01')
|
||||
implementation platform('androidx.compose:compose-bom:2026.06.00')
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation 'androidx.compose.material:material'
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
@@ -107,5 +114,8 @@ dependencies {
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
implementation 'androidx.activity:activity-compose:1.13.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.13.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
|
||||
implementation "androidx.datastore:datastore-preferences:1.2.1"
|
||||
implementation "com.google.crypto.tink:tink-android:1.22.0"
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nextcloud.android.sso.AccountImporter
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -42,6 +41,7 @@ import net.helcel.cowspent.android.main.MainConstants
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import net.helcel.cowspent.util.CospendClientUtil
|
||||
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
import java.net.URLDecoder
|
||||
import java.util.Locale
|
||||
|
||||
@@ -69,7 +69,6 @@ class AccountActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private var oldPassword = ""
|
||||
private var useWebLogin = true
|
||||
private var showLoginDialog by mutableStateOf(false)
|
||||
|
||||
@@ -129,8 +128,6 @@ class AccountActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
oldPassword = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS) ?: ""
|
||||
viewModel.validateUrl()
|
||||
}
|
||||
|
||||
@@ -224,11 +221,7 @@ class AccountActivity : AppCompatActivity() {
|
||||
private fun legacyLogin() {
|
||||
val url = CospendClientUtil.formatURL(viewModel.serverUrl.trim())
|
||||
val username = viewModel.username
|
||||
var password = viewModel.password
|
||||
|
||||
if (password.isEmpty()) {
|
||||
password = oldPassword
|
||||
}
|
||||
val password = viewModel.password
|
||||
|
||||
performLogin(url, username, password)
|
||||
}
|
||||
@@ -311,10 +304,10 @@ class AccountActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (status == LoginStatus.OK) {
|
||||
SecureStorage.savePassword(applicationContext, SETTINGS_PASSWORD, password)
|
||||
preferences.edit {
|
||||
putString(SETTINGS_URL, url)
|
||||
putString(SETTINGS_USERNAME, username)
|
||||
putString(SETTINGS_PASSWORD, password)
|
||||
remove(SETTINGS_KEY_ETAG)
|
||||
remove(SETTINGS_KEY_LAST_MODIFIED)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.cowspent.util.CospendClientUtil
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
|
||||
class AccountViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
@@ -60,14 +61,15 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
} else {
|
||||
preferences.getString(AccountActivity.SETTINGS_USERNAME, "")
|
||||
}
|
||||
val password = if (useSso) {
|
||||
""
|
||||
} else {
|
||||
preferences.getString(AccountActivity.SETTINGS_PASSWORD, "")
|
||||
}
|
||||
|
||||
if (!url.isNullOrEmpty() && !username.isNullOrEmpty()) {
|
||||
viewModelScope.launch {
|
||||
val password = if (useSso) {
|
||||
""
|
||||
} else {
|
||||
SecureStorage.getPassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
||||
}
|
||||
|
||||
isValidatingLogin = true
|
||||
isLoggedIn = withContext(Dispatchers.IO) {
|
||||
if (useSso) {
|
||||
@@ -87,6 +89,9 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
SecureStorage.removePassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
||||
}
|
||||
preferences.edit {
|
||||
remove(AccountActivity.SETTINGS_USE_SSO)
|
||||
remove(AccountActivity.SETTINGS_SSO_URL)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.helcel.cowspent.android.bill_edit
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
@@ -31,7 +32,7 @@ class EditBillViewModel : ViewModel() {
|
||||
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
||||
var mainCurrencyName by mutableStateOf("")
|
||||
var selectedCurrencyName by mutableStateOf("")
|
||||
var selectedCurrencyRate by mutableStateOf(1.0)
|
||||
var selectedCurrencyRate by mutableDoubleStateOf(1.0)
|
||||
var members by mutableStateOf<List<DBMember>>(emptyList())
|
||||
|
||||
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
||||
|
||||
@@ -4,11 +4,32 @@ import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
@@ -28,10 +49,9 @@ import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.AlertDialog
|
||||
import net.helcel.cowspent.android.helper.TextIcon
|
||||
import net.helcel.cowspent.android.helper.TextIconDisplay
|
||||
import net.helcel.cowspent.android.helper.formatAmount
|
||||
import net.helcel.cowspent.model.DBCurrency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun ManageCurrenciesScreen(
|
||||
@@ -96,7 +116,7 @@ fun ManageCurrenciesScreen(
|
||||
) {
|
||||
LaunchedEffect(viewModel.mainCurrencyName) {
|
||||
if (viewModel.mainCurrencyName.isNotEmpty()) {
|
||||
delay(500)
|
||||
delay(500.milliseconds)
|
||||
onSaveMain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,11 @@ fun Drawer(
|
||||
|
||||
// Members Section
|
||||
val membersState = rememberLazyListState()
|
||||
val sortedMembers = remember(members, memberBalances) {
|
||||
members.sortedWith(compareBy<DBMember> {
|
||||
val sortedMembers = remember(members, memberBalances, selectedMemberId) {
|
||||
members.filter {
|
||||
val balance = memberBalances[it.id] ?: 0.0
|
||||
it.isActivated || balance > 0.01 || balance < -0.01 || it.id == selectedMemberId
|
||||
}.sortedWith(compareBy<DBMember> {
|
||||
val balance = memberBalances[it.id] ?: 0.0
|
||||
when {
|
||||
balance > 0.01 -> 0
|
||||
|
||||
@@ -4,37 +4,49 @@ import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.zxing.*
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.main.MainConstants
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@@ -2,9 +2,25 @@ package net.helcel.cowspent.android.main
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Receipt
|
||||
import androidx.compose.material.icons.filled.Repeat
|
||||
@@ -16,7 +32,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.formatAmount
|
||||
import net.helcel.cowspent.android.helper.MemberAvatar
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package net.helcel.cowspent.android.main
|
||||
|
||||
import android.Manifest
|
||||
import android.app.SearchManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -26,10 +24,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircleOutline
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
||||
@@ -49,7 +45,6 @@ import net.helcel.cowspent.model.Category
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBProject
|
||||
import net.helcel.cowspent.model.GroupedBill
|
||||
import net.helcel.cowspent.model.ProjectType
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
|
||||
@@ -121,13 +121,7 @@ fun NewProjectScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (viewModel.whatTodoIsCreate) {
|
||||
if (viewModel.projectType == ProjectType.COSPEND) {
|
||||
Button(
|
||||
onClick = onChooseFromNextcloud
|
||||
) {
|
||||
Text(stringResource(R.string.new_project_from_nextcloud_tooltip))
|
||||
}
|
||||
} else if (viewModel.projectType == ProjectType.LOCAL) {
|
||||
if (viewModel.projectType == ProjectType.LOCAL) {
|
||||
Button(
|
||||
onClick = onImportFile
|
||||
) {
|
||||
|
||||
@@ -12,6 +12,6 @@ class DBAccountProject(
|
||||
) : Serializable {
|
||||
|
||||
override fun toString(): String {
|
||||
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, $password, archivedTs=$archivedTs"
|
||||
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, [SECURE], archivedTs=$archivedTs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.main.BillsListViewActivity
|
||||
import net.helcel.cowspent.model.*
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
import net.helcel.cowspent.util.SupportUtil
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -316,11 +317,12 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val db = writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(key_remoteId, accountProject.remoteId)
|
||||
values.put(key_password, accountProject.password)
|
||||
values.put(key_ncUrl, accountProject.ncUrl)
|
||||
values.put(key_name, accountProject.name)
|
||||
values.put(key_archived, accountProject.archivedTs ?: 0L)
|
||||
return db.insert(table_account_projects, null, values)
|
||||
val id = db.insert(table_account_projects, null, values)
|
||||
SecureStorage.savePasswordSync(context, "AccountProjectPassword_$id", accountProject.password)
|
||||
return id
|
||||
}
|
||||
|
||||
val accountProjects: List<DBAccountProject>
|
||||
@@ -344,11 +346,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
|
||||
@SuppressLint("Range")
|
||||
private fun getAccountProjectFromCursor(cursor: Cursor): DBAccountProject {
|
||||
val id = cursor.getLong(cursor.getColumnIndex(key_id))
|
||||
val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived))
|
||||
val password = SecureStorage.getPasswordSync(context, "AccountProjectPassword_$id")
|
||||
return DBAccountProject(
|
||||
cursor.getLong(cursor.getColumnIndex(key_id)),
|
||||
id,
|
||||
cursor.getString(cursor.getColumnIndex(key_remoteId)),
|
||||
cursor.getString(cursor.getColumnIndex(key_password)),
|
||||
password,
|
||||
cursor.getString(cursor.getColumnIndex(key_name)),
|
||||
cursor.getString(cursor.getColumnIndex(key_ncUrl)),
|
||||
if (archivedTs > 0) archivedTs else null
|
||||
@@ -608,14 +612,15 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val db = writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(key_remoteId, project.remoteId)
|
||||
values.put(key_password, project.password)
|
||||
values.put(key_bearer_token, project.bearerToken)
|
||||
values.put(key_email, project.email)
|
||||
values.put(key_name, project.name)
|
||||
values.put(key_ihmUrl, project.serverUrl)
|
||||
values.put(key_type, project.type.id)
|
||||
values.put(key_archived, project.archivedTs ?: 0L)
|
||||
return db.insert(table_projects, null, values)
|
||||
val id = db.insert(table_projects, null, values)
|
||||
SecureStorage.savePasswordSync(context, "ProjectPassword_$id", project.password)
|
||||
return id
|
||||
}
|
||||
|
||||
fun getProject(id: Long): DBProject? {
|
||||
@@ -644,11 +649,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
|
||||
@SuppressLint("Range")
|
||||
private fun getProjectFromCursor(cursor: Cursor): DBProject {
|
||||
val id = cursor.getLong(cursor.getColumnIndex(key_id))
|
||||
val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived))
|
||||
val password = SecureStorage.getPasswordSync(context, "ProjectPassword_$id") ?: ""
|
||||
return DBProject(
|
||||
cursor.getLong(cursor.getColumnIndex(key_id)),
|
||||
id,
|
||||
cursor.getString(cursor.getColumnIndex(key_remoteId)),
|
||||
cursor.getString(cursor.getColumnIndex(key_password)),
|
||||
password,
|
||||
cursor.getString(cursor.getColumnIndex(key_name)),
|
||||
cursor.getString(cursor.getColumnIndex(key_ihmUrl)),
|
||||
cursor.getString(cursor.getColumnIndex(key_email)),
|
||||
@@ -671,6 +678,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
}
|
||||
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
|
||||
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
|
||||
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
|
||||
}
|
||||
|
||||
fun updateProject(
|
||||
@@ -685,7 +693,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val values = ContentValues()
|
||||
if (newName != null) values.put(key_name, newName)
|
||||
if (newEmail != null) values.put(key_email, newEmail)
|
||||
if (newPassword != null) values.put(key_password, newPassword)
|
||||
if (newPassword != null) SecureStorage.savePasswordSync(context, "ProjectPassword_$projId", newPassword)
|
||||
if (newBearerToken != null) values.put(key_bearer_token, newBearerToken)
|
||||
if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId)
|
||||
if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp)
|
||||
@@ -725,7 +733,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val values = ContentValues()
|
||||
if (newName != null) values.put(key_name, newName)
|
||||
if (newEmail != null) values.put(key_email, newEmail)
|
||||
if (newPassword != null) values.put(key_password, newPassword)
|
||||
if (newPassword != null) SecureStorage.savePasswordSync(context, "ProjectPassword_$projId", newPassword)
|
||||
if (newBearerToken != null) values.put(key_bearer_token, newBearerToken)
|
||||
if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId)
|
||||
if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp)
|
||||
|
||||
@@ -35,13 +35,13 @@ class NextcloudClient(
|
||||
val method = if (useOcsApi) METHOD_GET else METHOD_POST
|
||||
return if (nextcloudAPI != null) {
|
||||
Log.d(javaClass.simpleName, "using SSO to get/sync account projects")
|
||||
Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
// Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
ServerResponse.AccountProjectsResponse(
|
||||
requestServerWithSSO(nextcloudAPI, target, method, null, useOcsApi),
|
||||
useOcsApi
|
||||
)
|
||||
} else {
|
||||
Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
// Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
ServerResponse.AccountProjectsResponse(
|
||||
requestServer(target, method, null, "", true, useOcsApi),
|
||||
useOcsApi
|
||||
@@ -184,7 +184,7 @@ class NextcloudClient(
|
||||
): VersatileProjectSyncClient.ResponseData {
|
||||
val result = StringBuilder()
|
||||
val targetURL = url + target.replace("^/".toRegex(), "")
|
||||
Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
|
||||
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
|
||||
val httpCon = SupportUtil.getHttpURLConnection(targetURL)
|
||||
httpCon.requestMethod = method
|
||||
if (needLogin) {
|
||||
@@ -206,7 +206,7 @@ class NextcloudClient(
|
||||
var paramData: ByteArray? = null
|
||||
if (params != null) {
|
||||
paramData = params.toString().toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $params")
|
||||
// Log.d(javaClass.simpleName, "Params: $params")
|
||||
httpCon.setFixedLengthStreamingMode(paramData.size)
|
||||
httpCon.setRequestProperty("Content-Type", application_json)
|
||||
httpCon.doOutput = true
|
||||
@@ -263,11 +263,11 @@ class NextcloudClient(
|
||||
httpCon.setRequestProperty("OCS-APIRequest", "true")
|
||||
}
|
||||
httpCon.connectTimeout = 10 * 1000 // 10 seconds
|
||||
Log.d(javaClass.simpleName, "$method $targetURL")
|
||||
// Log.d(javaClass.simpleName, "$method $targetURL")
|
||||
var paramData: ByteArray?
|
||||
if (params != null) {
|
||||
paramData = params.toString().toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $params")
|
||||
// Log.d(javaClass.simpleName, "Params: $params")
|
||||
httpCon.setFixedLengthStreamingMode(paramData.size)
|
||||
httpCon.setRequestProperty("Content-Type", application_json)
|
||||
httpCon.doOutput = true
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.helcel.cowspent.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.crypto.tink.Aead
|
||||
import com.google.crypto.tink.KeyTemplates
|
||||
import com.google.crypto.tink.RegistryConfiguration
|
||||
import com.google.crypto.tink.aead.AeadConfig
|
||||
import com.google.crypto.tink.integration.android.AndroidKeysetManager
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "secure_prefs")
|
||||
|
||||
object SecureStorage {
|
||||
private var aead: Aead? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getAead(context: Context): Aead {
|
||||
aead?.let { return it }
|
||||
AeadConfig.register()
|
||||
val masterKeyUri = "android-keystore://secure_storage_master_key"
|
||||
val keysetManager = AndroidKeysetManager.Builder()
|
||||
.withSharedPref(context, "tink_keyset", "secure_storage_keyset_prefs")
|
||||
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
|
||||
.withMasterKeyUri(masterKeyUri)
|
||||
.build()
|
||||
val newAead = keysetManager.keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
|
||||
aead = newAead
|
||||
return newAead
|
||||
}
|
||||
|
||||
private fun encrypt(context: Context, value: String): String {
|
||||
val encrypted = getAead(context).encrypt(value.toByteArray(), null)
|
||||
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
private fun decrypt(context: Context, encryptedValue: String): String? {
|
||||
return try {
|
||||
val decoded = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
||||
val decrypted = getAead(context).decrypt(decoded, null)
|
||||
String(decrypted)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePassword(context: Context, key: String, password: String?) {
|
||||
if (password == null) {
|
||||
removePassword(context, key)
|
||||
} else {
|
||||
val encrypted = encrypt(context, password)
|
||||
context.dataStore.edit { it[stringPreferencesKey(key)] = encrypted }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPassword(context: Context, key: String): String? {
|
||||
val encrypted = context.dataStore.data.map { it[stringPreferencesKey(key)] }.first()
|
||||
return encrypted?.let { decrypt(context, it) }
|
||||
}
|
||||
|
||||
suspend fun removePassword(context: Context, key: String) {
|
||||
context.dataStore.edit { it.remove(stringPreferencesKey(key)) }
|
||||
}
|
||||
|
||||
// Synchronous alternatives for legacy code
|
||||
fun savePasswordSync(context: Context, key: String, password: String?) = runBlocking {
|
||||
savePassword(context, key, password)
|
||||
}
|
||||
|
||||
fun getPasswordSync(context: Context, key: String): String? = runBlocking {
|
||||
getPassword(context, key)
|
||||
}
|
||||
|
||||
fun removePasswordSync(context: Context, key: String) = runBlocking {
|
||||
removePassword(context, key)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package net.helcel.cowspent.util
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import net.helcel.cowspent.model.*
|
||||
import net.helcel.cowspent.model.CreditDebt
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.model.Transaction
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
@@ -81,13 +81,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getProjectInfo")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
Log.i(TAG, "using new API for getProjectInfo")
|
||||
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
|
||||
@@ -99,7 +95,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for getProjectInfo")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -172,13 +167,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteProject")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
Log.i(TAG, "using new API for editRemoteProject")
|
||||
ServerResponse.EditRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
|
||||
@@ -190,7 +181,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -239,13 +229,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteMember")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
Log.i(TAG, "using new API for editRemoteMember")
|
||||
ServerResponse.EditRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
@@ -257,7 +243,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteMember")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
@@ -333,9 +318,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
@@ -351,7 +333,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
@@ -389,9 +370,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteProject")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
@@ -407,7 +385,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -439,9 +416,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
@@ -457,7 +431,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
@@ -528,9 +501,7 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createAuthenticatedRemoteProject")
|
||||
}
|
||||
|
||||
return ServerResponse.CreateRemoteProjectResponse(
|
||||
requestServer(
|
||||
target, METHOD_POST, paramKeys, paramValues,
|
||||
@@ -594,9 +565,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
|
||||
@@ -612,7 +580,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills"
|
||||
@@ -664,9 +631,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteMember")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
|
||||
@@ -682,7 +646,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
|
||||
@@ -717,9 +680,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getBills")
|
||||
}
|
||||
return ServerResponse.BillsResponse(
|
||||
requestServer(
|
||||
target, METHOD_GET, null, null,
|
||||
@@ -750,7 +710,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync
|
||||
Log.i(TAG, "using public API, target is: ${target}for getBills")
|
||||
return ServerResponse.BillsResponse(
|
||||
requestServer(
|
||||
target, METHOD_GET, null, null,
|
||||
@@ -790,9 +749,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getMembers, projectId: " + project.remoteId)
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
|
||||
@@ -808,7 +764,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
Log.i(TAG, "using public API, target is: ${target}for getMembers")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
|
||||
@@ -847,9 +802,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency"
|
||||
@@ -865,7 +817,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency"
|
||||
@@ -904,9 +855,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
@@ -922,7 +870,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
@@ -954,9 +901,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
@@ -972,7 +916,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
@@ -1071,7 +1014,6 @@ class VersatileProjectSyncClient(
|
||||
httpCon.setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
httpCon.connectTimeout = 10 * 1000 // 10 seconds
|
||||
Log.d(javaClass.simpleName, "$method $target")
|
||||
if (paramKeys != null && paramValues != null) {
|
||||
var dataString = ""
|
||||
for (i in paramKeys.indices) {
|
||||
@@ -1084,7 +1026,6 @@ class VersatileProjectSyncClient(
|
||||
dataString += URLEncoder.encode(value, "UTF-8")
|
||||
}
|
||||
val data = dataString.toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $dataString")
|
||||
httpCon.setFixedLengthStreamingMode(data.size)
|
||||
httpCon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpCon.setRequestProperty("Content-Length", data.size.toString())
|
||||
|
||||
@@ -9,17 +9,6 @@
|
||||
android:translateX="13.153846"
|
||||
android:translateY="13.153846">
|
||||
|
||||
<!-- Circle Outline -->
|
||||
<path
|
||||
android:strokeColor="#000"
|
||||
android:strokeWidth="2.5"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<path
|
||||
android:strokeColor="#000"
|
||||
android:strokeWidth="2.5"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Cow Outlines -->
|
||||
<group android:name="color">
|
||||
<path android:fillColor="#000" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
|
||||
@@ -27,5 +16,16 @@
|
||||
<path android:fillColor="#000" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#000" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
|
||||
</group>
|
||||
<group android:name="line">
|
||||
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
|
||||
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<!-- Inner Face (Expanded path) -->
|
||||
<path
|
||||
android:fillColor="#DAA520"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Center Detail (Pure path replacement for the center circle) -->
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
|
||||
|
||||
|
||||
<group android:name="color">
|
||||
<path android:fillColor="#fff" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
|
||||
<path android:fillColor="#a57939" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#a57939" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#f4aa41" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
|
||||
|
||||
</group>
|
||||
<group android:name="line">
|
||||
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
|
||||
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
|
||||
</group>
|
||||
|
||||
|
||||
<!-- Massive Background Coin Base -->
|
||||
<path
|
||||
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
|
||||
android:fillColor="#FBBF24"/>
|
||||
|
||||
<!-- Coin Decorative Dotted Inner Rim -->
|
||||
<path
|
||||
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
|
||||
android:strokeColor="#D97706"
|
||||
android:strokeWidth="4"/>
|
||||
|
||||
</vector>
|
||||
@@ -20,14 +20,9 @@
|
||||
<string name="save_or_discard_bill_dialog_save">Save</string>
|
||||
<string name="save_or_discard_bill_dialog_discard">Discard</string>
|
||||
<string name="currency_manager">Currency manager</string>
|
||||
<string name="add_currency_title">Add currency</string>
|
||||
<string name="currency_management_unavailable">Currency management is only available for Cospend projects</string>
|
||||
<string name="setting_comment">Comment</string>
|
||||
<string name="currency_dialog_title">Convert current amount from another currency into %s</string>
|
||||
<string name="no_currency_error">There is no additional currency</string>
|
||||
<string name="main_currency">Main currency</string>
|
||||
<string name="currency_edit_name">Name</string>
|
||||
<string name="currency_rate">Exchange rate</string>
|
||||
<string name="setting_owers">For whom?</string>
|
||||
<string name="setting_project_id">Project ID/name</string>
|
||||
|
||||
@@ -75,7 +70,6 @@
|
||||
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
|
||||
<string name="simple_yes">Yes</string>
|
||||
<string name="simple_no">No</string>
|
||||
<string name="simple_add">Add</string>
|
||||
<string name="simple_edit_bill">Edit bill</string>
|
||||
<string name="simple_new_bill">New bill</string>
|
||||
<string name="simple_add_project">Add project in Cowspent</string>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user