Initial commit: Add financial viewer

This commit is contained in:
2024-12-19 14:02:38 +01:00
commit e8bd7d475b
125 changed files with 5736 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-27T16:19:47.507081400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=e05f0e1e" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

329
.idea/other.xml generated Normal file
View File

@@ -0,0 +1,329 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-F956B" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

54
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,54 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}
android {
namespace = "com.financialviewer"
compileSdk = 34
defaultConfig {
applicationId = "com.financialviewer"
minSdk = 31
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation (libs.androidx.work.runtime.ktx)
ksp (libs.androidx.room.compiler)
implementation (libs.androidx.room.ktx)
implementation (libs.smbj)
implementation(libs.opencsv)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.financialviewer
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.financialviewer", appContext.packageName)
}
}

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FinancialViewer" >
<activity
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Login Activity -->
<activity android:name="com.financialviewer.ui.LoginActivity" />
<!-- Home Activity -->
<activity android:name="com.financialviewer.ui.HomeActivity" />
<!-- Fixed Costs Activity -->
<activity android:name="com.financialviewer.ui.FixedCostsActivity" />
<!-- Fixed Cost Details Activity -->
<activity android:name="com.financialviewer.ui.FixedCostDetailsActivity" />
<!-- Loan Overview Activity -->
<activity android:name="com.financialviewer.ui.LoanOverviewActivity" />
<!-- Loan Details Activity -->
<activity android:name="com.financialviewer.ui.LoanDetailsActivity" />
<!-- Bank Transactions Activity -->
<activity android:name="com.financialviewer.ui.BankTransactionsActivity" />
<!-- Bank Transaction Details Activity -->
<activity android:name="com.financialviewer.ui.BankTransactionDetailsActivity" />
<!-- Monthly Summary Activity -->
<activity android:name="com.financialviewer.ui.MonthlySummaryActivity" />
<!-- Yearly Summary Activity -->
<activity android:name="com.financialviewer.ui.YearlySummaryActivity" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,25 @@
package com.financialviewer
import android.app.Application
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.work.StartupWorker
class App : Application() {
override fun onCreate() {
super.onCreate()
val reset = false
if (reset) {
this.deleteDatabase("app_database")
val appPreferences = SharedPreferencesHelper(this)
appPreferences.clearAllSharedPreferences()
}
val workRequest = OneTimeWorkRequest.Builder(StartupWorker::class.java).build()
WorkManager.getInstance(this).enqueue(workRequest)
}
}

View File

@@ -0,0 +1,29 @@
package com.financialviewer.constants
val COUNTERPARTY_MAP = mapOf(
"1118 Reembroden 31-43" to Pair("Hausmann Hausverwaltung", "物业费"),
"AMERICAN EXPRESS EUROPE" to Pair("American Express", "信用卡帐"),
"Commerzbank AG" to Pair("Commerzbank AG", "房贷"),
"DKV KRANKENVERS. AG" to Pair("DKV Zahnzusatzversicherung", "牙医险"),
"Einkommensteuer" to Pair("Steuerkasse Hamburg", "补交所得税"),
"Grundsteuer" to Pair("Steuerkasse Hamburg", "房产税"),
"HAFEN UND." to Pair("HHLA", "工资"),
"Haftpflichtvers. SV95925167" to Pair("ERGO Haftpflichtversicherung", "第三责任险"),
"Hauptzollamt Hamburg" to Pair("Hauptzollamt Hamburg", "车税"),
"Hausratvers." to Pair("ERGO Hausratversicherung", "家财险"),
"KFZ-Versicherung" to Pair("HuK KFZ-Versicherung", "车险"),
"MIETE" to Pair("Reembroden 35 Miete", "房租"),
"Rechtsschutzversicherung" to Pair("ERGO Rechtsschutzversicherung", "法律险"),
"Rundfunk" to Pair("Rundfunk ARD,ZDF,DRadio", "广播电视费"),
"Stadtreinigung" to Pair("Stadtreinigung Hamburg", "垃圾费"),
"Stromnetz" to Pair("Stromnetz Hamburg", "电网"),
"Telefonica Germany GmbH" to Pair("O2 Germany", "手机费"),
"Unfallversicherung" to Pair("ERGO Unfallversicherung", "意外险"),
"VATTENFALL EUROPE SALES" to Pair("Vattenfall Europe", "电费"),
"Vodafone Deutschland GmbH" to Pair("Vodafone Kabel", "网费"),
"Vorsorge LV AG" to Pair("ERGO Lebensversicherung", "人寿险"),
"Wasserwerke" to Pair("Hamburger Wasserwerke", "水费"),
"Wohngebaeudevers." to Pair("ERGO Wohngebaeudeversicherung", "房屋险")
)

View File

@@ -0,0 +1,64 @@
package com.financialviewer.constants
import com.financialviewer.data.CategoryItem
val CATEGORY_LIST = listOf(
CategoryItem("1.收入", listOf("工资", "房租", "电网")),
CategoryItem("2.税", listOf("补交所得税", "房产税", "车税")),
CategoryItem("3.生活成本", listOf("房贷", "电费", "水费", "网费", "手机费", "垃圾费")),
CategoryItem("4.保险", listOf("第三责任险", "人寿险", "意外险", "车险", "房屋险", "家财险", "牙医险", "法律险")),
CategoryItem("5.其他", listOf("物业费", "信用卡帐", "广播电视费")),
)
val CATEGORY_FIXED_COST_LIST = listOf(
CategoryItem("", listOf("房产税", "车税")),
CategoryItem("生活成本", listOf("房贷", "电费", "水费", "网费", "手机费", "垃圾费")),
CategoryItem("保险", listOf("第三责任险", "人寿险", "意外险", "车险", "房屋险", "家财险", "牙医险", "法律险")),
CategoryItem("其他", listOf("物业费", "广播电视费"))
)
const val MONTHLY = "monthly"
const val QUARTERLY = "quarterly"
const val YEARLY = "yearly"
val FIXED_COST_TYPE = mapOf(
"房贷" to MONTHLY,
"电费" to MONTHLY,
"水费" to MONTHLY,
"网费" to MONTHLY,
"手机费" to MONTHLY,
"牙医险" to MONTHLY,
"物业费" to MONTHLY,
"房产税" to QUARTERLY,
"垃圾费" to QUARTERLY,
"广播电视费" to QUARTERLY,
"车税" to YEARLY,
"第三责任险" to YEARLY,
"人寿险" to YEARLY,
"意外险" to YEARLY,
"车险" to YEARLY,
"房屋险" to YEARLY,
"家财险" to YEARLY,
"法律险" to YEARLY
)
val FIXED_COST_COUNT = mapOf(
"房贷" to 4,
"电费" to 2,
"水费" to 1,
"网费" to 1,
"手机费" to 1,
"牙医险" to 1,
"物业费" to 2,
"房产税" to 3,
"垃圾费" to 1,
"广播电视费" to 1,
"车税" to 1,
"第三责任险" to 1,
"人寿险" to 2,
"意外险" to 1,
"车险" to 1,
"房屋险" to 1,
"家财险" to 1,
"法律险" to 1
)

View File

@@ -0,0 +1,88 @@
package com.financialviewer.constants
import com.financialviewer.db.BankTransaction
import com.financialviewer.utils.convertStringToDouble
val COMDIRECT_LIST = listOf(
BankTransaction(
"Comdirect1",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-310,83"),
"房贷",
"Commerzbank AG",
"SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect1 IBAN DE79200400000504264302 BIC COBADEFF",
true),
BankTransaction(
"Comdirect2",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-852,5"),
"房贷",
"Commerzbank AG",
"SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect2 IBAN DE09200400000504264301 BIC COBADEFF",
true),
BankTransaction(
"Comdirect3",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-756,25"),
"房贷",
"Commerzbank AG",
"SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect3 IBAN DE09200400000504264301 BIC COBADEFF",
true),
BankTransaction(
"Comdirect4",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("9"),
"电网",
"Stromnetz Hamburg",
"ÜBERWEISUNG VON Stromnetz Hamburg GmbH Comdirect4 IBAN DE17500500000090085242 BIC HELADEFFXXX",
false),
BankTransaction(
"Comdirect5",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-65,44"),
"电费",
"Vattenfall Europe",
"SEPA-LASTSCHRIFT VON VATTENFALL EUROPE SALES Comdirect5 IBAN DE93500500000090085135 BIC HELADEFF",
true),
BankTransaction(
"Comdirect6",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-73,44"),
"电费",
"Vattenfall Europe",
"SEPA-LASTSCHRIFT VON VATTENFALL EUROPE SALES Comdirect6 IBAN DE93500500000090085135 BIC HELADEFF",
true),
BankTransaction(
"Comdirect7",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-776,87"),
"车险",
"ERGO KFZ-Versicherung",
"SEPA-LASTSCHRIFT VON ERGO VERSICHERUNG AG Comdirect7 IBAN DE67302201900004471610 BIC HYVEDEMM KFZ-Versicherung",
true),
BankTransaction(
"Comdirect8",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-20"),
"物业费",
"Hausmann Hausverwaltung",
"SEPA-LASTSCHRIFT VON WEG 1118 Reembroden 31-43 Hausmann Hausv Comdirect8 IBAN DE77217919060000773573 BIC GENODEFF",
true),
BankTransaction(
"Comdirect9",
"ComdirectAccount",
"2024-01-02",
convertStringToDouble("-325"),
"物业费",
"Hausmann Hausverwaltung",
"SEPA-LASTSCHRIFT VON WEG 1118 Reembroden 31-43 Hausmann Hausv Comdirec9 IBAN DE77217919060000773573 BIC GENODEFF",
true)
)

View File

@@ -0,0 +1,19 @@
package com.financialviewer.constants
import com.financialviewer.db.BankTransaction
const val YEAR = 2024
const val FIXED_COST_TIP = "该转账为固定支出: 是"
const val NON_FIXED_COST_TIP = "该转账为固定支出: 否"
val BLANK_BANK_TRANSACTION =
BankTransaction(
"",
"",
"",
0.0,
"",
"",
"",
false
)

View File

@@ -0,0 +1,49 @@
package com.financialviewer.constants
import com.financialviewer.db.Loan
import java.util.Calendar
val lastUpdate = Calendar.getInstance().timeInMillis
val LoanList = mutableListOf(
Loan(
"R1",
"Reembroden 35 (1)",
"150.000,00",
"3,66%",
"1.168,59",
"30.08.2027",
"38.669,79",
lastUpdate
),
Loan(
"R2",
"Reembroden 35 (2)",
"100.000,00",
"1,74%",
"310,83",
"30.11.2033",
"91.365,58",
lastUpdate
),
Loan(
"V1",
"Voßstraat 24 (1)",
"300.000,00",
"1,42%",
"852,50",
"30.11.2028",
"274.266,59",
lastUpdate
),
Loan(
"V2",
"Voßstraat 24 (2)",
"250.000,00",
"1,64%",
"756,25",
"30.11.2033",
"220.338,01",
lastUpdate
)
)

View File

@@ -0,0 +1,6 @@
package com.financialviewer.data
data class CategoryItem (
val title: String,
val subList: List<String>
)

View File

@@ -0,0 +1,8 @@
package com.financialviewer.data
data class LoanMonthlyDetails(
val month: String,
val interest: String,
val principal: String,
val remaining: String
)

View File

@@ -0,0 +1,7 @@
package com.financialviewer.data
data class YearMonthDay(
val year: Int,
val month: Int,
val day: Int
)

View File

@@ -0,0 +1,37 @@
package com.financialviewer.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [
Loan::class,
FixedCost::class,
BankTransaction::class,
YearlySummary::class,
MonthlySummary::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun loanDao(): LoanDao
abstract fun fixedCostDao(): FixedCostDao
abstract fun bankTransactionDao(): BankTransactionDao
abstract fun yearlySummaryDao(): YearlySummaryDao
abstract fun monthlySummaryDao(): MonthlySummaryDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.financialviewer.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "bank_transactions")
data class BankTransaction(
@PrimaryKey val refId: String,
@ColumnInfo(name = "account") val account: String,
@ColumnInfo(name = "date") val date: String,
@ColumnInfo(name = "amount") val amount: Double,
@ColumnInfo(name = "category") val category: String,
@ColumnInfo(name = "counterparty") val counterparty: String,
@ColumnInfo(name = "reference") val reference: String,
@ColumnInfo(name = "is_fixed_cost") var isFixedCost: Boolean
)

View File

@@ -0,0 +1,45 @@
package com.financialviewer.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface BankTransactionDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBankTransaction(bankTransaction: BankTransaction)
@Update
suspend fun updateBankTransaction(bankTransaction: BankTransaction)
@Query("SELECT COUNT(*) FROM bank_Transactions")
suspend fun getRowCount(): Int
@Query("SELECT * FROM bank_Transactions WHERE refId = :refId")
suspend fun getBankTransactionById(refId: String): BankTransaction?
@Query("SELECT * FROM bank_Transactions ORDER BY date DESC LIMIT 1")
suspend fun getLastBankTransaction(): BankTransaction?
@Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' ORDER BY date DESC")
suspend fun getAllBankTransactionsDesc(year: String): List<BankTransaction>
@Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' AND category = :category ORDER BY date DESC")
suspend fun getBankTransactionsByCategoryDesc(year: String, category: String): List<BankTransaction>
@Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' AND category IN (:categoryList) ORDER BY date DESC")
suspend fun getBankTransactionsByCategoryListDesc(year: String, categoryList: MutableList<String>): List<BankTransaction>
@Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-' || :month || '%' ORDER BY date DESC")
suspend fun getBankTransactionsByMonthDesc(year: String, month: String): List<BankTransaction>
@Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-' || :month || '%' AND category IN (:categoryList) ORDER BY date DESC")
suspend fun getBankTransactionsByMonthAndCategoryListDesc(year: String, month: String, categoryList: MutableList<String>): List<BankTransaction>
@Delete
suspend fun deleteBankTransaction(bankTransaction: BankTransaction)
}

View File

@@ -0,0 +1,13 @@
package com.financialviewer.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "fixed_costs")
data class FixedCost(
@PrimaryKey val category: String,
@ColumnInfo(name = "amount") var amount: Double,
@ColumnInfo(name = "type") val type: String,
@ColumnInfo(name = "refIds") var refIds: String
)

View File

@@ -0,0 +1,33 @@
package com.financialviewer.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface FixedCostDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFixedCost(fixedCost: FixedCost)
@Update
suspend fun updateFixedCost(fixedCost: FixedCost)
@Query("SELECT COUNT(*) FROM fixed_costs")
suspend fun getRowCount(): Int
@Query("SELECT * FROM fixed_costs WHERE category = :category")
suspend fun getFixedCostByCategory(category: String): FixedCost?
@Query("SELECT * FROM fixed_costs")
suspend fun getAllFixedCosts(): List<FixedCost>
@Query("SELECT * FROM fixed_costs WHERE type = :type")
suspend fun getAllFixedCostsByType(type: String): List<FixedCost>
@Delete
suspend fun deleteFixedCost(fixedCost: FixedCost)
}

