Compare commits
119 Commits
4620e62e69
...
0.1.1
Author | SHA1 | Date | |
---|---|---|---|
4d0dc03c69 | |||
bdb3fc5c7a | |||
da2ae6206a | |||
f5b22c6c1c | |||
9f7773f724 | |||
dec343af09 | |||
0890e15bfb | |||
55fd4092ac | |||
247b88daa2 | |||
d604a59ad1 | |||
5bf6a0889c | |||
f3d6b8abbe | |||
8ea85586d5 | |||
d1633fd155 | |||
26718a41e0 | |||
2c241d9113 | |||
adea1eeb02 | |||
f441caebf7 | |||
48615b3438 | |||
bc3b3a4109 | |||
5ec246228e | |||
579a611bc2 | |||
62379f54b0 | |||
e7dc369c4f | |||
15b713215f | |||
e57432767d | |||
8b9e0bfe54 | |||
a92cc00291 | |||
b4aa72440d | |||
b8096833b3 | |||
777d263efe | |||
84f1ec2f70 | |||
8d7591b766 | |||
88d57e217f | |||
7d08f6c5cf | |||
e5e85b68ff | |||
f6f167dd83 | |||
8ef8e0dad9 | |||
0b937186aa | |||
a5e10e26cc | |||
029ee8e9a8 | |||
7947d64b3b | |||
778b4e2f39 | |||
02f08093c9 | |||
c2939f81c0 | |||
8ae728f6bf | |||
f6179d1fae | |||
52fdd5c471 | |||
42548d437c | |||
be66f52cbf | |||
992195b9a0 | |||
991eea4d54 | |||
064884dc07 | |||
98e511056c | |||
58102bab02 | |||
0b4937b141 | |||
d35c250913 | |||
21f67b8a36 | |||
9924860181 | |||
60363c29f2 | |||
83dece8749 | |||
737264fa2f | |||
bea9a4bc36 | |||
8e6b574023 | |||
fcf4fca33b | |||
0aa8fdf271 | |||
3bf3dd3190 | |||
ab9b3e0bf9 | |||
7a3eaf70b5 | |||
f5f153f692 | |||
362f1214e8 | |||
d77e732551 | |||
cc33458457 | |||
f5635d6120 | |||
ef58a06dfa | |||
5425b22ba2 | |||
305012ffd4 | |||
8c31d868b9 | |||
9d478b9cbf | |||
452eb73773 | |||
631555af59 | |||
0f65166123 | |||
595aaefedc | |||
9d8a5e6685 | |||
2acabf4d3b | |||
15fa4aadbd | |||
78a407d0ec | |||
54cd86c34b | |||
66fd966de8 | |||
5561f50736 | |||
6b25e6e552 | |||
360a36f024 | |||
ecbac615e9 | |||
87392cc73c | |||
1b95feb5d4 | |||
99b1ec82e6 | |||
7ead0e5ebb | |||
bcae40e0e2 | |||
aa3c2f9304 | |||
96811f5bfd | |||
c44d4553c3 | |||
d722172cb3 | |||
43dd151cb4 | |||
c2cc71eae0 | |||
0c9b365f7f | |||
f820265dba | |||
85b7a0a3e6 | |||
861e8bf904 | |||
aaa95bd3a6 | |||
45b5e33491 | |||
8a2dfc2131 | |||
3b7313ca10 | |||
b07a8f9c0a | |||
19e10e6792 | |||
811313dc2c | |||
26f688cfe9 | |||
a4ad5823aa | |||
b227ddc849 | |||
92ebe3c4e0 |
6
.gitignore
vendored
@ -32,7 +32,6 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@ -43,3 +42,8 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/android/app/.*
|
||||
|
||||
# Ignore the releases folder.
|
||||
releases/*
|
||||
|
30
.metadata
@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3"
|
||||
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@ -13,26 +13,26 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: android
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: ios
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: linux
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: macos
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: web
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: windows
|
||||
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
|
||||
# User provided section
|
||||
|
||||
|
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Hyperling
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
10
README.md
@ -1,8 +1,6 @@
|
||||
# flutter_expense_tracker
|
||||
# Recurring Expense Tracker
|
||||
|
||||
Recurring expense tracker app for Android.
|
||||
Recurring expense tracker app for Linux and Android.
|
||||
|
||||
Add a debit as daily, weekly, monthly, etc, then see how it affects your liquid
|
||||
assets based on your reported income over different time projections.
|
||||
|
||||
Useful for many applications!
|
||||
Add an expense as daily, weekly, monthly, etc, then see how it affects your
|
||||
liquid assets based on your reported income over different time projections.
|
||||
|
@ -1,28 +1 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.flutter_expense_tracker"
|
||||
namespace = "com.hyperling.expense_tracker"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.flutter_expense_tracker"
|
||||
applicationId = "com.hyperling.expense_tracker"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
44
android/app/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.hyperling.expense_tracker"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.hyperling.expense_tracker"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="flutter_expense_tracker"
|
||||
android:label="Expenses"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/icon_v003"
|
||||
android:roundIcon="@mipmap/icon_v003_round">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -41,5 +42,10 @@
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
<!-- Allow opening of web URLs -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
@ -1,5 +0,0 @@
|
||||
package com.example.flutter_expense_tracker
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
@ -0,0 +1,5 @@
|
||||
package com.hyperling.expense_tracker
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
BIN
android/app/src/main/res/mipmap-hdpi/icon_v003.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/icon_v003_round.png
Normal file
After Width: | Height: | Size: 35 KiB |
21
android/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
25
android/settings.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
46
create_release_files.sh
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DIR="$(dirname -- "${BASH_SOURCE[0]}")"
|
||||
RELEASE_DIR=$DIR/releases
|
||||
|
||||
|
||||
echo "*** Prepare for Builds ***"
|
||||
cd $DIR
|
||||
pwd
|
||||
mkdir -pv $RELEASE_DIR
|
||||
|
||||
echo "*** Get Version ***"
|
||||
grep 'version:' pubspec.yaml | while read v_text v_number; do
|
||||
# Skip any newlines found while grepping.
|
||||
if [[ -z $v_number ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Creating assets for version '$v_number'."
|
||||
|
||||
# Set Up Variables
|
||||
ANDROID_APK="build/app/outputs/flutter-apk/app-release.apk"
|
||||
APK_RENAME="$RELEASE_DIR/ExpenseTracker_$v_number.apk"
|
||||
LINUX_BUNDLE="build/linux/x64/release/bundle"
|
||||
BUNDLE_RENAME="LinuxBundle_$v_number"
|
||||
|
||||
# Build Android App
|
||||
echo -e "\n*** Android APK ***"
|
||||
rm -v "$APK_RENAME"
|
||||
flutter build apk
|
||||
mv -v $ANDROID_APK "$APK_RENAME"
|
||||
ls -sh "$APK_RENAME"
|
||||
|
||||
# Build Linux Project
|
||||
echo -e "\n*** Linux Bundle ***"
|
||||
rm -rv "$RELEASE_DIR/$BUNDLE_RENAME"*
|
||||
flutter build linux
|
||||
mv -v $LINUX_BUNDLE "$RELEASE_DIR/$BUNDLE_RENAME"
|
||||
cd $RELEASE_DIR
|
||||
zip -r $BUNDLE_RENAME.zip $BUNDLE_RENAME
|
||||
rm -rv $BUNDLE_RENAME
|
||||
ls -sh $BUNDLE_RENAME.zip
|
||||
|
||||
# Only one version should be found, but just in case, only use the top one!
|
||||
break
|
||||
done
|
@ -368,7 +368,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -384,7 +384,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -401,7 +401,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@ -416,7 +416,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@ -547,7 +547,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -569,7 +569,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Flutter Expense Tracker</string>
|
||||
<string>Flutter Empty</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>flutter_expense_tracker</string>
|
||||
<string>flutter_empty</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
257
lib/db.dart
Normal file
@ -0,0 +1,257 @@
|
||||
// https://docs.flutter.dev/cookbook/persistence/sqlite
|
||||
|
||||
// SQLite
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
// Local
|
||||
import "/globals.dart";
|
||||
import '/models/expense.dart';
|
||||
import '/models/income.dart';
|
||||
import '/models/asset.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
// Leaned on this example:
|
||||
// https://learnflutterwithme.com/sqlite
|
||||
class DatabaseHelper {
|
||||
DatabaseHelper._privateConstructor();
|
||||
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
|
||||
|
||||
static Database? _db;
|
||||
Future<Database> get db async => _db ??= await _initDatabase();
|
||||
|
||||
Future<Database> _initDatabase() async {
|
||||
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
String path = join(documentsDirectory.path, "ExpenseTracker.sqlite.db");
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 2,
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
debugPrint(
|
||||
"_onCreate(): version=$version",
|
||||
);
|
||||
_createExpense(db);
|
||||
_createIncome(db);
|
||||
_createAsset(db);
|
||||
}
|
||||
|
||||
Future _onUpgrade(Database db, int previousVersion, int newVersion) async {
|
||||
debugPrint(
|
||||
"_onUpgrade(): previousVersion=$previousVersion, newVersion=$newVersion",
|
||||
);
|
||||
// Added in DB version 2.
|
||||
if (previousVersion < 2 && newVersion >= 2) {
|
||||
_createIncome(db);
|
||||
_createAsset(db);
|
||||
}
|
||||
}
|
||||
|
||||
void _createExpense(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE expense
|
||||
( id INTEGER PRIMARY KEY
|
||||
, name TEXT NOT NULL UNIQUE
|
||||
, cost DOUBLE NOT NULL
|
||||
, frequency TEXT NOT NULL
|
||||
, description TEXT
|
||||
)""");
|
||||
}
|
||||
|
||||
void _createIncome(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE income
|
||||
( id INTEGER PRIMARY KEY
|
||||
, name TEXT NOT NULL UNIQUE
|
||||
, revenue DOUBLE NOT NULL
|
||||
, frequency TEXT NOT NULL
|
||||
, description TEXT
|
||||
)""");
|
||||
}
|
||||
|
||||
void _createAsset(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE asset
|
||||
( id INTEGER PRIMARY KEY
|
||||
, name TEXT NOT NULL UNIQUE
|
||||
, amount DOUBLE NOT NULL
|
||||
, description TEXT
|
||||
)""");
|
||||
}
|
||||
|
||||
/// Expense Section
|
||||
///
|
||||
Future<List<Expense>> getExpenses() async {
|
||||
if (testing) debugPrint("getExpenses(): Accessing db.");
|
||||
Database db = await instance.db;
|
||||
|
||||
if (testing) debugPrint("getExpenses(): Querying table.");
|
||||
var expenses = await db.query("expense", orderBy: "name");
|
||||
|
||||
if (testing) debugPrint("getExpenses(): Putting into object list.");
|
||||
List<Expense> expenseList = expenses.isNotEmpty
|
||||
? expenses.map((c) => Expense.fromMap(c)).toList()
|
||||
: [];
|
||||
|
||||
if (testing) debugPrint("getExpenses(): Returning!");
|
||||
return expenseList;
|
||||
}
|
||||
|
||||
Future<int> addExpense(TrackedItem expense) async {
|
||||
Database db = await instance.db;
|
||||
return await db.insert(
|
||||
"expense",
|
||||
expense.toMap(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeExpense(int id) async {
|
||||
Database db = await instance.db;
|
||||
return await db.delete(
|
||||
"expense",
|
||||
where: "id = ?",
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateExpense(TrackedItem expense) async {
|
||||
Database db = await instance.db;
|
||||
return await db.update(
|
||||
"expense",
|
||||
expense.toMap(),
|
||||
where: "id = ?",
|
||||
whereArgs: [expense.id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> checkExpenseNameExists(String name) async {
|
||||
Database db = await instance.db;
|
||||
var expenses = await db.query(
|
||||
"expense",
|
||||
where: "name = ?",
|
||||
whereArgs: [name],
|
||||
);
|
||||
return expenses.isNotEmpty;
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
/// Income Section
|
||||
///
|
||||
Future<List<Income>> getIncomes() async {
|
||||
if (testing) debugPrint("getIncomes(): Accessing db.");
|
||||
Database db = await instance.db;
|
||||
|
||||
if (testing) debugPrint("getIncomes(): Querying table.");
|
||||
var incomes = await db.query("income", orderBy: "name");
|
||||
|
||||
if (testing) debugPrint("getIncomes(): Putting into object list.");
|
||||
List<Income> incomeList = incomes.isNotEmpty
|
||||
? incomes.map((c) => Income.fromMap(c)).toList()
|
||||
: [];
|
||||
|
||||
if (testing) debugPrint("getIncomes(): Returning!");
|
||||
return incomeList;
|
||||
}
|
||||
|
||||
Future<int> addIncome(TrackedItem income) async {
|
||||
Database db = await instance.db;
|
||||
return await db.insert(
|
||||
"income",
|
||||
income.toMap(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeIncome(int id) async {
|
||||
Database db = await instance.db;
|
||||
return await db.delete(
|
||||
"income",
|
||||
where: "id = ?",
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateIncome(TrackedItem income) async {
|
||||
Database db = await instance.db;
|
||||
return await db.update(
|
||||
"income",
|
||||
income.toMap(),
|
||||
where: "id = ?",
|
||||
whereArgs: [income.id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> checkIncomeNameExists(String name) async {
|
||||
Database db = await instance.db;
|
||||
var incomes = await db.query(
|
||||
"income",
|
||||
where: "name = ?",
|
||||
whereArgs: [name],
|
||||
);
|
||||
return incomes.isNotEmpty;
|
||||
}
|
||||
|
||||
///
|
||||
/// Liquid Asset Section
|
||||
Future<List<Asset>> getAssets() async {
|
||||
if (testing) debugPrint("getAssets(): Accessing db.");
|
||||
Database db = await instance.db;
|
||||
|
||||
if (testing) debugPrint("getAssets(): Querying table.");
|
||||
var assets = await db.query("asset", orderBy: "name");
|
||||
|
||||
if (testing) debugPrint("getAssets(): Putting into object list.");
|
||||
List<Asset> assetList =
|
||||
assets.isNotEmpty ? assets.map((c) => Asset.fromMap(c)).toList() : [];
|
||||
|
||||
if (testing) debugPrint("getAssets(): Returning!");
|
||||
return assetList;
|
||||
}
|
||||
|
||||
Future<int> addAsset(TrackedItem asset) async {
|
||||
Database db = await instance.db;
|
||||
return await db.insert(
|
||||
"asset",
|
||||
asset.toMap(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeAsset(int id) async {
|
||||
Database db = await instance.db;
|
||||
return await db.delete(
|
||||
"asset",
|
||||
where: "id = ?",
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateAsset(TrackedItem asset) async {
|
||||
Database db = await instance.db;
|
||||
return await db.update(
|
||||
"asset",
|
||||
asset.toMap(),
|
||||
where: "id = ?",
|
||||
whereArgs: [asset.id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> checkAssetNameExists(String name) async {
|
||||
Database db = await instance.db;
|
||||
var assets = await db.query(
|
||||
"asset",
|
||||
where: "name = ?",
|
||||
whereArgs: [name],
|
||||
);
|
||||
return assets.isNotEmpty;
|
||||
}
|
||||
|
||||
///
|
||||
}
|
1
lib/globals.dart
Normal file
@ -0,0 +1 @@
|
||||
const bool testing = false;
|
153
lib/main.dart
@ -1,125 +1,58 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Local
|
||||
import '/globals.dart';
|
||||
import '/pages/home.dart';
|
||||
|
||||
// SQLite
|
||||
import 'dart:io';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
// I see no good explanations of why to use this other package yet, but
|
||||
// trying this to see if it fixes the DB factory errors.
|
||||
// "Unhandled Exception: Bad state: databaseFactory not initialized databaseFactory is only initialized when using sqflite. When using `sqflite_common_ffi`You must call `databaseFactory = databaseFactoryFfi;` before using global openDatabase API
|
||||
// https://stackoverflow.com/questions/76158800/databasefactory-not-initialized-when-using-sqflite-in-flutter
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
// Initialize FFI
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Remove the DB and recreate it to test the Database Helper multiple times.
|
||||
if (testing) {
|
||||
() async {
|
||||
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
String path = join(documentsDirectory.path, 'ExpenseTracker.sqlite.db');
|
||||
await deleteDatabase(path);
|
||||
}();
|
||||
}
|
||||
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'Recurring Expense Tracker',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'You have pushed the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: Colors.green,
|
||||
),
|
||||
themeMode: ThemeMode.system,
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
22
lib/models/asset.dart
Normal file
@ -0,0 +1,22 @@
|
||||
// Local
|
||||
import '/models/item_type.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
class Asset extends TrackedItem {
|
||||
static String amountText = "Amount";
|
||||
|
||||
Asset({
|
||||
super.id,
|
||||
super.type = ItemType.asset,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.description,
|
||||
});
|
||||
|
||||
factory Asset.fromMap(Map<String, dynamic> json) => Asset(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
amount: json['amount'],
|
||||
description: json['description'],
|
||||
);
|
||||
}
|
57
lib/models/database_backup.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// Local
|
||||
import '/models/expense.dart';
|
||||
import '/models/income.dart';
|
||||
import '/models/asset.dart';
|
||||
import '/db.dart';
|
||||
|
||||
/// TODO:
|
||||
/// - Test the JSON data to ensure it's formed properly.
|
||||
/// - Enable the functions for income and assets once they are implemented.
|
||||
/// - Create a method (factory?) to load JSON data and delete/insert the data.
|
||||
|
||||
class DatabaseBackup {
|
||||
List<Expense> expenseTable = [];
|
||||
List<Income> incomeTable = [];
|
||||
List<Asset> assetTable = [];
|
||||
|
||||
void loadTables() async {
|
||||
expenseTable = await DatabaseHelper.instance.getExpenses();
|
||||
//incomeTable = await DatabaseHelper.instance.getIncomes();
|
||||
//assetTable = await DatabaseHelper.instance.getAssets();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
loadTables(); // TODO: Do we somehow need to wait for this to finish?
|
||||
return {
|
||||
'expense_table': [
|
||||
for (Expense e in expenseTable)
|
||||
{
|
||||
'id': e.id,
|
||||
'name': e.name,
|
||||
'cost': e.amount,
|
||||
'frequency': e.frequency!.title,
|
||||
'description': e.description,
|
||||
},
|
||||
],
|
||||
'income_table': [
|
||||
for (Income i in incomeTable)
|
||||
{
|
||||
'id': i.id,
|
||||
'name': i.name,
|
||||
'revenue': i.amount,
|
||||
'frequency': i.frequency!.title,
|
||||
'description': i.description,
|
||||
},
|
||||
],
|
||||
'asset_table': [
|
||||
for (Asset a in assetTable)
|
||||
{
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'amount': a.amount,
|
||||
'description': a.description,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
39
lib/models/expense.dart
Normal file
@ -0,0 +1,39 @@
|
||||
// Local
|
||||
import '/models/item_type.dart';
|
||||
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/frequency.dart';
|
||||
|
||||
class Expense extends TrackedItem {
|
||||
static String amountText = "Cost";
|
||||
|
||||
Expense({
|
||||
super.id,
|
||||
super.type = ItemType.expense,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.frequency,
|
||||
required super.description,
|
||||
});
|
||||
|
||||
factory Expense.fromMap(Map<String, dynamic> json) => Expense(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
amount: json['cost'],
|
||||
frequency: Frequency.values
|
||||
.where((freq) => freq.title == json['frequency'])
|
||||
.first,
|
||||
description: json['description'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'cost': amount,
|
||||
'frequency': frequency!.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
}
|
63
lib/models/frequency.dart
Normal file
@ -0,0 +1,63 @@
|
||||
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
||||
enum Frequency {
|
||||
daily(
|
||||
title: "Daily",
|
||||
hint: "Once Per Day",
|
||||
timesPerYear: 364.25,
|
||||
numDays: 1,
|
||||
),
|
||||
weekly(
|
||||
title: "Weekly",
|
||||
hint: "Once Per Week",
|
||||
timesPerYear: (364.25 / 7),
|
||||
numDays: 7,
|
||||
),
|
||||
biweekly(
|
||||
title: "Biweekly",
|
||||
hint: "Every Other Week",
|
||||
timesPerYear: (364.25 / 14),
|
||||
numDays: 14,
|
||||
),
|
||||
bimonthly(
|
||||
title: "Bimonthly",
|
||||
hint: "Twice Per Month",
|
||||
timesPerYear: 24,
|
||||
numDays: (364.25 / 24),
|
||||
),
|
||||
monthly(
|
||||
title: "Monthly",
|
||||
hint: "Once Per Month",
|
||||
timesPerYear: 12,
|
||||
numDays: (364.25 / 12),
|
||||
),
|
||||
quarterly(
|
||||
title: "Quarterly",
|
||||
hint: "Every Three Months",
|
||||
timesPerYear: 4,
|
||||
numDays: (364.25 / 4),
|
||||
),
|
||||
biannual(
|
||||
title: "Biannual",
|
||||
hint: "Twice Per Year",
|
||||
timesPerYear: 2,
|
||||
numDays: (364.25 / 2),
|
||||
),
|
||||
yearly(
|
||||
title: "Yearly",
|
||||
hint: "Once Per Year",
|
||||
timesPerYear: 1,
|
||||
numDays: 364.25,
|
||||
);
|
||||
|
||||
const Frequency({
|
||||
required this.title,
|
||||
required this.hint,
|
||||
required this.timesPerYear,
|
||||
required this.numDays,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String hint;
|
||||
final double timesPerYear;
|
||||
final double numDays;
|
||||
}
|
38
lib/models/income.dart
Normal file
@ -0,0 +1,38 @@
|
||||
// Local
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/frequency.dart';
|
||||
import '/models/item_type.dart';
|
||||
|
||||
class Income extends TrackedItem {
|
||||
static String amountText = "Revenue";
|
||||
|
||||
Income({
|
||||
super.id,
|
||||
super.type = ItemType.income,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.frequency,
|
||||
required super.description,
|
||||
});
|
||||
|
||||
factory Income.fromMap(Map<String, dynamic> json) => Income(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
amount: json['revenue'],
|
||||
frequency: Frequency.values
|
||||
.where((freq) => freq.title == json['frequency'])
|
||||
.first,
|
||||
description: json['description'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'revenue': amount,
|
||||
'frequency': frequency!.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
}
|
22
lib/models/item_type.dart
Normal file
@ -0,0 +1,22 @@
|
||||
enum ItemType {
|
||||
expense(
|
||||
title: "Expense",
|
||||
plural: "Expenses",
|
||||
),
|
||||
income(
|
||||
title: "Income",
|
||||
plural: "Incomes",
|
||||
),
|
||||
asset(
|
||||
title: "Asset",
|
||||
plural: "Assets",
|
||||
);
|
||||
|
||||
const ItemType({
|
||||
required this.title,
|
||||
required this.plural,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String plural;
|
||||
}
|
55
lib/models/tracked_item.dart
Normal file
@ -0,0 +1,55 @@
|
||||
// Local
|
||||
import '/models/item_type.dart';
|
||||
|
||||
import '/models/frequency.dart';
|
||||
|
||||
abstract class TrackedItem {
|
||||
int? id;
|
||||
ItemType? type;
|
||||
String name;
|
||||
double amount;
|
||||
Frequency? frequency;
|
||||
String description;
|
||||
|
||||
TrackedItem({
|
||||
this.id,
|
||||
this.type,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
this.frequency,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
static String amountText = "Amount";
|
||||
String getAmountText() => amountText;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toMap().toString();
|
||||
}
|
||||
|
||||
double calcComparableAmountYearly() {
|
||||
return frequency == null ? 0 : amount * frequency!.timesPerYear;
|
||||
}
|
||||
|
||||
double calcComparableAmountDaily() {
|
||||
return frequency == null ? 0 : amount / frequency!.numDays;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return frequency == null
|
||||
? {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'description': description,
|
||||
}
|
||||
: {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'frequency': frequency!.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
}
|
112
lib/pages/help.dart
Normal file
@ -0,0 +1,112 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
_launchSite(String url) async {
|
||||
try {
|
||||
if (await canLaunchUrlString(url)) {
|
||||
await launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} else {
|
||||
throw "System does not think it can launch '$url'.";
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
throw e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class HelpPage extends StatelessWidget {
|
||||
const HelpPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"\t\t This app is meant to be a simple budgeting tool,"
|
||||
" allowing you to view your income and expenses at a high"
|
||||
" level without micro managing specific budget items or"
|
||||
" adding receipts.",
|
||||
),
|
||||
Text(
|
||||
"\n\t\t Tracked items can be swiped left to right for ,"
|
||||
" Deletion or right to left for Editing. Items are sorted"
|
||||
" from highest to lowest so that the biggest impacts are"
|
||||
" always in view.",
|
||||
),
|
||||
Text(
|
||||
"\n\t\t To subscribe to app updates, install the Obtanium"
|
||||
" app, then use the URL from the Source Code button below."
|
||||
" Otherwise the app needs installed manually by downloading"
|
||||
" APKs from the Source Code /releases/ page.",
|
||||
),
|
||||
//Text("Another paragraph.")
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
_launchSite(
|
||||
"https://git.hyperling.com/me/flutter-expense-tracker",
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.code),
|
||||
label: Text("Source Code"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
_launchSite(
|
||||
"https://hyperling.com",
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.web_asset),
|
||||
label: Text("Personal Website"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
168
lib/pages/home.dart
Normal file
@ -0,0 +1,168 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/item_type.dart';
|
||||
import 'dart:io';
|
||||
|
||||
// Local
|
||||
import '/pages/tracked_item.dart';
|
||||
import '/pages/report.dart';
|
||||
import '/pages/settings.dart';
|
||||
import '/pages/help.dart';
|
||||
import '/db.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
var pageSelected = 0;
|
||||
|
||||
refresh() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget page;
|
||||
Widget? dialog;
|
||||
switch (pageSelected) {
|
||||
case 0:
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getExpenses(),
|
||||
notifyParent: refresh,
|
||||
);
|
||||
dialog = TrackedItemInputDialog(
|
||||
notifyParent: refresh,
|
||||
type: ItemType.expense,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getIncomes(),
|
||||
notifyParent: refresh,
|
||||
);
|
||||
dialog = TrackedItemInputDialog(
|
||||
notifyParent: refresh,
|
||||
type: ItemType.income,
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getAssets(),
|
||||
notifyParent: refresh,
|
||||
);
|
||||
dialog = TrackedItemInputDialog(
|
||||
notifyParent: refresh,
|
||||
type: ItemType.asset,
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
page = ProjectionPage();
|
||||
break;
|
||||
case 4:
|
||||
page = SettingsPage();
|
||||
break;
|
||||
case 5:
|
||||
page = HelpPage();
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError('No widget for page $pageSelected yet!');
|
||||
}
|
||||
|
||||
Future<void> addNewValue(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(content: dialog),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? floatingButton;
|
||||
if (dialog != null) {
|
||||
floatingButton = IconButton(
|
||||
onPressed: () {
|
||||
addNewValue(context);
|
||||
},
|
||||
icon: Icon(Icons.add),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
}
|
||||
|
||||
Widget navigation = NavigationRail(
|
||||
extended: true,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.payment),
|
||||
label: Text('Expenses'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.account_balance),
|
||||
label: Text('Income'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.attach_money),
|
||||
label: Text('Liquid Assets'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.bar_chart),
|
||||
label: Text('Reports'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.settings),
|
||||
label: Text('Settings'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.help),
|
||||
label: Text('Help'),
|
||||
),
|
||||
],
|
||||
selectedIndex: pageSelected,
|
||||
onDestinationSelected: (value) {
|
||||
setState(() {
|
||||
pageSelected = value;
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Widget main = Container(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Center(child: page),
|
||||
);
|
||||
|
||||
Widget? drawer;
|
||||
Widget body;
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
drawer = navigation;
|
||||
body = SafeArea(child: main);
|
||||
} else {
|
||||
drawer = null;
|
||||
body = Row(
|
||||
children: [
|
||||
SafeArea(child: navigation),
|
||||
Expanded(child: main),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Expense Tracker"),
|
||||
),
|
||||
drawer: drawer,
|
||||
body: body,
|
||||
floatingActionButton: floatingButton,
|
||||
extendBody: false,
|
||||
extendBodyBehindAppBar: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
213
lib/pages/report.dart
Normal file
@ -0,0 +1,213 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/item_type.dart';
|
||||
|
||||
// Local
|
||||
import '/db.dart';
|
||||
import '/widgets/cards.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
/// TODO:
|
||||
/// - Projected Assets:
|
||||
/// - Allow customization?
|
||||
/// - Fix bug where editing an item does not reflect immediately when returning to Reports page.
|
||||
/// - Currently reflects after going back to Reports the 2nd time.
|
||||
|
||||
double _assetTotal = 0,
|
||||
_expenseMonthly = 0,
|
||||
_expenseYearly = 0,
|
||||
_incomeMonthly = 0,
|
||||
_incomeYearly = 0;
|
||||
|
||||
class ProjectionPage extends StatefulWidget {
|
||||
const ProjectionPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProjectionPage> createState() => _ProjectionPageState();
|
||||
}
|
||||
|
||||
class _ProjectionPageState extends State<ProjectionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Summaries for display as well as calculation of totals for projections.
|
||||
Widget expenseSummary = SummaryCardForTotals(
|
||||
list: DatabaseHelper.instance.getExpenses(),
|
||||
summaryTypeLabel: ItemType.expense.title,
|
||||
);
|
||||
Widget incomeSummary = SummaryCardForTotals(
|
||||
list: DatabaseHelper.instance.getIncomes(),
|
||||
summaryTypeLabel: ItemType.income.title,
|
||||
);
|
||||
Widget assetSummary = SummaryCardForTotals(
|
||||
list: DatabaseHelper.instance.getAssets(),
|
||||
summaryTypeLabel: ItemType.asset.title,
|
||||
);
|
||||
|
||||
// Calculations for the projections.
|
||||
double oneMonth = _assetTotal + _incomeMonthly - _expenseMonthly,
|
||||
threeMonths = _assetTotal + (3 * (_incomeMonthly - _expenseMonthly)),
|
||||
sixMonths = _assetTotal + (6 * (_incomeMonthly - _expenseMonthly)),
|
||||
oneYear = _assetTotal + (_incomeYearly - _expenseYearly),
|
||||
twoYears = _assetTotal + (2 * (_incomeYearly - _expenseYearly)),
|
||||
fiveYears = _assetTotal + (5 * (_incomeYearly - _expenseYearly));
|
||||
|
||||
// Widgets to show the projections.
|
||||
Widget proj1 = SummaryCard(
|
||||
name: "One month from now...",
|
||||
leftText: "",
|
||||
middleText: oneMonth.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
Widget proj2 = SummaryCard(
|
||||
name: "Three months from now...",
|
||||
leftText: "",
|
||||
middleText: threeMonths.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
Widget proj3 = SummaryCard(
|
||||
name: "Half a year from now...",
|
||||
leftText: "",
|
||||
middleText: sixMonths.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
Widget proj4 = SummaryCard(
|
||||
name: "One year from now...",
|
||||
leftText: "",
|
||||
middleText: oneYear.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
Widget proj5 = SummaryCard(
|
||||
name: "Two years from now...",
|
||||
leftText: "",
|
||||
middleText: twoYears.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
Widget proj6 = SummaryCard(
|
||||
name: "Five years from now...",
|
||||
leftText: "",
|
||||
middleText: fiveYears.toStringAsFixed(2),
|
||||
rightText: "",
|
||||
);
|
||||
|
||||
// Return all of the UI elements.
|
||||
return ListView(
|
||||
children: [
|
||||
TitleCard(title: "Summaries"),
|
||||
expenseSummary,
|
||||
incomeSummary,
|
||||
assetSummary,
|
||||
TitleCard(title: "Projections"),
|
||||
proj1,
|
||||
proj2,
|
||||
proj3,
|
||||
proj4,
|
||||
proj5,
|
||||
proj6,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryCardForTotals extends StatelessWidget {
|
||||
const SummaryCardForTotals({
|
||||
super.key,
|
||||
required this.list,
|
||||
required this.summaryTypeLabel,
|
||||
});
|
||||
|
||||
final Future<List<TrackedItem>> list;
|
||||
final String summaryTypeLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<TrackedItem>>(
|
||||
future: list,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<TrackedItem>> snapshot,
|
||||
) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text('Loading $summaryTypeLabel Section...');
|
||||
}
|
||||
|
||||
// Calculate the total fields based on item type.
|
||||
double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0;
|
||||
ItemType? itemType;
|
||||
for (TrackedItem e in snapshot.data!) {
|
||||
if (itemType == null) {
|
||||
itemType = e.type!;
|
||||
} else if (itemType != e.type) {
|
||||
throw "List in SummaryCardForTotals has multiple item types, abort!";
|
||||
}
|
||||
|
||||
if (e.type == ItemType.asset) {
|
||||
monthlyTotal += e.amount;
|
||||
} else {
|
||||
dailyTotal += e.calcComparableAmountDaily();
|
||||
monthlyTotal += e.calcComparableAmountYearly() / 12;
|
||||
yearlyTotal += e.calcComparableAmountYearly();
|
||||
}
|
||||
}
|
||||
|
||||
/* Load page variables based on calculated totals. */
|
||||
switch (itemType) {
|
||||
case null:
|
||||
break;
|
||||
|
||||
case ItemType.asset:
|
||||
_assetTotal = monthlyTotal;
|
||||
break;
|
||||
|
||||
case ItemType.expense:
|
||||
_expenseMonthly = monthlyTotal;
|
||||
_expenseYearly = yearlyTotal;
|
||||
break;
|
||||
|
||||
case ItemType.income:
|
||||
_incomeMonthly = monthlyTotal;
|
||||
_incomeYearly = yearlyTotal;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw UnimplementedError(
|
||||
"Item type ${itemType.title} not handled in SummaryCardForTotals!",
|
||||
);
|
||||
}
|
||||
|
||||
/* Determine what needs displayed for the item type. */
|
||||
// Header
|
||||
String plural = snapshot.data!.length == 1 ? "" : "s";
|
||||
String header = "$summaryTypeLabel Total";
|
||||
header += itemType == ItemType.asset ? "" : "s";
|
||||
header += " (${snapshot.data!.length} Item$plural)";
|
||||
|
||||
// Total Fields
|
||||
String dailyEstimate =
|
||||
dailyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
|
||||
monthlyEstimate =
|
||||
monthlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
|
||||
yearlyEstimate =
|
||||
yearlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~";
|
||||
String leftText = "", middleText = "", rightText = "";
|
||||
if (itemType == ItemType.asset) {
|
||||
middleText = "$monthlyEstimate${monthlyTotal.toStringAsFixed(2)}";
|
||||
} else {
|
||||
leftText = "$dailyEstimate${dailyTotal.toStringAsFixed(2)} Daily";
|
||||
middleText =
|
||||
"$monthlyEstimate${monthlyTotal.toStringAsFixed(2)} Monthly";
|
||||
rightText =
|
||||
"$yearlyEstimate${yearlyTotal.toStringAsFixed(2)} Yearly";
|
||||
}
|
||||
|
||||
// Return the UI element.
|
||||
return SummaryCard(
|
||||
name: header,
|
||||
leftText: leftText,
|
||||
middleText: middleText,
|
||||
rightText: rightText,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
22
lib/pages/settings.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// TODO:
|
||||
/// - Export DB (JSON?)
|
||||
/// - Import DB (JSON?)
|
||||
/// - Choose color scheme
|
||||
/// - Delete / Reset DB (pull code from `main`'s `if (testing)`)
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"No settings yet. :)",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
550
lib/pages/tracked_item.dart
Normal file
@ -0,0 +1,550 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/asset.dart';
|
||||
import '/models/income.dart';
|
||||
|
||||
// Local
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/item_type.dart';
|
||||
import '/models/expense.dart';
|
||||
import '/models/frequency.dart';
|
||||
import '/db.dart';
|
||||
|
||||
class TrackedItemPage extends StatefulWidget {
|
||||
final Future<List<TrackedItem>> assetsToLoad;
|
||||
final Function() notifyParent;
|
||||
|
||||
const TrackedItemPage({
|
||||
super.key,
|
||||
required this.assetsToLoad,
|
||||
required this.notifyParent,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TrackedItemPage> createState() => _TrackedItemPageState();
|
||||
}
|
||||
|
||||
class _TrackedItemPageState extends State<TrackedItemPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FutureBuilder<List<TrackedItem>>(
|
||||
future: widget.assetsToLoad,
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text('Loading...');
|
||||
}
|
||||
snapshot.data!.sort(
|
||||
(a, b) => (b.calcComparableAmountYearly() -
|
||||
a.calcComparableAmountYearly())
|
||||
.toInt(),
|
||||
);
|
||||
return snapshot.data!.isEmpty
|
||||
? Text(
|
||||
"Add items to get started.",
|
||||
softWrap: true,
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (_, index) {
|
||||
final TrackedItem curr = snapshot.data![index];
|
||||
|
||||
final itemKey = Key(curr.id!.toString());
|
||||
final String itemTitle = curr.name;
|
||||
|
||||
final String itemAmount;
|
||||
if (curr.frequency != null) {
|
||||
itemAmount =
|
||||
"${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}";
|
||||
} else {
|
||||
itemAmount = curr.amount.toStringAsFixed(2);
|
||||
}
|
||||
final String itemDescription = curr.description;
|
||||
|
||||
final double itemDayAmount, itemMonthAmount, itemYearAmount;
|
||||
final String estimateSymbolDaily,
|
||||
estimateSymbolMonthly,
|
||||
estimateSymbolYearly;
|
||||
|
||||
if (curr.frequency != null) {
|
||||
itemDayAmount = curr.calcComparableAmountDaily();
|
||||
estimateSymbolDaily = curr.frequency!.numDays
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemDayAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
|
||||
itemMonthAmount =
|
||||
(curr.calcComparableAmountYearly() / 12);
|
||||
estimateSymbolMonthly = curr.frequency!.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemMonthAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
|
||||
itemYearAmount = curr.calcComparableAmountYearly();
|
||||
estimateSymbolYearly = curr.frequency!.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemYearAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
} else {
|
||||
itemDayAmount = -1;
|
||||
estimateSymbolDaily = "";
|
||||
itemMonthAmount = curr.amount;
|
||||
estimateSymbolMonthly = "";
|
||||
itemYearAmount = -1;
|
||||
estimateSymbolYearly = "";
|
||||
}
|
||||
|
||||
final String monthlyTitle = curr.type == ItemType.asset
|
||||
? ""
|
||||
: " ${Frequency.monthly.title}";
|
||||
|
||||
final String itemTopText = itemDayAmount < 0
|
||||
? ""
|
||||
: "$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}";
|
||||
final String itemMiddleText = itemMonthAmount < 0
|
||||
? ""
|
||||
: "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)}$monthlyTitle";
|
||||
final String itemBottomText = itemYearAmount < 0
|
||||
? ""
|
||||
: "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Dismissible(
|
||||
key: itemKey,
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.delete),
|
||||
Text("Delete"),
|
||||
],
|
||||
),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.orange,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("Edit"),
|
||||
Icon(Icons.edit),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
setState(() {
|
||||
snapshot.data!.remove(curr);
|
||||
switch (direction) {
|
||||
case DismissDirection.startToEnd:
|
||||
// Remove the item from the database.
|
||||
if (curr is Expense) {
|
||||
DatabaseHelper.instance.removeExpense(
|
||||
curr.id!,
|
||||
);
|
||||
} else if (curr is Income) {
|
||||
DatabaseHelper.instance.removeIncome(
|
||||
curr.id!,
|
||||
);
|
||||
} else if (curr is Asset) {
|
||||
DatabaseHelper.instance.removeAsset(
|
||||
curr.id!,
|
||||
);
|
||||
} else {
|
||||
throw UnimplementedError(
|
||||
"Cannot remove unimplemented item type.");
|
||||
}
|
||||
break;
|
||||
case DismissDirection.endToStart:
|
||||
// Open an edit dialog, then remove the item from the list.
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
content: TrackedItemInputDialog(
|
||||
notifyParent: widget.notifyParent,
|
||||
entry: curr,
|
||||
amountText: curr.getAmountText(),
|
||||
type: curr.type!,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
UnimplementedError(
|
||||
"Direction ${direction.toString()} not recognized.",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
itemTitle,
|
||||
style: TextStyle(fontSize: 20.0),
|
||||
),
|
||||
Text(
|
||||
itemAmount,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
itemDescription,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
itemTopText,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
Text(
|
||||
itemMiddleText,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
Text(
|
||||
itemBottomText,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TrackedItemInputDialog extends StatefulWidget {
|
||||
final Function() notifyParent;
|
||||
final TrackedItem? entry;
|
||||
final String? amountText;
|
||||
final ItemType? type;
|
||||
|
||||
const TrackedItemInputDialog({
|
||||
super.key,
|
||||
required this.notifyParent,
|
||||
this.entry,
|
||||
this.amountText,
|
||||
this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TrackedItemInputDialog> createState() => _TrackedItemInputDialogState();
|
||||
}
|
||||
|
||||
class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
int? _id;
|
||||
String _name = "";
|
||||
double _amount = 0;
|
||||
Frequency _freq = Frequency.monthly;
|
||||
String _desc = "";
|
||||
ItemType? _type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.type == null &&
|
||||
(widget.entry != null && widget.entry!.type == null)) {
|
||||
throw FlutterError("No ItemType provided for TrackedItemInputDialog.");
|
||||
}
|
||||
_type = widget.type;
|
||||
|
||||
if (widget.entry != null) {
|
||||
_id = widget.entry!.id;
|
||||
_name = widget.entry!.name;
|
||||
_amount = widget.entry!.amount;
|
||||
widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!;
|
||||
_desc = widget.entry!.description;
|
||||
_type = widget.entry!.type!;
|
||||
}
|
||||
|
||||
String amountText =
|
||||
widget.amountText != null ? widget.amountText! : TrackedItem.amountText;
|
||||
|
||||
return Column(
|
||||
// prevent AlertDialog from taking full vertical height.
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
alignment: FractionalOffset.topRight,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (widget.entry != null) {
|
||||
setState(() {
|
||||
switch (_type) {
|
||||
case ItemType.expense:
|
||||
DatabaseHelper.instance.addExpense(widget.entry!);
|
||||
break;
|
||||
case ItemType.income:
|
||||
DatabaseHelper.instance.addIncome(widget.entry!);
|
||||
break;
|
||||
case ItemType.asset:
|
||||
DatabaseHelper.instance.addAsset(widget.entry!);
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError(
|
||||
"Cannot add unimplemented type.");
|
||||
}
|
||||
widget.notifyParent();
|
||||
});
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
),
|
||||
),
|
||||
AlertDialog(
|
||||
insetPadding: EdgeInsets.all(0),
|
||||
title: Center(
|
||||
child: widget.entry == null
|
||||
? Text("New ${_type!.title}")
|
||||
: Text("Edit ${_type!.title}"),
|
||||
),
|
||||
content: FutureBuilder<List<Expense>>(
|
||||
future: DatabaseHelper.instance.getExpenses(),
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<Expense>> snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: Text('Loading...'));
|
||||
}
|
||||
List<Expense> expenses = snapshot.data!;
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name",
|
||||
hintText: "Example: Red Pocket",
|
||||
hintStyle: TextStyle(fontSize: 10.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
initialValue: _name,
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return "Name must be provided.";
|
||||
}
|
||||
if (!expenses.every((expense) =>
|
||||
expense.name != value || expense.id == _id)) {
|
||||
return "Name must be unique, already in use.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_name = value!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: amountText,
|
||||
hintText: "Example: 10.00",
|
||||
hintStyle: TextStyle(fontSize: 10.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
initialValue: _amount != 0 ? _amount.toString() : "",
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "$amountText must be provided.";
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return "$amountText must be a valid number.";
|
||||
}
|
||||
if (double.parse(value) < 0) {
|
||||
return "Please use the Income page rather than having negative expenses.";
|
||||
}
|
||||
if (double.parse(value) < 0.01) {
|
||||
return "$amountText must be one hundreth (0.01) or higher.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_amount = double.parse(value!);
|
||||
},
|
||||
),
|
||||
if (_type != ItemType.asset)
|
||||
DropdownButtonFormField(
|
||||
items: Frequency.values
|
||||
.map(
|
||||
(freq) => DropdownMenuItem(
|
||||
value: freq,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
freq.title,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(1.0),
|
||||
child: Text(
|
||||
" (${freq.hint})",
|
||||
style: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: _freq,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Frequency",
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return "Frequency must be provided.";
|
||||
}
|
||||
if (!Frequency.values.contains(value)) {
|
||||
return "Value not valid.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
_freq = value!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Description",
|
||||
hintText:
|
||||
"Example: 1GB data with unlimited talk & text.",
|
||||
hintStyle: TextStyle(fontSize: 8.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
initialValue: _desc,
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_desc = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
setState(
|
||||
() {
|
||||
switch (_type) {
|
||||
case ItemType.expense:
|
||||
Expense expense = Expense(
|
||||
id: _id,
|
||||
name: _name,
|
||||
amount: _amount,
|
||||
frequency: _freq,
|
||||
description: _desc,
|
||||
);
|
||||
if (_id != null) {
|
||||
DatabaseHelper.instance.updateExpense(
|
||||
expense,
|
||||
);
|
||||
} else {
|
||||
DatabaseHelper.instance.addExpense(
|
||||
expense,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ItemType.income:
|
||||
Income income = Income(
|
||||
id: _id,
|
||||
name: _name,
|
||||
amount: _amount,
|
||||
frequency: _freq,
|
||||
description: _desc,
|
||||
);
|
||||
if (_id != null) {
|
||||
DatabaseHelper.instance.updateIncome(
|
||||
income,
|
||||
);
|
||||
} else {
|
||||
DatabaseHelper.instance.addIncome(
|
||||
income,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ItemType.asset:
|
||||
Asset asset = Asset(
|
||||
id: _id,
|
||||
name: _name,
|
||||
amount: _amount,
|
||||
description: _desc,
|
||||
);
|
||||
if (_id != null) {
|
||||
DatabaseHelper.instance.updateAsset(
|
||||
asset,
|
||||
);
|
||||
} else {
|
||||
DatabaseHelper.instance.addAsset(
|
||||
asset,
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError(
|
||||
"No code for type ${_type!.title}",
|
||||
);
|
||||
}
|
||||
widget.notifyParent();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.save),
|
||||
label: Text('Submit'),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
87
lib/widgets/cards.dart
Normal file
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TitleCard extends StatelessWidget {
|
||||
const TitleCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryCard extends StatelessWidget {
|
||||
const SummaryCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.leftText,
|
||||
required this.middleText,
|
||||
required this.rightText,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String leftText;
|
||||
final String middleText;
|
||||
final String rightText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Theme.of(context).cardColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
color: Theme.of(context).highlightColor,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(
|
||||
flex: 3,
|
||||
),
|
||||
Text(
|
||||
leftText,
|
||||
),
|
||||
Spacer(
|
||||
flex: 1,
|
||||
),
|
||||
Text(
|
||||
middleText,
|
||||
),
|
||||
Spacer(
|
||||
flex: 1,
|
||||
),
|
||||
Text(
|
||||
rightText,
|
||||
),
|
||||
Spacer(
|
||||
flex: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "flutter_expense_tracker")
|
||||
set(BINARY_NAME "expense_tracker")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.flutter_expense_tracker")
|
||||
set(APPLICATION_ID "com.hyperling.expense_tracker")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "flutter_expense_tracker");
|
||||
gtk_header_bar_set_title(header_bar, "Recurring Expense Tracker");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "flutter_expense_tracker");
|
||||
gtk_window_set_title(window, "Recurring Expense Tracker");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
@ -5,6 +5,12 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import path_provider_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
@ -64,7 +64,7 @@
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_expense_tracker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* flutter_empty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_empty.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@ -131,7 +131,7 @@
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */,
|
||||
33CC10ED2044A3C60003C045 /* flutter_empty.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
@ -217,7 +217,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* flutter_empty.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@ -385,10 +385,10 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -399,10 +399,10 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -413,10 +413,10 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
|
@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "flutter_expense_tracker.app"
|
||||
BuildableName = "flutter_empty.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "flutter_expense_tracker.app"
|
||||
BuildableName = "flutter_empty.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@ -65,7 +65,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "flutter_expense_tracker.app"
|
||||
BuildableName = "flutter_empty.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@ -82,7 +82,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "flutter_expense_tracker.app"
|
||||
BuildableName = "flutter_empty.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
@ -5,10 +5,10 @@
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = flutter_expense_tracker
|
||||
PRODUCT_NAME = flutter_empty
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved.
|
||||
|
BIN
media/icon_v001.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
media/icon_v001.xcf
Normal file
BIN
media/icon_v001_round.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
media/icon_v002.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
media/icon_v002.xcf
Normal file
BIN
media/icon_v002_round.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
media/icon_v003.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
media/icon_v003.xcf
Normal file
BIN
media/icon_v003_round.png
Normal file
After Width: | Height: | Size: 35 KiB |
315
pubspec.lock
@ -5,58 +5,58 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.19.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -75,22 +75,27 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -111,10 +116,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -127,18 +132,82 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.16"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -148,50 +217,186 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.5"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.15"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -204,10 +409,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.3.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.6.1 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
83
pubspec.yaml
@ -1,89 +1,24 @@
|
||||
name: flutter_expense_tracker
|
||||
description: "A new Flutter project."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
name: expense_tracker
|
||||
description: Track recurring expenses against income and liquid assets.
|
||||
publish_to: 'none'
|
||||
version: 0.1.1
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.1
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
path: ^1.9.0
|
||||
path_provider: ^2.1.5
|
||||
sqflite: ^2.4.1
|
||||
sqflite_common_ffi: ^2.3.4+4
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
3
run_offline.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
flutter run --no-pub
|
@ -8,12 +8,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flutter_expense_tracker/main.dart';
|
||||
import 'package:expense_tracker/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpWidget(const MainApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
@ -23,13 +23,13 @@
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="flutter_expense_tracker">
|
||||
<meta name="apple-mobile-web-app-title" content="flutter_empty">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>flutter_expense_tracker</title>
|
||||
<title>flutter_empty</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flutter_expense_tracker",
|
||||
"short_name": "flutter_expense_tracker",
|
||||
"name": "flutter_empty",
|
||||
"short_name": "flutter_empty",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(flutter_expense_tracker LANGUAGES CXX)
|
||||
project(flutter_empty LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "flutter_expense_tracker")
|
||||
set(BINARY_NAME "flutter_empty")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
@ -6,6 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -90,12 +90,12 @@ BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "FileDescription", "flutter_expense_tracker" "\0"
|
||||
VALUE "FileDescription", "flutter_empty" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "flutter_expense_tracker" "\0"
|
||||
VALUE "InternalName", "flutter_empty" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "flutter_expense_tracker.exe" "\0"
|
||||
VALUE "ProductName", "flutter_expense_tracker" "\0"
|
||||
VALUE "OriginalFilename", "flutter_empty.exe" "\0"
|
||||
VALUE "ProductName", "flutter_empty" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
|
@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"flutter_expense_tracker", origin, size)) {
|
||||
if (!window.Create(L"flutter_empty", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|