Initial and done prolly

This commit is contained in:
Andrew 2025-01-05 16:01:21 +07:00
commit 6f88b9966f
175 changed files with 15445 additions and 0 deletions

45
.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ee624bc4fd41413cbb89099b0701a42287643d9a"
channel: "beta"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: android
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: ios
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: linux
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: macos
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: web
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
- platform: windows
create_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
base_revision: ee624bc4fd41413cbb89099b0701a42287643d9a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# groceries_manager
A new Flutter project.

5
analysis_options.yaml Normal file
View file

@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"

13
android/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle Normal file
View 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.example.groceries_manager"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.groceries_manager"
// 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.debug
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="groceries_manager"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package com.example.groceries_manager
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View file

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View file

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

25
android/settings.gradle Normal file
View file

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return 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.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

7
build.yaml Normal file
View file

@ -0,0 +1,7 @@
targets:
$default:
builders:
drift_dev:
options:
databases:
my_database: lib/db/database.dart

3
devtools_options.yaml Normal file
View file

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

34
ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

44
ios/Podfile Normal file
View file

@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View file

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.groceriesManager;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Groceries Manager</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>groceries_manager</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

165
lib/common/icons.dart Normal file
View file

@ -0,0 +1,165 @@
import 'package:flutter/widgets.dart';
import 'package:get/utils.dart';
import 'package:icons_plus/icons_plus.dart';
enum ProductCategoryIcons {
birthday_2,
bone,
bowl,
bread,
cake,
candy_2,
candy,
carrot,
champagne,
chicken,
chopsticks,
cookie,
cookie_man,
cupcake,
drink,
egg_crack,
egg,
fish,
fork_knife,
fork,
fork_spoon,
glass_cup,
ice_cream_2,
ice_cream,
lollipop,
spoon,
sugar_coated_haws,
teacup,
wine,
wineglass_2,
wineglass;
IconData get icon {
switch (this) {
case ProductCategoryIcons.birthday_2:
return MingCute.birthday_2_line;
case ProductCategoryIcons.bone:
return MingCute.bone_line;
case ProductCategoryIcons.bowl:
return MingCute.bowl_line;
case ProductCategoryIcons.bread:
return MingCute.bread_line;
case ProductCategoryIcons.cake:
return MingCute.cake_line;
case ProductCategoryIcons.candy_2:
return MingCute.candy_2_line;
case ProductCategoryIcons.candy:
return MingCute.candy_line;
case ProductCategoryIcons.carrot:
return MingCute.carrot_line;
case ProductCategoryIcons.champagne:
return MingCute.champagne_line;
case ProductCategoryIcons.chicken:
return MingCute.chicken_line;
case ProductCategoryIcons.chopsticks:
return MingCute.chopsticks_line;
case ProductCategoryIcons.cookie:
return MingCute.cookie_line;
case ProductCategoryIcons.cookie_man:
return MingCute.cookie_man_line;
case ProductCategoryIcons.cupcake:
return MingCute.cupcake_line;
case ProductCategoryIcons.drink:
return MingCute.drink_line;
case ProductCategoryIcons.egg_crack:
return MingCute.egg_crack_line;
case ProductCategoryIcons.egg:
return MingCute.egg_line;
case ProductCategoryIcons.fish:
return MingCute.fish_line;
case ProductCategoryIcons.fork_knife:
return MingCute.fork_knife_line;
case ProductCategoryIcons.fork:
return MingCute.fork_line;
case ProductCategoryIcons.fork_spoon:
return MingCute.fork_spoon_line;
case ProductCategoryIcons.glass_cup:
return MingCute.glass_cup_line;
case ProductCategoryIcons.ice_cream_2:
return MingCute.ice_cream_2_line;
case ProductCategoryIcons.ice_cream:
return MingCute.ice_cream_line;
case ProductCategoryIcons.lollipop:
return MingCute.lollipop_line;
case ProductCategoryIcons.spoon:
return MingCute.spoon_line;
case ProductCategoryIcons.sugar_coated_haws:
return MingCute.sugar_coated_haws_line;
case ProductCategoryIcons.teacup:
return MingCute.teacup_line;
case ProductCategoryIcons.wine:
return MingCute.wine_line;
case ProductCategoryIcons.wineglass_2:
return MingCute.wineglass_2_line;
case ProductCategoryIcons.wineglass:
return MingCute.wineglass_line;
}
}
String get betterName {
final parts = name.split("_");
return parts.indexed.map((e) {
if (e.$1 == 0) {
return e.$2.capitalize;
}
return e.$2;
}).join(" ");
}
static ProductCategoryIcons fromName(String name) {
for (final v in values) {
if (v.name == name) {
return v;
}
}
return ProductCategoryIcons.fork;
}
}
enum StorageLocationIcon {
box_2,
box_3,
inbox,
package,
package_2;
IconData get icon {
switch (this) {
case StorageLocationIcon.box_2:
return MingCute.box_2_line;
case StorageLocationIcon.box_3:
return MingCute.box_3_line;
case StorageLocationIcon.inbox:
return MingCute.inbox_line;
case StorageLocationIcon.package:
return MingCute.package_line;
case StorageLocationIcon.package_2:
return MingCute.package_2_line;
}
}
static StorageLocationIcon fromName(String name) {
for (final v in values) {
if (v.name == name) {
return v;
}
}
return StorageLocationIcon.box_2;
}
String get betterName {
final parts = name.split("_");
return parts.indexed.map((e) {
if (e.$1 == 0) {
return e.$2.capitalize;
}
return e.$2;
}).join(" ");
}
}

View file

@ -0,0 +1,52 @@
part of '../database.dart';
extension ProductCategoryCrud on AppDatabase {
Stream<List<ProductCategoryData>> get productsCategoriesSubscription =>
managers.productCategory.watch();
Future<void> addProductCategory({
required String name,
required String icon,
}) async {
await managers.productCategory.create((c) => c(
name: name,
icon: icon,
));
}
Future<void> updateProductCategory({
required int id,
String? name,
String? icon,
}) async {
await managers.productCategory.filter((f) => f.id(id)).update((u) => u(
id: Value(id),
name: Value.absentIfNull(name),
icon: Value.absentIfNull(icon),
));
}
Future<void> deleteProductCategory(ProductCategoryData item) async {
await managers.productCategory.filter((f) => f.id(item.id)).delete();
}
Future<List<ProductCategoryData>> getProductCategories() async {
final categories = await managers.productCategory.get();
return categories;
}
Future<List<(ProductCategoryData, int)>>
getProductCategoriesWithCounts() async {
final categories = await managers.productCategory.get();
return [
for (final category in categories)
(
category,
await managers.product
.filter((f) => f.category.id(category.id))
.count()
),
];
}
}

View file

@ -0,0 +1,137 @@
part of '../database.dart';
extension ProductCrud on AppDatabase {
Stream<List<(ProductData, ProductCategoryData, StorageLocationData)>>
get productsSubscription => managers.product
.orderBy((o) => o.expiryDate.asc())
.withReferences((prefetch) => prefetch(
category: true,
storage: true,
))
.watch()
.asyncMap(
(products) async => [
for (final (item, refs) in products)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
],
);
Stream<List<(ProductData, ProductCategoryData, StorageLocationData)>>
get soonExpirySubscription => managers.product
.filter((f) =>
f.expiryDate.isAfter(DateTime.now().add(const Duration(days: 3))))
.orderBy((o) => o.expiryDate.asc())
.withReferences((prefetch) => prefetch(
category: true,
storage: true,
))
.watch()
.asyncMap(
(products) async => [
for (final (item, refs) in products)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
],
);
Future<void> addProduct({
required String name,
required ProductCategoryData category,
required StorageLocationData storage,
required double quantity,
required String unit,
DateTime? purchaseDate,
required DateTime expiryDate,
required String barcode,
}) async {
await managers.product.create((c) => c(
name: name,
category: category.id,
storage: storage.id,
quantity: quantity,
unit: unit,
barcode: barcode,
purchaseDate: Value(purchaseDate ?? DateTime.now()),
expiryDate: Value(expiryDate),
));
}
Future<void> updateProduct({
required int id,
String? name,
ProductCategoryData? category,
StorageLocationData? storage,
double? quantity,
String? unit,
DateTime? purchaseDate,
DateTime? expiryDate,
String? barcode,
}) async {
await managers.product.filter((f) => f.id(id)).update((u) => u(
name: Value.absentIfNull(name),
category: Value.absentIfNull(category?.id),
storage: Value.absentIfNull(storage?.id),
quantity: Value.absentIfNull(quantity),
unit: Value.absentIfNull(unit),
barcode: Value.absentIfNull(barcode),
purchaseDate: Value.absentIfNull(purchaseDate),
expiryDate: Value.absentIfNull(expiryDate),
));
}
Future<void> deleteProduct(ProductData item) async {
await managers.product.filter((f) => f.id(item.id)).delete();
}
Future<List<(ProductData, ProductCategoryData, StorageLocationData)>>
getProducts() async {
final products = await managers.product
.orderBy((o) => o.purchaseDate.asc())
.withReferences((prefetch) => prefetch(
category: true,
storage: true,
))
.get();
return [
for (final (item, refs) in products)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
];
}
Future<List<(ProductData, ProductCategoryData, StorageLocationData)>>
getSoonExpiryProducts() async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final products = await managers.product
.filter(
(f) => f.expiryDate.isBefore(today.add(const Duration(days: 3))),
)
.orderBy((o) => o.expiryDate.desc())
.withReferences((prefetch) => prefetch(
category: true,
storage: true,
))
.get();
return [
for (final (item, refs) in products)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
];
}
}