View File

@@ -0,0 +1,17 @@
package com.financialviewer.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "loans")
data class Loan(
@PrimaryKey val id: String,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "amount") val amount: String,
@ColumnInfo(name = "rate") var rate: String,
@ColumnInfo(name = "payment") var payment: String,
@ColumnInfo(name = "maturity") var maturity: String,
@ColumnInfo(name = "remaining_loan") var remainingLoan: String,
@ColumnInfo(name = "last_update") var lastUpdate: Long
)

View File

@@ -0,0 +1,30 @@
package com.financialviewer.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface LoanDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLoan(loan: Loan)
@Update
suspend fun updateLoan(loan: Loan)
@Query("SELECT COUNT(*) FROM loans")
suspend fun getRowCount(): Int
@Query("SELECT * FROM loans WHERE id = :loanId")
suspend fun getLoanById(loanId: String): Loan?
@Query("SELECT * FROM loans")
suspend fun getAllLoans(): List<Loan>
@Delete
suspend fun deleteLoan(loan: Loan)
}

View File

@@ -0,0 +1,14 @@
package com.financialviewer.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "monthly_summary")
data class MonthlySummary(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "year") val year: Int,
@ColumnInfo(name = "month") val month: Int,
@ColumnInfo(name = "income") var income: Double,
@ColumnInfo(name = "cost") var cost: Double
)

View File

@@ -0,0 +1,27 @@
package com.financialviewer.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface MonthlySummaryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMonthlySummary(monthlySummary: MonthlySummary)
@Update
suspend fun updateMonthlySummary(monthlySummary: MonthlySummary)
@Query("SELECT * FROM monthly_summary WHERE year = :year")
suspend fun getMonthlySummary(year: Int): List<MonthlySummary>
@Query("SELECT * FROM monthly_summary WHERE year = :year AND month = :month")
suspend fun getMonthlySummaryByMonth(year: Int, month: Int): MonthlySummary?
@Delete
suspend fun deleteMonthlySummary(monthlySummary: MonthlySummary)
}

View File

@@ -0,0 +1,13 @@
package com.financialviewer.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "yearly_summary")
data class YearlySummary(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "year") val year: Int,
@ColumnInfo(name = "category") val category: String,
@ColumnInfo(name = "total") var total: Double
)

View File

@@ -0,0 +1,27 @@
package com.financialviewer.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface YearlySummaryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertYearlySummary(yearlySummary: YearlySummary)
@Update
suspend fun updateYearlySummary(yearlySummary: YearlySummary)
@Query("SELECT * FROM yearly_summary WHERE year = :year")
suspend fun getYearlySummary(year: Int): List<YearlySummary>
@Query("SELECT * FROM yearly_summary WHERE year = :year AND category = :category")
suspend fun getYearlySummaryByCategory(year: Int, category: String): YearlySummary?
@Delete
suspend fun deleteYearlySummary(yearlySummary: YearlySummary)
}

View File

@@ -0,0 +1,10 @@
package com.financialviewer.network
object ConnectionData {
const val SHARED_FOLDER: String = "Working"
const val BANK_CSV_FOLDER: String = "Bank_CSV"
const val USERNAME: String = "halio"
const val PASSWORD: String = "zhao8888"
const val DOMAIN: String = ""
const val HOSTNAME: String = "192.168.0.10"
}

View File

@@ -0,0 +1,123 @@
package com.financialviewer.network
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import com.financialviewer.utils.pathJoin
import com.hierynomus.msdtyp.AccessMask
import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation
import com.hierynomus.mssmb2.SMB2CreateDisposition
import com.hierynomus.mssmb2.SMB2ShareAccess
import com.hierynomus.smbj.SMBClient
import com.hierynomus.smbj.auth.AuthenticationContext
import com.hierynomus.smbj.connection.Connection
import com.hierynomus.smbj.session.Session
import com.hierynomus.smbj.share.DiskShare
import com.hierynomus.smbj.share.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.io.InputStream
class SmbClientHelper(private val context: Context) {
private lateinit var connection: Connection
private lateinit var session: Session
private lateinit var share: DiskShare
suspend fun connect(): Boolean {
return try {
val client = SMBClient()
// 使用 withContext 切换到 IO 线程执行连接操作
withContext(Dispatchers.IO) {
connection = client.connect(ConnectionData.HOSTNAME)
Log.d("SmbClientHelper", "Connected to server: ${ConnectionData.HOSTNAME}")
val ac = AuthenticationContext(
ConnectionData.USERNAME, ConnectionData.PASSWORD.toCharArray(),
ConnectionData.DOMAIN
)
session = connection.authenticate(ac)
Log.d("SmbClientHelper", "Authenticated with username: ${ConnectionData.USERNAME}")
share = session.connectShare(ConnectionData.SHARED_FOLDER) as DiskShare
Log.d("SmbClientHelper", "Connected to share: ${ConnectionData.SHARED_FOLDER}")
true
}
} catch (e: Exception) {
// 捕获 SMB 连接异常
Log.e("SmbClientHelper", "Error connecting to share", e)
false
}
}
fun renameFile(file: File, newName: String) {
}
fun getCSVFromShare(): List<FileIdBothDirectoryInformation> {
val allFiles = share.list(ConnectionData.BANK_CSV_FOLDER)
val csvFiles = allFiles.filter { file -> file.fileName.endsWith(".csv", ignoreCase = true) }
return csvFiles
}
fun getCSVFile(filename: String): File {
return share.openFile(
pathJoin(ConnectionData.BANK_CSV_FOLDER, filename),
setOf(AccessMask.GENERIC_READ),
null,
SMB2ShareAccess.ALL,
SMB2CreateDisposition.FILE_OPEN,
null
)
}
suspend fun getImagePathsFromShare(title: String, episode: String): MutableList<String> {
val episodeFolderPath = pathJoin(ConnectionData.BANK_CSV_FOLDER, title, episode)
return withContext(Dispatchers.IO) {
var imageNames = mutableListOf<String>()
for (item in share.list(episodeFolderPath)) {
val fileInfo = item as FileIdBothDirectoryInformation
if (fileInfo.fileName.endsWith(".jpg", true)) {
imageNames.add(fileInfo.fileName)
}
}
imageNames = imageNames.sortedBy { it.substringBefore('.').toInt() }.toMutableList()
imageNames.map { "$episodeFolderPath/$it" }.toMutableList()
}
}
suspend fun loadImageFromShare(imagePath: String): Bitmap? {
return withContext(Dispatchers.IO) {
val file = share.openFile(
imagePath,
setOf(AccessMask.GENERIC_READ),
null,
SMB2ShareAccess.ALL,
SMB2CreateDisposition.FILE_OPEN,
null
)
val outputStream = ByteArrayOutputStream()
val inputStream: InputStream = file.inputStream
inputStream.copyTo(outputStream)
file.close()
val imageData = outputStream.toByteArray()
BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
}
}
suspend fun disconnect() {
try {
withContext(Dispatchers.IO) {
share.close()
session.close()
connection.close()
}
} catch (e: Exception) {
Log.e("SmbClientHelper", "Error disconnecting", e)
}
}
}

View File

@@ -0,0 +1,62 @@
package com.financialviewer.repository
import android.content.Context
import com.financialviewer.R
import com.financialviewer.constants.YEAR
import com.financialviewer.db.AppDatabase
import com.financialviewer.db.BankTransaction
class BankTransactionRepository(context: Context) {
private val bankTransactionDao = AppDatabase.getDatabase(context).bankTransactionDao()
private val showAllMonth: String = context.getString(R.string.show_all_month)
private val showAllCategory: String = context.getString(R.string.show_all_category)
suspend fun insertBankTransaction(bankTransaction: BankTransaction) {
bankTransactionDao.insertBankTransaction(bankTransaction)
}
suspend fun updateBankTransaction(bankTransaction: BankTransaction) {
bankTransactionDao.updateBankTransaction(bankTransaction)
}
suspend fun getRowCount(): Int{
return bankTransactionDao.getRowCount()
}
suspend fun getBankTransactionById(refId: String): BankTransaction? {
return bankTransactionDao.getBankTransactionById(refId)
}
suspend fun getLatestYearOfBankTransaction(): String {
val bankTransaction = bankTransactionDao.getLastBankTransaction()
return if (bankTransaction != null) {
bankTransaction.date.split("-")[0]
} else {
YEAR.toString()
}
}
suspend fun getBankTransactionsByDateAndCategoryDesc(year: String, month: String, categoryList: MutableList<String>): MutableList<BankTransaction> {
return if (categoryList[0].contains(showAllCategory) && month.contains(showAllMonth)) {
bankTransactionDao.getAllBankTransactionsDesc(year).toMutableList()
} else if (categoryList[0].contains(showAllCategory)) {
bankTransactionDao.getBankTransactionsByMonthDesc(year, month).toMutableList()
} else if (month.contains(showAllMonth)) {
bankTransactionDao.getBankTransactionsByCategoryListDesc(year, categoryList).toMutableList()
} else {
bankTransactionDao.getBankTransactionsByMonthAndCategoryListDesc(year, month, categoryList).toMutableList()
}
}
suspend fun getBankTransactionsByMonth(year: String, month: String): MutableList<BankTransaction> {
return bankTransactionDao.getBankTransactionsByMonthDesc(year, month).toMutableList()
}
suspend fun getBankTransactionsByCategory(year: String, category: String): MutableList<BankTransaction> {
return bankTransactionDao.getBankTransactionsByCategoryDesc(year, category).toMutableList()
}
suspend fun deleteBankTransaction(bankTransaction: BankTransaction) {
bankTransactionDao.deleteBankTransaction(bankTransaction)
}
}

View File

@@ -0,0 +1,37 @@
package com.financialviewer.repository
import android.content.Context
import com.financialviewer.db.AppDatabase
import com.financialviewer.db.FixedCost
class FixedCostRepository(context: Context) {
private val fixedCostDao = AppDatabase.getDatabase(context).fixedCostDao()
suspend fun insertFixedCost(fixedCost: FixedCost) {
fixedCostDao.insertFixedCost(fixedCost)
}
suspend fun updateFixedCost(fixedCost: FixedCost) {
fixedCostDao.updateFixedCost(fixedCost)
}
suspend fun getRowCount(): Int {
return fixedCostDao.getRowCount()
}
suspend fun getFixedCostByCategory(category: String): FixedCost? {
return fixedCostDao.getFixedCostByCategory(category)
}
suspend fun getAllFixedCosts(): MutableList<FixedCost> {
return fixedCostDao.getAllFixedCosts().toMutableList()
}
suspend fun getAllFixedCostsByType(type: String): MutableList<FixedCost> {
return fixedCostDao.getAllFixedCostsByType(type).toMutableList()
}
suspend fun deleteFixedCost(fixedCost: FixedCost) {
fixedCostDao.deleteFixedCost(fixedCost)
}
}

View File

@@ -0,0 +1,44 @@
package com.financialviewer.repository
import android.content.Context
import com.financialviewer.constants.LoanList
import com.financialviewer.db.AppDatabase
import com.financialviewer.db.Loan
import com.financialviewer.utils.calculateRemainingLoan
import java.util.Calendar
class LoanRepository(context: Context) {
private val loanDao = AppDatabase.getDatabase(context).loanDao()
private suspend fun insertLoan(loan: Loan) {
loanDao.insertLoan(loan)
}
suspend fun updateLoan(loan: Loan) {
loanDao.updateLoan(loan)
}
suspend fun getRowCount(): Int {
return loanDao.getRowCount()
}
suspend fun getLoanById(loanId: String): Loan? {
return loanDao.getLoanById(loanId)
}
suspend fun getAllLoans(): MutableList<Loan> {
return loanDao.getAllLoans().toMutableList()
}
suspend fun initDB() {
for (loan in LoanList) {
insertLoan(loan)
}
}
suspend fun updateRemainingLoan(loan: Loan, monthDiff: Int) {
loan.remainingLoan = calculateRemainingLoan(loan, monthDiff)
loan.lastUpdate = Calendar.getInstance().timeInMillis
updateLoan(loan)
}
}

View File

@@ -0,0 +1,30 @@
package com.financialviewer.repository
import android.content.Context
import com.financialviewer.db.AppDatabase
import com.financialviewer.db.MonthlySummary
class MonthlySummaryRepository(context: Context) {
private val monthlySummaryDao = AppDatabase.getDatabase(context).monthlySummaryDao()
suspend fun insertMonthlySummary(monthlySummary: MonthlySummary) {
monthlySummaryDao.insertMonthlySummary(monthlySummary)
}
suspend fun updateMonthlySummary(monthlySummary: MonthlySummary) {
monthlySummaryDao.updateMonthlySummary(monthlySummary)
}
suspend fun getMonthlySummary(year: Int): MutableList<MonthlySummary> {
return monthlySummaryDao.getMonthlySummary(year).toMutableList()
}
suspend fun getMonthlySummaryByMonth(year: Int, month: Int): MonthlySummary? {
return monthlySummaryDao.getMonthlySummaryByMonth(year, month)
}
suspend fun deleteMonthlySummary(monthlySummary: MonthlySummary) {
monthlySummaryDao.deleteMonthlySummary(monthlySummary)
}
}

View File

@@ -0,0 +1,29 @@
package com.financialviewer.repository
import android.content.Context
import com.financialviewer.db.AppDatabase
import com.financialviewer.db.YearlySummary
class YearlySummaryRepository(context: Context) {
private val yearlySummaryDao = AppDatabase.getDatabase(context).yearlySummaryDao()
suspend fun insertYearlySummary(yearlySummary: YearlySummary) {
yearlySummaryDao.insertYearlySummary(yearlySummary)
}
suspend fun updateYearlySummary(yearlySummary: YearlySummary) {
yearlySummaryDao.updateYearlySummary(yearlySummary)
}
suspend fun getYearlySummary(year: Int): MutableList<YearlySummary> {
return yearlySummaryDao.getYearlySummary(year).toMutableList()
}
suspend fun getYearlySummaryByCategory(year: Int, category: String): YearlySummary? {
return yearlySummaryDao.getYearlySummaryByCategory(year, category)
}
suspend fun deleteYearlySummary(yearlySummary: YearlySummary) {
yearlySummaryDao.deleteYearlySummary(yearlySummary)
}
}

View File

