init
@@ -0,0 +1,173 @@
|
|||||||
|
# Do not remove or rename entries in this file, only add new ones
|
||||||
|
# See https://github.com/flutter/flutter/issues/128635 for more context.
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.lock
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# As packages are no longer pinned, we use a lockfile for testing locally.
|
||||||
|
# When unpinning packages, Using lockfiles ensures that failures in PRs are
|
||||||
|
# actually due to those PRs, not due to a package being updated.
|
||||||
|
!/pubspec.lock
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Visual Studio Code related
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.vscode/*
|
||||||
|
.ccls-cache
|
||||||
|
|
||||||
|
# This file, on the master branch, should never exist or be checked-in.
|
||||||
|
#
|
||||||
|
# On a *final* release branch, that is, what will ship to stable or beta, the
|
||||||
|
# file can be force added (git add --force) and checked-in in order to effectively
|
||||||
|
# "pin" the engine artifact version so the flutter tool does not need to use git
|
||||||
|
# to determine the engine artifacts.
|
||||||
|
#
|
||||||
|
# See https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md.
|
||||||
|
/bin/internal/engine.version
|
||||||
|
|
||||||
|
# Flutter repo-specific
|
||||||
|
/bin/cache/
|
||||||
|
/bin/internal/bootstrap.bat
|
||||||
|
/bin/internal/bootstrap.sh
|
||||||
|
/bin/internal/engine.realm
|
||||||
|
/bin/mingit/
|
||||||
|
/dev/benchmarks/mega_gallery/
|
||||||
|
/dev/bots/.recipe_deps
|
||||||
|
/dev/bots/android_tools/
|
||||||
|
/dev/devicelab/ABresults*.json
|
||||||
|
/dev/docs/doc/
|
||||||
|
/dev/docs/api_docs.zip
|
||||||
|
/dev/docs/flutter.docs.zip
|
||||||
|
/dev/docs/lib/
|
||||||
|
/dev/docs/pubspec.yaml
|
||||||
|
/dev/integration_tests/**/xcuserdata
|
||||||
|
/dev/integration_tests/**/Pods
|
||||||
|
/packages/flutter/coverage/
|
||||||
|
version
|
||||||
|
analysis_benchmark.json
|
||||||
|
|
||||||
|
# packages file containing multi-root paths
|
||||||
|
.packages.generated
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
**/generated_plugin_registrant.dart
|
||||||
|
.packages
|
||||||
|
.pub-preload-cache/
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
.gradle/
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/android/key.properties
|
||||||
|
*.jks
|
||||||
|
local.properties
|
||||||
|
**/.cxx/
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/ephemeral
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
**/macos/Flutter/ephemeral
|
||||||
|
**/xcuserdata/
|
||||||
|
|
||||||
|
# Swift package manager (for code indexing)
|
||||||
|
**/.build/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
**/windows/flutter/ephemeral/
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/ephemeral/
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Symbols
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
!/dev/ci/**/Gemfile.lock
|
||||||
|
!.vscode/settings.json
|
||||||
|
|
||||||
|
# Monorepo
|
||||||
|
.cipd
|
||||||
|
.gclient
|
||||||
|
.gclient_entries
|
||||||
|
.python-version
|
||||||
|
.gclient_previous_custom_vars
|
||||||
|
.gclient_previous_sync_commits
|
||||||
|
|
||||||
|
# AI related
|
||||||
|
.agents/rules/personal-*
|
||||||
|
.agents/skills/personal-*
|
||||||
|
/.agent-shell/
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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: "ad70ec4617166f1c38e5d2bfd388af71fda14f06"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: android
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: web
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: windows
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
|
||||||
|
# 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'
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# kooltab2
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||||
|
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.brammie15.kooltab2"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "dev.brammie15.kooltab2"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="kooltab2"
|
||||||
|
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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.brammie15.kooltab2
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
# This newDsl flag was added by the Flutter template
|
||||||
|
android.newDsl=false
|
||||||
|
# This builtInKotlin flag was added by the Flutter template
|
||||||
|
android.builtInKotlin=false
|
||||||
@@ -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-9.1.0-all.zip
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "9.0.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
- drift: true
|
||||||
|
- provider: true
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kooltab2/theme.dart';
|
||||||
|
|
||||||
|
import 'router.dart';
|
||||||
|
|
||||||
|
class KoolTabApp extends StatelessWidget {
|
||||||
|
const KoolTabApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'KoolTab',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
routerConfig: appRouter,
|
||||||
|
// theme: ThemeData(
|
||||||
|
// useMaterial3: false,
|
||||||
|
// colorSchemeSeed: Colors.redAccent,
|
||||||
|
// brightness: Brightness.dark,
|
||||||
|
// ),
|
||||||
|
theme: neoBrutalDarkTheme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../viewmodels/bar_screen_view_model.dart';
|
||||||
|
import '../viewmodels/product_list_view_model.dart';
|
||||||
|
|
||||||
|
class AppBootstrap extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const AppBootstrap({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppBootstrap> createState() => _AppBootstrapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppBootstrapState extends State<AppBootstrap> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
context.read<ProductListViewModel>().ensureLoaded();
|
||||||
|
context.read<BarScreenViewModel>().ensureLoaded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../views/bar_screen_view.dart';
|
||||||
|
import '../views/product_form_view.dart';
|
||||||
|
import '../views/product_list_view.dart';
|
||||||
|
|
||||||
|
final appRouter = GoRouter(
|
||||||
|
initialLocation: '/bar',
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/bar',
|
||||||
|
builder: (context, state) => const BarScreenView(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/products',
|
||||||
|
builder: (context, state) => const ProductListView(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'new',
|
||||||
|
builder: (context, state) => const ProductFormView(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: ':id/edit',
|
||||||
|
builder: (context, state) {
|
||||||
|
final productId = state.pathParameters['id']!;
|
||||||
|
|
||||||
|
return ProductFormView(productId: productId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
|
@DataClassName('ProductRow')
|
||||||
|
class Products extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
TextColumn get name => text()();
|
||||||
|
|
||||||
|
TextColumn get category => text()();
|
||||||
|
|
||||||
|
IntColumn get stockQuantity => integer()();
|
||||||
|
|
||||||
|
IntColumn get lowStockThreshold => integer()();
|
||||||
|
|
||||||
|
IntColumn get priceInCents => integer()();
|
||||||
|
|
||||||
|
TextColumn get imagePath => text().nullable()();
|
||||||
|
|
||||||
|
BoolColumn get active => boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
@DataClassName('BarTabRow')
|
||||||
|
class BarTabs extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
TextColumn get customerName => text()();
|
||||||
|
|
||||||
|
TextColumn get status => text().withDefault(const Constant('open'))();
|
||||||
|
|
||||||
|
DateTimeColumn get openedAt => dateTime()();
|
||||||
|
|
||||||
|
DateTimeColumn get closedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
@DataClassName('TabItemRow')
|
||||||
|
class TabItems extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
TextColumn get tabId => text().references(
|
||||||
|
BarTabs,
|
||||||
|
#id,
|
||||||
|
onDelete: KeyAction.cascade,
|
||||||
|
)();
|
||||||
|
|
||||||
|
TextColumn get productId => text().references(
|
||||||
|
Products,
|
||||||
|
#id,
|
||||||
|
)();
|
||||||
|
|
||||||
|
TextColumn get productName => text()();
|
||||||
|
|
||||||
|
IntColumn get quantity => integer()();
|
||||||
|
|
||||||
|
IntColumn get unitPriceInCents => integer()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
@DriftDatabase(
|
||||||
|
tables: [
|
||||||
|
Products,
|
||||||
|
BarTabs,
|
||||||
|
TabItems,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: (migrator) async {
|
||||||
|
await migrator.createAll();
|
||||||
|
},
|
||||||
|
onUpgrade: (migrator, from, to) async {
|
||||||
|
if (from < 2) {
|
||||||
|
await migrator.createTable(barTabs);
|
||||||
|
await migrator.createTable(tabItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from < 3) {
|
||||||
|
await migrator.addColumn(products, products.imagePath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(
|
||||||
|
name: 'kooltab',
|
||||||
|
native: const DriftNativeOptions(
|
||||||
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'app/app.dart';
|
||||||
|
import 'app/app_bootstrap.dart';
|
||||||
|
import 'database/app_database.dart';
|
||||||
|
import 'services/bar_tab_service.dart';
|
||||||
|
import 'services/product_service.dart';
|
||||||
|
import 'viewmodels/bar_screen_view_model.dart';
|
||||||
|
import 'viewmodels/product_list_view_model.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
await SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider<AppDatabase>(
|
||||||
|
create: (_) => AppDatabase(),
|
||||||
|
dispose: (_, database) => database.close(),
|
||||||
|
),
|
||||||
|
Provider<ProductService>(
|
||||||
|
create: (context) => DriftProductService(
|
||||||
|
database: context.read<AppDatabase>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider<BarTabService>(
|
||||||
|
create: (context) => DriftBarTabService(
|
||||||
|
database: context.read<AppDatabase>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<ProductListViewModel>(
|
||||||
|
create: (context) => ProductListViewModel(
|
||||||
|
productService: context.read<ProductService>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<BarScreenViewModel>(
|
||||||
|
create: (context) => BarScreenViewModel(
|
||||||
|
barTabService: context.read<BarTabService>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const AppBootstrap(
|
||||||
|
child: KoolTabApp(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'tab_item.dart';
|
||||||
|
|
||||||
|
class BarTab {
|
||||||
|
final String id;
|
||||||
|
final String customerName;
|
||||||
|
final String status;
|
||||||
|
final DateTime openedAt;
|
||||||
|
final DateTime? closedAt;
|
||||||
|
final List<TabItem> items;
|
||||||
|
|
||||||
|
const BarTab({
|
||||||
|
required this.id,
|
||||||
|
required this.customerName,
|
||||||
|
required this.status,
|
||||||
|
required this.openedAt,
|
||||||
|
required this.items,
|
||||||
|
this.closedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
int get totalInCents {
|
||||||
|
return items.fold<int>(
|
||||||
|
0,
|
||||||
|
(total, item) => total + item.lineTotalInCents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get itemCount {
|
||||||
|
return items.fold<int>(
|
||||||
|
0,
|
||||||
|
(total, item) => total + item.quantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get formattedTotal {
|
||||||
|
return '€${(totalInCents / 100).toStringAsFixed(2)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isOpen => status == 'open';
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
class Product {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String category;
|
||||||
|
final int stockQuantity;
|
||||||
|
final int lowStockThreshold;
|
||||||
|
final int priceInCents;
|
||||||
|
final String? imagePath;
|
||||||
|
final bool active;
|
||||||
|
|
||||||
|
const Product({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.stockQuantity,
|
||||||
|
required this.lowStockThreshold,
|
||||||
|
required this.priceInCents,
|
||||||
|
this.imagePath,
|
||||||
|
this.active = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isLowStock => stockQuantity <= lowStockThreshold;
|
||||||
|
|
||||||
|
String get formattedPrice {
|
||||||
|
final euros = priceInCents / 100;
|
||||||
|
|
||||||
|
return '€${euros.toStringAsFixed(2)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Product copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? category,
|
||||||
|
int? stockQuantity,
|
||||||
|
int? lowStockThreshold,
|
||||||
|
int? priceInCents,
|
||||||
|
String? imagePath,
|
||||||
|
bool? active,
|
||||||
|
}) {
|
||||||
|
return Product(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
category: category ?? this.category,
|
||||||
|
stockQuantity: stockQuantity ?? this.stockQuantity,
|
||||||
|
lowStockThreshold: lowStockThreshold ?? this.lowStockThreshold,
|
||||||
|
priceInCents: priceInCents ?? this.priceInCents,
|
||||||
|
imagePath: imagePath ?? this.imagePath,
|
||||||
|
active: active ?? this.active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
class TabItem {
|
||||||
|
final String id;
|
||||||
|
final String tabId;
|
||||||
|
final String productId;
|
||||||
|
final String productName;
|
||||||
|
final int quantity;
|
||||||
|
final int unitPriceInCents;
|
||||||
|
|
||||||
|
const TabItem({
|
||||||
|
required this.id,
|
||||||
|
required this.tabId,
|
||||||
|
required this.productId,
|
||||||
|
required this.productName,
|
||||||
|
required this.quantity,
|
||||||
|
required this.unitPriceInCents,
|
||||||
|
});
|
||||||
|
|
||||||
|
int get lineTotalInCents => quantity * unitPriceInCents;
|
||||||
|
|
||||||
|
String get formattedLineTotal {
|
||||||
|
return '€${(lineTotalInCents / 100).toStringAsFixed(2)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get formattedUnitPrice {
|
||||||
|
return '€${(unitPriceInCents / 100).toStringAsFixed(2)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../database/app_database.dart';
|
||||||
|
import '../models/bar_tab.dart';
|
||||||
|
import '../models/product.dart';
|
||||||
|
import '../models/tab_item.dart';
|
||||||
|
|
||||||
|
abstract class BarTabService {
|
||||||
|
Future<List<BarTab>> getOpenTabs();
|
||||||
|
|
||||||
|
Future<BarTab?> getTabById(String id);
|
||||||
|
|
||||||
|
Future<BarTab> createTab({
|
||||||
|
required String customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> addProductToTab({
|
||||||
|
required String tabId,
|
||||||
|
required Product product,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> updateTabItemQuantity({
|
||||||
|
required String tabItemId,
|
||||||
|
required int quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> closeTab(String tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriftBarTabService implements BarTabService {
|
||||||
|
final AppDatabase database;
|
||||||
|
final _uuid = const Uuid();
|
||||||
|
|
||||||
|
DriftBarTabService({
|
||||||
|
required this.database,
|
||||||
|
});
|
||||||
|
|
||||||
|
TabItem _mapItemRow(TabItemRow row) {
|
||||||
|
return TabItem(
|
||||||
|
id: row.id,
|
||||||
|
tabId: row.tabId,
|
||||||
|
productId: row.productId,
|
||||||
|
productName: row.productName,
|
||||||
|
quantity: row.quantity,
|
||||||
|
unitPriceInCents: row.unitPriceInCents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BarTab _mapTabRow(
|
||||||
|
BarTabRow row,
|
||||||
|
List<TabItem> items,
|
||||||
|
) {
|
||||||
|
return BarTab(
|
||||||
|
id: row.id,
|
||||||
|
customerName: row.customerName,
|
||||||
|
status: row.status,
|
||||||
|
openedAt: row.openedAt,
|
||||||
|
closedAt: row.closedAt,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TabItem>> _getItemsForTab(String tabId) async {
|
||||||
|
final query = database.select(database.tabItems)
|
||||||
|
..where((item) => item.tabId.equals(tabId))
|
||||||
|
..orderBy([
|
||||||
|
(item) => OrderingTerm.asc(item.createdAt),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final rows = await query.get();
|
||||||
|
|
||||||
|
return rows.map(_mapItemRow).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<BarTab>> getOpenTabs() async {
|
||||||
|
final query = database.select(database.barTabs)
|
||||||
|
..where((tab) => tab.status.equals('open'))
|
||||||
|
..orderBy([
|
||||||
|
(tab) => OrderingTerm.desc(tab.openedAt),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final tabRows = await query.get();
|
||||||
|
|
||||||
|
final tabs = <BarTab>[];
|
||||||
|
|
||||||
|
for (final tabRow in tabRows) {
|
||||||
|
final items = await _getItemsForTab(tabRow.id);
|
||||||
|
|
||||||
|
tabs.add(_mapTabRow(tabRow, items));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<BarTab?> getTabById(String id) async {
|
||||||
|
final query = database.select(database.barTabs)
|
||||||
|
..where((tab) => tab.id.equals(id));
|
||||||
|
|
||||||
|
final tabRow = await query.getSingleOrNull();
|
||||||
|
|
||||||
|
if (tabRow == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = await _getItemsForTab(tabRow.id);
|
||||||
|
|
||||||
|
return _mapTabRow(tabRow, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<BarTab> createTab({
|
||||||
|
required String customerName,
|
||||||
|
}) async {
|
||||||
|
final id = _uuid.v4();
|
||||||
|
|
||||||
|
await database.into(database.barTabs).insert(
|
||||||
|
BarTabsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
customerName: customerName,
|
||||||
|
status: const Value('open'),
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tab = await getTabById(id);
|
||||||
|
|
||||||
|
if (tab == null) {
|
||||||
|
throw Exception('Could not create tab.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addProductToTab({
|
||||||
|
required String tabId,
|
||||||
|
required Product product,
|
||||||
|
}) async {
|
||||||
|
await database.transaction(() async {
|
||||||
|
final existingItemQuery = database.select(database.tabItems)
|
||||||
|
..where(
|
||||||
|
(item) =>
|
||||||
|
item.tabId.equals(tabId) &
|
||||||
|
item.productId.equals(product.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
final existingItem = await existingItemQuery.getSingleOrNull();
|
||||||
|
|
||||||
|
if (existingItem != null) {
|
||||||
|
final updateQuery = database.update(database.tabItems)
|
||||||
|
..where((item) => item.id.equals(existingItem.id));
|
||||||
|
|
||||||
|
await updateQuery.write(
|
||||||
|
TabItemsCompanion(
|
||||||
|
quantity: Value(existingItem.quantity + 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.into(database.tabItems).insert(
|
||||||
|
TabItemsCompanion.insert(
|
||||||
|
id: _uuid.v4(),
|
||||||
|
tabId: tabId,
|
||||||
|
productId: product.id,
|
||||||
|
productName: product.name,
|
||||||
|
quantity: 1,
|
||||||
|
unitPriceInCents: product.priceInCents,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateTabItemQuantity({
|
||||||
|
required String tabItemId,
|
||||||
|
required int quantity,
|
||||||
|
}) async {
|
||||||
|
if (quantity <= 0) {
|
||||||
|
final deleteQuery = database.delete(database.tabItems)
|
||||||
|
..where((item) => item.id.equals(tabItemId));
|
||||||
|
|
||||||
|
await deleteQuery.go();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updateQuery = database.update(database.tabItems)
|
||||||
|
..where((item) => item.id.equals(tabItemId));
|
||||||
|
|
||||||
|
await updateQuery.write(
|
||||||
|
TabItemsCompanion(
|
||||||
|
quantity: Value(quantity),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> closeTab(String tabId) async {
|
||||||
|
final updateQuery = database.update(database.barTabs)
|
||||||
|
..where((tab) => tab.id.equals(tabId));
|
||||||
|
|
||||||
|
await updateQuery.write(
|
||||||
|
BarTabsCompanion(
|
||||||
|
status: const Value('closed'),
|
||||||
|
closedAt: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../database/app_database.dart';
|
||||||
|
import '../models/product.dart';
|
||||||
|
|
||||||
|
abstract class ProductService {
|
||||||
|
Future<List<Product>> getProducts();
|
||||||
|
|
||||||
|
Future<Product?> getProductById(String id);
|
||||||
|
|
||||||
|
Future<void> createProduct({
|
||||||
|
required String name,
|
||||||
|
required String category,
|
||||||
|
required int stockQuantity,
|
||||||
|
required int lowStockThreshold,
|
||||||
|
required int priceInCents,
|
||||||
|
required String? imagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> updateProduct(Product product);
|
||||||
|
|
||||||
|
Future<void> deleteProduct(String id);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriftProductService implements ProductService {
|
||||||
|
final AppDatabase database;
|
||||||
|
final _uuid = const Uuid();
|
||||||
|
|
||||||
|
DriftProductService({
|
||||||
|
required this.database,
|
||||||
|
});
|
||||||
|
|
||||||
|
Product _mapRowToProduct(ProductRow row) {
|
||||||
|
return Product(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
category: row.category,
|
||||||
|
stockQuantity: row.stockQuantity,
|
||||||
|
lowStockThreshold: row.lowStockThreshold,
|
||||||
|
priceInCents: row.priceInCents,
|
||||||
|
imagePath: row.imagePath,
|
||||||
|
active: row.active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Product>> getProducts() async {
|
||||||
|
final query = database.select(database.products)
|
||||||
|
..where((product) => product.active.equals(true))
|
||||||
|
..orderBy([
|
||||||
|
(product) => OrderingTerm.asc(product.name),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final rows = await query.get();
|
||||||
|
|
||||||
|
return rows.map(_mapRowToProduct).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Product?> getProductById(String id) async {
|
||||||
|
final query = database.select(database.products)
|
||||||
|
..where((product) => product.id.equals(id));
|
||||||
|
|
||||||
|
final row = await query.getSingleOrNull();
|
||||||
|
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mapRowToProduct(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createProduct({
|
||||||
|
required String name,
|
||||||
|
required String category,
|
||||||
|
required int stockQuantity,
|
||||||
|
required int lowStockThreshold,
|
||||||
|
required int priceInCents,
|
||||||
|
required String? imagePath,
|
||||||
|
}) async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: _uuid.v4(),
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
stockQuantity: stockQuantity,
|
||||||
|
lowStockThreshold: lowStockThreshold,
|
||||||
|
priceInCents: priceInCents,
|
||||||
|
imagePath: Value(imagePath),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateProduct(Product product) async {
|
||||||
|
final query = database.update(database.products)
|
||||||
|
..where((row) => row.id.equals(product.id));
|
||||||
|
|
||||||
|
await query.write(
|
||||||
|
ProductsCompanion(
|
||||||
|
name: Value(product.name),
|
||||||
|
category: Value(product.category),
|
||||||
|
stockQuantity: Value(product.stockQuantity),
|
||||||
|
lowStockThreshold: Value(product.lowStockThreshold),
|
||||||
|
priceInCents: Value(product.priceInCents),
|
||||||
|
imagePath: Value(product.imagePath),
|
||||||
|
active: Value(product.active),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteProduct(String id) async {
|
||||||
|
final query = database.delete(database.products)
|
||||||
|
..where((product) => product.id.equals(id));
|
||||||
|
|
||||||
|
await query.go();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final ThemeData darkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: const Color(0xFF7C4DFF),
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
|
||||||
|
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||||
|
canvasColor: const Color(0xFF121212),
|
||||||
|
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.white.withOpacity(0.06),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: const Color(0xFF7C4DFF),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.white.withOpacity(0.15),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFF242424),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 18,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.white.withOpacity(0.08),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Color(0xFF7C4DFF),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: Color(0xFF7C4DFF),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
|
||||||
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
|
backgroundColor: const Color(0xFF1A1A1A),
|
||||||
|
indicatorColor: const Color(0xFF7C4DFF).withOpacity(0.25),
|
||||||
|
labelTextStyle: WidgetStatePropertyAll(
|
||||||
|
TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
tileColor: const Color(0xFF1E1E1E),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
iconColor: const Color(0xFFB39DFF),
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: const Color(0xFF2A2A2A),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
dividerTheme: DividerThemeData(
|
||||||
|
color: Colors.white.withOpacity(0.08),
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
displayLarge: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
headlineMedium: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
labelLarge: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData neoBrutalDarkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
|
||||||
|
colorScheme: const ColorScheme.dark(
|
||||||
|
primary: Color(0xFFFFD60A), // Bright yellow
|
||||||
|
secondary: Color(0xFF00E5FF), // Cyan
|
||||||
|
surface: Color(0xFF1A1A1A),
|
||||||
|
error: Color(0xFFFF5252),
|
||||||
|
),
|
||||||
|
|
||||||
|
scaffoldBackgroundColor: const Color(0xFF0E0E0E),
|
||||||
|
canvasColor: const Color(0xFF0E0E0E),
|
||||||
|
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
displayLarge: TextStyle(
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: -1,
|
||||||
|
),
|
||||||
|
headlineMedium: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Color(0xFF0E0E0E),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: const Color(0xFF242424),
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFFFD60A),
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 0,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 18,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFF242424),
|
||||||
|
contentPadding: const EdgeInsets.all(18),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Color(0xFFFFD60A),
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: Color(0xFFFF5252),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: const Color(0xFF242424),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
thickness: 3,
|
||||||
|
),
|
||||||
|
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
tileColor: const Color(0xFF242424),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textColor: Colors.white,
|
||||||
|
iconColor: const Color(0xFFFFD60A),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../models/bar_tab.dart';
|
||||||
|
import '../models/product.dart';
|
||||||
|
import '../models/tab_item.dart';
|
||||||
|
import '../services/bar_tab_service.dart';
|
||||||
|
import '../services/product_service.dart';
|
||||||
|
|
||||||
|
class BarScreenViewModel extends ChangeNotifier {
|
||||||
|
final BarTabService barTabService;
|
||||||
|
|
||||||
|
BarScreenViewModel({
|
||||||
|
required this.barTabService,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<BarTab> _tabs = [];
|
||||||
|
String? _selectedTabId;
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _hasLoaded = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
List<BarTab> get tabs => _tabs;
|
||||||
|
|
||||||
|
String? get selectedTabId => _selectedTabId;
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
|
bool get hasLoaded => _hasLoaded;
|
||||||
|
|
||||||
|
String? get errorMessage => _errorMessage;
|
||||||
|
|
||||||
|
BarTab? get selectedTab {
|
||||||
|
if (_selectedTabId == null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return _tabs.firstWhere((tab) => tab.id == _selectedTabId);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureLoaded() async {
|
||||||
|
if (_hasLoaded || _isLoading) return;
|
||||||
|
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_tabs = await barTabService.getOpenTabs();
|
||||||
|
|
||||||
|
if (_selectedTabId == null ||
|
||||||
|
!_tabs.any((tab) => tab.id == _selectedTabId)) {
|
||||||
|
_selectedTabId = _tabs.isEmpty ? null : _tabs.first.id;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_errorMessage = 'Could not load bar screen.';
|
||||||
|
} finally {
|
||||||
|
_hasLoaded = true;
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectTab(String tabId) {
|
||||||
|
_selectedTabId = tabId;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createTab(String customerName) async {
|
||||||
|
final tab = await barTabService.createTab(
|
||||||
|
customerName: customerName,
|
||||||
|
);
|
||||||
|
|
||||||
|
_selectedTabId = tab.id;
|
||||||
|
await _reloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addProductToSelectedTab(Product product) async {
|
||||||
|
final tab = selectedTab;
|
||||||
|
|
||||||
|
if (tab == null) return;
|
||||||
|
|
||||||
|
await barTabService.addProductToTab(
|
||||||
|
tabId: tab.id,
|
||||||
|
product: product,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _reloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeItemQuantity(
|
||||||
|
TabItem item,
|
||||||
|
int quantity,
|
||||||
|
) async {
|
||||||
|
await barTabService.updateTabItemQuantity(
|
||||||
|
tabItemId: item.id,
|
||||||
|
quantity: quantity,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _reloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeSelectedTab() async {
|
||||||
|
final tab = selectedTab;
|
||||||
|
|
||||||
|
if (tab == null) return;
|
||||||
|
|
||||||
|
await barTabService.closeTab(tab.id);
|
||||||
|
|
||||||
|
_selectedTabId = null;
|
||||||
|
await _reloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeTab(String tabId) async {
|
||||||
|
await barTabService.closeTab(tabId);
|
||||||
|
|
||||||
|
if (_selectedTabId == tabId) {
|
||||||
|
_selectedTabId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _reloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reloadTabs() async {
|
||||||
|
_tabs = await barTabService.getOpenTabs();
|
||||||
|
|
||||||
|
if (_selectedTabId == null || !_tabs.any((tab) => tab.id == _selectedTabId)) {
|
||||||
|
_selectedTabId = _tabs.isEmpty ? null : _tabs.first.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../models/product.dart';
|
||||||
|
import '../services/product_service.dart';
|
||||||
|
|
||||||
|
class ProductListViewModel extends ChangeNotifier {
|
||||||
|
final ProductService productService;
|
||||||
|
|
||||||
|
ProductListViewModel({
|
||||||
|
required this.productService,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const List<String> _defaultCategories = [
|
||||||
|
'Bier',
|
||||||
|
'Wijn',
|
||||||
|
'Frisdrank',
|
||||||
|
'Cocktails',
|
||||||
|
'Snacks',
|
||||||
|
'Coffee',
|
||||||
|
'Thee',
|
||||||
|
'Other',
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Product> _products = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _hasLoaded = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
List<Product> get products => _products;
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
|
bool get hasLoaded => _hasLoaded;
|
||||||
|
|
||||||
|
String? get errorMessage => _errorMessage;
|
||||||
|
|
||||||
|
Future<void> ensureLoaded() async {
|
||||||
|
if (_hasLoaded || _isLoading) return;
|
||||||
|
|
||||||
|
await loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadProducts() async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_products = await productService.getProducts();
|
||||||
|
} catch (_) {
|
||||||
|
_errorMessage = 'Could not load products.';
|
||||||
|
} finally {
|
||||||
|
_hasLoaded = true;
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addProduct({
|
||||||
|
required String name,
|
||||||
|
required String category,
|
||||||
|
required int stockQuantity,
|
||||||
|
required int lowStockThreshold,
|
||||||
|
required int priceInCents,
|
||||||
|
required String? imagePath,
|
||||||
|
}) async {
|
||||||
|
await productService.createProduct(
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
stockQuantity: stockQuantity,
|
||||||
|
lowStockThreshold: lowStockThreshold,
|
||||||
|
priceInCents: priceInCents,
|
||||||
|
imagePath: imagePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProduct(Product product) async {
|
||||||
|
await productService.updateProduct(product);
|
||||||
|
await loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteProduct(String id) async {
|
||||||
|
await productService.deleteProduct(id);
|
||||||
|
await loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Product?> getProductById(String id) {
|
||||||
|
return productService.getProductById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get categories {
|
||||||
|
final categories = {
|
||||||
|
..._defaultCategories,
|
||||||
|
..._products
|
||||||
|
.map((p) => p.category.trim())
|
||||||
|
.where((c) => c.isNotEmpty),
|
||||||
|
}.toList();
|
||||||
|
|
||||||
|
categories.sort();
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widget_previews.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:kooltab2/viewmodels/product_list_view_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
|
|
||||||
|
import '../models/bar_tab.dart';
|
||||||
|
import '../models/product.dart';
|
||||||
|
import '../models/tab_item.dart';
|
||||||
|
import '../viewmodels/bar_screen_view_model.dart';
|
||||||
|
|
||||||
|
class BarScreenView extends StatelessWidget {
|
||||||
|
const BarScreenView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final viewModel = context.watch<BarScreenViewModel>();
|
||||||
|
final productsViewModel = context.watch<ProductListViewModel>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Manage products',
|
||||||
|
onPressed: () => context.go('/products'),
|
||||||
|
icon: const Icon(Icons.inventory_2_outlined),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
onPressed: viewModel.load,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (viewModel.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.errorMessage != null) {
|
||||||
|
return Center(child: Text(viewModel.errorMessage!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: _ProductGrid(
|
||||||
|
products: productsViewModel.products,
|
||||||
|
hasSelectedTab: viewModel.selectedTab != null,
|
||||||
|
onProductTap: (product) async {
|
||||||
|
if (viewModel.selectedTab == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Open or select a tab first.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewModel.addProductToSelectedTab(product);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(width: 1),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: _TabPanel(
|
||||||
|
tabs: viewModel.tabs,
|
||||||
|
selectedTab: viewModel.selectedTab,
|
||||||
|
selectedTabId: viewModel.selectedTabId,
|
||||||
|
onNewTabPressed: () => _showNewTabDialog(context),
|
||||||
|
onTabSelected: viewModel.selectTab,
|
||||||
|
onItemQuantityChanged: viewModel.changeItemQuantity,
|
||||||
|
onCloseTabPressed: () => _confirmCloseTab(context),
|
||||||
|
onTabClosed: viewModel.closeTab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showNewTabDialog(BuildContext context) async {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
|
||||||
|
final name = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Open new tab'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Customer / group name',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
Navigator.of(dialogContext).pop(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop(controller.text);
|
||||||
|
},
|
||||||
|
child: const Text('Open tab'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (name == null || name.trim().isEmpty) return;
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
await context.read<BarScreenViewModel>().createTab(name.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmCloseTab(BuildContext context) async {
|
||||||
|
final viewModel = context.read<BarScreenViewModel>();
|
||||||
|
final tab = viewModel.selectedTab;
|
||||||
|
|
||||||
|
if (tab == null) return;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Close ${tab.customerName}ʼs tab?'),
|
||||||
|
content: Text(
|
||||||
|
'Current total: ${tab.formattedTotal}\n\n'
|
||||||
|
'Payments are not handled yet, so this only marks the tab as closed.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
child: const Text('Close tab'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
await viewModel.closeSelectedTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductGrid extends StatelessWidget {
|
||||||
|
final List<Product> products;
|
||||||
|
final bool hasSelectedTab;
|
||||||
|
final ValueChanged<Product> onProductTap;
|
||||||
|
|
||||||
|
const _ProductGrid({
|
||||||
|
required this.products,
|
||||||
|
required this.hasSelectedTab,
|
||||||
|
required this.onProductTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (products.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.inventory_2_outlined, size: 48),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('No products yet.'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => context.go('/products/new'),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add product'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final columns = ((constraints.maxWidth / 170).floor())
|
||||||
|
.clamp(3, 6)
|
||||||
|
.toInt();
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: products.length,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: columns,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = products[index];
|
||||||
|
|
||||||
|
return _ProductTile(
|
||||||
|
product: product,
|
||||||
|
enabled: hasSelectedTab,
|
||||||
|
onTap: () => onProductTap(product),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductTile extends StatelessWidget {
|
||||||
|
final Product product;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ProductTile({
|
||||||
|
required this.product,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get _hasImage {
|
||||||
|
final imagePath = product.imagePath;
|
||||||
|
|
||||||
|
if (imagePath == null || imagePath.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(imagePath).existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: enabled ? onTap : null,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: enabled ? 1 : 0.45,
|
||||||
|
child: _hasImage
|
||||||
|
? Image.file(
|
||||||
|
File(product.imagePath!),
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.image_not_supported_outlined, size: 42),
|
||||||
|
Text("No Image found!"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabPanel extends StatefulWidget {
|
||||||
|
final List<BarTab> tabs;
|
||||||
|
final BarTab? selectedTab;
|
||||||
|
final String? selectedTabId;
|
||||||
|
final VoidCallback onNewTabPressed;
|
||||||
|
final ValueChanged<String> onTabSelected;
|
||||||
|
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||||
|
final VoidCallback onCloseTabPressed;
|
||||||
|
final ValueChanged<String> onTabClosed;
|
||||||
|
|
||||||
|
const _TabPanel({
|
||||||
|
required this.tabs,
|
||||||
|
required this.selectedTab,
|
||||||
|
required this.selectedTabId,
|
||||||
|
required this.onNewTabPressed,
|
||||||
|
required this.onTabSelected,
|
||||||
|
required this.onItemQuantityChanged,
|
||||||
|
required this.onCloseTabPressed,
|
||||||
|
required this.onTabClosed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TabPanel> createState() => _TabPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabPanelState extends State<_TabPanel> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredTabs = widget.tabs.where((tab) {
|
||||||
|
return tab.customerName.toLowerCase().contains(
|
||||||
|
_searchQuery.toLowerCase(),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: widget.onNewTabPressed,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Open tab'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Search name...',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Open tabs',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: _OpenTabsList(
|
||||||
|
tabs: filteredTabs,
|
||||||
|
selectedTabId: widget.selectedTabId,
|
||||||
|
onTabSelected: widget.onTabSelected,
|
||||||
|
onTabClosed: widget.onTabClosed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(height: 24),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: widget.selectedTab == null
|
||||||
|
? const Center(child: Text('Select or open a tab.'))
|
||||||
|
: _SelectedTabDetails(
|
||||||
|
tab: widget.selectedTab!,
|
||||||
|
onItemQuantityChanged: widget.onItemQuantityChanged,
|
||||||
|
onCloseTabPressed: widget.onCloseTabPressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OpenTabsList extends StatelessWidget {
|
||||||
|
final List<BarTab> tabs;
|
||||||
|
final String? selectedTabId;
|
||||||
|
final ValueChanged<String> onTabSelected;
|
||||||
|
final ValueChanged<String> onTabClosed;
|
||||||
|
|
||||||
|
const _OpenTabsList({
|
||||||
|
required this.tabs,
|
||||||
|
required this.selectedTabId,
|
||||||
|
required this.onTabSelected,
|
||||||
|
required this.onTabClosed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (tabs.isEmpty) {
|
||||||
|
return const Center(child: Text('No open tabs.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: tabs.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final tab = tabs[index];
|
||||||
|
final selected = tab.id == selectedTabId;
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
child: Slidable(
|
||||||
|
key: ValueKey(tab.id),
|
||||||
|
endActionPane: ActionPane(
|
||||||
|
motion: const DrawerMotion(),
|
||||||
|
extentRatio: 0.65,
|
||||||
|
children: [
|
||||||
|
SlidableAction(
|
||||||
|
onPressed: (_) {
|
||||||
|
onTabSelected(tab.id);
|
||||||
|
// TODO: rename/edit tab
|
||||||
|
},
|
||||||
|
icon: Icons.edit_outlined,
|
||||||
|
label: 'Edit',
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
),
|
||||||
|
SlidableAction(
|
||||||
|
onPressed: (_) => onTabClosed(tab.id),
|
||||||
|
icon: Icons.close,
|
||||||
|
label: 'Close',
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: selected
|
||||||
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
|
: null,
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
selected: selected,
|
||||||
|
title: Text(
|
||||||
|
tab.customerName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text('${tab.itemCount} items • ${tab.formattedTotal}'),
|
||||||
|
onTap: () => onTabSelected(tab.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectedTabDetails extends StatelessWidget {
|
||||||
|
final BarTab tab;
|
||||||
|
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||||
|
final VoidCallback onCloseTabPressed;
|
||||||
|
|
||||||
|
const _SelectedTabDetails({
|
||||||
|
required this.tab,
|
||||||
|
required this.onItemQuantityChanged,
|
||||||
|
required this.onCloseTabPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
tab.customerName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Total: ${tab.formattedTotal}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: tab.items.isEmpty
|
||||||
|
? const Center(child: Text('Tap products to add them.'))
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: tab.items.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = tab.items[index];
|
||||||
|
|
||||||
|
return _TabItemRow(
|
||||||
|
item: item,
|
||||||
|
onQuantityChanged: onItemQuantityChanged,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
tab.formattedTotal,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: onCloseTabPressed,
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabItemRow extends StatelessWidget {
|
||||||
|
final TabItem item;
|
||||||
|
final Future<void> Function(TabItem item, int quantity) onQuantityChanged;
|
||||||
|
|
||||||
|
const _TabItemRow({required this.item, required this.onQuantityChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.productName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${item.quantity} × ${item.formattedUnitPrice}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () => onQuantityChanged(item, item.quantity - 1),
|
||||||
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
|
),
|
||||||
|
Text('${item.quantity}'),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () => onQuantityChanged(item, item.quantity + 1),
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 72,
|
||||||
|
child: Text(item.formattedLineTotal, textAlign: TextAlign.end),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../models/product.dart';
|
||||||
|
import '../viewmodels/product_list_view_model.dart';
|
||||||
|
|
||||||
|
class ProductFormView extends StatefulWidget {
|
||||||
|
final String? productId;
|
||||||
|
|
||||||
|
const ProductFormView({
|
||||||
|
super.key,
|
||||||
|
this.productId,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isEditing => productId != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProductFormView> createState() => _ProductFormViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductFormViewState extends State<ProductFormView> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _priceController = TextEditingController();
|
||||||
|
final _stockController = TextEditingController();
|
||||||
|
final _lowStockController = TextEditingController();
|
||||||
|
|
||||||
|
String? _selectedCategory;
|
||||||
|
final _categoryController = TextEditingController();
|
||||||
|
|
||||||
|
final _imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
Product? _existingProduct;
|
||||||
|
String? _imagePath;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadProductIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadProductIfNeeded() async {
|
||||||
|
if (!widget.isEditing) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final viewModel = context.read<ProductListViewModel>();
|
||||||
|
final product = await viewModel.getProductById(widget.productId!);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (product == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Product not found.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.go('/products');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_existingProduct = product;
|
||||||
|
_nameController.text = product.name;
|
||||||
|
_selectedCategory = product.category;
|
||||||
|
_priceController.text = (product.priceInCents / 100).toStringAsFixed(2);
|
||||||
|
_stockController.text = product.stockQuantity.toString();
|
||||||
|
_lowStockController.text = product.lowStockThreshold.toString();
|
||||||
|
_imagePath = product.imagePath;
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_priceController.dispose();
|
||||||
|
_stockController.dispose();
|
||||||
|
_lowStockController.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _copyImageToAppFolder(XFile pickedFile) async {
|
||||||
|
final appDirectory = await getApplicationSupportDirectory();
|
||||||
|
final imagesDirectory = Directory(
|
||||||
|
path.join(appDirectory.path, 'product_images'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!await imagesDirectory.exists()) {
|
||||||
|
await imagesDirectory.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final extension = path.extension(pickedFile.path);
|
||||||
|
final fileName = 'product_${DateTime.now().millisecondsSinceEpoch}$extension';
|
||||||
|
final newPath = path.join(imagesDirectory.path, fileName);
|
||||||
|
|
||||||
|
final copiedFile = await File(pickedFile.path).copy(newPath);
|
||||||
|
|
||||||
|
return copiedFile.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final pickedFile = await _imagePicker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 85,
|
||||||
|
maxWidth: 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedFile == null) return;
|
||||||
|
|
||||||
|
final copiedImagePath = await _copyImageToAppFolder(pickedFile);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_imagePath = copiedImagePath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int _parsePriceToCents(String value) {
|
||||||
|
final normalized = value.replaceAll(',', '.');
|
||||||
|
final euros = double.tryParse(normalized) ?? 0;
|
||||||
|
|
||||||
|
return (euros * 100).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
if (_imagePath == null || _imagePath!.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Choose a product image.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedCategory == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please select a category.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSaving = true);
|
||||||
|
|
||||||
|
final viewModel = context.read<ProductListViewModel>();
|
||||||
|
|
||||||
|
final name = _nameController.text.trim();
|
||||||
|
final category = _selectedCategory!;
|
||||||
|
final priceInCents = _parsePriceToCents(_priceController.text);
|
||||||
|
final stockQuantity = int.parse(_stockController.text);
|
||||||
|
final lowStockThreshold = int.parse(_lowStockController.text);
|
||||||
|
|
||||||
|
if (widget.isEditing) {
|
||||||
|
final updatedProduct = _existingProduct!.copyWith(
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
priceInCents: priceInCents,
|
||||||
|
stockQuantity: stockQuantity,
|
||||||
|
lowStockThreshold: lowStockThreshold,
|
||||||
|
imagePath: _imagePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await viewModel.updateProduct(updatedProduct);
|
||||||
|
} else {
|
||||||
|
await viewModel.addProduct(
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
priceInCents: priceInCents,
|
||||||
|
stockQuantity: stockQuantity,
|
||||||
|
lowStockThreshold: lowStockThreshold,
|
||||||
|
imagePath: _imagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isSaving = false);
|
||||||
|
context.go('/products');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _delete() async {
|
||||||
|
if (!widget.isEditing) return;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Delete product?'),
|
||||||
|
content: const Text(
|
||||||
|
'This will remove the product from the product list.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
final viewModel = context.read<ProductListViewModel>();
|
||||||
|
await viewModel.deleteProduct(widget.productId!);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
context.go('/products');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePicker(BuildContext context) {
|
||||||
|
final hasImage = _imagePath != null && File(_imagePath!).existsSync();
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: _pickImage,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
height: 220,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: hasImage
|
||||||
|
? Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Image.file(
|
||||||
|
File(_imagePath!),
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 12,
|
||||||
|
bottom: 12,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: _pickImage,
|
||||||
|
icon: const Icon(Icons.image),
|
||||||
|
label: const Text('Change image'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add_photo_alternate_outlined, size: 48),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Choose product image',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final title = widget.isEditing ? 'Edit product' : 'Add product';
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/products'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (widget.isEditing)
|
||||||
|
IconButton(
|
||||||
|
onPressed: _delete,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: _isLoading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 720),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
children: [
|
||||||
|
_buildImagePicker(context),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Product name',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Enter a product name.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Consumer<ProductListViewModel>(
|
||||||
|
builder: (context, viewModel, child) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return DropdownMenu<String>(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
initialSelection: _selectedCategory,
|
||||||
|
enableFilter: true,
|
||||||
|
enableSearch: true,
|
||||||
|
controller: _categoryController,
|
||||||
|
requestFocusOnTap: true,
|
||||||
|
label: const Text('Category'),
|
||||||
|
hintText: 'Select a category',
|
||||||
|
dropdownMenuEntries: viewModel.categories
|
||||||
|
.map(
|
||||||
|
(category) => DropdownMenuEntry<String>(
|
||||||
|
value: category,
|
||||||
|
label: category,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedCategory = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _priceController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Price',
|
||||||
|
prefixText: '€ ',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Enter a price.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = value.replaceAll(',', '.');
|
||||||
|
final price = double.tryParse(normalized);
|
||||||
|
|
||||||
|
if (price == null || price < 0) {
|
||||||
|
return 'Enter a valid price.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stockController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current stock',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: (value) {
|
||||||
|
final number = int.tryParse(value ?? '');
|
||||||
|
|
||||||
|
if (number == null || number < 0) {
|
||||||
|
return 'Enter a valid stock amount.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _lowStockController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Low stock warning threshold',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
final number = int.tryParse(value ?? '');
|
||||||
|
|
||||||
|
if (number == null || number < 0) {
|
||||||
|
return 'Enter a valid threshold.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _isSaving ? null : _save,
|
||||||
|
icon: _isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: Text(
|
||||||
|
widget.isEditing ? 'Save changes' : 'Add product',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../viewmodels/product_list_view_model.dart';
|
||||||
|
|
||||||
|
class ProductListView extends StatelessWidget {
|
||||||
|
const ProductListView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final viewModel = context.watch<ProductListViewModel>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Products'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Back to bar',
|
||||||
|
onPressed: () => context.go('/bar'),
|
||||||
|
icon: const Icon(Icons.point_of_sale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () => context.go('/products/new'),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add product'),
|
||||||
|
),
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (viewModel.isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.errorMessage != null) {
|
||||||
|
return Center(
|
||||||
|
child: Text(viewModel.errorMessage!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.products.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No products yet.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: viewModel.products.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = viewModel.products[index];
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
child: Center(
|
||||||
|
child: Image.file(
|
||||||
|
File(product.imagePath!),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(product.name),
|
||||||
|
subtitle: Text(
|
||||||
|
'${product.category} • ${product
|
||||||
|
.formattedPrice} • Stock: ${product.stockQuantity}',
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => context.go('/products/${product.id}/edit'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+4"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.9"
|
||||||
|
drift:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift
|
||||||
|
sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.34.0"
|
||||||
|
drift_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift_flutter
|
||||||
|
sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.35"
|
||||||
|
flutter_slidable:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_slidable
|
||||||
|
sha256: ea369262929d3cc6ebf9d8a00c196127966f117fe433a5e5cb47fb08008ca203
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.3"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "17.3.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+19"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+6"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.19"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.2"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.1"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
path:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.6"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
sqlcipher_flutter_libs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlcipher_flutter_libs
|
||||||
|
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0+eol"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "752d9d746052359a2022f588bb979f2e7c4e0f9e4b6a1c3121f7626a1574974b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.4"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3_flutter_libs
|
||||||
|
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0+eol"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.12.2 <4.0.0"
|
||||||
|
flutter: ">=3.44.0"
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
name: kooltab2
|
||||||
|
description: "A new Flutter project."
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.12.2
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
go_router: ^17.3.0
|
||||||
|
provider: ^6.1.5+1
|
||||||
|
uuid: ^4.5.3
|
||||||
|
drift: ^2.34.0
|
||||||
|
drift_flutter: ^0.3.0
|
||||||
|
path_provider: ^2.1.6
|
||||||
|
image_picker: ^1.2.3
|
||||||
|
path: ^1.9.1
|
||||||
|
flutter_slidable: ^4.0.3
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/to/asset-from-package
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/to/font-from-package
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// This is a basic Flutter widget test.
|
||||||
|
//
|
||||||
|
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||||
|
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||||
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:kooltab2/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="kooltab2">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>kooltab2</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
You can customize the "flutter_bootstrap.js" script.
|
||||||
|
This is useful to provide a custom configuration to the Flutter loader
|
||||||
|
or to give the user feedback during the initialization process.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://docs.flutter.dev/platform-integration/web/initialization
|
||||||
|
-->
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "kooltab2",
|
||||||
|
"short_name": "kooltab2",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0175C2",
|
||||||
|
"theme_color": "#0175C2",
|
||||||
|
"description": "A new Flutter project.",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(kooltab2 LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "kooltab2")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(VERSION 3.14...3.25)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
|
||||||
|
# Wrapper sources needed for a plugin.
|
||||||
|
add_library(flutter_wrapper_plugin STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_plugin)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||||
|
|
||||||
|
# Wrapper sources needed for the runner.
|
||||||
|
add_library(flutter_wrapper_app STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_app)
|
||||||
|
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_app PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME} WIN32
|
||||||
|
"flutter_window.cpp"
|
||||||
|
"main.cpp"
|
||||||
|
"utils.cpp"
|
||||||
|
"win32_window.cpp"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
"Runner.rc"
|
||||||
|
"runner.exe.manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
# Add dependency libraries and include directories. Add any application-specific
|
||||||
|
# dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#pragma code_page(65001)
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION)
|
||||||
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "dev.brammie15" "\0"
|
||||||
|
VALUE "FileDescription", "kooltab2" "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "kooltab2" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2026 dev.brammie15. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "kooltab2.exe" "\0"
|
||||||
|
VALUE "ProductName", "kooltab2" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
: project_(project) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
if (!Win32Window::OnCreate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
|
// Ensure that basic setup of the controller was successful.
|
||||||
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
this->Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flutter can complete the first frame before the "show window" callback is
|
||||||
|
// registered. The following call ensures a frame is pending to ensure the
|
||||||
|
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||||
|
flutter_controller_->ForceRedraw();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
#define RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
// A window that does nothing but host a Flutter view.
|
||||||
|
class FlutterWindow : public Win32Window {
|
||||||
|
public:
|
||||||
|
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||||
|
explicit FlutterWindow(const flutter::DartProject& project);
|
||||||
|
virtual ~FlutterWindow();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Win32Window:
|
||||||
|
bool OnCreate() override;
|
||||||
|
void OnDestroy() override;
|
||||||
|
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The project to run.
|
||||||
|
flutter::DartProject project_;
|
||||||
|
|
||||||
|
// The Flutter instance hosted by this window.
|
||||||
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
|
// new console when running with a debugger.
|
||||||
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
CreateAndAttachConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize COM, so that it is available for use in the library and/or
|
||||||
|
// plugins.
|
||||||
|
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
|
||||||
|
flutter::DartProject project(L"data");
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments =
|
||||||
|
GetCommandLineArguments();
|
||||||
|
|
||||||
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
|
FlutterWindow window(project);
|
||||||
|
Win32Window::Point origin(10, 10);
|
||||||
|
Win32Window::Size size(1280, 720);
|
||||||
|
if (!window.Create(L"kooltab2", origin, size)) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
window.SetQuitOnClose(true);
|
||||||
|
|
||||||
|
::MSG msg;
|
||||||
|
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||||
|
::TranslateMessage(&msg);
|
||||||
|
::DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::CoUninitialize();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//{{NO_DEPENDENCIES}}
|
||||||
|
// Microsoft Visual C++ generated include file.
|
||||||
|
// Used by Runner.rc
|
||||||
|
//
|
||||||
|
#define IDI_APP_ICON 101
|
||||||
|
|
||||||
|
// Next default values for new objects
|
||||||
|
//
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||||
|
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||||
|
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||||
|
#define _APS_NEXT_SYMED_VALUE 101
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 and Windows 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
void CreateAndAttachConsole() {
|
||||||
|
if (::AllocConsole()) {
|
||||||
|
FILE *unused;
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||||
|
_dup2(_fileno(stdout), 1);
|
||||||
|
}
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||||
|
_dup2(_fileno(stdout), 2);
|
||||||
|
}
|
||||||
|
std::ios::sync_with_stdio();
|
||||||
|
FlutterDesktopResyncOutputStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GetCommandLineArguments() {
|
||||||
|
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||||
|
int argc;
|
||||||
|
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||||
|
if (argv == nullptr) {
|
||||||
|
return std::vector<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments;
|
||||||
|
|
||||||
|
// Skip the first argument as it's the binary name.
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
::LocalFree(argv);
|
||||||
|
|
||||||
|
return command_line_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||||
|
if (utf16_string == nullptr) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
// First, find the length of the string with a safe upper bound (CWE-126).
|
||||||
|
// UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING.
|
||||||
|
int input_length = static_cast<int>(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS));
|
||||||
|
// Now use that bounded length to determine the required buffer size.
|
||||||
|
// When an explicit length is passed, WideCharToMultiByte does not include
|
||||||
|
// the null terminator in its returned size.
|
||||||
|
int target_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, nullptr, 0, nullptr, nullptr);
|
||||||
|
std::string utf8_string;
|
||||||
|
if (target_length == 0 || static_cast<size_t>(target_length) > utf8_string.max_size()) {
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
|
utf8_string.resize(target_length);
|
||||||
|
int converted_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||||
|
if (converted_length == 0) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef RUNNER_UTILS_H_
|
||||||
|
#define RUNNER_UTILS_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Creates a console for the process, and redirects stdout and stderr to
|
||||||
|
// it for both the runner and the Flutter library.
|
||||||
|
void CreateAndAttachConsole();
|
||||||
|
|
||||||
|
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||||
|
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||||
|
|
||||||
|
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||||
|
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||||
|
std::vector<std::string> GetCommandLineArguments();
|
||||||
|
|
||||||
|
#endif // RUNNER_UTILS_H_
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// Window attribute that enables dark mode window decorations.
|
||||||
|
///
|
||||||
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
|
/// version 10.0.22000.0.
|
||||||
|
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
|
|
||||||
|
/// Registry key for app theme preference.
|
||||||
|
///
|
||||||
|
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||||
|
/// value indicates apps should use light mode.
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||||
|
|
||||||
|
// The number of Win32Window objects that currently exist.
|
||||||
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
|
// scale factor
|
||||||
|
int Scale(int source, double scale_factor) {
|
||||||
|
return static_cast<int>(source * scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||||
|
// This API is only needed for PerMonitor V1 awareness mode.
|
||||||
|
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||||
|
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||||
|
if (!user32_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto enable_non_client_dpi_scaling =
|
||||||
|
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||||
|
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||||
|
if (enable_non_client_dpi_scaling != nullptr) {
|
||||||
|
enable_non_client_dpi_scaling(hwnd);
|
||||||
|
}
|
||||||
|
FreeLibrary(user32_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Manages the Win32Window's window class registration.
|
||||||
|
class WindowClassRegistrar {
|
||||||
|
public:
|
||||||
|
~WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
// Returns the singleton registrar instance.
|
||||||
|
static WindowClassRegistrar* GetInstance() {
|
||||||
|
if (!instance_) {
|
||||||
|
instance_ = new WindowClassRegistrar();
|
||||||
|
}
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the window class, registering the class if it hasn't
|
||||||
|
// previously been registered.
|
||||||
|
const wchar_t* GetWindowClass();
|
||||||
|
|
||||||
|
// Unregisters the window class. Should only be called if there are no
|
||||||
|
// instances of the window.
|
||||||
|
void UnregisterWindowClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
static WindowClassRegistrar* instance_;
|
||||||
|
|
||||||
|
bool class_registered_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||||
|
|
||||||
|
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||||
|
if (!class_registered_) {
|
||||||
|
WNDCLASS window_class{};
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
window_class.cbClsExtra = 0;
|
||||||
|
window_class.cbWndExtra = 0;
|
||||||
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
|
window_class.hIcon =
|
||||||
|
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||||
|
window_class.hbrBackground = 0;
|
||||||
|
window_class.lpszMenuName = nullptr;
|
||||||
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
|
RegisterClass(&window_class);
|
||||||
|
class_registered_ = true;
|
||||||
|
}
|
||||||
|
return kWindowClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
|
class_registered_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::Win32Window() {
|
||||||
|
++g_active_window_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::~Win32Window() {
|
||||||
|
--g_active_window_count;
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Create(const std::wstring& title,
|
||||||
|
const Point& origin,
|
||||||
|
const Size& size) {
|
||||||
|
Destroy();
|
||||||
|
|
||||||
|
const wchar_t* window_class =
|
||||||
|
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||||
|
|
||||||
|
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||||
|
static_cast<LONG>(origin.y)};
|
||||||
|
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||||
|
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||||
|
double scale_factor = dpi / 96.0;
|
||||||
|
|
||||||
|
HWND window = CreateWindow(
|
||||||
|
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||||
|
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||||
|
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||||
|
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTheme(window);
|
||||||
|
|
||||||
|
return OnCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Show() {
|
||||||
|
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||||
|
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||||
|
|
||||||
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
|
that->window_handle_ = window;
|
||||||
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
Win32Window::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
switch (message) {
|
||||||
|
case WM_DESTROY:
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
Destroy();
|
||||||
|
if (quit_on_close_) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||||
|
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||||
|
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||||
|
|
||||||
|
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||||
|
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_SIZE: {
|
||||||
|
RECT rect = GetClientArea();
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
// Size and position the child window.
|
||||||
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||||
|
rect.bottom - rect.top, TRUE);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_ACTIVATE:
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||||
|
UpdateTheme(hwnd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::Destroy() {
|
||||||
|
OnDestroy();
|
||||||
|
|
||||||
|
if (window_handle_) {
|
||||||
|
DestroyWindow(window_handle_);
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||||
|
return reinterpret_cast<Win32Window*>(
|
||||||
|
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
|
child_content_ = content;
|
||||||
|
SetParent(content, window_handle_);
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
|
frame.bottom - frame.top, true);
|
||||||
|
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND Win32Window::GetHandle() {
|
||||||
|
return window_handle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||||
|
quit_on_close_ = quit_on_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::OnCreate() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::OnDestroy() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::UpdateTheme(HWND const window) {
|
||||||
|
DWORD light_mode;
|
||||||
|
DWORD light_mode_size = sizeof(light_mode);
|
||||||
|
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||||
|
kGetPreferredBrightnessRegValue,
|
||||||
|
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||||
|
&light_mode_size);
|
||||||
|
|
||||||
|
if (result == ERROR_SUCCESS) {
|
||||||
|
BOOL enable_dark_mode = light_mode == 0;
|
||||||
|
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||||
|
// inherited from by classes that wish to specialize with custom
|
||||||
|
// rendering and input handling
|
||||||
|
class Win32Window {
|
||||||
|
public:
|
||||||
|
struct Point {
|
||||||
|
unsigned int x;
|
||||||
|
unsigned int y;
|
||||||
|
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
Size(unsigned int width, unsigned int height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Win32Window();
|
||||||
|
virtual ~Win32Window();
|
||||||
|
|
||||||
|
// Creates a win32 window with |title| that is positioned and sized using
|
||||||
|
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||||
|
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||||
|
// consistent size this function will scale the inputted width and height as
|
||||||
|
// as appropriate for the default monitor. The window is invisible until
|
||||||
|
// |Show| is called. Returns true if the window was created successfully.
|
||||||
|
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||||
|
|
||||||
|
// Show the current window. Returns true if the window was successfully shown.
|
||||||
|
bool Show();
|
||||||
|
|
||||||
|
// Release OS resources associated with window.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Inserts |content| into the window tree.
|
||||||
|
void SetChildContent(HWND content);
|
||||||
|
|
||||||
|
// Returns the backing Window handle to enable clients to set icon and other
|
||||||
|
// window properties. Returns nullptr if the window has been destroyed.
|
||||||
|
HWND GetHandle();
|
||||||
|
|
||||||
|
// If true, closing this window will quit the application.
|
||||||
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Processes and route salient window messages for mouse handling,
|
||||||
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
|
// inheriting classes can handle.
|
||||||
|
virtual LRESULT MessageHandler(HWND window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Called when CreateAndShow is called, allowing subclass window-related
|
||||||
|
// setup. Subclasses should return false if setup fails.
|
||||||
|
virtual bool OnCreate();
|
||||||
|
|
||||||
|
// Called when Destroy is called.
|
||||||
|
virtual void OnDestroy();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WindowClassRegistrar;
|
||||||
|
|
||||||
|
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||||
|
// is passed when the non-client area is being created and enables automatic
|
||||||
|
// non-client DPI scaling so that the non-client area automatically
|
||||||
|
// responds to changes in DPI. All other messages are handled by
|
||||||
|
// MessageHandler.
|
||||||
|
static LRESULT CALLBACK WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Retrieves a class instance pointer for |window|
|
||||||
|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||||
|
|
||||||
|
// Update the window frame's theme to match the system theme.
|
||||||
|
static void UpdateTheme(HWND const window);
|
||||||
|
|
||||||
|
bool quit_on_close_ = false;
|
||||||
|
|
||||||
|
// window handle for top level window.
|
||||||
|
HWND window_handle_ = nullptr;
|
||||||
|
|
||||||
|
// window handle for hosted content.
|
||||||
|
HWND child_content_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_WIN32_WINDOW_H_
|
||||||