View file

@ -0,0 +1,84 @@
part of '../database.dart';
extension ShoppingListItemCrud on AppDatabase {
Stream<List<(ShoppingListItemData, ProductCategoryData, StorageLocationData)>>
get shoppingListSubscription => managers.shoppingListItem
.withReferences(
(prefetch) => prefetch(
category: true,
storage: true,
),
)
.watch()
.asyncMap(
(shoppingList) async => [
for (final (item, refs) in shoppingList)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
],
);
Future<void> addShoppingListItem({
required double quantity,
required String name,
required ProductCategoryData category,
required StorageLocationData storage,
required String unit,
}) async {
await managers.shoppingListItem.create((c) => c(
quantity: quantity,
name: name,
category: category.id,
storage: storage.id,
unit: unit,
));
}
Future<void> updateShoppingListItem({
required int id,
double? quantity,
String? name,
ProductCategoryData? category,
StorageLocationData? storage,
String? unit,
bool? isPurchased,
}) async {
await managers.shoppingListItem.filter((f) => f.id(id)).update((u) => u(
id: Value(id),
quantity: Value.absentIfNull(quantity),
isPurchased: Value.absentIfNull(isPurchased),
name: Value.absentIfNull(name),
category: Value.absentIfNull(category?.id),
storage: Value.absentIfNull(storage?.id),
unit: Value.absentIfNull(unit),
));
}
Future<void> deleteShoppingListItem(ShoppingListItemData item) async {
await managers.shoppingListItem.filter((f) => f.id(item.id)).delete();
}
Future<List<(ShoppingListItemData, ProductCategoryData, StorageLocationData)>>
getShoppingList() async {
final shoppingList = await managers.shoppingListItem
.withReferences(
(prefetch) => prefetch(
category: true,
storage: true,
),
)
.get();
return [
for (final (item, refs) in shoppingList)
(
item,
await refs.category.getSingle(),
await refs.storage.getSingle(),
),
];
}
}

View file

@ -0,0 +1,88 @@
part of '../database.dart';
extension StorageLocationsCrud on AppDatabase {
Stream<List<StorageLocationData>> get storageLocationsSubscription =>
managers.storageLocation.watch();
Future<int> addStorageLocation({
required String name,
required String description,
required TemperatureMode temperatureMode,
required String icon,
}) async {
return await managers.storageLocation.create((c) => c(
name: name,
description: description,
temperatureMode: temperatureMode.name,
icon: icon,
));
}
Future<int> updateStorageLocation({
required int id,
String? name,
String? description,
TemperatureMode? temperatureMode,
double? capacity,
String? icon,
}) async {
return await managers.storageLocation
.filter((f) => f.id(id))
.update((u) => u(
name: Value.absentIfNull(name),
description: Value.absentIfNull(description),
temperatureMode: Value.absentIfNull(temperatureMode?.name),
icon: Value.absentIfNull(icon),
));
}
Future<void> deleteStorageLocation(StorageLocationData item) async {
await managers.storageLocation.filter((f) => f.id(item.id)).delete();
}
Future<List<(StorageLocationData, List<(ProductData, ProductCategoryData)>)>>
getStorageLocations() async {
final storageLocations = await managers.storageLocation
.withReferences((prefetch) => prefetch(productRefs: true))
.get();
final result =
<(StorageLocationData, List<(ProductData, ProductCategoryData)>)>[];
for (final (storage, refs) in storageLocations) {
final products = await refs.productRefs
.withReferences((prefetch) => prefetch(category: true))
.get();
final productsWithCategories = <(ProductData, ProductCategoryData)>[];
for (final (product, refs) in products) {
productsWithCategories.add((
product,
await refs.category.getSingle(),
));
}
result.add((
storage,
productsWithCategories,
));
}
return result;
}
Future<bool> defaultExists() async {
final exists = await managers.storageLocation
.filter((f) => f.isDefault(true))
.exists();
return exists;
}
Future<void> switchDefault(int newDefaultId) async {
await managers.storageLocation
.filter((f) => f.isDefault(true))
.update((u) => u(isDefault: const Value(false)));
await managers.storageLocation
.filter((f) => f.id(newDefaultId))
.update((u) => u(isDefault: const Value(true)));
}
}

View file

@ -0,0 +1,84 @@
part of '../database.dart';
extension UserCrud on AppDatabase {
String hashPassword(String password) {
final passwordBytes = utf8.encode(password);
final hashedPassword = sha512224.convert(passwordBytes);
return hashedPassword.toString();
}
Future<UserData?> addUser({
required String login,
required String password,
}) async {
return await managers.user.createReturningOrNull((c) => c(
login: login,
password: hashPassword(password),
));
}
Future<List<UserData>> getUsers() async {
return await managers.user.get();
}
Future<bool> anyUserExists() async {
return await managers.user.exists();
}
Future<UserData?> findUser({
required String login,
required String password,
}) async {
return await managers.user
.filter((f) => f.login(login))
.filter((f) => f.password(hashPassword(password)))
.getSingleOrNull();
}
Future<UserData> getUser(UserData user) async {
return await managers.user.filter((f) => f.id(user.id)).getSingle();
}
Future<bool> updateUser(
UserData currentUser,
UserData updatedUser, {
String? login,
String? password,
}) async {
if (currentUser.id == updatedUser.id) {
return false;
}
await managers.user.filter((f) => f.id(updatedUser.id)).update((u) => u(
login: Value.absentIfNull(login),
password: Value.absentIfNull(
password != null ? hashPassword(password) : null),
));
return true;
}
Future<bool> updateUserPassword(
UserData user, {
required String password,
}) async {
await managers.user.filter((f) => f.id(user.id)).update((u) => u(
password: Value(hashPassword(password)),
));
return true;
}
Future<bool> deleteUser(UserData user) async {
final usersCount = await managers.user.count();
if (usersCount == 0) {
return false;
}
final deletedCount = await managers.user
.filter(
(f) => f.id(user.id),
)
.delete();
return deletedCount != 0;
}
}

50
lib/db/database.dart Normal file
View file

@ -0,0 +1,50 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import '../models/enums/temperature_mode.dart';
import 'database.steps.dart';
import 'tables/product.dart';
import 'tables/product_category.dart';
import 'tables/shopping_list_item.dart';
import 'tables/storage_location.dart';
import 'tables/users_table.dart';
part 'database.g.dart';
part 'crud/product_crud.dart';
part 'crud/product_category_crud.dart';
part 'crud/shopping_list_crud.dart';
part 'crud/storage_locations_crud.dart';
part 'crud/user_crud.dart';
@DriftDatabase(
tables: [ProductCategory, Product, ShoppingListItem, StorageLocation, User])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 2;
static QueryExecutor _openConnection() {
return driftDatabase(name: "groceries_manager_db");
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: stepByStep(
from1To2: (m, schema) async {
m.createTable(schema.user);
},
),
);
}
}
extension BetterConversionStorageLocation on StorageLocationData {
TemperatureMode get temperatureModeE =>
TemperatureMode.fromName(temperatureMode);
}

3265
lib/db/database.g.dart Normal file

File diff suppressed because it is too large Load diff

273
lib/db/database.steps.dart Normal file
View file