@@ -0,0 +1,88 @@
package com.financialviewer.ui
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.lifecycle.lifecycleScope
import com.financialviewer.R
import com.financialviewer.constants.BLANK_BANK_TRANSACTION
import com.financialviewer.constants.FIXED_COST_TIP
import com.financialviewer.constants.FIXED_COST_TYPE
import com.financialviewer.constants.NON_FIXED_COST_TIP
import com.financialviewer.db.BankTransaction
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.utils.convertDoubleToString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BankTransactionDetailsActivity: AppCompatActivity() {
private lateinit var bankTransactionRepository: BankTransactionRepository
private lateinit var bankTransaction: BankTransaction
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bank_transaction_details)
bankTransactionRepository = BankTransactionRepository(this)
val refId = intent.getStringExtra("refId") ?: ""
val accountTextView: TextView = findViewById(R.id.account)
val dateTextView: TextView = findViewById(R.id.first)
val amountTextView: TextView = findViewById(R.id.second)
val counterpartyTextView: TextView = findViewById(R.id.third)
val referenceTextView: TextView = findViewById(R.id.reference)
val isFixedCostTextView: TextView = findViewById(R.id.isFixedCostTextView)
val switch: SwitchCompat = findViewById(R.id.isFixedCostSwitch)
if (refId != "") {
lifecycleScope.launch {
bankTransaction = withContext(Dispatchers.IO) {
bankTransactionRepository.getBankTransactionById(refId) ?: BLANK_BANK_TRANSACTION
}
withContext(Dispatchers.Main) {
accountTextView.text = bankTransaction.account
dateTextView.text = bankTransaction.date
amountTextView.text = convertDoubleToString(bankTransaction.amount)
counterpartyTextView.text = bankTransaction.counterparty
referenceTextView.text = bankTransaction.reference
if (FIXED_COST_TYPE.containsKey(bankTransaction.category)) {
isFixedCostTextView.visibility = View.VISIBLE
isFixedCostTextView.text = if (bankTransaction.isFixedCost) {
FIXED_COST_TIP
} else {
NON_FIXED_COST_TIP
}
switch.visibility = View.VISIBLE
switch.isChecked = bankTransaction.isFixedCost
switch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
isFixedCostTextView.text = FIXED_COST_TIP
} else {
isFixedCostTextView.text = NON_FIXED_COST_TIP
}
}
}
}
}
}
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
bankTransaction.isFixedCost = switch.isChecked
bankTransactionRepository.updateBankTransaction(bankTransaction)
}
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
}
}

View File

@@ -0,0 +1,184 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.constants.YEAR
import com.financialviewer.db.BankTransaction
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.ui.adapter.BankTransactionAdapter
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.generateCategoryWithMainCategory
import com.financialviewer.utils.generateMonth
import com.financialviewer.utils.generateYears
import com.financialviewer.utils.getSelectedCategory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BankTransactionsActivity: AppCompatActivity() {
private lateinit var bankTransactionRepository: BankTransactionRepository
private lateinit var bankTransactionAdapter: BankTransactionAdapter
private lateinit var bankTransactionList: MutableList<BankTransaction>
private lateinit var sumAmountTextView: TextView
private var sumAmount = 0.0
private var yearSpinnerIsInitialized = false
private var monthSpinnerIsInitialized = false
private var categorySpinnerIsInitialized = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bank_transactions)
bankTransactionRepository = BankTransactionRepository(this)
val appPreferences = SharedPreferencesHelper((this))
var year = intent.getIntExtra("year", 0).toString()
if (year == "0") {
year = appPreferences.readDefaultYear(YEAR).toString()
}
var month = intent.getIntExtra("month", 0).toString().padStart(2, '0')
if (month == "00") {
month = getString(R.string.show_all_month)
}
val category = intent.getStringExtra("category") ?: ""
var resultCategories: MutableList<String> = mutableListOf()
if (category == "") {
resultCategories.add(getString(R.string.show_all_category))
} else {
resultCategories.add(category)
}
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
bankTransactionAdapter = BankTransactionAdapter(this, mutableListOf()) { selectedBankTransaction ->
onBankTransactionClick(selectedBankTransaction)
}
recyclerView.adapter = bankTransactionAdapter
sumAmountTextView = findViewById(R.id.sumAmountValue)
updateBankTransactionList(year, month, resultCategories)
val yearSpinner: Spinner = findViewById(R.id.yearSpinner)
val latestYear = appPreferences.readDefaultYear(YEAR).toString()
val yearList = generateYears(latestYear)
val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList)
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
yearSpinner.adapter = yearAdapter
val yearPosition = yearList.indexOf(year)
yearSpinner.setSelection(yearPosition)
yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedItem = yearList[position]
year = selectedItem
if (isInitialized()) {
updateBankTransactionList(year, month, resultCategories)
}
yearSpinnerIsInitialized = true
}
override fun onNothingSelected(parent: AdapterView<*>) {
yearSpinnerIsInitialized = true
}
}
val monthSpinner: Spinner = findViewById(R.id.monthSpinner)
val monthList = generateMonth().toMutableList()
monthList.add(0, getString(R.string.show_all_month))
val monthAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, monthList)
monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
monthSpinner.adapter = monthAdapter
val monthPosition = monthList.indexOf(month)
monthSpinner.setSelection(monthPosition)
monthSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedItem = monthList[position]
month = selectedItem
if (isInitialized()) {
updateBankTransactionList(year, month, resultCategories)
}
monthSpinnerIsInitialized = true
}
override fun onNothingSelected(parent: AdapterView<*>) {
monthSpinnerIsInitialized = true
}
}
val categorySpinner: Spinner = findViewById(R.id.categorySpinner)
val categoryList = generateCategoryWithMainCategory().toMutableList()
categoryList.add(0, getString(R.string.show_all_category))
val categoryAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, categoryList)
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
categorySpinner.adapter = categoryAdapter
val categoryPosition = categoryList.indexOf(category)
categorySpinner.setSelection(categoryPosition)
categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedItem = categoryList[position]
resultCategories = getSelectedCategory(selectedItem)
if (isInitialized()) {
updateBankTransactionList(year, month, resultCategories)
}
categorySpinnerIsInitialized = true
}
override fun onNothingSelected(parent: AdapterView<*>) {
categorySpinnerIsInitialized = true
}
}
}
private fun onBankTransactionClick(selectedBankTransaction: BankTransaction) {
val intent = Intent(this@BankTransactionsActivity, BankTransactionDetailsActivity::class.java)
intent.putExtra("refId", selectedBankTransaction.refId)
startActivity(intent)
}
private fun updateBankTransactionList(year: String, month: String, categories: MutableList<String>) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
bankTransactionList = bankTransactionRepository.getBankTransactionsByDateAndCategoryDesc(year,month,
categories
)
sumAmount = calculateSumAmount(bankTransactionList)
}
withContext(Dispatchers.Main) {
bankTransactionAdapter.updateBankTransactionList(bankTransactionList)
sumAmountTextView.text = convertDoubleToString(sumAmount)
val color = if (sumAmount > 0) {
ContextCompat.getColor(this@BankTransactionsActivity, R.color.green)
} else {
ContextCompat.getColor(this@BankTransactionsActivity, R.color.red)
}
sumAmountTextView.setTextColor(color)
}
}
}
private fun calculateSumAmount(bankTransactionList: MutableList<BankTransaction>): Double {
var sum = 0.0
for (bankTransaction in bankTransactionList) {
sum += bankTransaction.amount
}
return sum
}
private fun isInitialized(): Boolean {
return yearSpinnerIsInitialized && monthSpinnerIsInitialized && categorySpinnerIsInitialized
}
}

View File

@@ -0,0 +1,68 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.constants.YEAR
import com.financialviewer.db.BankTransaction
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.ui.adapter.FixedCostDetailsAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FixedCostDetailsActivity : AppCompatActivity() {
private lateinit var bankTransactionRepository: BankTransactionRepository
private lateinit var sourceAdapter: FixedCostDetailsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fixed_cost_details)
bankTransactionRepository = BankTransactionRepository(this)
val refIds = intent.getStringExtra("refIds") ?: ""
val category = intent.getStringExtra("category") ?: ""
val refIdList = refIds.split(",")
val sourceRecyclerView: RecyclerView = findViewById(R.id.sourceRecyclerView)
sourceRecyclerView.layoutManager = LinearLayoutManager(this)
val currentSourceList: MutableList<BankTransaction> = mutableListOf()
val allSourceList: MutableList<BankTransaction> = mutableListOf()
lifecycleScope.launch {
withContext(Dispatchers.IO) {
for (refId in refIdList) {
val bankTransaction = bankTransactionRepository.getBankTransactionById(refId)
if (bankTransaction != null) {
currentSourceList.add(bankTransaction)
}
}
allSourceList.addAll(
bankTransactionRepository.getBankTransactionsByCategory(YEAR.toString(), category))
allSourceList.addAll(
bankTransactionRepository.getBankTransactionsByCategory((YEAR - 1).toString(), category))
}
withContext(Dispatchers.Main) {
sourceAdapter =
FixedCostDetailsAdapter(this@FixedCostDetailsActivity, allSourceList, currentSourceList) { selectedBankTransaction ->
onBankTransactionClick(selectedBankTransaction)
}
sourceRecyclerView.adapter = sourceAdapter
}
}
}
private fun onBankTransactionClick(selectedBankTransaction: BankTransaction) {
val intent = Intent(this@FixedCostDetailsActivity, BankTransactionDetailsActivity::class.java)
intent.putExtra("refId", selectedBankTransaction.refId)
startActivity(intent)
}
}

View File

@@ -0,0 +1,147 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.constants.MONTHLY
import com.financialviewer.constants.QUARTERLY
import com.financialviewer.constants.YEAR
import com.financialviewer.db.FixedCost
import com.financialviewer.repository.FixedCostRepository
import com.financialviewer.ui.adapter.FixedCostAdapter
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.work.UpdateFixedCosts
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class FixedCostsActivity : AppCompatActivity() {
private lateinit var fixedCostRepository: FixedCostRepository
private lateinit var monthlyAdapter: FixedCostAdapter
private lateinit var quarterlyAdapter: FixedCostAdapter
private lateinit var yearlyAdapter: FixedCostAdapter
private var year = YEAR
private val monthlyList: MutableList<FixedCost> = mutableListOf()
private val quarterlyList: MutableList<FixedCost> = mutableListOf()
private val yearlyList: MutableList<FixedCost> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fixed_costs)
fixedCostRepository = FixedCostRepository(this)
val appPreferences = SharedPreferencesHelper((this))
year = appPreferences.readDefaultYear(YEAR)
val lastUpdateTextView: TextView = findViewById(R.id.lastUpdateValue)
lastUpdateTextView.text = appPreferences.readCalculateFixedCostTime()
val monthlyRecyclerView: RecyclerView = findViewById(R.id.monthlyRecyclerView)
monthlyRecyclerView.layoutManager = LinearLayoutManager(this)
val quarterlyRecyclerView: RecyclerView = findViewById(R.id.quarterlyRecyclerView)
quarterlyRecyclerView.layoutManager = LinearLayoutManager(this)
val yearlyRecyclerView: RecyclerView = findViewById(R.id.yearlyRecyclerView)
yearlyRecyclerView.layoutManager = LinearLayoutManager(this)
val monthlyFixedCostTextView: TextView = findViewById(R.id.monthlyFixedCostValue)
val yearlyFixedCostTextView: TextView = findViewById(R.id.yearlyFixedCostValue)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
generateFixedCostList()
}
withContext(Dispatchers.Main) {
monthlyAdapter = FixedCostAdapter(this@FixedCostsActivity, monthlyList) { monthlyFixedCost ->
onFixedCostClick(monthlyFixedCost)
}
monthlyRecyclerView.adapter = monthlyAdapter
quarterlyAdapter = FixedCostAdapter(this@FixedCostsActivity, quarterlyList) { quarterlyFixedCost ->
onFixedCostClick(quarterlyFixedCost)
}
quarterlyRecyclerView.adapter = quarterlyAdapter
yearlyAdapter = FixedCostAdapter(this@FixedCostsActivity, yearlyList) { yearlyFixedCost ->
onFixedCostClick(yearlyFixedCost)
}
yearlyRecyclerView.adapter = yearlyAdapter
val monthlyFixedCost = calculateMonthlyFixedCost()
monthlyFixedCostTextView.text = convertDoubleToString(monthlyFixedCost)
yearlyFixedCostTextView.text = convertDoubleToString(monthlyFixedCost * 12)
}
}
val recalculateButton: Button = findViewById(R.id.recalculateButton)
recalculateButton.setOnClickListener {
val updateFixedCost = UpdateFixedCosts(this)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
updateFixedCost.updateDB()
}
}
}
}
private suspend fun generateFixedCostList() {
val fixedCostList = fixedCostRepository.getAllFixedCosts()
for (fixedCost in fixedCostList) {
when (fixedCost.type) {
MONTHLY -> {
monthlyList.add(fixedCost)
}
QUARTERLY -> {
quarterlyList.add(fixedCost)
}
else -> {
yearlyList.add(fixedCost)
}
}
}
}
private fun calculateMonthlyFixedCost(): Double {
var monthlySum = BigDecimal(0.0)
var quarterlySum = BigDecimal(0.0)
var yearlySum = BigDecimal(0.0)
for (monthly in monthlyList) {
monthlySum += BigDecimal(monthly.amount)
}
for (quarterly in quarterlyList) {
quarterlySum += BigDecimal(quarterly.amount)
}
for (yearly in yearlyList) {
yearlySum += BigDecimal(yearly.amount)
}
return (monthlySum +
(quarterlySum / BigDecimal("3")) +
(yearlySum / BigDecimal("12")))
.toDouble()
}
private fun onFixedCostClick(fixedCost: FixedCost) {
val intent = Intent(this@FixedCostsActivity, FixedCostDetailsActivity::class.java)
intent.putExtra("refIds", fixedCost.refIds)
intent.putExtra("category", fixedCost.category)
startActivity(intent)
}
}

View File

@@ -0,0 +1,98 @@
package com.financialviewer.ui
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.financialviewer.R
import com.financialviewer.work.UpdateBankTransactions
import kotlinx.coroutines.launch
class HomeActivity: AppCompatActivity() {
private lateinit var progressBarContainer: LinearLayout
private var isProgressing = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
progressBarContainer = findViewById(R.id.progressBarContainer)
val loanOverviewTextView : TextView =findViewById(R.id.loanOverview)
loanOverviewTextView.setOnClickListener {
if (!isProgressing) {
val intent = Intent(this@HomeActivity, LoanOverviewActivity::class.java)
startActivity(intent)
}
}
val fixedCostsTextView : TextView =findViewById(R.id.fixedCosts)
fixedCostsTextView.setOnClickListener {
if (!isProgressing) {
val intent = Intent(this@HomeActivity, FixedCostsActivity::class.java)
startActivity(intent)
}
}
val bankTransactionTextView : TextView =findViewById(R.id.bankTransaction)
bankTransactionTextView.setOnClickListener {
if (!isProgressing) {
val intent = Intent(this@HomeActivity, BankTransactionsActivity::class.java)
startActivity(intent)
}
}
val monthlySummaryTextView : TextView =findViewById(R.id.monthlySummary)
monthlySummaryTextView.setOnClickListener {
if (!isProgressing) {
val intent = Intent(this@HomeActivity, MonthlySummaryActivity::class.java)
startActivity(intent)
}
}
val yearlySummaryTextView : TextView =findViewById(R.id.yearlySummary)
yearlySummaryTextView.setOnClickListener {
if (!isProgressing) {
val intent = Intent(this@HomeActivity, YearlySummaryActivity::class.java)
startActivity(intent)
}
}
val updateTransactionTextView : TextView =findViewById(R.id.updateTransaction)
updateTransactionTextView.setOnClickListener {
if (!isProgressing) {
AlertDialog.Builder(this)
.setTitle("确认更新")
.setMessage("是否确定更新银行流水?")
.setPositiveButton("确定") { dialog, _ ->
showProgressBar()
lifecycleScope.launch {
val updateBankTransaction = UpdateBankTransactions(this@HomeActivity)
updateBankTransaction.updateDB()
hideProgressBar()
dialog.dismiss()
}
}
.setNegativeButton("取消") { dialog, _ ->
dialog.dismiss()
}
.show()
}
}
}
private fun showProgressBar() {
progressBarContainer.visibility = View.VISIBLE
isProgressing = true
}
private fun hideProgressBar() {
progressBarContainer.visibility = View.GONE
isProgressing = false
}
}

View File

@@ -0,0 +1,165 @@
package com.financialviewer.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.icu.util.Calendar
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.data.LoanMonthlyDetails
import com.financialviewer.db.Loan
import com.financialviewer.repository.LoanRepository
import com.financialviewer.ui.adapter.LoanDetailsAdapter
import com.financialviewer.utils.calculateNextInterest
import com.financialviewer.utils.calculateNextPrincipal
import com.financialviewer.utils.calculateNextRemaining
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.convertInputStringToDouble
import com.financialviewer.utils.convertStringToDouble
import com.financialviewer.utils.getNextMonth
import kotlinx.coroutines.launch
class LoanDetailsActivity: AppCompatActivity() {
private lateinit var loanRepository: LoanRepository
private lateinit var loanDetailsAdapter: LoanDetailsAdapter
private var remainingUpdated: Boolean = false
private var sumInterest: Double = 0.0
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loan_details)
loanRepository = LoanRepository(this)
val loanId = intent.getStringExtra("loanId") ?: "R1"
val titleTextView: TextView = findViewById(R.id.loanTitle)
val paymentTextView: TextView = findViewById(R.id.payment)
val remainingTextView: TextView = findViewById(R.id.remaining)
val rateTextView: TextView = findViewById(R.id.rate)
val sumInterestTextView: TextView = findViewById(R.id.sumInterest)
lifecycleScope.launch {
val loan = loanRepository.getLoanById(loanId)
if (loan != null) {
titleTextView.text = loan.title
remainingTextView.text = "当前欠款:" + loan.remainingLoan
paymentTextView.text = "月供: " + loan.payment
rateTextView.text = "利率:" + loan.rate
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this@LoanDetailsActivity)
loanDetailsAdapter = LoanDetailsAdapter(generateMonthlyDetails(loan))
recyclerView.adapter = loanDetailsAdapter
sumInterestTextView.text = "预计还需支付利息:" + convertDoubleToString(sumInterest)
}
}
val refreshButton: Button = findViewById(R.id.refresh)
refreshButton.setOnClickListener {
lifecycleScope.launch {
val loan = loanRepository.getLoanById(loanId)
if (loan != null) {
loanDetailsAdapter.updateLoanDetailsList(generateMonthlyDetails(loan))
sumInterestTextView.text = "预计还需支付利息:" + convertDoubleToString(sumInterest)
}
}
}
val specialRepaymentButton: Button = findViewById(R.id.specialRepayment)
specialRepaymentButton.setOnClickListener {
// 创建一个EditText作为输入框
val input = EditText(this@LoanDetailsActivity)
// 创建并显示AlertDialog
val dialog = AlertDialog.Builder(this@LoanDetailsActivity)
.setTitle("提前还款金额")
.setView(input) // 将EditText添加到对话框
.setPositiveButton("Confirm") { _, _ ->
// 获取用户输入并更新TextView的内容
val inputString = input.text.toString()
try {
val inputDouble = convertInputStringToDouble(inputString) // 成功转换
lifecycleScope.launch {
val loan = loanRepository.getLoanById(loanId)
if (loan != null) {
val newRemainingLoan = convertStringToDouble(loan.remainingLoan) - inputDouble
remainingTextView.text = "当前欠款:" + convertDoubleToString(newRemainingLoan)
loan.remainingLoan = convertDoubleToString(newRemainingLoan)
loanRepository.updateLoan(loan)
remainingUpdated = true
}
}
} catch (e: NumberFormatException) {
// 如果输入不是有效的数字,弹出错误提示
Toast.makeText(this@LoanDetailsActivity, "Invalid input. Please enter a valid number.", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("Cancel", null)
.create()
dialog.show()
}
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (remainingUpdated) {
val resultIntent = Intent().apply {
putExtra("loanId", loanId)
}
setResult(Activity.RESULT_OK, resultIntent)
}
finish()
}
})
}
private fun generateMonthlyDetails(loan: Loan): MutableList<LoanMonthlyDetails> {
sumInterest = 0.0
val monthlyDetails: MutableList<LoanMonthlyDetails> = mutableListOf()
val now = Calendar.getInstance()
var currentYear = now.get(Calendar.YEAR)
var currentMonth = now.get(Calendar.MONTH) + 1
var base = convertStringToDouble(loan.remainingLoan)
var remaining = 1.0
while (remaining > 0) {
val month = getNextMonth(currentYear, currentMonth)
currentYear = month.split(".")[1].toInt()
currentMonth = month.split(".")[0].toInt()
val interest = calculateNextInterest(loan, base)
var principal = calculateNextPrincipal(loan, base)
remaining = calculateNextRemaining(loan, base)
if (remaining < 0) {
remaining = 0.0
principal = base
}
base = remaining
monthlyDetails.add(LoanMonthlyDetails(
month,
convertDoubleToString(interest),
convertDoubleToString(principal),
convertDoubleToString(remaining)
))
sumInterest += interest
}
return monthlyDetails
}
}

View File

@@ -0,0 +1,162 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.Loan
import com.financialviewer.repository.LoanRepository
import com.financialviewer.ui.adapter.LoanOverviewAdapter
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.convertInputStringToDouble
import com.financialviewer.utils.convertPercentageStringToDouble
import com.financialviewer.utils.convertPercentageToString
import com.financialviewer.utils.convertStringToDouble
import com.financialviewer.utils.convertStringToGermanDate
import kotlinx.coroutines.launch
import java.time.format.DateTimeParseException
class LoanOverviewActivity : AppCompatActivity() {
private lateinit var loanRepository: LoanRepository
private lateinit var recyclerView: RecyclerView
private var loansInDB: MutableList<Loan> = mutableListOf()
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
val loanId = data?.getStringExtra("loanId")
loanId?.let {
// 在当前 Activity 的协程作用域中调用 refreshData
lifecycleScope.launch {
refresh(it)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loan_overview)
loanRepository = LoanRepository(this)
recyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
var sumLoan = 0.0
var sumPayment = 0.0
lifecycleScope.launch {
loansInDB = loanRepository.getAllLoans()
val adapter = LoanOverviewAdapter(
loansInDB,
onEditClick = { loan, field, value, position ->
// 处理编辑点击事件,例如弹出对话框
showEditDialog(loan, field, value, position)
},
onTitleClick = { loan ->
// 处理标题点击事件,例如显示详细信息
showLoanDetails(loan)
}
)
recyclerView.adapter = adapter
for (loan in loansInDB) {
sumLoan += convertStringToDouble(loan.remainingLoan)
sumPayment += convertStringToDouble(loan.payment)
}
val sumLoanValueTextView: TextView = findViewById(R.id.sumLoanValue)
sumLoanValueTextView.text = convertDoubleToString(sumLoan)
val sumPaymentValueTextView: TextView = findViewById(R.id.sumPaymentValue)
sumPaymentValueTextView.text = convertDoubleToString(sumPayment)
}
}
private fun showLoanDetails(loan: Loan) {
val intent = Intent(this@LoanOverviewActivity, LoanDetailsActivity::class.java)
intent.putExtra("loanId", loan.id)
startForResult.launch(intent)
}
private fun showEditDialog(loan: Loan, field: String, value: String, position: Int) {
val builder = AlertDialog.Builder(this)
builder.setTitle("Edit $field")
val input = EditText(this)
input.setText(value)
builder.setView(input)
builder.setPositiveButton("OK") { dialog, _ ->
val inputString = input.text.toString()
try {
// 更新字段值
when (field) {
"rate" -> {
val validValue = convertPercentageStringToDouble(inputString)
loan.rate = convertPercentageToString(validValue)
}
"payment" -> {
val validValue = convertInputStringToDouble(inputString)
loan.payment = convertDoubleToString(validValue)
}
"maturity" -> {
val validValue = convertStringToGermanDate(inputString)
loan.maturity = validValue
}
"remainingLoan" -> {
val validValue = convertInputStringToDouble(inputString)
loan.remainingLoan = convertDoubleToString(validValue)
}
}
// 更新数据库中的数据
lifecycleScope.launch {
loanRepository.updateLoan(loan)
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(position)
}
}
} catch (e: NumberFormatException) {
// 如果输入不是有效的数字,弹出错误提示
Toast.makeText(this@LoanOverviewActivity, "Invalid input. Please enter a valid number.", Toast.LENGTH_SHORT).show()
} catch (e: DateTimeParseException) {
// 如果输入不是有效的数字,弹出错误提示
Toast.makeText(this@LoanOverviewActivity, "Invalid input. Please enter a valid date.", Toast.LENGTH_SHORT).show()
}
dialog.dismiss()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
}
builder.show()
}
private fun refresh(loanId: String) {
val position = loansInDB.indexOfFirst { it.id == loanId }
if (position != -1) {
lifecycleScope.launch {
val newLoan = loanRepository.getLoanById(loanId)
if (newLoan != null) {
loansInDB[position] = newLoan
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(position)
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.financialviewer.R
import com.financialviewer.utils.SharedPreferencesHelper
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val password: EditText = findViewById(R.id.password)
// 处理登录逻辑
val loginButton: Button = findViewById(R.id.button)
loginButton.setOnClickListener {
val enteredPassword = password.text.toString()
if (enteredPassword.isEmpty()) {
Toast.makeText(this, "请输入密码", Toast.LENGTH_SHORT).show()
} else {
// 处理登录逻辑,例如验证密码
if (enteredPassword == "3713") {
val appPreferences = SharedPreferencesHelper((this))
appPreferences.saveAuthStatus("is_authenticated", true)
// 跳转到主页面
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "密码错误", Toast.LENGTH_SHORT).show()
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.financialviewer.utils.SharedPreferencesHelper
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appPreferences = SharedPreferencesHelper((this))
appPreferences.saveAuthStatus("is_authenticated", false)
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@@ -0,0 +1,109 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.constants.YEAR
import com.financialviewer.db.MonthlySummary
import com.financialviewer.repository.MonthlySummaryRepository
import com.financialviewer.ui.adapter.MonthlySummaryAdapter
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.generateYears
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class MonthlySummaryActivity : AppCompatActivity() {
private lateinit var monthlySummaryRepository: MonthlySummaryRepository
private lateinit var monthlySummaryAdapter: MonthlySummaryAdapter
private lateinit var monthlySummaryList: MutableList<MonthlySummary>
private lateinit var incomeSumTextView: TextView
private lateinit var costSumTextView: TextView
private var year = YEAR
private var yearSpinnerIsInitialized = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_monthly_summary)
monthlySummaryRepository = MonthlySummaryRepository(this)
val appPreferences = SharedPreferencesHelper((this))
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
monthlySummaryAdapter = MonthlySummaryAdapter(this, mutableListOf()) { selectedMonthlySummary ->
onMonthlySummaryClick(selectedMonthlySummary)
}
recyclerView.adapter = monthlySummaryAdapter
incomeSumTextView = findViewById(R.id.sumIncomeValue)
costSumTextView = findViewById(R.id.sumCostValue)
updateYearlySummaryList()
year = appPreferences.readDefaultYear(YEAR)
val yearSpinner: Spinner = findViewById(R.id.yearSpinner)
val yearList = generateYears(year.toString())
val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList)
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
yearSpinner.adapter = yearAdapter
yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedItem = yearList[position]
year = selectedItem.toInt()
if (isInitialized()) {
updateYearlySummaryList()
}
yearSpinnerIsInitialized = true
}
override fun onNothingSelected(parent: AdapterView<*>) {
yearSpinnerIsInitialized = true
}
}
}
private fun updateYearlySummaryList() {
lifecycleScope.launch {
var incomeSum = BigDecimal("0.0")
var costSum = BigDecimal("0.0")
withContext(Dispatchers.IO) {
monthlySummaryList = monthlySummaryRepository.getMonthlySummary(year)
for (monthlySummary in monthlySummaryList) {
incomeSum += BigDecimal(monthlySummary.income.toString())
costSum += BigDecimal(monthlySummary.cost.toString())
}
}
withContext(Dispatchers.Main) {
monthlySummaryAdapter.updateMonthlySummaryList(monthlySummaryList)
incomeSumTextView.text = convertDoubleToString(incomeSum.toDouble())
costSumTextView.text = convertDoubleToString(costSum.toDouble())
}
}
}
private fun onMonthlySummaryClick(monthlySummary: MonthlySummary) {
val intent = Intent(this@MonthlySummaryActivity, BankTransactionsActivity::class.java)
intent.putExtra("year", monthlySummary.year)
intent.putExtra("month", monthlySummary.month)
startActivity(intent)
}
private fun isInitialized(): Boolean {
return yearSpinnerIsInitialized
}
}

View File

@@ -0,0 +1,110 @@
package com.financialviewer.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.constants.YEAR
import com.financialviewer.db.YearlySummary
import com.financialviewer.repository.YearlySummaryRepository
import com.financialviewer.ui.adapter.YearlySummaryAdapter
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.generateYears
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class YearlySummaryActivity : AppCompatActivity() {
private lateinit var yearlySummaryRepository: YearlySummaryRepository
private lateinit var yearlySummaryAdapter: YearlySummaryAdapter
private lateinit var yearlySummaryList: MutableList<YearlySummary>
private lateinit var sumAmountTextView: TextView
private var year = YEAR
private var yearSpinnerIsInitialized = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_yearly_summary)
yearlySummaryRepository = YearlySummaryRepository(this)
val appPreferences = SharedPreferencesHelper((this))
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
yearlySummaryAdapter = YearlySummaryAdapter(this, mutableListOf()) { selectedYearlySummary ->
onYearlySummaryClick(selectedYearlySummary)
}
recyclerView.adapter = yearlySummaryAdapter
sumAmountTextView = findViewById(R.id.sumAmountValue)
updateYearlySummaryList()
year = appPreferences.readDefaultYear(YEAR)
val yearSpinner: Spinner = findViewById(R.id.yearSpinner)
val yearList = generateYears(year.toString())
val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList)
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
yearSpinner.adapter = yearAdapter
yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedItem = yearList[position]
year = selectedItem.toInt()
if (isInitialized()) {
updateYearlySummaryList()
}
yearSpinnerIsInitialized = true
}
override fun onNothingSelected(parent: AdapterView<*>) {
yearSpinnerIsInitialized = true
}
}
}
private fun updateYearlySummaryList() {
lifecycleScope.launch {
var sum = BigDecimal("0.0")
withContext(Dispatchers.IO) {
yearlySummaryList = yearlySummaryRepository.getYearlySummary(year)
for (yearlySummary in yearlySummaryList) {
sum += BigDecimal(yearlySummary.total.toString())
}
}
withContext(Dispatchers.Main) {
yearlySummaryAdapter.updateYearlySummaryList(yearlySummaryList)
sumAmountTextView.text = convertDoubleToString(sum.toDouble())
val color = if (sum.toDouble() > 0) {
ContextCompat.getColor(this@YearlySummaryActivity, R.color.green)
} else {
ContextCompat.getColor(this@YearlySummaryActivity, R.color.red)
}
sumAmountTextView.setTextColor(color)
}
}
}
private fun onYearlySummaryClick(yearlySummary: YearlySummary) {
val intent = Intent(this@YearlySummaryActivity, BankTransactionsActivity::class.java)
intent.putExtra("year", yearlySummary.year)
intent.putExtra("category", yearlySummary.category)
startActivity(intent)
}
private fun isInitialized(): Boolean {
return yearSpinnerIsInitialized
}
}