@ -0,0 +1,273 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema {
Schema2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
productCategory,
storageLocation,
product,
shoppingListItem,
user,
];
late final Shape0 productCategory = Shape0(
source: i0.VersionedTable(
entityName: 'product_category',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 storageLocation = Shape1(
source: i0.VersionedTable(
entityName: 'storage_location',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_3,
_column_4,
_column_2,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 product = Shape2(
source: i0.VersionedTable(
entityName: 'product',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_6,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 shoppingListItem = Shape3(
source: i0.VersionedTable(
entityName: 'shopping_list_item',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_6,
_column_7,
_column_8,
_column_9,
_column_13,
_column_14,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 user = Shape4(
source: i0.VersionedTable(
entityName: 'user',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_15,
_column_16,
],
attachedDatabase: database,
),
alias: null);
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get icon =>
columnsByName['icon']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>('icon', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get temperatureMode =>
columnsByName['temperature_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get icon =>
columnsByName['icon']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isDefault =>
columnsByName['is_default']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
i1.GeneratedColumn<String>('temperature_mode', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
i1.GeneratedColumn<bool>('is_default', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_default" IN (0, 1))'),
defaultValue: const Constant(false));
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get category =>
columnsByName['category']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get storage =>
columnsByName['storage']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<double> get quantity =>
columnsByName['quantity']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<String> get unit =>
columnsByName['unit']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get purchaseDate =>
columnsByName['purchase_date']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get expiryDate =>
columnsByName['expiry_date']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get barcode =>
columnsByName['barcode']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('category', aliasedName, false,
type: i1.DriftSqlType.int,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES product_category (id)'));
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
i1.GeneratedColumn<int>('storage', aliasedName, false,
type: i1.DriftSqlType.int,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES storage_location (id)'));
i1.GeneratedColumn<double> _column_8(String aliasedName) =>
i1.GeneratedColumn<double>('quantity', aliasedName, false,
type: i1.DriftSqlType.double);
i1.GeneratedColumn<String> _column_9(String aliasedName) =>
i1.GeneratedColumn<String>('unit', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
i1.GeneratedColumn<DateTime>('purchase_date', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
i1.GeneratedColumn<DateTime>('expiry_date', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>('barcode', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(maxTextLength: 20),
type: i1.DriftSqlType.string);
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get category =>
columnsByName['category']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get storage =>
columnsByName['storage']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<double> get quantity =>
columnsByName['quantity']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<String> get unit =>
columnsByName['unit']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isPurchased =>
columnsByName['is_purchased']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get dateAdded =>
columnsByName['date_added']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<bool> _column_13(String aliasedName) =>
i1.GeneratedColumn<bool>('is_purchased', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_purchased" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<DateTime> _column_14(String aliasedName) =>
i1.GeneratedColumn<DateTime>('date_added', aliasedName, true,
type: i1.DriftSqlType.dateTime, defaultValue: currentDateAndTime);
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get login =>
columnsByName['login']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get password =>
columnsByName['password']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>('login', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
i1.GeneratedColumn<String>('password', aliasedName, false,
type: i1.DriftSqlType.string);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 1:
final schema = Schema2(database: database);
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
));

View file

@ -0,0 +1,15 @@
import 'package:drift/drift.dart';
import 'package:groceries_manager/db/tables/product_category.dart';
import 'package:groceries_manager/db/tables/storage_location.dart';
class Product extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get category => integer().references(ProductCategory, #id)();
IntColumn get storage => integer().references(StorageLocation, #id)();
RealColumn get quantity => real()();
TextColumn get unit => text()();
DateTimeColumn get purchaseDate => dateTime().nullable()();
DateTimeColumn get expiryDate => dateTime().nullable()();
TextColumn get barcode => text().withLength(max: 20)();
}

View file

@ -0,0 +1,7 @@
import 'package:drift/drift.dart';
class ProductCategory extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get icon => text()();
}

View file

@ -0,0 +1,16 @@
import 'package:drift/drift.dart';
import 'product_category.dart';
import 'storage_location.dart';
class ShoppingListItem extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get category => integer().references(ProductCategory, #id)();
IntColumn get storage => integer().references(StorageLocation, #id)();
RealColumn get quantity => real()();
TextColumn get unit => text()();
BoolColumn get isPurchased => boolean().withDefault(const Constant(false))();
DateTimeColumn get dateAdded =>
dateTime().withDefault(currentDateAndTime).nullable()();
}

View file

@ -0,0 +1,10 @@
import 'package:drift/drift.dart';
class StorageLocation extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get description => text()();
TextColumn get temperatureMode => text()();
TextColumn get icon => text()();
BoolColumn get isDefault => boolean().withDefault(const Constant(false))();
}

View file

@ -0,0 +1,7 @@
import 'package:drift/drift.dart';
class User extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get login => text().unique()();
TextColumn get password => text()();
}

60
lib/main.dart Normal file
View file

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:toastification/toastification.dart';
import 'db/database.dart';
import 'pages/login/login_controller.dart';
import 'pages/login/login_page.dart';
import 'pages/main/home_controller.dart';
import 'pages/main/home_page.dart';
import 'pages/redirect/redirect_controller.dart';
import 'pages/redirect/redirect_page.dart';
import 'services/db_service.dart';
import 'services/toaster_service.dart';
void main() async {
Get.put(ToasterService());
await Get.put(DBService()).init();
WidgetsFlutterBinding.ensureInitialized();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return ToastificationWrapper(
child: GetMaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: "/",
getPages: [
GetPage(
name: "/",
page: () {
final user = Get.arguments;
if (user is! UserData) {
Get.put(RedirectController(redirectTo: "/login"));
return RedirectPage();
}
Get.put(HomeController(), permanent: true).setUser(user);
return const HomePage();
},
),
GetPage(
name: "/login",
page: () {
Get.put(LoginController());
return const LoginPage();
},
),
],
),
);
}
}

View file

@ -0,0 +1,23 @@
enum TemperatureMode {
refrigerated,
frozen,
dryRoom,
unspecified;
factory TemperatureMode.fromName(String name) {
for (final v in values) {
if (v.name == name) {
return v;
}
}
return unspecified;
}
String get betterName => switch (this) {
TemperatureMode.refrigerated => "Охлаждённое",
TemperatureMode.frozen => "Замороженное",
TemperatureMode.dryRoom => "Сухое",
TemperatureMode.unspecified => "Не указано",
};
}

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/db/database.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
enum LoginState {
idle,
loading,
}
class LoginController extends GetxController {
final loginController = TextEditingController();
final passwordController = TextEditingController();
final anyUserExists = false.obs;
final loginState = LoginState.idle.obs;
@override
void onInit() {
super.onInit();
init();
}
Future<void> init() async {
loginState.value = LoginState.loading;
anyUserExists.value = await DBService.to.db.anyUserExists();
loginState.value = LoginState.idle;
}
@override
void onClose() {
loginController.dispose();
passwordController.dispose();
super.onClose();
}
String sanitize(String value) {
return value.replaceAll(RegExp(r'[^а-яА-Яa-zA-Z0-9]'), '');
}
Future<void> register() async {
final login = loginController.text;
final password = passwordController.text;
if (login.isEmpty || password.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Логин и пароль не могут быть пустыми",
);
return;
}
loginState.value = LoginState.loading;
final UserData? user;
try {
user = await DBService.to.db.addUser(
login: login,
password: password,
);
} catch (e) {
ToasterService.to.error(
title: "Ошибка",
message: "Пользователь с таким логином уже существует",
);
return;
}
loginState.value = LoginState.idle;
if (user == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Пользователь с таким логином уже существует",
);
return;
}
ToasterService.to.success(
title: "Успех",
message: "Пользователь успешно зарегистрирован",
);
Get.offAllNamed("/", arguments: user);
}
Future<void> login() async {
final login = loginController.text;
final password = passwordController.text;
if (login.isEmpty || password.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Логин и пароль не могут быть пустыми!",
);
return;
}
loginState.value = LoginState.loading;
final user = await DBService.to.db.findUser(
login: login,
password: password,
);
loginState.value = LoginState.idle;
if (user == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Пользователь с такой связкой данных не существует!",
);
return;
}
ToasterService.to.success(
title: "Успех",
message: "Привет, ${user.login}!",
);
Get.offAllNamed("/", arguments: user);
}
}

View file

@ -0,0 +1,93 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'login_controller.dart';
class LoginPage extends GetView<LoginController> {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return Obx(
() => Scaffold(
body: switch (controller.loginState.value) {
LoginState.idle => Center(
child: SizedBox(
width: max(size.width * 0.3, 300),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Представьтесь!",
style: TextStyle(
fontSize: 20,
),
),
const SizedBox(height: 16),
TextFormField(
controller: controller.loginController,
decoration: const InputDecoration(
label: Text("Логин"),
),
onChanged: (value) {
controller.loginController.text =
controller.sanitize(value);
},
),
const SizedBox(height: 4),
TextFormField(
controller: controller.passwordController,
decoration: const InputDecoration(
label: Text("Пароль"),
),
obscureText: true,
onChanged: (value) {
controller.passwordController.text =
controller.sanitize(value);
},
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () => controller.register(),
child: const Text("Зарегистрироваться"),
),
if (controller.anyUserExists.value)
ElevatedButton(
onPressed: () => controller.login(),
child: const Text("Войти"),
),
],
),
],
),
),
),
LoginState.loading => const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"GM",
style: TextStyle(
fontSize: 20,
),
),
Text("Проверяем штуки..."),
SizedBox(height: 16),
CircularProgressIndicator(),
],
),
),
},
),
);
}
}