View File

@@ -0,0 +1,62 @@
package com.financialviewer.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.BankTransaction
import com.financialviewer.utils.convertDoubleToString
import com.financialviewer.utils.convertStandardToGermanDate
class BankTransactionAdapter (
private val context: Context,
private val bankTransactionList: MutableList<BankTransaction>,
private val onItemClick: (BankTransaction) -> Unit,
) : RecyclerView.Adapter<BankTransactionAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val date: TextView = itemView.findViewById(R.id.first)
val category: TextView = itemView.findViewById(R.id.second)
val amount: TextView = itemView.findViewById(R.id.third)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_three_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bankTransaction = bankTransactionList[position]
val dateParts = convertStandardToGermanDate(bankTransaction.date).split(".")
holder.date.text = "${dateParts[0]}.${dateParts[1]}"
holder.category.text = bankTransaction.category
holder.amount.text = convertDoubleToString(bankTransaction.amount)
val color = if (bankTransaction.amount > 0) {
ContextCompat.getColor(context, R.color.green)
} else {
ContextCompat.getColor(context, R.color.red)
}
holder.amount.setTextColor(color)
holder.itemView.setOnClickListener {
onItemClick(bankTransaction)
}
}
override fun getItemCount() = bankTransactionList.size
private fun initBankTransactionList() {
bankTransactionList.clear()
}
fun updateBankTransactionList(newList: List<BankTransaction>) {
initBankTransactionList()
bankTransactionList.addAll(newList)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,52 @@
package com.financialviewer.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.FixedCost
import com.financialviewer.utils.convertDoubleToString
class FixedCostAdapter (
private val context: Context,
private val fixedCostList: MutableList<FixedCost>,
private val onItemClick: (FixedCost) -> Unit,
) : RecyclerView.Adapter<FixedCostAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val category: TextView = itemView.findViewById(R.id.first)
val amount: TextView = itemView.findViewById(R.id.second)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_two_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fixedCost = fixedCostList[position]
holder.category.text = fixedCost.category
holder.amount.text = convertDoubleToString(fixedCost.amount)
holder.amount.setTextColor(ContextCompat.getColor(context, R.color.red))
holder.itemView.setOnClickListener {
onItemClick(fixedCost)
}
}
override fun getItemCount() = fixedCostList.size
private fun initBankTransactionList() {
fixedCostList.clear()
}
fun updateFixedCostList(newList: List<FixedCost>) {
initBankTransactionList()
fixedCostList.addAll(newList)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,59 @@
package com.financialviewer.ui.adapter
import android.content.Context
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.BankTransaction
import com.financialviewer.utils.convertDoubleToString
class FixedCostDetailsAdapter (
private val context: Context,
private val allSourceList: MutableList<BankTransaction>,
private val currentSourceList: MutableList<BankTransaction>,
private val onItemClick: (BankTransaction) -> Unit,
) : RecyclerView.Adapter<FixedCostDetailsAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val date: TextView = itemView.findViewById(R.id.first)
val category: TextView = itemView.findViewById(R.id.second)
val amount: TextView = itemView.findViewById(R.id.third)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_three_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bankTransaction = allSourceList[position]
holder.date.text = bankTransaction.date
holder.category.text = bankTransaction.category
holder.amount.text = convertDoubleToString(bankTransaction.amount)
holder.amount.setTextColor(ContextCompat.getColor(context, R.color.red))
if (currentSourceList.contains(bankTransaction)) {
holder.date.setTypeface(null, Typeface.BOLD)
holder.category.setTypeface(null, Typeface.BOLD)
holder.amount.setTypeface(null, Typeface.BOLD)
}
if (!bankTransaction.isFixedCost) {
holder.date.setTypeface(null, Typeface.ITALIC)
holder.category.setTypeface(null, Typeface.ITALIC)
holder.amount.setTypeface(null, Typeface.ITALIC)
}
holder.itemView.setOnClickListener {
onItemClick(bankTransaction)
}
}
override fun getItemCount() = allSourceList.size
}

View File

@@ -0,0 +1,46 @@
package com.financialviewer.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.data.LoanMonthlyDetails
class LoanDetailsAdapter (
private val loanDetailsList: MutableList<LoanMonthlyDetails>
) : RecyclerView.Adapter<LoanDetailsAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val monthText: TextView = itemView.findViewById(R.id.first)
val interestText: TextView = itemView.findViewById(R.id.second)
val principalText: TextView = itemView.findViewById(R.id.third)
val remainingDebtText: TextView = itemView.findViewById(R.id.fourth)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_four_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val loanDetails = loanDetailsList[position]
holder.monthText.text = loanDetails.month
holder.interestText.text = loanDetails.interest
holder.principalText.text = loanDetails.principal
holder.remainingDebtText.text = loanDetails.remaining
}
override fun getItemCount() = loanDetailsList.size
private fun initLoanDetailsList() {
loanDetailsList.clear()
}
fun updateLoanDetailsList(newList: List<LoanMonthlyDetails>) {
initLoanDetailsList()
loanDetailsList.addAll(newList)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,69 @@
package com.financialviewer.ui.adapter
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.Loan
class LoanOverviewAdapter (
private val loans: MutableList<Loan>,
private val onTitleClick: (Loan) -> Unit,
private val onEditClick: (Loan, String, String, Int) -> Unit
) : RecyclerView.Adapter<LoanOverviewAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.loanTitle)
val amount: TextView = itemView.findViewById(R.id.loanAmount)
val rate: TextView = itemView.findViewById(R.id.loanRate)
val payment: TextView = itemView.findViewById(R.id.loanPayment)
val maturity: TextView = itemView.findViewById(R.id.loanMaturity)
val remainingLoan: TextView = itemView.findViewById(R.id.loanRemainingLoan)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_loan_overview, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val loan = loans[position]
holder.title.text = loan.title
holder.amount.text = loan.amount
holder.rate.text = loan.rate
holder.payment.text = loan.payment
holder.maturity.text = loan.maturity
holder.remainingLoan.text = loan.remainingLoan
// 设置 title 为加粗大号字体
holder.title.setTypeface(null, Typeface.BOLD)
holder.title.textSize = 20f
holder.title.setOnClickListener {
onTitleClick(loan)
}
// 为可编辑字段设置点击事件
holder.rate.setOnClickListener {
onEditClick(loan, "rate", loan.rate, position)
}
holder.payment.setOnClickListener {
onEditClick(loan, "payment", loan.payment, position)
}
holder.maturity.setOnClickListener {
onEditClick(loan, "maturity", loan.maturity, position)
}
holder.remainingLoan.setOnClickListener {
onEditClick(loan, "remainingLoan", loan.remainingLoan, position)
}
}
override fun getItemCount(): Int = loans.size
}

View File

@@ -0,0 +1,65 @@
package com.financialviewer.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.MonthlySummary
import com.financialviewer.utils.convertDoubleToString
import java.text.DateFormatSymbols
import java.util.Locale
class MonthlySummaryAdapter (
private val context: Context,
private val monthlySummaryList: MutableList<MonthlySummary>,
private val onItemClick: (MonthlySummary) -> Unit,
) : RecyclerView.Adapter<MonthlySummaryAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val month: TextView = itemView.findViewById(R.id.first)
val income: TextView = itemView.findViewById(R.id.second)
val cost: TextView = itemView.findViewById(R.id.third)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_three_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val monthlySummary = monthlySummaryList[position]
holder.month.text = getMonthNameInGerman(monthlySummary.month)
holder.income.text = convertDoubleToString(monthlySummary.income)
holder.cost.text = convertDoubleToString(monthlySummary.cost)
holder.itemView.setOnClickListener {
onItemClick(monthlySummary)
}
holder.income.setTextColor(ContextCompat.getColor(context, R.color.green))
holder.cost.setTextColor(ContextCompat.getColor(context, R.color.red))
}
override fun getItemCount() = monthlySummaryList.size
private fun initMonthlySummaryList() {
monthlySummaryList.clear()
}
fun updateMonthlySummaryList(newList: List<MonthlySummary>) {
initMonthlySummaryList()
monthlySummaryList.addAll(newList)
notifyDataSetChanged()
}
fun getMonthNameInGerman(month: Int): String {
// 获取德文的月份名称数组
val monthsInGerman = DateFormatSymbols(Locale.GERMAN).months
// 检查输入是否在有效范围内
return if (month in 1..12) monthsInGerman[month - 1] else "Ungültiger Monat"
}
}

View File

@@ -0,0 +1,58 @@
package com.financialviewer.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.financialviewer.R
import com.financialviewer.db.YearlySummary
import com.financialviewer.utils.convertDoubleToString
class YearlySummaryAdapter (
private val context: Context,
private val yearlySummaryList: MutableList<YearlySummary>,
private val onItemClick: (YearlySummary) -> Unit,
) : RecyclerView.Adapter<YearlySummaryAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val category: TextView = itemView.findViewById(R.id.first)
val amount: TextView = itemView.findViewById(R.id.second)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_two_equal_texts, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val yearlySummary = yearlySummaryList[position]
holder.category.text = yearlySummary.category
holder.amount.text = convertDoubleToString(yearlySummary.total)
val color = if (yearlySummary.total > 0) {
ContextCompat.getColor(context, R.color.green)
} else {
ContextCompat.getColor(context, R.color.red)
}
holder.amount.setTextColor(color)
holder.itemView.setOnClickListener {
onItemClick(yearlySummary)
}
}
override fun getItemCount() = yearlySummaryList.size
private fun initYearlySummaryList() {
yearlySummaryList.clear()
}
fun updateYearlySummaryList(newList: List<YearlySummary>) {
initYearlySummaryList()
yearlySummaryList.addAll(newList)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,33 @@
package com.financialviewer.utils
import com.financialviewer.constants.COUNTERPARTY_MAP
fun getCounterpartyKey(ref: String): String? {
return COUNTERPARTY_MAP.keys.find { key -> ref.contains(key) }
}
fun getCounterparty(ref: String): String {
var counterparty = ""
val counterpartyOriginal = getCounterpartyKey(ref)
if (counterpartyOriginal != null) {
counterparty = COUNTERPARTY_MAP[counterpartyOriginal]?.first ?: ""
}
return counterparty
}
fun getCategory(ref: String): String {
var category = ""
val counterpartyOriginal = getCounterpartyKey(ref)
if (counterpartyOriginal != null) {
category = COUNTERPARTY_MAP[counterpartyOriginal]?.second ?: ""
}
return category
}
fun getBankTransactionRefId(ref: String): String {
val regex = """\b(\w+)\s+IBAN\b""".toRegex() // 匹配 string1 和 "IBAN"
val matchResult = regex.find(ref)
val refId = matchResult?.groupValues?.get(1) ?: ""
return refId
}

View File

@@ -0,0 +1,66 @@
package com.financialviewer.utils
import com.financialviewer.constants.CATEGORY_LIST
import java.nio.file.Paths
fun pathJoin(vararg parts: String): String {
return Paths.get(parts[0], *parts.sliceArray(1 until parts.size)).toString()
}
fun generateYears(latestYearOfBankTransaction: String): List<String> {
return (2024..latestYearOfBankTransaction.toInt()).map { it.toString() }
}
fun insertItemIntoList(list: List<String>, item: String, index: Int): List<String> {
val mutableList = list.toMutableList()
mutableList.add(index, item)
return mutableList.toList()
}
fun generateMonth(): List<String> {
return (1..12).map { it.toString().padStart(2, '0') }
}
fun generateCategory(): List<String> {
val mutableList: MutableList<String> = mutableListOf()
for (firstLevel in CATEGORY_LIST) {
for (secondLevel in firstLevel.subList) {
mutableList.add(secondLevel)
}
}
return mutableList
}
fun generateCategoryWithMainCategory(): List<String> {
val mutableList: MutableList<String> = mutableListOf()
for (firstLevel in CATEGORY_LIST) {
mutableList.add(firstLevel.title)
for (secondLevel in firstLevel.subList) {
mutableList.add(secondLevel)
}
}
return mutableList
}
fun generateOnlyMainCategory(): List<String> {
val mutableList: MutableList<String> = mutableListOf()
for (firstLevel in CATEGORY_LIST) {
mutableList.add(firstLevel.title)
}
return mutableList
}
fun getSelectedCategory(selectedItem: String): MutableList<String> {
val categoryList: MutableList<String> = mutableListOf()
if (selectedItem.contains(".")) {
val firstLevelItem = CATEGORY_LIST.find { it.title == selectedItem }
if (firstLevelItem != null) {
categoryList.addAll(firstLevelItem.subList)
}
} else {
categoryList.add(selectedItem)
}
return categoryList
}

View File

@@ -0,0 +1,63 @@
package com.financialviewer.utils
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Locale
fun convertStringToDouble(value: String): Double {
val cleanedValue = value.replace(".", "").replace(",", ".")
return cleanedValue.toDouble()
}
fun convertInputStringToDouble(value: String): Double {
return if (value.contains(".") && value.contains(",")) {
convertStringToDouble(value)
} else {
value.replace(",", ".").toDouble()
}
}
fun convertDoubleToString(value: Double): String {
val symbols = DecimalFormatSymbols(Locale.GERMAN) // 使用德语区域符号
val decimalFormat = DecimalFormat("#,##0.00", symbols) // 格式化规则
return decimalFormat.format(value) // 返回格式化后的字符串
}
fun convertPercentageStringToDouble(value: String): Double {
val cleanedValue = value.replace("%", "")
val normalizedValue = cleanedValue.replace(",", ".")
return normalizedValue.toDouble() / 100
}
fun convertPercentageToString(value: Double): String {
val symbols = DecimalFormatSymbols(Locale.GERMAN) // 使用德语区域符号
val decimalFormat = DecimalFormat("#0.00%", symbols)
return decimalFormat.format(value) // 返回格式化后的字符串
}
fun convertStringToGermanDate(value: String): String {
val inputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN)
val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN)
val date = LocalDate.parse(value, inputFormatter)
return outputFormatter.format(date)
}
fun convertGermanDateToStandard(dateString: String): String {
val germanFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN)
val standardFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val localDate = LocalDate.parse(dateString, germanFormatter)
return localDate.format(standardFormatter)
}
fun convertStandardToGermanDate(dateString: String): String {
val standardFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val germanFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY)
val localDate = LocalDate.parse(dateString, standardFormatter)
return localDate.format(germanFormatter)
}
fun windowsFileTimeToMillis(windowsFileTime: Long): Long {
return (windowsFileTime / 10000) - 11644473600000
}