View file

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
import '../../../db/database.dart';
class CategoryEditorController extends GetxController {
final editedCategory = Rxn<ProductCategoryData>();
final categoryIcon = ProductCategoryIcons.fork.obs;
final nameController = TextEditingController();
void setEditedCategory(ProductCategoryData ecd) {
editedCategory.value = ecd;
nameController.text = ecd.name;
categoryIcon.value = ProductCategoryIcons.fromName(ecd.icon);
}
Future<void> save() async {
final text = nameController.text.trim();
if (text.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Имя категории не может быть пустым",
);
return;
}
if (editedCategory.value != null) {
await DBService.to.db.updateProductCategory(
id: editedCategory.value!.id,
icon: categoryIcon.value.name,
name: text,
);
ToasterService.to.success(
title: "Успех",
message: "Категория '$text' изменена",
);
} else {
await DBService.to.db.addProductCategory(
icon: categoryIcon.value.name,
name: text,
);
ToasterService.to.success(
title: "Успех",
message: "Категория '$text' создана",
);
}
cancel();
}
void cancel() {
Get.backLegacy();
}
}
class CategoryEditorDialog extends GetView<CategoryEditorController> {
const CategoryEditorDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: controller.editedCategory.value != null
? const Text("Редактируем категорию")
: const Text("Новая категория"),
content: SizedBox(
width: size.width * 0.5,
height: size.height * 0.9,
child: ListView(
children: [
TextFormField(
controller: controller.nameController,
decoration: const InputDecoration(label: Text("Название")),
),
Obx(
() => DropdownButtonFormField<ProductCategoryIcons>(
items: [
for (final item in ProductCategoryIcons.values)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
ProductCategoryIcons.fromName(item.name).icon,
),
const SizedBox(width: 8),
Text(item.betterName),
],
),
),
],
value: controller.categoryIcon.value,
isExpanded: true,
hint: const Text("Иконка"),
onChanged: (catIcon) {
if (catIcon != null) {
controller.categoryIcon.value = catIcon;
}
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Отмена"),
),
TextButton(
onPressed: () => controller.save(),
child: const Text("Сохранить"),
),
],
);
}
static Future<void> show({
ProductCategoryData? ecd,
}) async {
final controller = Get.put(CategoryEditorController());
if (ecd != null) {
controller.setEditedCategory(ecd);
}
await Get.dialog(
const CategoryEditorDialog(),
id: "CategoryEditorDialog",
name: "CategoryEditorDialog",
);
Get.delete<CategoryEditorController>();
}
}

View file

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
import 'package:groceries_manager/utils/get_interface_extension.dart';
import '../../../db/database.dart';
import 'category_editor_dialog.dart';
class CategoryManagerController extends GetxController {
final categories = <(ProductCategoryData, int)>[].obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
final categories = await DBService.to.db.getProductCategoriesWithCounts();
this.categories.clear();
this.categories.addAll(categories);
}
void cancel() {
Get.backLegacy();
}
Future<void> deleteCategory(ProductCategoryData item) async {
final count = categories.firstWhere((e) => e.$1.id == item.id).$2;
if (count > 0) {
ToasterService.to.error(
title: "Ошибка",
message: "Нельзя удалить категорию, к которой привязаны продукты!",
);
return;
}
final confirmed = await Get.confirm(
title: "Внимание!",
content: "Удаление категории необратимо!\n"
"Точно хотите удалить '${item.name}'?",
);
if (!confirmed) return;
await DBService.to.db.deleteProductCategory(item);
refreshData();
}
Future<void> newCategory() async {
await CategoryEditorDialog.show();
refreshData();
}
Future<void> editCategory(ProductCategoryData item) async {
await CategoryEditorDialog.show(ecd: item);
refreshData();
}
}
class CategoryManagerDialog extends GetView<CategoryManagerController> {
const CategoryManagerDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: const Text("Управление категориями"),
content: SizedBox(
width: size.width * 0.9,
height: size.height * 0.9,
child: SingleChildScrollView(
child: Obx(
() => Table(
border: TableBorder.all(color: Colors.blueGrey.withAlpha(50)),
children: [
const TableRow(
children: [
TableCell(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Иконка"),
),
),
TableCell(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Название"),
),
),
TableCell(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Продукты в категории"),
),
),
TableCell(
child: SizedBox(),
),
],
),
for (final item in controller.categories)
TableRow(
key: ValueKey(item),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
ProductCategoryIcons.fromName(item.$1.icon).icon,
),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.$1.name),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("${item.$2}"),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
ElevatedButton.icon(
onPressed: () =>
controller.editCategory(item.$1),
icon: const Icon(Icons.edit_rounded),
label: const Text("Редактировать"),
),
const SizedBox(height: 4),
ElevatedButton.icon(
onPressed: () =>
controller.deleteCategory(item.$1),
icon: const Icon(Icons.delete_forever),
label: const Text("Удалить"),
),
],
),
),
),
],
),
],
),
),
),
),
actions: [
ElevatedButton.icon(
onPressed: () => controller.newCategory(),
icon: const Icon(Icons.add),
label: const Text("Новая категория"),
),
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Закрыть"),
),
],
);
}
static Future<void> show({
ProductCategoryData? ecd,
}) async {
Get.put(CategoryManagerController());
await Get.dialog(
const CategoryManagerDialog(),
id: "CategoryManagerDialog",
name: "CategoryManagerDialog",
);
Get.delete<CategoryManagerController>();
}
}

View file

@ -0,0 +1,325 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/utils/format_datetime_extension.dart';
import 'package:groceries_manager/pages/main/dialogs/category_editor_dialog.dart';
import '../../../db/database.dart';
import '../../../services/db_service.dart';
import '../../../services/toaster_service.dart';
import 'storage_location_editor_dialog.dart';
class ProductEditorController extends GetxController {
ProductData? editedProduct;
late final RxList<ProductCategoryData> categories;
late final RxList<StorageLocationData> storages;
final nameController = TextEditingController();
final category = Rxn<ProductCategoryData>();
final storage = Rxn<StorageLocationData>();
final quantityController = TextEditingController();
final unitController = TextEditingController();
final expiryDate = Rxn<DateTime>();
final barcodeController = TextEditingController();
void setEditedProduct(ProductData epd) {
editedProduct = epd;
nameController.text = epd.name;
category.value =
categories.firstWhere((e) => e.id == editedProduct!.category);
storage.value = storages.firstWhere((e) => e.id == editedProduct!.storage);
quantityController.text = epd.quantity.toString();
unitController.text = epd.unit;
expiryDate.value = epd.expiryDate;
barcodeController.text = epd.barcode;
}
Future<void> save() async {
final name = nameController.text.trim();
if (name.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Имя категории не может быть пустым",
);
return;
}
final category = this.category.value;
if (category == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Необходимо выбрать категорию продукта",
);
return;
}
final storage = this.storage.value;
if (storage == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Необходимо выбрать место хранения продукта",
);
return;
}
final quantity = quantityController.text;
if (!quantity.isNum) {
ToasterService.to.error(
title: "Ошибка",
message: "Кол-во продукта должно быть числом",
);
return;
}
final unit = unitController.text;
if (unit.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Единица измерения продукта должна быть указана",
);
return;
}
final expiryDate = this.expiryDate.value;
if (expiryDate == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Дата истечения срока годности должна быть указана",
);
return;
}
final barcode = barcodeController.text;
if (editedProduct != null) {
await DBService.to.db.updateProduct(
id: editedProduct!.id,
name: name,
category: category,
storage: storage,
quantity: double.parse(quantity),
unit: unit,
expiryDate: expiryDate,
barcode: barcode,
);
ToasterService.to.success(
title: "Успех",
message: "Продукт '$name' изменён",
);
} else {
await DBService.to.db.addProduct(
name: name,
category: category,
storage: storage,
quantity: double.parse(quantity),
unit: unit,
expiryDate: expiryDate,
barcode: barcode,
);
ToasterService.to.success(
title: "Успех",
message: "Продукт '$name' создан",
);
}
cancel();
}
void cancel() {
Get.backLegacy();
}
void setCategories(RxList<ProductCategoryData> categories) {
this.categories = categories;
}
void setStorages(RxList<StorageLocationData> storages) {
this.storages = storages;
}
void setStorage(StorageLocationData storage) {
this.storage.value = storage;
}
Future<void> newCategory() async {
await CategoryEditorDialog.show();
}
Future<void> newStorage() async {
await StorageLocationEditorDialog.show();
}
}
class ProductEditorDialog extends GetView<ProductEditorController> {
const ProductEditorDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: controller.editedProduct != null
? const Text("Редактируем продукт")
: const Text("Новый продукт"),
content: SizedBox(
width: size.width * 0.5,
height: size.height * 0.9,
child: ListView(
children: [
TextFormField(
controller: controller.nameController,
decoration: const InputDecoration(label: Text("Наименование")),
),
Row(
children: [
Expanded(
child: Obx(
() => DropdownButtonFormField<ProductCategoryData>(
items: [
for (final item in controller.categories)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
ProductCategoryIcons.fromName(item.icon).icon,
),
const SizedBox(width: 8),
Text(item.name),
],
),
),
],
value: controller.category.value,
isExpanded: true,
hint: const Text("Категория"),
onChanged: (cat) {
if (cat != null) {
controller.category.value = cat;
}
},
),
),
),
IconButton(
onPressed: () => controller.newCategory(),
icon: const Icon(Icons.add),
),
],
),
Row(
children: [
Expanded(
child: Obx(
() => DropdownButtonFormField<StorageLocationData>(
items: [
for (final item in controller.storages)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
StorageLocationIcon.fromName(item.icon).icon,
),
const SizedBox(width: 8),
Text(item.name),
],
),
),
],
value: controller.storage.value,
isExpanded: true,
hint: const Text("Место хранения"),
onChanged: (st) {
if (st != null) {
controller.storage.value = st;
}
},
),
),
),
IconButton(
onPressed: () => controller.newStorage(),
icon: const Icon(Icons.add),
),
],
),
TextFormField(
controller: controller.quantityController,
decoration: const InputDecoration(label: Text("Количество")),
),
TextFormField(
controller: controller.unitController,
decoration:
const InputDecoration(label: Text("Единица измерения")),
),
Obx(
() => TextFormField(
readOnly: true,
controller: TextEditingController(
text: controller.expiryDate.value?.simpleDateFormat,
),
onTap: () async {
final dt = await showDatePicker(
context: context,
initialDate: controller.expiryDate.value ??
DateTime.now().add(1.days),
firstDate: DateTime.now(),
lastDate: DateTime(9999),
);
if (dt != null) {
controller.expiryDate.value = dt;
}
},
decoration: const InputDecoration(
label: Text("Годен до"),
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Отмена"),
),
TextButton(
onPressed: () => controller.save(),
child: const Text("Сохранить"),
),
],
);
}
static Future<void> show({
required RxList<ProductCategoryData> categories,
required RxList<StorageLocationData> storages,
ProductData? editedProduct,
StorageLocationData? preselectedStorage,
}) async {
final controller = Get.put(ProductEditorController());
controller.setCategories(categories);
controller.setStorages(storages);
if (preselectedStorage != null) {
controller.setStorage(preselectedStorage);
}
if (editedProduct != null) {
controller.setEditedProduct(editedProduct);
}
const transDuration = Duration(milliseconds: 300);
await Get.dialog(
const ProductEditorDialog(),
id: "ProductEditorDialog",
name: "ProductEditorDialog",
transitionDuration: transDuration,
);
await (transDuration + 10.milliseconds).delay();
Get.delete<ProductEditorController>();
}
}