View File

@@ -0,0 +1,44 @@
package com.financialviewer.utils
import com.financialviewer.db.Loan
fun calculateRemainingLoan(loan: Loan, monthDiff: Int): String {
var remainingLoan = convertStringToDouble(loan.remainingLoan)
val payment = convertStringToDouble(loan.payment)
val rate = convertPercentageStringToDouble(loan.rate)
for (i in 0 until monthDiff) {
val interest = (remainingLoan * rate) / 12
val calResult = remainingLoan - payment + interest
remainingLoan = calResult
}
return convertDoubleToString(remainingLoan)
}
fun calculateNextInterest(loan: Loan, base: Double): Double {
val rate = convertPercentageStringToDouble(loan.rate)
return (base * rate) / 12
}
fun calculateNextPrincipal(loan: Loan, base: Double): Double {
val payment = convertStringToDouble(loan.payment)
val interest = calculateNextInterest(loan, base)
return payment - interest
}
fun calculateNextRemaining(loan: Loan, base: Double): Double {
val principal = calculateNextPrincipal(loan, base)
return base - principal
}
fun getNextMonth(year: Int, month: Int): String {
var newYear = year
var newMonth = month
if (month == 12) {
newYear += 1
newMonth = 1
} else {
newMonth += 1
}
return newMonth.toString().padStart(2, '0') + "." + newYear.toString()
}

View File

@@ -0,0 +1,94 @@
package com.financialviewer.utils
import android.content.Context
import android.util.Log
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class SharedPreferencesHelper(
private val context: Context
) {
private val fileName = "app_prefs"
private val sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
private fun saveData(key: String, value: String) {
val editor = sharedPreferences.edit()
editor.putString(key, value)
editor.apply()
}
fun saveDefaultYear(value: Int) {
val editor = sharedPreferences.edit()
editor.putInt("default_year_of_bank_Transaction", value)
editor.apply()
}
fun saveAuthStatus(key: String, value: Boolean) {
val editor = sharedPreferences.edit()
editor.putBoolean(key, value)
editor.apply()
}
fun saveCurrentUpdateTimeToSharedPreferences() {
val currentDateTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val formattedDateTime = currentDateTime.format(formatter)
saveData("bank_transaction_last_Update_date", formattedDateTime)
}
fun saveCalculateFixedCostTime() {
val currentDateTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val formattedDateTime = currentDateTime.format(formatter)
saveData("calculate_fixed_cost_time", formattedDateTime)
}
private fun readData(key: String): String {
return sharedPreferences.getString(key, "") ?: ""
}
fun readDefaultYear(defaultValue: Int): Int {
return sharedPreferences.getInt("default_year_of_bank_Transaction", defaultValue)
}
fun readLastUpdateTimeToSharedPreferences(): String {
return readData("bank_transaction_last_Update_date")
}
fun readCalculateFixedCostTime(): String {
return readData("calculate_fixed_cost_time")
}
fun getAllSharedPreferences(): MutableList<String> {
val allEntries: Map<String, *> = sharedPreferences.all
for ((key, value) in allEntries) {
Log.d("SharedPreferences", "$key: $value")
}
return sharedPreferences.all.keys.toMutableList()
}
fun removeSpecificSharedPreference(key: String) {
val editor = sharedPreferences.edit()
editor.remove(key)
editor.apply() // or editor.commit()
}
fun clearAllSharedPreferences() {
val editor = sharedPreferences.edit()
editor.clear()
editor.apply() // or editor.commit()
}
private fun deleteSharedPreferencesFile(fileName: String) {
val dir = context.filesDir.parentFile
val sharedPrefsFile = File(dir, "shared_prefs/$fileName.xml")
if (sharedPrefsFile.exists()) {
sharedPrefsFile.delete()
}
}
}

View File

@@ -0,0 +1,28 @@
package com.financialviewer.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.financialviewer.repository.FixedCostRepository
class StartupWorker (context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val updateLoans = UpdateLoans(context = applicationContext)
updateLoans.initDB()
updateLoans.updateDB()
val updateBankTransactions = UpdateBankTransactions(context = applicationContext)
updateBankTransactions.initDB()
val fixedCostRepository = FixedCostRepository(context = applicationContext)
val updateFixedCosts = UpdateFixedCosts(context = applicationContext)
if (fixedCostRepository.getRowCount() == 0) {
updateFixedCosts.updateDB()
}
return Result.success()
}
}

View File