View file

@ -0,0 +1,286 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/pages/main/dialogs/category_editor_dialog.dart';
import 'package:groceries_manager/pages/main/dialogs/storage_location_editor_dialog.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
import '../../../db/database.dart';
class ShoppingItemEditorController extends GetxController {
final editedItem = Rxn<ShoppingListItemData>();
late final RxList<ProductCategoryData> categories;
late final RxList<StorageLocationData> storages;
final nameController = TextEditingController();
final category = Rxn<ProductCategoryData>();
final storage = Rxn<StorageLocationData>();
final quantityController = TextEditingController();
final unitController = TextEditingController();
void setEditedItem(
(ShoppingListItemData, ProductCategoryData, StorageLocationData) esi) {
final (item, category, storage) = esi;
editedItem.value = item;
nameController.text = item.name;
this.category.value = category;
this.storage.value = storage;
quantityController.text = item.quantity.toString();
unitController.text = item.unit;
}
void fromProduct(ProductData product) {
nameController.text = product.name;
category.value = categories.firstWhere((e) => e.id == product.category);
storage.value = storages.firstWhere((e) => e.id == product.storage);
quantityController.text = product.quantity.toString();
unitController.text = product.unit;
}
Future<void> save({bool clone = false}) async {
final name = nameController.text.trim();
if (name.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Имя категории не может быть пустым",
);
return;
}
final category = this.category.value;
if (category == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Необходимо выбрать категорию продукта",
);
return;
}
final storage = this.storage.value;
if (storage == null) {
ToasterService.to.error(
title: "Ошибка",
message: "Необходимо выбрать место хранения продукта",
);
return;
}
final quantity = quantityController.text;
if (!quantity.isNum) {
ToasterService.to.error(
title: "Ошибка",
message: "Кол-во продукта должно быть числом",
);
return;
}
final unit = unitController.text;
if (unit.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Единица измерения продукта должна быть указана",
);
return;
}
if (editedItem.value != null && !clone) {
await DBService.to.db.updateShoppingListItem(
id: editedItem.value!.id,
name: name,
category: category,
storage: storage,
quantity: double.parse(quantity),
unit: unit,
);
ToasterService.to.success(
title: "Успех",
message: "Запись списка покупок изменена: "
"'$name' - $quantity $unit",
);
} else {
await DBService.to.db.addShoppingListItem(
name: name,
category: category,
storage: storage,
quantity: double.parse(quantity),
unit: unit,
);
ToasterService.to.success(
title: "Успех",
message: "Запись добавлена в список покупок: "
"'$name' - $quantity $unit",
);
}
cancel();
}
void cancel() {
Get.backLegacy();
}
Future<void> newCategory() async {
await CategoryEditorDialog.show();
}
Future<void> newStorage() async {
await StorageLocationEditorDialog.show();
}
}
class ShoppingItemEditorDialog extends GetView<ShoppingItemEditorController> {
const ShoppingItemEditorDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: controller.editedItem.value != null
? const Text("Редактируем запись списка покупок")
: const Text("Новая запись списка покупок"),
content: SizedBox(
width: size.width * 0.5,
height: size.height * 0.9,
child: ListView(
children: [
TextFormField(
controller: controller.nameController,
decoration: const InputDecoration(label: Text("Наименование")),
),
Row(
children: [
Expanded(
child: Obx(
() => DropdownButtonFormField<ProductCategoryData>(
items: [
for (final item in controller.categories)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
ProductCategoryIcons.fromName(item.icon).icon,
),
const SizedBox(width: 8),
Text(item.name),
],
),
),
],
value: controller.category.value,
isExpanded: true,
hint: const Text("Категория"),
onChanged: (prod) {
if (prod != null) {
controller.category.value = prod;
}
},
),
),
),
IconButton(
onPressed: () => controller.newCategory(),
icon: const Icon(Icons.add),
),
],
),
Row(
children: [
Expanded(
child: Obx(
() => DropdownButtonFormField<StorageLocationData>(
items: [
for (final item in controller.storages)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
StorageLocationIcon.fromName(item.icon).icon,
),
const SizedBox(width: 8),
Text(item.name),
],
),
),
],
value: controller.storage.value,
isExpanded: true,
hint: const Text("Место хранения"),
onChanged: (item) {
if (item != null) {
controller.storage.value = item;
}
},
),
),
),
IconButton(
onPressed: () => controller.newStorage(),
icon: const Icon(Icons.add),
),
],
),
TextFormField(
controller: controller.quantityController,
decoration: const InputDecoration(label: Text("Количество")),
),
TextFormField(
controller: controller.unitController,
decoration:
const InputDecoration(label: Text("Единица измерения")),
),
],
),
),
actions: [
if (controller.editedItem.value != null)
TextButton(
onPressed: () => controller.save(clone: true),
child: const Text("Клонировать"),
),
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Отмена"),
),
TextButton(
onPressed: () => controller.save(),
child: const Text("Сохранить"),
),
],
);
}
static Future<void> show({
required RxList<ProductCategoryData> categories,
required RxList<StorageLocationData> storages,
(
ShoppingListItemData,
ProductCategoryData,
StorageLocationData
)? editedItem,
ProductData? fromProduct,
}) async {
final controller = Get.put(ShoppingItemEditorController());
controller.categories = categories;
controller.storages = storages;
if (editedItem != null) {
controller.setEditedItem(editedItem);
}
if (fromProduct != null) {
controller.fromProduct(fromProduct);
}
await Get.dialog(
const ShoppingItemEditorDialog(),
id: "ShoppingItemEditorDialog",
name: "ShoppingItemEditorDialog",
);
Get.delete<ShoppingItemEditorController>();
}
}

View file

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
import 'package:icons_plus/icons_plus.dart';
import '../../../db/database.dart';
import '../../../models/enums/temperature_mode.dart';
class StorageLocationEditorController extends GetxController {
final editedStorageLocation = Rxn<StorageLocationData>();
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final temperatureMode = TemperatureMode.unspecified.obs;
final storageLocationIcon = StorageLocationIcon.box_2.obs;
final setAsDefault = false.obs;
void setEditedStorageLocation(StorageLocationData ecd) {
editedStorageLocation.value = ecd;
nameController.text = ecd.name;
descriptionController.text = ecd.description;
temperatureMode.value = TemperatureMode.fromName(ecd.temperatureMode);
storageLocationIcon.value = StorageLocationIcon.fromName(ecd.icon);
}
Future<void> save() async {
final name = nameController.text.trim();
if (name.isEmpty) {
ToasterService.to.error(
title: "Ошибка",
message: "Имя места хранения не может быть пустым",
);
return;
}
final description = descriptionController.text.trim();
if (editedStorageLocation.value != null) {
final updateId = await DBService.to.db.updateStorageLocation(
id: editedStorageLocation.value!.id,
name: name,
description: description,
temperatureMode: temperatureMode.value,
icon: storageLocationIcon.value.name,
);
if (setAsDefault.value) {
await DBService.to.db.switchDefault(updateId);
}
ToasterService.to.success(
title: "Успех",
message: "Место хранения '$name' изменено",
);
} else {
final newId = await DBService.to.db.addStorageLocation(
name: name,
description: description,
temperatureMode: temperatureMode.value,
icon: storageLocationIcon.value.name,
);
if (setAsDefault.value) {
await DBService.to.db.switchDefault(newId);
}
ToasterService.to.success(
title: "Успех",
message: "Место хранения '$name' создано",
);
}
cancel();
}
void cancel() {
Get.backLegacy();
}
}
class StorageLocationEditorDialog
extends GetView<StorageLocationEditorController> {
const StorageLocationEditorDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: controller.editedStorageLocation.value != null
? const Text("Редактируем место хранения")
: const Text("Новое место хранения"),
content: SizedBox(
width: size.width * 0.5,
height: size.height * 0.9,
child: ListView(
children: [
TextFormField(
controller: controller.nameController,
decoration: const InputDecoration(label: Text("Название")),
),
TextFormField(
controller: controller.descriptionController,
decoration: const InputDecoration(label: Text("Описание")),
),
Obx(
() => DropdownButtonFormField<TemperatureMode>(
items: [
for (final tempMode in TemperatureMode.values)
DropdownMenuItem(
value: tempMode,
child: Text(tempMode.betterName),
),
],
value: controller.temperatureMode.value,
isExpanded: true,
icon: const Icon(FontAwesome.temperature_empty_solid),
hint: const Text("Температурный режим"),
onChanged: (tempMode) {
if (tempMode != null) {
controller.temperatureMode.value = tempMode;
}
},
),
),
Obx(
() => DropdownButtonFormField<StorageLocationIcon>(
items: [
for (final item in StorageLocationIcon.values)
DropdownMenuItem(
value: item,
child: Row(
children: [
Icon(
StorageLocationIcon.fromName(item.name).icon,
),
const SizedBox(width: 8),
Text(item.betterName),
],
),
),
],
value: controller.storageLocationIcon.value,
isExpanded: true,
hint: const Text("Иконка"),
onChanged: (slIcon) {
if (slIcon != null) {
controller.storageLocationIcon.value = slIcon;
}
},
),
),
Obx(
() => SwitchListTile(
contentPadding: EdgeInsets.zero,
value: controller.setAsDefault.value,
onChanged: (newVal) {
controller.setAsDefault.value = newVal;
},
title: const Text("Назначить основным"),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Отмена"),
),
TextButton(
onPressed: () => controller.save(),
child: const Text("Сохранить"),
),
],
);
}
static Future<void> show({
StorageLocationData? esl,
}) async {
final controller = Get.put(StorageLocationEditorController());
if (esl != null) {
controller.setEditedStorageLocation(esl);
}
await Get.dialog(
const StorageLocationEditorDialog(),
id: "StorageLocationEditorDialog",
name: "StorageLocationEditorDialog",
);
Get.delete<StorageLocationEditorController>();
}
}

View file

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/pages/main/home_controller.dart';
import 'package:groceries_manager/services/db_service.dart';
import 'package:groceries_manager/services/toaster_service.dart';
import 'package:groceries_manager/utils/get_interface_extension.dart';
import '../../../db/database.dart';
class UserManagerController extends GetxController {
final users = <UserData>[].obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
final users = await DBService.to.db.getUsers();
this.users.clear();
this.users.addAll(users);
}
void cancel() {
Get.backLegacy();
}
Future<void> changeUserPassword(UserData item) async {
final newPassword = await Get.prompt(
title: "Изменение пароля для '${item.login}'",
label: "Новый пароль",
stringValidator: (value) {
if (RegExp(r'[^а-яА-Яa-zA-Z0-9]').hasMatch(value ?? "")) {
return "Пароль не соответствует требованиям: а-яА-Яa-zA-Z0-9";
}
return null;
},
);
if (newPassword == null) {
return;
}
await DBService.to.db.updateUserPassword(
item,
password: newPassword,
);
refreshData();
}
Future<void> deleteUser(UserData item) async {
final currentUser = Get<HomeController>().user.value;
if (currentUser.id == item.id) {
ToasterService.to.error(
title: "Ошибка",
message: "Нельзя удалить самого себя!",
);
return;
}
final confirmed = await Get.confirm(
title: "Внимание!",
content: "Удаление пользователя необратимо!\n"
"Точно хотите удалить '${item.login}'?",
);
if (!confirmed) return;
final deleted = await DBService.to.db.deleteUser(item);
if (!deleted) {
ToasterService.to.error(
title: "Ошибка",
message: "Нельзя единственного пользователя!",
);
}
refreshData();
}
}
class UserManagerDialog extends GetView<UserManagerController> {
const UserManagerDialog({super.key});
@override
Widget build(BuildContext context) {
final size = Get.size;
return AlertDialog(
title: const Text("Управление категориями"),
content: SizedBox(
width: size.width * 0.9,
height: size.height * 0.9,
child: SingleChildScrollView(
child: Obx(
() => Table(
border: TableBorder.all(color: Colors.blueGrey.withAlpha(50)),
children: [
const TableRow(
children: [
TableCell(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Логин"),
),
),
TableCell(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Частичный хэш пароля"),
),
),
TableCell(
child: SizedBox(),
),
],
),
for (final item in controller.users)
TableRow(
key: ValueKey(item),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.login),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("${item.password.substring(0, 20)}..."),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
ElevatedButton.icon(
onPressed: () =>
controller.changeUserPassword(item),
icon: const Icon(Icons.edit_rounded),
label: const Text("Изменить пароль"),
),
const SizedBox(height: 4),
ElevatedButton.icon(
onPressed: () => controller.deleteUser(item),
icon: const Icon(Icons.delete_forever),
label: const Text("Удалить"),
),
],
),
),
),
],
),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => controller.cancel(),
child: const Text("Закрыть"),
),
],
);
}
static Future<void> show({
UserData? ecd,
}) async {
Get.put(UserManagerController());
await Get.dialog(
const UserManagerDialog(),
id: "UserManagerDialog",
name: "UserManagerDialog",
);
Get.delete<UserManagerController>();
}
}

View file