@@ -0,0 +1,142 @@
package com.financialviewer.work
import android.content.Context
import com.financialviewer.constants.COMDIRECT_LIST
import com.financialviewer.constants.FIXED_COST_TYPE
import com.financialviewer.db.BankTransaction
import com.financialviewer.network.SmbClientHelper
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.utils.SharedPreferencesHelper
import com.financialviewer.utils.convertGermanDateToStandard
import com.financialviewer.utils.convertStringToDouble
import com.financialviewer.utils.getBankTransactionRefId
import com.financialviewer.utils.getCategory
import com.financialviewer.utils.getCounterparty
import com.financialviewer.utils.windowsFileTimeToMillis
import com.hierynomus.smbj.share.File
import com.opencsv.CSVReaderBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class UpdateBankTransactions (context: Context) {
private val bankTransactionRepository = BankTransactionRepository(context)
private val appPreferences = SharedPreferencesHelper(context)
private var smbClientHelper = SmbClientHelper(context)
private val appContext = context.applicationContext
suspend fun initDB() {
if (bankTransactionRepository.getRowCount() == 0) {
for (comdirect in COMDIRECT_LIST) {
bankTransactionRepository.insertBankTransaction(comdirect)
}
}
}
suspend fun updateDB() {
var isUpdated = false
var isConnect = false
try {
isConnect = smbClientHelper.connect()
if (isConnect) {
withContext(Dispatchers.IO) {
val csvFiles = smbClientHelper.getCSVFromShare()
if (csvFiles.isNotEmpty()) {
for (fileInfo in csvFiles) {
val file = smbClientHelper.getCSVFile(fileInfo.fileName)
val fileTime = fileInfo.lastWriteTime.windowsTimeStamp
if (updateNecessary(fileTime)) {
insertBankTransactionsIntoDB(file)
isUpdated = true
}
}
}
}
}
} finally {
if (isConnect) {
smbClientHelper.disconnect()
}
}
if (isUpdated) {
appPreferences.saveCurrentUpdateTimeToSharedPreferences()
val year = bankTransactionRepository.getLatestYearOfBankTransaction()
appPreferences.saveDefaultYear(year.toInt())
updateSummary(appContext)
UpdateFixedCosts(appContext).updateDB()
}
}
private fun updateNecessary(fileTime: Long): Boolean {
var necessary = false
val dateString = appPreferences.readLastUpdateTimeToSharedPreferences()
if (dateString != "") {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val localDateTime = LocalDateTime.parse(dateString, formatter)
val instantFromSharedPref = localDateTime.atZone(ZoneId.systemDefault()).toInstant()
val fileTimeMillis = windowsFileTimeToMillis(fileTime)
val instantFromFileTime = Instant.ofEpochMilli(fileTimeMillis)
if (instantFromSharedPref.isBefore(instantFromFileTime)) {
necessary = true
}
} else {
necessary = true
}
return necessary
}
private suspend fun insertBankTransactionsIntoDB(file: File) {
file.inputStream.use { inputStream ->
val content = inputStream.readBytes()
val byteArrayInputStream = ByteArrayInputStream(content)
CSVReaderBuilder(InputStreamReader(byteArrayInputStream, Charsets.UTF_8))
.withCSVParser(com.opencsv.CSVParserBuilder().withSeparator(';').build()) // 指定分隔符
.build().use { csvReader ->
val rows = csvReader.readAll()
var account = ""
if (rows.isNotEmpty()) {
account = rows[0][1]
}
rows.forEach { row ->
val reference = row.last()
val refId = getBankTransactionRefId(reference)
if (bankTransactionRepository.getBankTransactionById(refId) == null) {
val counterparty = getCounterparty(reference)
if (counterparty != "") {
val date = row.first()
val standardDate = convertGermanDateToStandard(date)
val amount = convertStringToDouble(row[2])
val category = getCategory(reference)
bankTransactionRepository.insertBankTransaction(
BankTransaction(
refId,
account,
standardDate,
amount,
category,
counterparty,
reference,
FIXED_COST_TYPE.containsKey(category)
)
)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
package com.financialviewer.work
import android.content.Context
import com.financialviewer.constants.FIXED_COST_COUNT
import com.financialviewer.constants.FIXED_COST_TYPE
import com.financialviewer.constants.MONTHLY
import com.financialviewer.constants.QUARTERLY
import com.financialviewer.constants.YEAR
import com.financialviewer.constants.YEARLY
import com.financialviewer.db.BankTransaction
import com.financialviewer.db.FixedCost
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.repository.FixedCostRepository
import com.financialviewer.utils.SharedPreferencesHelper
import java.time.LocalDate
import java.time.format.DateTimeFormatter
class UpdateFixedCosts(context: Context) {
private val bankTransactionRepository = BankTransactionRepository(context)
private val fixedCostRepository = FixedCostRepository(context)
private val appPreferences = SharedPreferencesHelper(context)
private var year = appPreferences.readDefaultYear(YEAR)
suspend fun updateDB() {
for (item in FIXED_COST_TYPE) {
val category = item.key
when (item.value) {
MONTHLY -> {
val bankTransactionList = getBankTransactionList(category, MONTHLY)
var sum = calculateAmount(bankTransactionList, category)
if (category == "手机费") {
sum += -10.0
}
val refIds = bankTransactionList.map(BankTransaction::refId)
updateFixedCost(category, sum, MONTHLY, refIds)
}
QUARTERLY -> {
val bankTransactionList = getBankTransactionList(category, QUARTERLY)
val sum = calculateAmount(bankTransactionList, category)
val refIds = bankTransactionList.map(BankTransaction::refId)
updateFixedCost(category, sum, QUARTERLY, refIds)
}
else -> {
val bankTransactionList = getBankTransactionList(category, YEARLY)
val sum = calculateAmount(bankTransactionList, category)
val refIds = bankTransactionList.map(BankTransaction::refId)
updateFixedCost(category, sum, YEARLY, refIds)
}
}
}
appPreferences.saveCalculateFixedCostTime()
}
private suspend fun getBankTransactionList(category: String, type: String): MutableList<BankTransaction> {
var resultList: MutableList<BankTransaction>
var bankTransactionList =
bankTransactionRepository.getBankTransactionsByCategory(year.toString(), category)
if (bankTransactionList.isEmpty()) {
bankTransactionList =
bankTransactionRepository.getBankTransactionsByCategory((year - 1).toString(), category)
}
when (type) {
YEARLY -> {
resultList = bankTransactionList
}
else -> {
resultList = getBankTransactionsFromLastMonth(bankTransactionList)
while (resultList.size != FIXED_COST_COUNT[category] && bankTransactionList.size > 0) {
bankTransactionList.removeAll(resultList)
resultList = getBankTransactionsFromLastMonth(bankTransactionList)
}
}
}
return resultList
}
private fun getBankTransactionsFromLastMonth(bankTransactionList: MutableList<BankTransaction>): MutableList<BankTransaction> {
val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val latestDate = LocalDate.parse(bankTransactionList.first().date, dateFormat)
val latestYear = latestDate.year
val latestMonth = latestDate.month
return bankTransactionList.filter {
val itemDate = LocalDate.parse(it.date, dateFormat)
itemDate.year == latestYear && itemDate.month == latestMonth && it.isFixedCost
}.toMutableList()
}
private fun calculateAmount(bankTransactionList: MutableList<BankTransaction>, category: String): Double {
var sum = 0.0
if (bankTransactionList.size == FIXED_COST_COUNT[category]) {
for (bankTransaction in bankTransactionList) {
sum += bankTransaction.amount
}
}
return sum
}
private suspend fun updateFixedCost(category: String, sum: Double, type: String, refIds: List<String>) {
val fixedCost = fixedCostRepository.getFixedCostByCategory(category)
if (fixedCost == null) {
fixedCostRepository.insertFixedCost(
FixedCost(
category = category,
amount = sum,
type = type,
refIds = refIds.joinToString(",")
)
)
} else {
fixedCost.amount = sum
fixedCost.refIds = refIds.joinToString(",")
fixedCostRepository.updateFixedCost(fixedCost)
}
}
}

View File

@@ -0,0 +1,56 @@
package com.financialviewer.work
import android.content.Context
import com.financialviewer.data.YearMonthDay
import com.financialviewer.db.Loan
import com.financialviewer.repository.LoanRepository
import java.util.Calendar
import java.util.TimeZone
class UpdateLoans(context: Context) {
private val germanyTimeZone = TimeZone.getTimeZone("Europe/Berlin")
private val loanRepository = LoanRepository(context)
suspend fun initDB() {
if (loanRepository.getRowCount() == 0) {
loanRepository.initDB()
}
}
suspend fun updateDB() {
val allLoansInDB = loanRepository.getAllLoans()
val monthDiff = getMonthDiff(allLoansInDB[0])
if ( monthDiff > 0) {
for (loan in allLoansInDB) {
loanRepository.updateRemainingLoan(loan, monthDiff)
}
}
}
private fun getLastUpdateDate(loan: Loan): YearMonthDay {
val calendar = Calendar.getInstance()
calendar.timeInMillis = loan.lastUpdate
return YearMonthDay(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
)
}
private fun getMonthDiff(loan: Loan): Int {
val calendar = Calendar.getInstance(germanyTimeZone)
val currentDate = YearMonthDay(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
)
val lastUpdateDate = getLastUpdateDate(loan)
val yearDiff = currentDate.year - lastUpdateDate.year
val monthDiff = currentDate.month - lastUpdateDate.month
return yearDiff * 12 + monthDiff
}
}

View File

@@ -0,0 +1,84 @@
package com.financialviewer.work
import android.content.Context
import com.financialviewer.constants.CATEGORY_LIST
import com.financialviewer.db.MonthlySummary
import com.financialviewer.db.YearlySummary
import com.financialviewer.repository.BankTransactionRepository
import com.financialviewer.repository.MonthlySummaryRepository
import com.financialviewer.repository.YearlySummaryRepository
import com.financialviewer.utils.generateYears
import java.math.BigDecimal
suspend fun updateSummary(context: Context) {
val bankTransactionRepository = BankTransactionRepository(context)
val monthlySummaryRepository = MonthlySummaryRepository(context)
val yearlySummaryRepository = YearlySummaryRepository(context)
val latestYear = bankTransactionRepository.getLatestYearOfBankTransaction()
val yearList = generateYears(latestYear)
val categoryList = getCategoryList()
for (year in yearList) {
for (category in categoryList) {
var sum = BigDecimal("0.0")
val transactionList = bankTransactionRepository.getBankTransactionsByCategory(year, category)
for (transaction in transactionList) {
sum += BigDecimal(transaction.amount.toString())
}
val yearlySummary = yearlySummaryRepository.getYearlySummaryByCategory(year.toInt(), category)
if (yearlySummary == null) {
yearlySummaryRepository.insertYearlySummary(
YearlySummary(
year = year.toInt(),
category = category,
total = sum.toDouble()
)
)
} else {
yearlySummary.total = sum.toDouble()
yearlySummaryRepository.updateYearlySummary(yearlySummary)
}
}
for (i in 1..12) {
var incomeSum = BigDecimal("0.0")
var costSum = BigDecimal("0.0")
val transactionList = bankTransactionRepository.getBankTransactionsByMonth(year, i.toString().padStart(2, '0'))
for (transaction in transactionList) {
if (transaction.amount > 0) {
incomeSum += BigDecimal(transaction.amount.toString())
} else {
costSum += BigDecimal(transaction.amount.toString())
}
}
if (incomeSum.toDouble() > 0 || costSum.toDouble() > 0) {
val monthlySummary = monthlySummaryRepository.getMonthlySummaryByMonth(year.toInt(), i)
if (monthlySummary == null) {
monthlySummaryRepository.insertMonthlySummary(
MonthlySummary(
year = year.toInt(),
month = i,
income = incomeSum.toDouble(),
cost = costSum.toDouble()
)
)
} else {
monthlySummary.income = incomeSum.toDouble()
monthlySummary.cost = costSum.toDouble()
monthlySummaryRepository.updateMonthlySummary(monthlySummary)
}
}
}
}
}
private fun getCategoryList(): MutableList<String> {
val categoryList: MutableList<String> = mutableListOf()
for (firstLevel in CATEGORY_LIST) {
for (secondLevel in firstLevel.subList) {
categoryList.add(secondLevel)
}
}
return categoryList
}

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="m5.5,10c-3.084,0-5.5,1.318-5.5,3v8c0,1.682,2.416,3,5.5,3s5.5-1.318,5.5-3v-8c0-1.682-2.416-3-5.5-3Zm0,13c-2.652,0-4.5-1.054-4.5-2v-2.254c.987.764,2.62,1.254,4.5,1.254s3.513-.49,4.5-1.254v2.254c0,.946-1.848,2-4.5,2Zm0-4c-2.652,0-4.5-1.054-4.5-2v-2.254c.987.764,2.62,1.254,4.5,1.254s3.513-.49,4.5-1.254v2.254c0,.946-1.848,2-4.5,2Zm0-4c-2.652,0-4.5-1.054-4.5-2s1.848-2,4.5-2,4.5,1.054,4.5,2-1.848,2-4.5,2Zm14.5-11h-12v5h12v-5Zm-1,4h-10v-3h10v3Zm1,5h-1v-2h1v2Zm-3,0h-1v-2h1v2Zm-3,0h-1v-2h1v2Zm-1,2h1v2h-1v-2Zm3,0h1v2h-1v-2Zm4,2h-1v-2h1v2Zm-7,2h7v1h-7v-1ZM24,2.5v21.5h-12.387c.329-.307.611-.639.828-1h10.559V2.5c0-.827-.673-1.5-1.5-1.5H6.5c-.827,0-1.5.673-1.5,1.5v5.515c-.341.013-.676.035-1,.074V2.5c0-1.378,1.121-2.5,2.5-2.5h15c1.379,0,2.5,1.122,2.5,2.5Z"/></vector>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#E7BD95"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="m22.275,6.833l-1.275-.999v-3.334c0-.276-.224-.5-.5-.5s-.5.224-.5.5v2.551L14.775.958c-1.634-1.28-3.916-1.28-5.55,0L1.725,6.833c-1.096.859-1.725,2.15-1.725,3.542v9.124c0,2.481,2.019,4.5,4.5,4.5h15c2.481,0,4.5-2.019,4.5-4.5v-9.124c0-1.393-.629-2.684-1.725-3.542Zm.725,12.667c0,1.93-1.57,3.5-3.5,3.5H4.5c-1.93,0-3.5-1.57-3.5-3.5v-9.124c0-1.083.489-2.087,1.342-2.755L9.841,1.745c.636-.498,1.397-.747,2.159-.747s1.523.249,2.158.747l7.5,5.876c.853.668,1.342,1.672,1.342,2.755v9.124Zm-8.063-9.757l-5,9c-.091.165-.262.257-.438.257-.082,0-.166-.02-.242-.063-.242-.134-.329-.438-.194-.68l5-9c.134-.241.439-.33.68-.194.242.134.329.438.194.68Zm.063,5.257c-1.103,0-2,.897-2,2s.897,2,2,2,2-.897,2-2-.897-2-2-2Zm0,3c-.551,0-1-.449-1-1s.449-1,1-1,1,.449,1,1-.449,1-1,1Zm-4-7c0-1.103-.897-2-2-2s-2,.897-2,2,.897,2,2,2,2-.897,2-2Zm-2,1c-.551,0-1-.449-1-1s.449-1,1-1,1,.449,1,1-.449,1-1,1Z"/></vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="M23.961,10.306h0v-.002l-3.425-8.134c-.184-.436-.501-.779-.883-1.013-.046-.049-.102-.091-.169-.119-.026-.011-.054-.01-.082-.016-.448-.201-.96-.259-1.461-.125l-5.44,1.449V.5c0-.276-.224-.5-.5-.5s-.5,.224-.5,.5V2.613l-5.723,1.524c-1.042,.277-1.912,1.036-2.328,2.029L.039,14.307c-.026,.062-.039,.986-.039,.986,0,2.43,1.8,4.49,4.099,4.689,.137,.012,.272,.018,.407,.018,1.129,0,2.194-.412,3.034-1.183,.928-.851,1.46-2.06,1.46-3.317,0,0-.015-1.139-.043-1.203L5.088,5.592c.275-.227,.597-.396,.947-.489l5.465-1.456V23H4.5c-.276,0-.5,.224-.5,.5s.224,.5,.5,.5h15c.276,0,.5-.224,.5-.5s-.224-.5-.5-.5h-7V3.382l5.698-1.517c.137-.037,.275-.044,.409-.032l-3.568,8.474c-.026,.061-.039,.986-.039,.986,0,2.43,1.8,4.49,4.099,4.689,.137,.012,.272,.018,.407,.018,1.129,0,2.194-.412,3.034-1.183,.928-.851,1.46-2.06,1.46-3.317,0,0-.012-1.13-.039-1.194Zm-15.961,5.194c0,.979-.414,1.919-1.136,2.581-.731,.671-1.678,.992-2.679,.906-1.786-.155-3.186-1.778-3.186-3.694v-.292h7v.5Zm-.269-1.5H1.252l3.12-7.447,.024-.056,3.335,7.503ZM19.475,2.347c.048,.069,.105,.131,.138,.211l3.133,7.441h-6.494l3.222-7.653Zm3.525,9.153c0,.979-.414,1.919-1.136,2.581-.731,.67-1.679,.995-2.679,.906-1.786-.155-3.186-1.778-3.186-3.694v-.292h7v.5Z"/></vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="M9.8,9.802c-.755,.753-1.122,1.476-1.122,2.208,0,.957,.64,1.706,1.122,2.187,.74,.739,1.47,1.108,2.2,1.108s1.46-.37,2.2-1.108c.755-.753,1.122-1.476,1.122-2.208,0-.957-.64-1.706-1.122-2.187-1.48-1.478-2.92-1.478-4.4,0Zm3.694,3.688c-1.251,1.248-2.042,.944-2.988,0-.565-.564-.828-1.034-.828-1.479,0-.454,.271-.944,.828-1.5,.583-.582,1.065-.826,1.511-.826,.511,0,.972,.322,1.478,.826,.565,.564,.828,1.034,.828,1.479,0,.454-.271,.944-.828,1.5Z"/><path android:fillColor="@android:color/white" android:pathData="M9.8,9.802c-.755,.753-1.122,1.476-1.122,2.208,0,.957,.64,1.706,1.122,2.187,.74,.739,1.47,1.108,2.2,1.108s1.46-.37,2.2-1.108c.755-.753,1.122-1.476,1.122-2.208,0-.957-.64-1.706-1.122-2.187-1.48-1.478-2.92-1.478-4.4,0Zm3.694,3.688c-1.251,1.248-2.042,.944-2.988,0-.565-.564-.828-1.034-.828-1.479,0-.454,.271-.944,.828-1.5,.583-.582,1.065-.826,1.511-.826,.511,0,.972,.322,1.478,.826,.565,.564,.828,1.034,.828,1.479,0,.454-.271,.944-.828,1.5Z"/><path android:fillColor="@android:color/white" android:pathData="M23.019,11.948c.008-1.595-.817-2.481-2.521-2.7-.188-.394-.417-.787-.694-1.188,1.205-1.456,1.21-2.694,.026-3.876-1.183-1.181-2.425-1.175-3.884,.029-.401-.274-.794-.501-1.188-.688-.217-1.706-1.134-2.524-2.705-2.524-1.602,.008-2.495,.849-2.725,2.569-.392,.189-.791,.421-1.19,.691-1.451-1.197-2.694-1.194-3.89,0-1.178,1.175-1.18,2.448-.001,3.882-.273,.4-.506,.798-.696,1.189-1.719,.229-2.56,1.122-2.568,2.721-.008,1.595,.817,2.481,2.521,2.7,.188,.394,.417,.787,.694,1.188-1.205,1.456-1.21,2.694-.026,3.876,1.184,1.182,2.425,1.176,3.884-.029,.401,.274,.794,.501,1.188,.688,.216,1.697,1.113,2.524,2.682,2.524,1.605,0,2.519-.849,2.749-2.569,.392-.189,.791-.421,1.19-.691,1.451,1.197,2.694,1.193,3.89,0,1.178-1.175,1.18-2.448,.001-3.882,.273-.4,.506-.798,.696-1.189,1.719-.229,2.56-1.122,2.568-2.721Zm-3.36,2.053c-.227,.515-.544,1.051-.942,1.594-.142,.193-.127,.459,.036,.635,1.373,1.487,.898,2.2,.295,2.802-.63,.628-1.321,1.078-2.812-.295-.175-.162-.44-.176-.634-.036-.541,.395-1.077,.71-1.595,.937-.167,.073-.282,.232-.297,.415-.139,1.615-.779,1.942-1.766,1.947-1.008-.027-1.609-.304-1.734-1.9-.015-.186-.131-.348-.302-.421-.521-.222-1.041-.526-1.588-.929-.089-.065-.193-.097-.296-.097-.122,0-.243,.044-.338,.132-1.503,1.383-2.211,.919-2.807,.324-.595-.594-1.06-1.299,.323-2.797,.163-.177,.178-.444,.035-.637-.4-.541-.716-1.075-.937-1.588-.073-.17-.234-.286-.419-.301-1.587-.126-1.903-.755-1.898-1.729,.005-.984,.333-1.622,1.946-1.761,.182-.016,.341-.129,.415-.296,.227-.515,.544-1.051,.942-1.594,.142-.193,.127-.459-.036-.635-1.373-1.487-.898-2.2-.295-2.802,.606-.604,1.321-1.08,2.812,.295,.176,.163,.441,.178,.634,.036,.541-.395,1.077-.71,1.595-.937,.167-.073,.282-.232,.297-.415,.139-1.615,.779-1.942,1.766-1.947,.99-.027,1.608,.304,1.734,1.9,.015,.186,.131,.348,.302,.421,.521,.222,1.041,.526,1.588,.929,.193,.142,.458,.127,.635-.035,1.504-1.382,2.212-.918,2.807-.324,.595,.594,1.06,1.299-.323,2.797-.163,.177-.178,.444-.035,.637,.4,.541,.716,1.075,.937,1.588,.073,.17,.234,.286,.419,.301,1.587,.126,1.903,.755,1.898,1.729-.005,.984-.333,1.622-1.946,1.761-.182,.016-.341,.129-.415,.296Z"/></vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="M24.026,3.5c0,.357-.139,.692-.392,.945l-2.409,2.409c-.195,.195-.512,.195-.707,0s-.195-.512,0-.707l2.146-2.146H12c-4.411,0-8,3.589-8,8,0,1.896,.675,3.734,1.9,5.176,.179,.211,.153,.526-.057,.705-.209,.178-.526,.154-.705-.057-1.379-1.623-2.139-3.691-2.139-5.824C3,7.038,7.038,3,12,3h10.666l-2.146-2.146c-.195-.195-.195-.512,0-.707s.512-.195,.707,0l2.409,2.409c.252,.252,.392,.588,.392,.945Zm-5.848,2.641c-.211,.178-.238,.494-.06,.705,1.213,1.439,1.882,3.27,1.882,5.155,0,4.411-3.589,8-8,8H1.334l2.147-2.146c.195-.195,.195-.512,0-.707s-.512-.195-.707,0L.366,19.554c-.522,.521-.522,1.371,0,1.893l2.408,2.407c.195,.195,.512,.195,.707,0s.195-.512,0-.707l-2.147-2.146H12c4.962,0,9-4.037,9-9,0-2.121-.752-4.18-2.117-5.799-.179-.211-.493-.239-.705-.06Zm-10.178,6.359c-.276,0-.5,.224-.5,.5s.224,.5,.5,.5h1c0,1.93,1.57,3.5,3.5,3.5,1.481,0,2.808-.938,3.301-2.333,.092-.261-.045-.547-.305-.639-.261-.092-.546,.045-.638,.305-.352,.997-1.299,1.667-2.357,1.667-1.378,0-2.5-1.121-2.5-2.5h2c.276,0,.5-.224,.5-.5s-.224-.5-.5-.5h-2v-1h2c.276,0,.5-.224,.5-.5s-.224-.5-.5-.5h-2c0-1.378,1.122-2.5,2.5-2.5,1.058,0,2.005,.67,2.357,1.667,.093,.261,.378,.396,.638,.305,.26-.092,.397-.378,.305-.638-.493-1.396-1.82-2.333-3.301-2.333-1.93,0-3.5,1.57-3.5,3.5h-1c-.276,0-.5,.224-.5,.5s.224,.5,.5,.5h1v1h-1Z"/></vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="m24,.5v2.5c0,.551-.448,1-1,1h-2.5c-.276,0-.5-.224-.5-.5s.224-.5.5-.5h1.492c-.935-1.247-2.396-2-3.992-2-2.605,0-4.746,1.954-4.979,4.545-.023.26-.242.455-.498.455-.015,0-.03,0-.045-.002-.275-.025-.478-.268-.453-.543.28-3.11,2.849-5.455,5.976-5.455,2.04,0,3.895,1.022,5,2.698V.5c0-.276.224-.5.5-.5s.5.224.5.5Zm-7,14v5.571c0,2.203-3.733,3.929-8.5,3.929S0,22.274,0,20.071V4.5C0,1.977,3.733,0,8.5,0c1.072,0,2.114.101,3.099.3.271.055.446.319.391.589-.048.237-.257.401-.49.401-.033,0-.066-.003-.1-.01-.92-.187-1.896-.281-2.901-.281C4.435,1,1,2.603,1,4.5s3.435,3.5,7.5,3.5c.329,0,.652-.01.97-.03.279-.012.513.193.529.469.017.275-.193.513-.469.529-.337.021-.681.032-1.03.032-3.287,0-6.081-.94-7.5-2.351v3.117c0,1.562,3.014,3.233,7.5,3.233.655,0,1.308-.038,1.941-.114.273-.036.523.164.556.437.033.274-.163.523-.438.556-.672.08-1.365.121-2.059.121-3.287,0-6.081-.884-7.5-2.211v2.978c0,1.562,3.014,3.233,7.5,3.233s7.5-1.672,7.5-3.233v-.267c0-.276.224-.5.5-.5s.5.224.5.5Zm-1,2.289c-1.419,1.327-4.213,2.211-7.5,2.211s-6.081-.884-7.5-2.211v3.283c0,1.385,3.08,2.929,7.5,2.929s7.5-1.544,7.5-2.929v-3.283Zm7.522-10.787c-.277-.03-.519.177-.543.453-.233,2.591-2.374,4.545-4.979,4.545-1.596,0-3.057-.753-3.992-2h1.492c.276,0,.5-.224.5-.5s-.224-.5-.5-.5h-2.5c-.551,0-1,.449-1,1v2.5c0,.276.224.5.5.5s.5-.224.5-.5v-2.198c1.105,1.675,2.96,2.698,5,2.698,3.126,0,5.695-2.345,5.976-5.455.024-.275-.178-.518-.453-.543Z"/></vector>

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="m24,23.524c0,.276-.225.5-.5.5h0l-20-.024c-1.929,0-3.5-1.57-3.5-3.5V.5C0,.224.224,0,.5,0s.5.224.5.5v20c0,1.378,1.122,2.5,2.5,2.5l20.001.024c.275,0,.499.225.499.5ZM5,12.524v6.976c0,.276.224.5.5.5s.5-.224.5-.5v-6.976c0-.276-.224-.5-.5-.5s-.5.224-.5.5Zm5-2v8.976c0,.276.224.5.5.5s.5-.224.5-.5v-8.976c0-.276-.224-.5-.5-.5s-.5.224-.5.5Zm5,3v5.976c0,.276.224.5.5.5s.5-.224.5-.5v-5.976c0-.276-.224-.5-.5-.5s-.5.224-.5.5Zm5-4.024v10c0,.276.224.5.5.5s.5-.224.5-.5v-10c0-.276-.224-.5-.5-.5s-.5.224-.5.5Zm-15.5-.5c.128,0,.256-.049.354-.146l4.474-4.474c.371-.371.974-.371,1.345,0l2.948,2.949c.762.759,1.998.759,2.76,0L22.854.854c.195-.195.195-.512,0-.707s-.512-.195-.707,0l-6.474,6.474c-.371.371-.975.371-1.346,0l-2.948-2.948c-.761-.761-1.998-.761-2.759,0l-4.474,4.474c-.195.195-.195.512,0,.707.098.098.226.146.354.146Z"/></vector>

View File

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="24dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/account" />
<TextView
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.4"
android:textSize="16sp"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/date" />
<TextView
android:id="@+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.4"
android:textSize="16sp"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/amount" />
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.4"
android:textSize="16sp"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/counterparty" />
<TextView
android:id="@+id/third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.4"
android:textSize="16sp"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/reference" />
<TextView
android:id="@+id/reference"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.4"
android:textSize="16sp"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/isFixedCostTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/fixed_costs"
android:visibility="gone"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/isFixedCostSwitch"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:textOn="@string/yes"
android:textOff="@string/no"
android:checked="false"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/yearLabel"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/year"
android:textSize="16sp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"/>
<Spinner
android:id="@+id/yearSpinner"
android:layout_width="200dp"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/yearLabel" />
<TextView
android:id="@+id/monthLabel"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/month"
android:textSize="16sp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/yearLabel"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"/>
<Spinner
android:id="@+id/monthSpinner"
android:layout_width="200dp"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@id/yearLabel"
app:layout_constraintStart_toEndOf="@+id/monthLabel" />
<TextView
android:id="@+id/categoryLabel"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/category"
android:textSize="16sp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/monthLabel"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"/>
<Spinner
android:id="@+id/categorySpinner"
android:layout_width="200dp"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@id/monthLabel"
app:layout_constraintStart_toEndOf="@+id/categoryLabel" />
<LinearLayout
android:id="@+id/header"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray"
app:layout_constraintTop_toBottomOf="@id/categoryLabel">
<TextView
android:id="@+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/date" />
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/category"
android:gravity="center"/>
<TextView
android:id="@+id/third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/amount"
android:gravity="end"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/footer"/>
<LinearLayout
android:id="@+id/footer"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/perl_gray"
app:layout_constraintTop_toBottomOf="@id/recyclerView"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/sumAmountLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_amount"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp" />
<TextView
android:id="@+id/sumAmountValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:paddingVertical="16dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fixed_cost_source"
android:textSize="20sp"
android:paddingHorizontal="24dp" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/date" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/category"
android:gravity="center"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/amount"
android:gravity="end"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sourceRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp" />
</LinearLayout>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="20dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp">
<TextView
android:id="@+id/lastUpdateLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/last_update"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/lastUpdateValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/last_update"
android:paddingTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lastUpdateLabel"/>
<Button
android:id="@+id/recalculateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recalculate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/monthly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/monthly"
android:background="@color/perl_gray"
android:textSize="18sp"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/monthlyRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"
android:nestedScrollingEnabled="false"/>
<TextView
android:id="@+id/quarterly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/quarterly"
android:background="@color/perl_gray"
android:textSize="18sp"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/quarterlyRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"
android:nestedScrollingEnabled="false"/>
<TextView
android:id="@+id/yearly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/yearly"
android:background="@color/perl_gray"
android:textSize="18sp"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/yearlyRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="24dp"
android:nestedScrollingEnabled="false"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/footer"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray">
<TextView
android:id="@+id/monthlyFixedCostLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monthly_fixed_costs"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/monthlyFixedCostValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/red"
android:paddingVertical="16dp"
app:layout_constraintStart_toEndOf="@id/monthlyFixedCostLabel"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/yearlyFixedCostLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/yearly_fixed_costs"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/monthlyFixedCostLabel"/>
<TextView
android:id="@+id/yearlyFixedCostValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/red"
android:paddingVertical="16dp"
app:layout_constraintStart_toEndOf="@id/yearlyFixedCostLabel"
app:layout_constraintTop_toBottomOf="@id/monthlyFixedCostValue"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,105 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<TextView
android:id="@+id/loanOverview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loan_overview"
android:textSize="20sp"
android:layout_marginTop="30dp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/loan_overview_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/fixedCosts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fixed_costs"
android:textSize="20sp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/fixed_costs_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loanOverview"/>
<TextView
android:id="@+id/bankTransaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bank_Transaction"
android:textSize="20sp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/transaction_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fixedCosts"/>
<TextView
android:id="@+id/monthlySummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monthly_summary"
android:textSize="20sp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/monthly_summary_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bankTransaction"/>
<TextView
android:id="@+id/yearlySummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/yearly_summary"
android:textSize="20sp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/yearly_summary_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/monthlySummary"/>
<TextView
android:id="@+id/updateTransaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update_transaction"
android:textSize="20sp"
android:layout_marginBottom="30dp"
android:paddingHorizontal="50dp"
android:paddingVertical="32dp"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/update_transaction_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<LinearLayout
android:id="@+id/progressBarContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:gravity="center"
android:visibility="gone">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/loanTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title"
android:textSize="18sp"
android:paddingTop="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/remaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/remaining_loan"
android:paddingTop="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/loanTitle"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/payment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monthly_payment"
android:paddingTop="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/remaining"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fixed_rate"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/payment"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/sumInterest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_interest"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/rate"
app:layout_constraintStart_toStartOf="parent"/>
<Button
android:id="@+id/specialRepayment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/special_repayment"
android:layout_marginTop="50dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/refresh_list"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/specialRepayment"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sumInterest"
android:paddingStart="32dp"
android:paddingEnd="24dp"
android:paddingTop="32dp">
<TextView
android:id="@+id/monthText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="月份"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/interestText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="利息"
app:layout_constraintStart_toEndOf="@id/monthText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/principalText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="本金"
app:layout_constraintStart_toEndOf="@id/interestText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/remainingDebtText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="剩余贷款"
app:layout_constraintStart_toEndOf="@id/principalText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.25" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:orientation="vertical"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/sumLoanTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_loan"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" >
</TextView>
<!-- Placeholder for the dynamically added TableLayouts -->
<TextView
android:id="@+id/sumLoanValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_loan"
android:textSize="16sp"
android:paddingVertical="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/sumLoanTitle" >
</TextView>
<TextView
android:id="@+id/sumPaymentTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_payment"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp"
app:layout_constraintTop_toBottomOf="@id/sumLoanTitle"
app:layout_constraintStart_toStartOf="parent" >
</TextView>
<!-- Placeholder for the dynamically added TableLayouts -->
<TextView
android:id="@+id/sumPaymentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_payment"
android:textSize="16sp"
android:paddingVertical="8dp"
app:layout_constraintTop_toBottomOf="@id/sumLoanValue"
app:layout_constraintStart_toEndOf="@id/sumPaymentTitle" >
</TextView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/sumPaymentTitle"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<EditText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:minHeight="48dp"
android:inputType="textPassword"
android:hint="@string/password"
android:autofillHints="password"
android:gravity="center"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter"
android:layout_marginTop="16dp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/yearLabel"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/year"
android:textSize="16sp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"/>
<Spinner
android:id="@+id/yearSpinner"
android:layout_width="100dp"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/yearLabel" />
<LinearLayout
android:id="@+id/header"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray"
app:layout_constraintTop_toBottomOf="@id/yearLabel">
<TextView
android:id="@+id/month"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/month" />
<TextView
android:id="@+id/income"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/income"
android:gravity="center"/>
<TextView
android:id="@+id/cost"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/cost"
android:gravity="end"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/header" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/footer"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/sumIncomeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_income"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/sumIncomeValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/green"
android:paddingVertical="16dp"
app:layout_constraintStart_toEndOf="@id/sumIncomeLabel"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/sumCostLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_cost"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sumIncomeLabel"/>
<TextView
android:id="@+id/sumCostValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/red"
android:paddingVertical="16dp"
app:layout_constraintStart_toEndOf="@id/sumCostLabel"
app:layout_constraintTop_toBottomOf="@id/sumIncomeValue"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/yearLabel"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/year"
android:textSize="16sp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"/>
<Spinner
android:id="@+id/yearSpinner"
android:layout_width="100dp"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/yearLabel" />
<LinearLayout
android:id="@+id/header"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/perl_gray"
app:layout_constraintTop_toBottomOf="@id/yearLabel">
<TextView
android:id="@+id/third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/category" />
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
android:text="@string/amount"
android:gravity="end"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/footer"/>
<LinearLayout
android:id="@+id/footer"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/perl_gray"
app:layout_constraintTop_toBottomOf="@id/recyclerView"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/sumAmountLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sum_amount"
android:textSize="16sp"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp" />
<TextView
android:id="@+id/sumAmountValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:paddingVertical="16dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/first"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/second"
app:layout_constraintStart_toEndOf="@id/first"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/third"
app:layout_constraintStart_toEndOf="@id/second"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/fourth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/fourth"
app:layout_constraintStart_toEndOf="@id/third"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.25" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<!-- Title with large bold text -->
<TextView
android:id="@+id/loanTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<!-- TableLayout for two columns of fields -->
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/loan_amount" />
<TextView
android:id="@+id/loanAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:textSize="16sp"
android:padding="8dp" />
</TableRow>
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/remaining_loan" />
<TextView
android:id="@+id/loanRemainingLoan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:textSize="16sp"
android:padding="8dp"
android:clickable="true"
android:focusable="true" />
</TableRow>
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/fixed_rate" />
<TextView
android:id="@+id/loanRate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:textSize="16sp"
android:padding="8dp"
android:clickable="true"
android:focusable="true" />
</TableRow>
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/monthly_payment" />
<TextView
android:id="@+id/loanPayment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:textSize="16sp"
android:padding="8dp"
android:clickable="true"
android:focusable="true" />
</TableRow>
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:textSize="16sp"
android:padding="8dp"
android:text="@string/fixed_rate_maturity" />
<TextView
android:id="@+id/loanMaturity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:textSize="16sp"
android:padding="8dp"
android:clickable="true"
android:focusable="true" />
</TableRow>
</TableLayout>
<!-- Add some space between loan items -->
<View
android:layout_width="match_parent"
android:layout_height="16dp" />
</LinearLayout>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingVertical="8dp"
android:text="@string/first"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingVertical="8dp"
android:text="@string/second"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
android:gravity="center"/>
<TextView
android:id="@+id/third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingVertical="8dp"
android:text="@string/third"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
android:gravity="end"/>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingVertical="8dp"
android:text="@string/first" />
<TextView
android:id="@+id/second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:paddingVertical="8dp"
android:text="@string/second"
android:gravity="end"/>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Some files were not shown because too many files have changed in this diff Show More