@ -0,0 +1,313 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:get/get.dart';
import '../../db/database.dart';
import '../../services/db_service.dart';
import '../../services/toaster_service.dart';
import '../../utils/get_interface_extension.dart';
import 'dialogs/category_manager_dialog.dart';
import 'dialogs/product_editor_dialog.dart';
import 'dialogs/shopping_item_editor_dialog.dart';
import 'dialogs/storage_location_editor_dialog.dart';
import 'dialogs/user_manager_dialog.dart';
class HomeController extends GetxController {
final soonExpiries =
<(ProductData, ProductCategoryData, StorageLocationData)>[].obs;
final stockProducts =
<(StorageLocationData, List<(ProductData, ProductCategoryData)>)>[].obs;
final shoppingList =
<(ShoppingListItemData, ProductCategoryData, StorageLocationData)>[].obs;
final groupedShoppingList = <bool,
List<
(
ShoppingListItemData,
ProductCategoryData,
StorageLocationData
)>>{}.obs;
final products = <ProductData>[].obs;
final categories = <ProductCategoryData>[].obs;
final storages = <StorageLocationData>[].obs;
late final StreamSubscription psSub;
late final StreamSubscription seSub;
late final StreamSubscription slSub;
late final StreamSubscription pcSub;
final expandedStorages = <bool>[].obs;
late final Rx<UserData> user;
@override
void onInit() {
super.onInit();
refreshAll();
psSub = DBService.to.db.productsSubscription.listen((data) {
refreshStockProducts();
});
seSub = DBService.to.db.soonExpirySubscription.listen((data) {
refreshSoonExpiries();
});
slSub = DBService.to.db.shoppingListSubscription.listen((data) {
refreshShoppingList();
});
pcSub = DBService.to.db.productsCategoriesSubscription.listen((data) {
refreshCategoriesList();
});
}
@override
void onClose() {
psSub.cancel();
seSub.cancel();
slSub.cancel();
pcSub.cancel();
super.onClose();
}
Future<void> refreshAll() async {
await refreshCategoriesList();
await refreshStockProducts();
await refreshSoonExpiries();
await refreshShoppingList();
user.value = await DBService.to.db.getUser(user.value);
}
Future<void> refreshSoonExpiries() async {
final sExps = await DBService.to.db.getSoonExpiryProducts();
print(sExps);
soonExpiries.clear();
soonExpiries.addAll(sExps);
}
Future<void> refreshStockProducts() async {
final stock = await DBService.to.db.getStorageLocations();
storages.clear();
storages.addAll(stock.map((e) => e.$1));
final expandedDiff = expandedStorages.length - storages.length;
if (expandedDiff < 0) {
expandedStorages.addAll(List.generate(expandedDiff.abs(), (idx) => true));
} else if (expandedDiff > 0) {
expandedStorages.value = expandedStorages.sublist(
0,
expandedStorages.length - expandedDiff,
);
}
products.clear();
products.addAll(stock.map((e) => e.$2).flattened.map((e) => e.$1));
stockProducts.clear();
stockProducts.addAll(stock);
}
Future<void> refreshShoppingList() async {
final stock = await DBService.to.db.getShoppingList();
shoppingList.clear();
shoppingList.addAll(stock);
groupedShoppingList.clear();
groupedShoppingList[true] = [];
groupedShoppingList[false] = [];
for (var e in shoppingList) {
groupedShoppingList[e.$1.isPurchased]!.add(e);
}
}
Future<void> refreshCategoriesList() async {
final categories = await DBService.to.db.getProductCategories();
this.categories.clear();
this.categories.addAll(categories);
}
void addNewProduct([StorageLocationData? psld]) {
ProductEditorDialog.show(
categories: categories,
storages: storages,
preselectedStorage: psld,
);
}
Future<void> editProduct(ProductData prod) async {
await ProductEditorDialog.show(
categories: categories,
storages: storages,
editedProduct: prod,
);
refreshAll();
}
Future<void> deleteProduct(ProductData prod) async {
final confirmed = await Get.confirm(
title: "Внимание!",
content: "Удаление продукта необратимо!\n"
"Точно хотите удалить '${prod.name} - ${prod.quantity} ${prod.unit}'?",
);
if (!confirmed) return;
await DBService.to.db.deleteProduct(prod);
ToasterService.to.success(
title: "Успех",
message: "Продукт '${prod.name}' удалён",
);
refreshAll();
}
Future<void> editStorageLocation(StorageLocationData storage) async {
await StorageLocationEditorDialog.show(esl: storage);
refreshAll();
}
Future<void> deleteStorageLocation(StorageLocationData storage) async {
final confirmed = await Get.confirm(
title: "Внимание!",
content: "Удаление места хранения необратимо!\n"
"Точно хотите удалить '${storage.name}'?",
);
if (!confirmed) return;
for (final (storageItem, items) in stockProducts) {
if (storageItem.id == storage.id && items.isNotEmpty) {
ToasterService.to.error(
title: "Ошибка",
message:
"Нельзя удалить место хранения, к которому привязаны предметы!",
);
return;
}
}
await DBService.to.db.deleteStorageLocation(storage);
ToasterService.to.success(
title: "Успех",
message: "Место хранения '${storage.name}' удалено",
);
refreshAll();
}
Future<void> addNewShoppingItem([ProductData? ppd]) async {
await ShoppingItemEditorDialog.show(
categories: categories,
storages: storages,
);
refreshShoppingList();
}
Future<void> switchShoppingItem(
ShoppingListItemData item,
bool isPurchased,
) async {
await DBService.to.db.updateShoppingListItem(
id: item.id,
isPurchased: isPurchased,
);
refreshShoppingList();
}
Future<void> editShoppingItem(
ShoppingListItemData item,
ProductCategoryData category,
StorageLocationData location,
) async {
await ShoppingItemEditorDialog.show(
categories: categories,
storages: storages,
editedItem: (item, category, location),
);
refreshShoppingList();
}
Future<void> deleteShoppingItem(ShoppingListItemData item) async {
final confirmed = await Get.confirm(
title: "Внимание!",
content: "Удаление записи из списка покупок необратимо!\n"
"Точно хотите удалить '${item.name} - ${item.quantity} ${item.unit}'?",
);
if (!confirmed) return;
await DBService.to.db.deleteShoppingListItem(item);
refreshShoppingList();
}
Future<void> saveShoppingItem(ShoppingListItemData item,
ProductCategoryData category, StorageLocationData location) async {
final expiryDate = await showDatePicker(
context: Get.context!,
helpText: "Срок годности продукта из списка покупок",
initialDate: DateTime.now().add(1.days),
firstDate: DateTime.now(),
lastDate: DateTime(9999),
);
if (expiryDate == null) {
return;
}
await DBService.to.db.deleteShoppingListItem(item);
await DBService.to.db.addProduct(
name: item.name,
category: category,
storage: location,
quantity: item.quantity,
unit: item.unit,
expiryDate: expiryDate,
barcode: "",
);
ToasterService.to.success(
title: "Успех",
message: "Продукт '${item.name}' перемещён "
"из списка покупок в список продуктов",
);
}
Future<void> addNewStorage() async {
await StorageLocationEditorDialog.show();
refreshStockProducts();
}
Future<void> addToShoppingList(ProductData p) async {
await ShoppingItemEditorDialog.show(
categories: categories,
storages: storages,
fromProduct: p,
);
refreshShoppingList();
}
void setUser(UserData user) {
this.user = Rx<UserData>(user);
}
Future<void> openCategoryManagerDialog() async {
await CategoryManagerDialog.show();
refreshAll();
}
Future<void> openUserManagerDialog() async {
await UserManagerDialog.show();
refreshAll();
}
Future<void> logout() async {
Get.offAllNamed("/login");
Get.delete<HomeController>(force: true);
}
}

View file

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';
import 'widgets/shopping_list_card.dart';
import 'widgets/soon_expiries_card.dart';
import 'widgets/stock_products_card.dart';
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Управление продуктами"),
actions: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() => Text("Привет, ${controller.user.value.login}!"),
),
const SizedBox(width: 8),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => controller.openCategoryManagerDialog(),
child: const ListTile(
title: Text("Управление категориями"),
),
),
PopupMenuItem(
onTap: () => controller.openUserManagerDialog(),
child: const ListTile(
title: Text("Управление пользователями"),
),
),
const PopupMenuItem(
padding: EdgeInsets.zero,
enabled: false,
child: PopupMenuDivider(),
),
PopupMenuItem(
onTap: () => controller.logout(),
child: const ListTile(
title: Text("Выйти"),
),
),
],
),
],
),
],
),
body: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card.filled(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.timelapse),
title: const Text("Скоро испортится!"),
subtitle: const Text("Надо о них позаботиться"),
trailing: IconButton(
onPressed: () => controller.refreshSoonExpiries(),
icon: const Icon(Icons.refresh_rounded),
),
),
const SoonExpiriesCard(),
],
),
),
),
Expanded(
child: Card.filled(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.shelves),
title: const Text("Продукты в наличии"),
subtitle: const Text("Эти у нас есть"),
trailing: IconButton(
onPressed: () => controller.addNewStorage(),
icon: const Icon(Icons.add_rounded),
),
),
const StockProductsCard()
],
),
),
),
Expanded(
child: Card.filled(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.shopping_bag_outlined),
title: const Text("Список покупок"),
subtitle: const Text("Это надо купить"),
trailing: IconButton(
onPressed: () => controller.addNewShoppingItem(),
icon: const Icon(Icons.add_rounded),
),
),
const ShoppingListCard()
],
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/common/icons.dart';
import 'package:groceries_manager/db/database.dart';
import 'package:groceries_manager/utils/format_datetime_extension.dart';
import 'package:groceries_manager/utils/pluralize_int_extension.dart';
class ProductWidget extends GetWidget {
final ProductData prod;
final ProductCategoryData cat;
final StorageLocationData store;
final void Function(ProductData) onEditClicked;
final void Function(ProductData) onDeleteClicked;
final void Function(ProductData) onAddToCartClicked;
const ProductWidget({
super.key,
required this.prod,
required this.cat,
required this.store,
required this.onEditClicked,
required this.onDeleteClicked,
required this.onAddToCartClicked,
});
@override
Widget build(BuildContext context) {
print(prod);
return Card.outlined(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
top: 4,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
key: ValueKey("se-item-${prod.id}"),
contentPadding: EdgeInsets.zero,
title: Text(prod.name),
subtitle: Text(
"Куплено: ${prod.purchaseDate?.simpleDateFormat}\n"
"Испортится через ${() {
final diff =
prod.expiryDate!.difference(DateTime.now());
if (diff.inDays == 0) {
return diff.inHours.pluralize(
name: "час",
absent: "часа",
absentMul: "часов",
);
}
return (diff.inDays + (diff.inHours > 24 ? 1 : 0))
.pluralize(
name: "день",
absent: "дня",
absentMul: "дней",
);
}()}",
),
),
const SizedBox(height: 16),
Chip(
avatar: Icon(
ProductCategoryIcons.fromName(cat.icon).icon,
),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
label: Text(cat.name),
),
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
onPressed: () => onAddToCartClicked(prod),
icon: const Icon(Icons.add_shopping_cart),
),
IconButton(
onPressed: () => onEditClicked(prod),
icon: const Icon(Icons.edit),
),
IconButton(
onPressed: () => onDeleteClicked(prod),
icon: const Icon(Icons.delete_forever),
),
],
),
],
),
),
);
}
}

View file

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/pages/main/home_controller.dart';
class ShoppingListCard extends GetView<HomeController> {
const ShoppingListCard({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
child: Obx(
() => controller.shoppingList.isEmpty
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_rounded, size: 64),
SizedBox(height: 16),
Text("Всё в наличии!"),
],
),
)
: Obx(
() => ListView.builder(
itemCount: controller.shoppingList.length + 3,
itemBuilder: (context, index) {
if (index == 0) {
return const ListTile(
title: Text("Купить"),
);
}
index -= 1;
final toBuyItems = controller.groupedShoppingList[false]!;
if (index < toBuyItems.length) {
final (item, category, location) = toBuyItems[index];
return Row(
children: [
Expanded(
child: ListTile(
dense: true,
key: ValueKey("slc-cbi-${item.id}-true"),
title: Text(item.name),
subtitle: Text("${item.quantity} ${item.unit}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: false,
onChanged: (en) {
if (en == true) {
controller.switchShoppingItem(
item,
true,
);
}
},
),
const SizedBox(width: 8),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () {
controller.editShoppingItem(
item,
category,
location,
);
},
child: const ListTile(
title: Text("Редактировать"),
),
),
PopupMenuItem(
onTap: () {
controller.deleteShoppingItem(item);
},
child: const ListTile(
title: Text("Удалить"),
),
),
],
),
],
),
),
),
],
);
}
index -= toBuyItems.length;
if (index == 0) {
return const Divider();
}
index -= 1;
if (index == 0) {
return const ListTile(
title: Text("Куплено"),
);
}
index -= 1;
final boughtItems = controller.groupedShoppingList[true]!;
if (index < boughtItems.length) {
final (item, category, location) = boughtItems[index];
return Row(
children: [
Expanded(
child: ListTile(
dense: true,
key: ValueKey("slc-cbi-${item.id}-true"),
title: Text(item.name),
subtitle: Text("${item.quantity} ${item.unit}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: true,
onChanged: (en) {
if (en == false) {
controller.switchShoppingItem(
item,
false,
);
}
},
),
const SizedBox(width: 8),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () {
controller.saveShoppingItem(
item,
category,
location,
);
},
child: const ListTile(
title: Text("Применить"),
),
),
],
),
],
),
),
),
],
);
}
return const ListTile(title: Text("UNREACHABLE"));
},
),
),
),
),
);
}
}

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:groceries_manager/pages/main/home_controller.dart';
import 'package:groceries_manager/pages/main/widgets/product_widget.dart';
class SoonExpiriesCard extends GetView<HomeController> {
const SoonExpiriesCard({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
child: Obx(
() => controller.soonExpiries.isEmpty
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.sunny,
size: 64,
),
SizedBox(height: 16),
Text("Ура, всем продуктам хорошо!"),
],
),
)
: ListView(
children: [
for (final (prod, cat, store) in controller.soonExpiries)
ProductWidget(
key: ValueKey("se-holder-${prod.id}"),
prod: prod,
cat: cat,
store: store,
onEditClicked: (p) => controller.editProduct(p),
onDeleteClicked: (p) => controller.deleteProduct(p),
onAddToCartClicked: (p) =>
controller.addToShoppingList(p),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../common/icons.dart';
import '../../../utils/pluralize_int_extension.dart';
import '../home_controller.dart';
import 'storages_list_item.dart';
class StockProductsCard extends GetView<HomeController> {
const StockProductsCard({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
clipBehavior: Clip.antiAlias,
child: Obx(
() => controller.stockProducts.isEmpty
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.question_mark_rounded,
size: 64,
),
SizedBox(height: 16),
Text("Пустовато, что-то упустили?"),
],
),
)
: const SingleChildScrollView(child: ItemsList()),
),
),
);
}
}
class ItemsList extends GetView<HomeController> {
const ItemsList({super.key});
@override
Widget build(BuildContext context) {
return Obx(
() => ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
controller.expandedStorages[index] = isExpanded;
},
children: [
for (final (idx, (storage, items))
in controller.stockProducts.indexed)
ExpansionPanel(
isExpanded: controller.expandedStorages[idx],
headerBuilder: (context, isExpanded) {
return ListTile(
leading: Icon(
StorageLocationIcon.fromName(storage.icon).icon,
),
title: Text(storage.name),
);
},
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (storage.description.isNotEmpty)
ListTile(
dense: true,
trailing: const Icon(Icons.description),
title: Text(storage.description),
),
ListTile(
dense: true,
trailing: const Icon(Icons.numbers),
title: Text(items.length.pluralize(
name: "предмет",
absent: "предмета",
absentMul: "предметов",
)),
),
ListTile(
dense: true,
onTap: () => controller.editStorageLocation(storage),
trailing: const Icon(Icons.edit),
title: const Text("Редактировать"),
),
ListTile(
dense: true,
onTap: () => controller.deleteStorageLocation(storage),
trailing: const Icon(Icons.delete_forever),
title: const Text("Удалить"),
),
ListTile(
dense: true,
onTap: () => controller.addNewProduct(storage),
trailing: const Icon(Icons.add),
title: const Text("Добавить продукт"),
),
const Divider(),
if (items.isEmpty)
const ListTile(
title: Text("Пусто :("),
),
for (final (prod, cat) in items)
StoragesListItem(
key: ValueKey(prod.hashCode + cat.hashCode),
prod: prod,
cat: cat,
),
],
),
)
],
),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../common/icons.dart';
import '../../../db/database.dart';
import '../../../utils/format_datetime_extension.dart';
import '../../../utils/pluralize_int_extension.dart';
import '../home_controller.dart';
class StoragesListItem extends GetWidget<HomeController> {
final ProductData prod;
final ProductCategoryData cat;
const StoragesListItem({
super.key,
required this.prod,
required this.cat,
});
@override
Widget build(BuildContext context) {
return Card.outlined(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
top: 4,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
key: ValueKey("se-item-${prod.id}"),
contentPadding: EdgeInsets.zero,
title: Text(prod.name),
subtitle: Text(
"Куплено: ${prod.purchaseDate?.simpleDateFormat}\n"
"Испортится через ${() {
final diff =
prod.expiryDate!.difference(DateTime.now());
if (diff.inDays == 0) {
return diff.inHours.pluralize(
name: "час",
absent: "часа",
absentMul: "часов",
);
}
return (diff.inDays + (diff.inHours > 24 ? 1 : 0))
.pluralize(
name: "день",
absent: "дня",
absentMul: "дней",
);
}()}",
),
),
const SizedBox(height: 16),
Wrap(
spacing: 4,
runSpacing: 4,
children: [
Chip(
avatar: Icon(
ProductCategoryIcons.fromName(cat.icon).icon,
),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
label: Text(cat.name),
),
Chip(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
label: Text("${prod.quantity} ${prod.unit}"),
),
],
),
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
onPressed: () => controller.addToShoppingList(prod),
icon: const Icon(Icons.add_shopping_cart),
),
IconButton(
onPressed: () => controller.editProduct(prod),
icon: const Icon(Icons.edit),
),
IconButton(
onPressed: () => controller.deleteProduct(prod),
icon: const Icon(Icons.delete_forever),
),
],
),
],
),
),
);
}
}

View file

@ -0,0 +1,18 @@
import 'package:get/get.dart';
class RedirectController extends GetxController {
final String redirectTo;
RedirectController({
required this.redirectTo,
});
@override
void onReady() {
super.onReady();
500.milliseconds.delay(() {
Get.offAllNamed(redirectTo);
});
}
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'redirect_controller.dart';
class RedirectPage extends GetView<RedirectController> {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Перенаправляю..."),
],
),
),
);
}
}

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