This commit is contained in:
mons 2026-08-02 15:50:25 -04:00
parent bdc1c78221
commit abb3c7e506
220 changed files with 12697 additions and 2 deletions

45
.gitignore vendored Normal file
View file

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

45
.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: android
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: ios
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: linux
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: macos
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: web
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: windows
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# 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'

View file

@ -1,3 +1,38 @@
# OTMovilGasfiter
# SMoya Gasfiter
Aplicación Flutter para registrar órdenes de trabajo de mantenimiento sin depender de conexión a internet.
Aplicación Flutter para registrar órdenes de trabajo de mantenimiento sin
depender de conexión a internet.
## Funcionalidades
- Creación y edición de órdenes como borrador.
- Varias tareas por orden, cada una con su observación.
- Varias fotografías antes y después por tarea, desde cámara o galería.
- Persistencia local mediante SQLite y archivos privados de la aplicación.
- Validación de evidencia antes de finalizar una orden.
- Informe PDF con el detalle completo y las fotografías.
- Correo preparado con resumen y PDF adjunto mediante la aplicación de correo
configurada en el dispositivo.
- Consulta, reenvío de correo y eliminación de órdenes guardadas.
## Ejecutar
```bash
flutter pub get
flutter run
```
Para usar cámara, galería y correo se recomienda ejecutar en un teléfono real.
En Android debe existir una aplicación de correo configurada. En iOS debe haber
una cuenta disponible para el compositor nativo de Mail.
## Verificar
```bash
flutter analyze
flutter test
```
La información se guarda dentro del espacio privado de la aplicación. Al
desinstalarla, el sistema operativo puede eliminar la base de datos, las fotos y
los PDF generados.

28
analysis_options.yaml Normal file
View file

@ -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

14
android/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "cl.otmovil.ot_movil"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "cl.otmovil.ot_movil"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

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

View file

@ -0,0 +1,55 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="SMoya Gasfiter"
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.SENDTO"/>
<data android:scheme="mailto"/>
</intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.speech.RecognitionService"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package cl.otmovil.ot_movil
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

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

View file

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

24
android/build.gradle.kts Normal file
View file

@ -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)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View file

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

View file

@ -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 "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

BIN
assets/icon/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

34
ios/.gitignore vendored Normal file
View file

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

View file

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

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

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

View file

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

View file

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

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

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>SMoya Gasfiter</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>SMoya Gasfiter</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>SMoya Gasfiter necesita la cámara para registrar evidencia antes y después de la mantención.</string>
<key>NSMicrophoneUsageDescription</key>
<string>SMoya Gasfiter necesita el micrófono para dictar las observaciones de las tareas.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>SMoya Gasfiter necesita acceso a tus fotos para adjuntar evidencia a la orden de trabajo.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>SMoya Gasfiter necesita convertir el dictado en texto para completar las observaciones.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View file

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

View file

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View file

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

283
lib/data/app_database.dart Normal file
View file

@ -0,0 +1,283 @@
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import '../models/work_order.dart';
class AppDatabase {
AppDatabase._();
static final AppDatabase instance = AppDatabase._();
Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
final databasePath = await getDatabasesPath();
_database = await openDatabase(
p.join(databasePath, 'ot_movil.db'),
version: 6,
onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
},
onCreate: (database, version) async {
await database.execute('''
CREATE TABLE work_orders (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
completed_at TEXT,
client TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
address TEXT NOT NULL DEFAULT '',
labor_cost INTEGER NOT NULL DEFAULT 0,
recipient_email TEXT,
pdf_path TEXT
)
''');
await database.execute('''
CREATE TABLE work_tasks (
id TEXT PRIMARY KEY,
work_order_id TEXT NOT NULL,
title TEXT NOT NULL,
observation TEXT NOT NULL,
position INTEGER NOT NULL,
FOREIGN KEY (work_order_id)
REFERENCES work_orders (id) ON DELETE CASCADE
)
''');
await database.execute('''
CREATE TABLE task_photos (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
kind TEXT NOT NULL,
path TEXT NOT NULL,
FOREIGN KEY (task_id)
REFERENCES work_tasks (id) ON DELETE CASCADE
)
''');
await database.execute('''
CREATE TABLE task_parts (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
name TEXT NOT NULL,
price INTEGER NOT NULL,
position INTEGER NOT NULL,
FOREIGN KEY (task_id)
REFERENCES work_tasks (id) ON DELETE CASCADE
)
''');
await database.execute(
'CREATE INDEX idx_tasks_order ON work_tasks(work_order_id)',
);
await database.execute(
'CREATE INDEX idx_photos_task ON task_photos(task_id)',
);
await database.execute(
'CREATE INDEX idx_parts_task ON task_parts(task_id)',
);
},
onUpgrade: (database, oldVersion, newVersion) async {
if (oldVersion < 2) {
await database.execute(
"ALTER TABLE work_orders "
"ADD COLUMN client TEXT NOT NULL DEFAULT ''",
);
await database.execute(
"ALTER TABLE work_orders "
"ADD COLUMN address TEXT NOT NULL DEFAULT ''",
);
}
if (oldVersion < 3) {
await database.execute('''
CREATE TABLE task_parts (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
name TEXT NOT NULL,
price INTEGER NOT NULL,
position INTEGER NOT NULL,
FOREIGN KEY (task_id)
REFERENCES work_tasks (id) ON DELETE CASCADE
)
''');
await database.execute(
'CREATE INDEX idx_parts_task ON task_parts(task_id)',
);
}
if (oldVersion < 4) {
await database.execute(
'ALTER TABLE work_orders '
'ADD COLUMN labor_cost INTEGER NOT NULL DEFAULT 0',
);
}
if (oldVersion < 5) {
await database.execute(
"ALTER TABLE work_orders "
"ADD COLUMN phone TEXT NOT NULL DEFAULT ''",
);
}
},
);
return _database!;
}
Future<List<WorkOrder>> getWorkOrders() async {
final db = await database;
final rows = await db.query('work_orders', orderBy: 'updated_at DESC');
final orders = <WorkOrder>[];
for (final row in rows) {
orders.add(await _workOrderFromRow(db, row));
}
return orders;
}
Future<WorkOrder?> getWorkOrder(String id) async {
final db = await database;
final rows = await db.query(
'work_orders',
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
if (rows.isEmpty) return null;
return _workOrderFromRow(db, rows.first);
}
Future<WorkOrder> _workOrderFromRow(
DatabaseExecutor db,
Map<String, Object?> row,
) async {
final orderId = row['id']! as String;
final taskRows = await db.query(
'work_tasks',
where: 'work_order_id = ?',
whereArgs: [orderId],
orderBy: 'position ASC',
);
final tasks = <WorkTask>[];
for (final taskRow in taskRows) {
final taskId = taskRow['id']! as String;
final photoRows = await db.query(
'task_photos',
where: 'task_id = ?',
whereArgs: [taskId],
orderBy: 'rowid ASC',
);
final partRows = await db.query(
'task_parts',
where: 'task_id = ?',
whereArgs: [taskId],
orderBy: 'position ASC',
);
tasks.add(
WorkTask(
id: taskId,
title: taskRow['title']! as String,
observation: taskRow['observation']! as String,
position: taskRow['position']! as int,
photos: photoRows
.map(
(photoRow) => TaskPhoto(
id: photoRow['id']! as String,
path: photoRow['path']! as String,
kind: PhotoKind.fromDatabase(photoRow['kind']! as String),
),
)
.toList(),
parts: partRows
.map(
(partRow) => TaskPart(
id: partRow['id']! as String,
name: partRow['name']! as String,
price: partRow['price']! as int,
),
)
.toList(),
),
);
}
return WorkOrder(
id: orderId,
title: row['title']! as String,
status: WorkOrderStatus.fromDatabase(row['status']! as String),
createdAt: DateTime.parse(row['created_at']! as String),
updatedAt: DateTime.parse(row['updated_at']! as String),
completedAt: row['completed_at'] == null
? null
: DateTime.parse(row['completed_at']! as String),
details: WorkOrderDetails(
client: row['client']! as String,
phone: row['phone']! as String,
address: row['address']! as String,
),
laborCost: row['labor_cost']! as int,
recipientEmail: row['recipient_email'] as String?,
pdfPath: row['pdf_path'] as String?,
tasks: tasks,
);
}
Future<void> saveWorkOrder(WorkOrder order) async {
final db = await database;
await db.transaction((transaction) async {
await transaction.insert('work_orders', {
'id': order.id,
'title': order.title,
'status': order.status.databaseValue,
'created_at': order.createdAt.toIso8601String(),
'updated_at': order.updatedAt.toIso8601String(),
'completed_at': order.completedAt?.toIso8601String(),
'client': order.details.client,
'phone': order.details.phone,
'address': order.details.address,
'labor_cost': order.laborCost,
'recipient_email': order.recipientEmail,
'pdf_path': order.pdfPath,
}, conflictAlgorithm: ConflictAlgorithm.replace);
await transaction.delete(
'work_tasks',
where: 'work_order_id = ?',
whereArgs: [order.id],
);
for (final task in order.tasks) {
await transaction.insert('work_tasks', {
'id': task.id,
'work_order_id': order.id,
'title': task.title,
'observation': task.observation,
'position': task.position,
});
for (final photo in task.photos) {
await transaction.insert('task_photos', {
'id': photo.id,
'task_id': task.id,
'kind': photo.kind.databaseValue,
'path': photo.path,
});
}
for (var index = 0; index < task.parts.length; index++) {
final part = task.parts[index];
await transaction.insert('task_parts', {
'id': part.id,
'task_id': task.id,
'name': part.name,
'price': part.price,
'position': index,
});
}
}
});
}
Future<void> deleteWorkOrder(String id) async {
final db = await database;
await db.delete('work_orders', where: 'id = ?', whereArgs: [id]);
}
}

105
lib/main.dart Normal file
View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const OtMovilApp());
}
class OtMovilApp extends StatelessWidget {
const OtMovilApp({super.key});
static const primary = Color(0xFF0B5C5E);
static const accent = Color(0xFFF4B740);
static const ink = Color(0xFF163334);
static const canvas = Color(0xFFF5F8F7);
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.fromSeed(
seedColor: primary,
brightness: Brightness.light,
primary: primary,
secondary: accent,
surface: Colors.white,
);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'SMoya Gasfiter',
theme: ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: canvas,
appBarTheme: const AppBarTheme(
backgroundColor: canvas,
foregroundColor: ink,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
color: ink,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
cardTheme: CardThemeData(
color: Colors.white,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: const BorderSide(color: Color(0xFFDCE7E5)),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFD4E1DF)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFD4E1DF)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: primary, width: 1.8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 15,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(0, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(fontWeight: FontWeight.w700),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
side: const BorderSide(color: Color(0xFFB8CFCC)),
textStyle: const TextStyle(fontWeight: FontWeight.w700),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
home: const HomeScreen(),
);
}
}

155
lib/models/work_order.dart Normal file
View file

@ -0,0 +1,155 @@
enum WorkOrderStatus {
open,
completed;
String get databaseValue => name;
static WorkOrderStatus fromDatabase(String value) {
return WorkOrderStatus.values.firstWhere(
(status) => status.name == value,
orElse: () => WorkOrderStatus.open,
);
}
}
enum PhotoKind {
before,
after;
String get databaseValue => name;
static PhotoKind fromDatabase(String value) {
return value == PhotoKind.after.name ? PhotoKind.after : PhotoKind.before;
}
}
class TaskPhoto {
const TaskPhoto({required this.id, required this.path, required this.kind});
final String id;
final String path;
final PhotoKind kind;
}
class TaskPart {
const TaskPart({required this.id, required this.name, required this.price});
final String id;
final String name;
final int price;
}
class WorkTask {
const WorkTask({
required this.id,
required this.title,
required this.observation,
required this.position,
this.photos = const [],
this.parts = const [],
});
final String id;
final String title;
final String observation;
final int position;
final List<TaskPhoto> photos;
final List<TaskPart> parts;
List<TaskPhoto> get beforePhotos =>
photos.where((photo) => photo.kind == PhotoKind.before).toList();
List<TaskPhoto> get afterPhotos =>
photos.where((photo) => photo.kind == PhotoKind.after).toList();
int get partsTotal => parts.fold(0, (total, part) => total + part.price);
}
class WorkOrderDetails {
const WorkOrderDetails({
this.client = '',
this.phone = '',
this.address = '',
});
final String client;
final String phone;
final String address;
bool get isComplete =>
client.trim().isNotEmpty &&
phone.trim().isNotEmpty &&
address.trim().isNotEmpty;
WorkOrderDetails copyWith({String? client, String? phone, String? address}) {
return WorkOrderDetails(
client: client ?? this.client,
phone: phone ?? this.phone,
address: address ?? this.address,
);
}
}
class WorkOrder {
const WorkOrder({
required this.id,
required this.title,
required this.createdAt,
required this.updatedAt,
required this.status,
this.details = const WorkOrderDetails(),
this.laborCost = 0,
this.completedAt,
this.recipientEmail,
this.pdfPath,
this.tasks = const [],
});
final String id;
final String title;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? completedAt;
final WorkOrderStatus status;
final WorkOrderDetails details;
final int laborCost;
final String? recipientEmail;
final String? pdfPath;
final List<WorkTask> tasks;
bool get isCompleted => status == WorkOrderStatus.completed;
int get totalPhotos =>
tasks.fold(0, (total, task) => total + task.photos.length);
int get totalPartsCost =>
tasks.fold(0, (total, task) => total + task.partsTotal);
int get totalCost => laborCost + totalPartsCost;
WorkOrder copyWith({
String? title,
DateTime? updatedAt,
DateTime? completedAt,
WorkOrderStatus? status,
WorkOrderDetails? details,
int? laborCost,
String? recipientEmail,
String? pdfPath,
List<WorkTask>? tasks,
}) {
return WorkOrder(
id: id,
title: title ?? this.title,
createdAt: createdAt,
updatedAt: updatedAt ?? this.updatedAt,
completedAt: completedAt ?? this.completedAt,
status: status ?? this.status,
details: details ?? this.details,
laborCost: laborCost ?? this.laborCost,
recipientEmail: recipientEmail ?? this.recipientEmail,
pdfPath: pdfPath ?? this.pdfPath,
tasks: tasks ?? this.tasks,
);
}
}

View file

@ -0,0 +1,596 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../data/app_database.dart';
import '../models/work_order.dart';
import '../services/image_storage_service.dart';
import 'work_order_details_screen.dart';
import 'work_order_screen.dart';
enum _OrderFilter { all, open, completed }
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _database = AppDatabase.instance;
final _imageStorage = ImageStorageService();
List<WorkOrder> _orders = const [];
_OrderFilter _filter = _OrderFilter.all;
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadOrders();
}
Future<void> _loadOrders() async {
setState(() {
_loading = true;
_error = null;
});
try {
final orders = await _database.getWorkOrders();
if (!mounted) return;
setState(() {
_orders = orders;
_loading = false;
});
} catch (_) {
if (!mounted) return;
setState(() {
_loading = false;
_error = 'No fue posible leer las órdenes guardadas.';
});
}
}
List<WorkOrder> get _visibleOrders {
return switch (_filter) {
_OrderFilter.all => _orders,
_OrderFilter.open =>
_orders.where((order) => !order.isCompleted).toList(),
_OrderFilter.completed =>
_orders.where((order) => order.isCompleted).toList(),
};
}
Future<void> _openOrder([WorkOrder? order]) async {
final details = await Navigator.of(context).push<WorkOrderDetails>(
MaterialPageRoute(builder: (_) => WorkOrderDetailsScreen(order: order)),
);
if (!mounted || details == null) return;
await Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => WorkOrderScreen(order: order, details: details),
),
);
await _loadOrders();
}
Future<void> _deleteOrder(WorkOrder order) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Eliminar orden'),
content: Text(
'Se eliminará “${order.title}” junto con sus fotos y su informe. '
'Esta acción no se puede deshacer.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancelar'),
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
onPressed: () => Navigator.pop(context, true),
child: const Text('Eliminar'),
),
],
),
);
if (confirmed != true) return;
try {
await _database.deleteWorkOrder(order.id);
await _imageStorage.deleteWorkOrderImages(order.id);
final pdfPath = order.pdfPath;
if (pdfPath != null) {
final report = File(pdfPath);
if (await report.exists()) await report.delete();
}
await _loadOrders();
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Orden eliminada.')));
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No fue posible eliminar la orden.')),
);
}
}
@override
Widget build(BuildContext context) {
final visibleOrders = _visibleOrders;
return Scaffold(
appBar: AppBar(
toolbarHeight: 72,
title: const _Brand(),
actions: [
IconButton(
tooltip: 'Actualizar',
onPressed: _loading ? null : _loadOrders,
icon: const Icon(Icons.refresh_rounded),
),
const SizedBox(width: 8),
],
),
body: RefreshIndicator(
onRefresh: _loadOrders,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Órdenes de trabajo',
style: Theme.of(context).textTheme.headlineMedium
?.copyWith(
color: const Color(0xFF163334),
fontWeight: FontWeight.w800,
letterSpacing: -.5,
),
),
const SizedBox(height: 6),
Text(
'Registra el mantenimiento y conserva su evidencia.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: const Color(0xFF627372),
),
),
const SizedBox(height: 20),
_SummaryStrip(orders: _orders),
const SizedBox(height: 18),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SegmentedButton<_OrderFilter>(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: _OrderFilter.all,
label: Text('Todas'),
),
ButtonSegment(
value: _OrderFilter.open,
label: Text('Borradores'),
),
ButtonSegment(
value: _OrderFilter.completed,
label: Text('Finalizadas'),
),
],
selected: {_filter},
onSelectionChanged: (selection) {
setState(() => _filter = selection.first);
},
),
),
],
),
),
),
if (_loading)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
)
else if (_error != null)
SliverFillRemaining(
hasScrollBody: false,
child: _MessageState(
icon: Icons.cloud_off_rounded,
title: 'No pudimos cargar las órdenes',
message: _error!,
buttonLabel: 'Reintentar',
onPressed: _loadOrders,
),
)
else if (visibleOrders.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: _MessageState(
icon: Icons.assignment_outlined,
title: _orders.isEmpty
? 'Aún no hay órdenes'
: 'No hay órdenes en este estado',
message: _orders.isEmpty
? 'Crea tu primera OT y agrega las tareas del mantenimiento.'
: 'Prueba seleccionando otro filtro.',
),
)
else
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 110),
sliver: SliverList.separated(
itemCount: visibleOrders.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final order = visibleOrders[index];
return _OrderCard(
order: order,
onTap: () => _openOrder(order),
onDelete: () => _deleteOrder(order),
);
},
),
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _openOrder(),
backgroundColor: const Color(0xFF0B5C5E),
foregroundColor: Colors.white,
icon: const Icon(Icons.add_rounded),
label: const Text(
'Nueva OT',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
);
}
}
class _Brand extends StatelessWidget {
const _Brand();
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF0B5C5E),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.handyman_rounded,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 11),
const Text('SMoya Gasfiter'),
],
);
}
}
class _SummaryStrip extends StatelessWidget {
const _SummaryStrip({required this.orders});
final List<WorkOrder> orders;
@override
Widget build(BuildContext context) {
final completed = orders.where((order) => order.isCompleted).length;
return Row(
children: [
Expanded(
child: _SummaryItem(
value: '${orders.length - completed}',
label: 'Borradores',
icon: Icons.edit_note_rounded,
color: const Color(0xFFF4B740),
),
),
const SizedBox(width: 10),
Expanded(
child: _SummaryItem(
value: '$completed',
label: 'Finalizadas',
icon: Icons.task_alt_rounded,
color: const Color(0xFF2E8B70),
),
),
],
);
}
}
class _SummaryItem extends StatelessWidget {
const _SummaryItem({
required this.value,
required this.label,
required this.icon,
required this.color,
});
final String value;
final String label;
final IconData icon;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFDCE7E5)),
),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: color.withValues(alpha: .15),
borderRadius: BorderRadius.circular(11),
),
child: Icon(icon, color: color, size: 21),
),
const SizedBox(width: 11),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: const TextStyle(
color: Color(0xFF163334),
fontWeight: FontWeight.w800,
fontSize: 20,
height: 1,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(color: Color(0xFF627372), fontSize: 12),
),
],
),
],
),
);
}
}
class _OrderCard extends StatelessWidget {
const _OrderCard({
required this.order,
required this.onTap,
required this.onDelete,
});
final WorkOrder order;
final VoidCallback onTap;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
final statusColor = order.isCompleted
? const Color(0xFF2E8B70)
: const Color(0xFFB77800);
final statusBackground = order.isCompleted
? const Color(0xFFE4F3ED)
: const Color(0xFFFFF3D8);
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: Padding(
padding: const EdgeInsets.fromLTRB(17, 17, 8, 17),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFE7F1F0),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.assignment_outlined,
color: Color(0xFF0B5C5E),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
order.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF163334),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 9,
vertical: 5,
),
decoration: BoxDecoration(
color: statusBackground,
borderRadius: BorderRadius.circular(20),
),
child: Text(
order.isCompleted ? 'Finalizada' : 'Borrador',
style: TextStyle(
color: statusColor,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 8),
if (order.details.client.isNotEmpty) ...[
_OrderCardDetail(
icon: Icons.person_outline_rounded,
text: order.details.client,
),
const SizedBox(height: 4),
],
if (order.details.address.isNotEmpty) ...[
_OrderCardDetail(
icon: Icons.location_on_outlined,
text: order.details.address,
),
const SizedBox(height: 7),
],
Text(
'${order.tasks.length} tarea(s) · '
'${order.totalPhotos} foto(s) · '
'${DateFormat('dd/MM/yyyy').format(order.updatedAt.toLocal())}',
style: const TextStyle(
color: Color(0xFF6A7A79),
fontSize: 12,
),
),
],
),
),
PopupMenuButton<String>(
tooltip: 'Más opciones',
onSelected: (value) {
if (value == 'delete') onDelete();
},
itemBuilder: (context) => const [
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline_rounded),
SizedBox(width: 10),
Text('Eliminar'),
],
),
),
],
),
],
),
),
),
);
}
}
class _OrderCardDetail extends StatelessWidget {
const _OrderCardDetail({required this.icon, required this.text});
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, size: 15, color: const Color(0xFF0B5C5E)),
const SizedBox(width: 6),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF526665), fontSize: 12),
),
),
],
);
}
}
class _MessageState extends StatelessWidget {
const _MessageState({
required this.icon,
required this.title,
required this.message,
this.buttonLabel,
this.onPressed,
});
final IconData icon;
final String title;
final String message;
final String? buttonLabel;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 20, 32, 100),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 74,
height: 74,
decoration: const BoxDecoration(
color: Color(0xFFE7F1F0),
shape: BoxShape.circle,
),
child: Icon(icon, size: 34, color: const Color(0xFF0B5C5E)),
),
const SizedBox(height: 20),
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: const Color(0xFF163334),
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF627372), height: 1.45),
),
if (buttonLabel != null) ...[
const SizedBox(height: 20),
FilledButton(onPressed: onPressed, child: Text(buttonLabel!)),
],
],
),
),
);
}
}

View file

@ -0,0 +1,42 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:pdf/pdf.dart';
import 'package:printing/printing.dart';
class PdfPreviewScreen extends StatelessWidget {
const PdfPreviewScreen({
required this.pdfPath,
required this.title,
super.key,
});
final String pdfPath;
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Informe PDF')),
body: PdfPreview(
build: (_) => File(pdfPath).readAsBytes(),
initialPageFormat: PdfPageFormat.a4,
pdfFileName: '$title.pdf',
canChangeOrientation: false,
canChangePageFormat: false,
allowPrinting: true,
allowSharing: true,
loadingWidget: const Center(child: CircularProgressIndicator()),
onError: (context, error) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
'No fue posible abrir el informe.\n$error',
textAlign: TextAlign.center,
),
),
),
),
);
}
}

View file

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import '../models/work_order.dart';
class WorkOrderDetailsScreen extends StatefulWidget {
const WorkOrderDetailsScreen({this.order, super.key});
final WorkOrder? order;
@override
State<WorkOrderDetailsScreen> createState() => _WorkOrderDetailsScreenState();
}
class _WorkOrderDetailsScreenState extends State<WorkOrderDetailsScreen> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _clientController;
late final TextEditingController _phoneController;
late final TextEditingController _addressController;
bool get _readOnly => widget.order?.isCompleted ?? false;
@override
void initState() {
super.initState();
final details = widget.order?.details ?? const WorkOrderDetails();
_clientController = TextEditingController(text: details.client);
_phoneController = TextEditingController(text: details.phone);
_addressController = TextEditingController(text: details.address);
}
@override
void dispose() {
_clientController.dispose();
_phoneController.dispose();
_addressController.dispose();
super.dispose();
}
Future<void> _continue() async {
if (!_readOnly && !_formKey.currentState!.validate()) return;
final details = WorkOrderDetails(
client: _clientController.text.trim(),
phone: _phoneController.text.trim(),
address: _addressController.text.trim(),
);
Navigator.of(context).pop(details);
}
String? _requiredField(String? value, String message) {
if (value == null || value.trim().isEmpty) return message;
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_readOnly ? 'Datos de la OT' : 'Completar datos de la OT'),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 120),
children: [
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFFE7F1F0),
borderRadius: BorderRadius.circular(18),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.assignment_ind_outlined, color: Color(0xFF0B5C5E)),
SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Encabezado de la orden',
style: TextStyle(
color: Color(0xFF163334),
fontSize: 17,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 5),
Text(
'Estos datos identificarán al cliente antes de '
'registrar el trabajo y las tareas.',
style: TextStyle(
color: Color(0xFF526665),
height: 1.35,
),
),
],
),
),
],
),
),
const SizedBox(height: 26),
TextFormField(
key: const Key('client-field'),
controller: _clientController,
readOnly: _readOnly,
autofocus: !_readOnly,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: 'Cliente',
hintText: 'Nombre del cliente',
prefixIcon: Icon(Icons.person_outline_rounded),
),
validator: _readOnly
? null
: (value) =>
_requiredField(value, 'Ingresa el nombre del cliente'),
),
const SizedBox(height: 16),
TextFormField(
key: const Key('phone-field'),
controller: _phoneController,
readOnly: _readOnly,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.telephoneNumber],
decoration: const InputDecoration(
labelText: 'Teléfono',
hintText: '+56 9 1234 5678',
prefixIcon: Icon(Icons.phone_outlined),
),
validator: _readOnly
? null
: (value) => _requiredField(
value,
'Ingresa el teléfono del cliente',
),
),
const SizedBox(height: 16),
TextFormField(
key: const Key('address-field'),
controller: _addressController,
readOnly: _readOnly,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.done,
maxLines: 2,
decoration: const InputDecoration(
labelText: 'Dirección',
hintText: 'Calle, número, comuna o referencia',
prefixIcon: Icon(Icons.location_on_outlined),
),
validator: _readOnly
? null
: (value) => _requiredField(
value,
'Ingresa la dirección del trabajo',
),
onFieldSubmitted: (_) => _continue(),
),
],
),
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 16),
child: FilledButton.icon(
key: const Key('continue-button'),
onPressed: _continue,
icon: Icon(
_readOnly
? Icons.visibility_outlined
: Icons.arrow_forward_rounded,
),
label: Text(
_readOnly
? 'Ver trabajo y tareas'
: 'Continuar a trabajo y tareas',
),
),
),
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
class DictationTextAccumulator {
String _baseText = '';
String _recognizedText = '';
String get text =>
[_baseText, _recognizedText].where((part) => part.isNotEmpty).join(' ');
void begin(String existingText) {
_baseText = existingText.trimRight();
_recognizedText = '';
}
String addResult(String words) {
final nextText = words.trim().replaceAll(RegExp(r'\s+'), ' ');
if (nextText.isEmpty) return text;
_recognizedText = _mergeWithoutDeleting(_recognizedText, nextText);
return text;
}
String _mergeWithoutDeleting(String previousText, String nextText) {
if (previousText.isEmpty) return nextText;
final previousWords = _words(previousText);
final nextWords = _words(nextText);
if (_startsWith(nextWords, previousWords)) return nextText;
if (_startsWith(previousWords, nextWords)) return previousText;
final commonPrefixLength = _commonPrefixLength(previousWords, nextWords);
if (commonPrefixLength > 0) {
if (nextWords.length > previousWords.length) return nextText;
if (nextWords.length < previousWords.length) return previousText;
return nextText.length >= previousText.length ? nextText : previousText;
}
final overlapLength = _suffixPrefixOverlap(previousWords, nextWords);
if (overlapLength > 0) {
final originalNextWords = nextText.split(' ');
final remainingWords = originalNextWords.skip(overlapLength).join(' ');
return remainingWords.isEmpty
? previousText
: '$previousText $remainingWords';
}
return '$previousText $nextText';
}
List<String> _words(String value) {
return value
.toLowerCase()
.replaceAll(RegExp(r'[.,;:!?¿¡]'), '')
.split(RegExp(r'\s+'))
.where((word) => word.isNotEmpty)
.toList();
}
bool _startsWith(List<String> words, List<String> prefix) {
if (prefix.length > words.length) return false;
for (var index = 0; index < prefix.length; index++) {
if (words[index] != prefix[index]) return false;
}
return true;
}
int _commonPrefixLength(List<String> first, List<String> second) {
final limit = first.length < second.length ? first.length : second.length;
var commonWords = 0;
while (commonWords < limit && first[commonWords] == second[commonWords]) {
commonWords++;
}
return commonWords;
}
int _suffixPrefixOverlap(List<String> previous, List<String> next) {
final limit = previous.length < next.length ? previous.length : next.length;
for (var overlap = limit; overlap > 0; overlap--) {
var matches = true;
for (var index = 0; index < overlap; index++) {
if (previous[previous.length - overlap + index] != next[index]) {
matches = false;
break;
}
}
if (matches) return overlap;
}
return 0;
}
}

View file

@ -0,0 +1,65 @@
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:intl/intl.dart';
import '../models/work_order.dart';
import 'pdf_service.dart';
class EmailService {
Future<void> sendReport({
required WorkOrder order,
required String recipient,
required String pdfPath,
required PdfReportType reportType,
}) async {
final priceFormat = NumberFormat.decimalPattern('es_CL');
String formatPrice(int value) => '\$${priceFormat.format(value)}';
final buffer = StringBuffer()
..writeln('Orden de trabajo: ${order.title}')
..writeln('Cliente: ${order.details.client}')
..writeln('Dirección: ${order.details.address}')
..writeln()
..writeln('Resumen del trabajo realizado:');
for (var index = 0; index < order.tasks.length; index++) {
final task = order.tasks[index];
buffer
..writeln()
..writeln('${index + 1}. ${task.title}')
..writeln('Observación: ${task.observation}')
..writeln(
'Evidencia: ${task.beforePhotos.length} foto(s) antes y '
'${task.afterPhotos.length} foto(s) después.',
);
if (reportType == PdfReportType.client && task.parts.isNotEmpty) {
buffer.writeln('Repuestos utilizados:');
for (final part in task.parts) {
buffer.writeln('- ${part.name}: ${formatPrice(part.price)}');
}
buffer.writeln(
'Subtotal de repuestos: ${formatPrice(task.partsTotal)}',
);
}
}
if (reportType == PdfReportType.client) {
buffer
..writeln()
..writeln('Costo de mano de obra: ${formatPrice(order.laborCost)}')
..writeln('Total de repuestos: ${formatPrice(order.totalPartsCost)}')
..writeln('Valor total de la OT: ${formatPrice(order.totalCost)}');
}
buffer
..writeln()
..writeln('El informe PDF adjunto contiene el detalle y las fotografías.')
..writeln()
..writeln('Enviado desde SMoya Gasfiter.');
final email = Email(
body: buffer.toString(),
subject: 'Informe para ${reportType.label.toLowerCase()}: ${order.title}',
recipients: [recipient],
attachmentPaths: [pdfPath],
isHTML: false,
);
await FlutterEmailSender.send(email);
}
}

View file

@ -0,0 +1,51 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
class ImageStorageService {
ImageStorageService({Uuid? uuid}) : _uuid = uuid ?? const Uuid();
final Uuid _uuid;
Future<String> storeImage({
required String sourcePath,
required String workOrderId,
required String taskId,
}) async {
final documents = await getApplicationDocumentsDirectory();
final destinationDirectory = Directory(
p.join(documents.path, 'ot_movil', 'images', workOrderId, taskId),
);
await destinationDirectory.create(recursive: true);
final sourceExtension = p.extension(sourcePath).toLowerCase();
final extension = sourceExtension.isEmpty ? '.jpg' : sourceExtension;
final destinationPath = p.join(
destinationDirectory.path,
'${_uuid.v4()}$extension',
);
await File(sourcePath).copy(destinationPath);
return destinationPath;
}
Future<void> deleteFiles(Iterable<String> paths) async {
for (final path in paths) {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
}
}
Future<void> deleteWorkOrderImages(String workOrderId) async {
final documents = await getApplicationDocumentsDirectory();
final directory = Directory(
p.join(documents.path, 'ot_movil', 'images', workOrderId),
);
if (await directory.exists()) {
await directory.delete(recursive: true);
}
}
}

View file

@ -0,0 +1,794 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import '../models/work_order.dart';
enum PdfReportType {
client(
label: 'Cliente',
description: 'Incluye mano de obra, repuestos y valores.',
fileSegment: 'cliente',
),
certifier(
label: 'Empresa certificadora',
description: 'Omite repuestos y toda la información económica.',
fileSegment: 'certificadora',
);
const PdfReportType({
required this.label,
required this.description,
required this.fileSegment,
});
final String label;
final String description;
final String fileSegment;
}
class PdfService {
static const _reportFilePrefix = 'OT_v12_';
bool isCurrentReport(String filePath, {PdfReportType? type}) {
final expectedPrefix = type == null
? _reportFilePrefix
: '$_reportFilePrefix${type.fileSegment}_';
return p.basename(filePath).startsWith(expectedPrefix);
}
Future<String> generate(
WorkOrder order, {
required PdfReportType type,
}) async {
final reportBytes = await build(order, type: type);
final documents = await getApplicationDocumentsDirectory();
final reportDirectory = Directory(
p.join(documents.path, 'ot_movil', 'reports'),
);
await reportDirectory.create(recursive: true);
final shortId = order.id.replaceAll('-', '').substring(0, 8);
final reportPath = p.join(
reportDirectory.path,
'$_reportFilePrefix${type.fileSegment}_'
'${DateFormat('yyyyMMdd_HHmm').format(DateTime.now())}_$shortId.pdf',
);
await File(reportPath).writeAsBytes(reportBytes, flush: true);
return reportPath;
}
Future<Uint8List> build(
WorkOrder order, {
required PdfReportType type,
}) async {
final document = pw.Document(
title: 'Orden de trabajo - ${order.title}',
author: 'SMoya Gasfiter',
subject: 'Informe de mantenimiento para ${type.label.toLowerCase()}',
);
final logoData = await rootBundle.load(
'assets/pdf/simon_moya_report_logo.png',
);
final logo = pw.MemoryImage(
logoData.buffer.asUint8List(
logoData.offsetInBytes,
logoData.lengthInBytes,
),
);
final signatureData = await rootBundle.load(
'assets/pdf/simon_moya_signature.png',
);
final signature = pw.MemoryImage(
signatureData.buffer.asUint8List(
signatureData.offsetInBytes,
signatureData.lengthInBytes,
),
);
final taskPhotos = <String, _TaskImages>{};
for (final task in order.tasks) {
taskPhotos[task.id] = await _loadTaskImages(task);
}
const primary = PdfColor.fromInt(0xFF0B5C5E);
const dark = PdfColor.fromInt(0xFF163334);
const muted = PdfColor.fromInt(0xFF5F6F70);
const pale = PdfColor.fromInt(0xFFE7F1F0);
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
final priceFormat = NumberFormat.decimalPattern('es_CL');
document.addPage(
pw.MultiPage(
pageTheme: pw.PageTheme(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.fromLTRB(38, 42, 38, 42),
),
header: (context) => _reportHeader(
order,
logo,
type: type,
primary: primary,
dark: dark,
muted: muted,
),
footer: (context) => _reportFooter(context, muted),
build: (context) => [
pw.SizedBox(height: 12),
pw.Text(
order.title,
style: pw.TextStyle(
color: dark,
fontSize: 21,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 6),
pw.Text(
'Orden de trabajo finalizada',
style: const pw.TextStyle(color: primary, fontSize: 9),
),
pw.SizedBox(height: 15),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 9),
decoration: pw.BoxDecoration(
color: pale,
borderRadius: pw.BorderRadius.circular(7),
),
child: pw.Row(
children: [
_summaryItem(
'Creada',
dateFormat.format(order.createdAt.toLocal()),
dark,
muted,
),
_summaryDivider(),
_summaryItem(
'Finalizada',
dateFormat.format(
(order.completedAt ?? order.updatedAt).toLocal(),
),
dark,
muted,
),
_summaryDivider(),
_summaryItem(
'Contenido',
'${order.tasks.length} tareas - ${order.totalPhotos} fotos',
dark,
muted,
),
if (type == PdfReportType.client) ...[
_summaryDivider(),
_summaryItem(
'Total OT',
'\$${priceFormat.format(order.totalCost)}',
dark,
muted,
),
],
],
),
),
pw.SizedBox(height: 20),
...order.tasks.expand(
(task) => _taskWidgets(
task,
taskPhotos[task.id]!,
primary: primary,
dark: dark,
muted: muted,
pale: pale,
priceFormat: priceFormat,
includeParts: type == PdfReportType.client,
),
),
if (type == PdfReportType.client) ...[
pw.SizedBox(height: 2),
_costSummary(
order,
primary: primary,
dark: dark,
muted: muted,
pale: pale,
priceFormat: priceFormat,
),
],
pw.SizedBox(height: 8),
_signatureBlock(signature, dark: dark, muted: muted),
],
),
);
return document.save();
}
pw.Widget _reportHeader(
WorkOrder order,
pw.ImageProvider logo, {
required PdfReportType type,
required PdfColor primary,
required PdfColor dark,
required PdfColor muted,
}) {
return pw.Container(
height: 78,
padding: const pw.EdgeInsets.only(bottom: 10),
decoration: pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: primary, width: 1.5)),
),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.SizedBox(
width: 175,
height: 67,
child: pw.Image(
logo,
fit: pw.BoxFit.cover,
alignment: pw.Alignment.center,
),
),
pw.SizedBox(
width: 245,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
'Informe para ${type.label.toLowerCase()}',
style: pw.TextStyle(color: muted, fontSize: 9),
),
pw.SizedBox(height: 7),
_headerDetail(
'Cliente',
order.details.client.isEmpty
? 'No informado'
: order.details.client,
dark,
muted,
),
pw.SizedBox(height: 3),
_headerDetail(
'Dirección',
order.details.address.isEmpty
? 'No informada'
: order.details.address,
dark,
muted,
),
],
),
),
],
),
);
}
pw.Widget _reportFooter(pw.Context context, PdfColor muted) {
return pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Generado por SMoya Gasfiter',
style: pw.TextStyle(color: muted, fontSize: 8),
),
pw.Text(
'Página ${context.pageNumber} de ${context.pagesCount}',
style: pw.TextStyle(color: muted, fontSize: 8),
),
],
);
}
pw.Widget _headerDetail(
String label,
String value,
PdfColor dark,
PdfColor muted,
) {
return pw.RichText(
textAlign: pw.TextAlign.right,
text: pw.TextSpan(
children: [
pw.TextSpan(
text: '${label.toUpperCase()}: ',
style: pw.TextStyle(
color: muted,
fontSize: 6.5,
fontWeight: pw.FontWeight.bold,
letterSpacing: .35,
),
),
pw.TextSpan(
text: value,
style: pw.TextStyle(
color: dark,
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
),
),
],
),
);
}
pw.Widget _summaryItem(
String label,
String value,
PdfColor dark,
PdfColor muted,
) {
return pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
label.toUpperCase(),
style: pw.TextStyle(
color: muted,
fontSize: 6.5,
fontWeight: pw.FontWeight.bold,
letterSpacing: .55,
),
),
pw.SizedBox(height: 3),
pw.Text(
value,
style: pw.TextStyle(
color: dark,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
),
),
],
),
);
}
pw.Widget _summaryDivider() {
return pw.Container(
height: 24,
margin: const pw.EdgeInsets.symmetric(horizontal: 8),
decoration: const pw.BoxDecoration(
border: pw.Border(
left: pw.BorderSide(color: PdfColor.fromInt(0xFFB8CFCD)),
),
),
);
}
List<pw.Widget> _taskWidgets(
WorkTask task,
_TaskImages images, {
required PdfColor primary,
required PdfColor dark,
required PdfColor muted,
required PdfColor pale,
required NumberFormat priceFormat,
required bool includeParts,
}) {
return [
pw.NewPage(freeSpace: includeParts && task.parts.isNotEmpty ? 220 : 145),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: pw.BoxDecoration(
color: dark,
borderRadius: pw.BorderRadius.circular(5),
),
child: pw.Row(
children: [
pw.Container(
width: 23,
height: 23,
alignment: pw.Alignment.center,
decoration: pw.BoxDecoration(
color: primary,
shape: pw.BoxShape.circle,
),
child: pw.Text(
'${task.position + 1}',
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 10,
fontWeight: pw.FontWeight.bold,
),
),
),
pw.SizedBox(width: 9),
pw.Expanded(
child: pw.Text(
task.title,
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 13,
fontWeight: pw.FontWeight.bold,
),
),
),
],
),
),
pw.SizedBox(height: 10),
pw.Text(
'OBSERVACIÓN',
style: pw.TextStyle(
color: primary,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
letterSpacing: .8,
),
),
pw.SizedBox(height: 4),
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(11),
decoration: pw.BoxDecoration(
color: pale,
borderRadius: pw.BorderRadius.circular(4),
),
child: pw.Text(
task.observation,
style: pw.TextStyle(color: dark, fontSize: 10, lineSpacing: 3),
),
),
if (includeParts && task.parts.isNotEmpty) ...[
pw.SizedBox(height: 13),
..._partsWidgets(
task.parts,
task.partsTotal,
primary: primary,
dark: dark,
muted: muted,
pale: pale,
priceFormat: priceFormat,
),
],
pw.SizedBox(height: 15),
..._photoSection(
'ANTES DE LA MANTENCIÓN',
images.before,
primary: primary,
muted: muted,
),
pw.SizedBox(height: 14),
..._photoSection(
'DESPUÉS DE LA MANTENCIÓN',
images.after,
primary: primary,
muted: muted,
),
pw.SizedBox(height: 28),
];
}
List<pw.Widget> _partsWidgets(
List<TaskPart> parts,
int subtotal, {
required PdfColor primary,
required PdfColor dark,
required PdfColor muted,
required PdfColor pale,
required NumberFormat priceFormat,
}) {
final widgets = <pw.Widget>[
pw.Row(
children: [
pw.Text(
'REPUESTOS UTILIZADOS',
style: pw.TextStyle(
color: primary,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
letterSpacing: .7,
),
),
pw.Spacer(),
pw.Text(
'${parts.length} ${parts.length == 1 ? 'repuesto' : 'repuestos'}',
style: pw.TextStyle(color: muted, fontSize: 8),
),
],
),
pw.SizedBox(height: 6),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: pw.BoxDecoration(
color: pale,
borderRadius: pw.BorderRadius.circular(3),
),
child: pw.Row(
children: [
pw.Expanded(
child: pw.Text(
'DESCRIPCIÓN',
style: pw.TextStyle(
color: muted,
fontSize: 6.5,
fontWeight: pw.FontWeight.bold,
),
),
),
pw.SizedBox(
width: 90,
child: pw.Text(
'PRECIO',
textAlign: pw.TextAlign.right,
style: pw.TextStyle(
color: muted,
fontSize: 6.5,
fontWeight: pw.FontWeight.bold,
),
),
),
],
),
),
for (final part in parts)
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: const pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
color: PdfColor.fromInt(0xFFD6E0DF),
width: .6,
),
),
),
child: pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Text(
part.name,
style: pw.TextStyle(color: dark, fontSize: 8),
),
),
pw.SizedBox(
width: 90,
child: pw.Text(
'\$${priceFormat.format(part.price)}',
textAlign: pw.TextAlign.right,
style: pw.TextStyle(
color: dark,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
),
),
),
],
),
),
pw.Container(
padding: const pw.EdgeInsets.only(top: 7),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Text(
'Subtotal: ',
style: pw.TextStyle(color: muted, fontSize: 8),
),
pw.Text(
'\$${priceFormat.format(subtotal)}',
style: pw.TextStyle(
color: primary,
fontSize: 9,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
];
if (parts.length <= 8) {
return [
pw.Inseparable(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: widgets,
),
),
];
}
return widgets;
}
List<pw.Widget> _photoSection(
String title,
List<pw.ImageProvider> images, {
required PdfColor primary,
required PdfColor muted,
}) {
final widgets = <pw.Widget>[
pw.Row(
children: [
pw.Text(
title,
style: pw.TextStyle(
color: primary,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
letterSpacing: .7,
),
),
pw.SizedBox(width: 8),
pw.Text(
'${images.length} ${images.length == 1 ? 'foto' : 'fotos'}',
style: pw.TextStyle(color: muted, fontSize: 8),
),
],
),
pw.SizedBox(height: 7),
];
for (var index = 0; index < images.length; index += 3) {
widgets.add(
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
for (var offset = 0; offset < 3; offset++) ...[
pw.Expanded(
child: index + offset < images.length
? _pdfImage(images[index + offset])
: pw.SizedBox(),
),
if (offset < 2) pw.SizedBox(width: 8),
],
],
),
);
widgets.add(pw.SizedBox(height: 8));
}
return widgets;
}
pw.Widget _costSummary(
WorkOrder order, {
required PdfColor primary,
required PdfColor dark,
required PdfColor muted,
required PdfColor pale,
required NumberFormat priceFormat,
}) {
pw.Widget costRow(String label, int value, {bool emphasize = false}) {
return pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
label,
style: pw.TextStyle(
color: emphasize ? dark : muted,
fontSize: emphasize ? 10 : 9,
fontWeight: emphasize ? pw.FontWeight.bold : pw.FontWeight.normal,
),
),
pw.Text(
'\$${priceFormat.format(value)}',
style: pw.TextStyle(
color: emphasize ? primary : dark,
fontSize: emphasize ? 11 : 9,
fontWeight: pw.FontWeight.bold,
),
),
],
);
}
return pw.Inseparable(
child: pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
color: pale,
borderRadius: pw.BorderRadius.circular(6),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [
pw.Text(
'RESUMEN DE COSTOS',
style: pw.TextStyle(
color: primary,
fontSize: 8,
fontWeight: pw.FontWeight.bold,
letterSpacing: .7,
),
),
pw.SizedBox(height: 9),
costRow('Costo de mano de obra', order.laborCost),
pw.SizedBox(height: 6),
costRow('Total de repuestos', order.totalPartsCost),
pw.SizedBox(height: 8),
pw.Divider(color: muted, thickness: .5),
pw.SizedBox(height: 6),
costRow('Valor total de la OT', order.totalCost, emphasize: true),
],
),
),
);
}
pw.Widget _signatureBlock(
pw.ImageProvider signature, {
required PdfColor dark,
required PdfColor muted,
}) {
return pw.Inseparable(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.SizedBox(width: double.infinity, height: 8),
pw.Text(
'Atentamente',
style: pw.TextStyle(color: muted, fontSize: 8),
),
pw.SizedBox(height: 2),
pw.SizedBox(
width: 105,
height: 78,
child: pw.Image(signature, fit: pw.BoxFit.contain),
),
pw.Text(
'Simón Moya Salinas',
style: pw.TextStyle(
color: dark,
fontSize: 10,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 2),
pw.Text(
'C.I.: 9.990.302-3 - Instalador de gas autorizado SEC',
style: pw.TextStyle(color: dark, fontSize: 8),
),
pw.SizedBox(height: 2),
pw.Text(
'WhatsApp: 933922780',
style: pw.TextStyle(color: muted, fontSize: 8),
),
],
),
);
}
pw.Widget _pdfImage(pw.ImageProvider image) {
return pw.Container(
height: 115,
decoration: pw.BoxDecoration(
borderRadius: pw.BorderRadius.circular(4),
border: pw.Border.all(
color: const PdfColor.fromInt(0xFFD6E0DF),
width: .7,
),
),
child: pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
);
}
Future<_TaskImages> _loadTaskImages(WorkTask task) async {
final before = <pw.ImageProvider>[];
final after = <pw.ImageProvider>[];
for (final photo in task.photos) {
final file = File(photo.path);
if (!await file.exists()) continue;
final Uint8List bytes = await file.readAsBytes();
final provider = pw.MemoryImage(bytes);
if (photo.kind == PhotoKind.before) {
before.add(provider);
} else {
after.add(provider);
}
}
return _TaskImages(before: before, after: after);
}
}
class _TaskImages {
const _TaskImages({required this.before, required this.after});
final List<pw.ImageProvider> before;
final List<pw.ImageProvider> after;
}

View file

@ -0,0 +1,110 @@
import 'package:flutter/foundation.dart';
import 'package:speech_to_text/speech_recognition_error.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
typedef DictationResultCallback =
void Function(String words, bool isFinalResult);
typedef DictationStatusCallback = void Function(bool isListening);
typedef DictationErrorCallback = void Function(String errorCode);
class SpeechDictationService {
SpeechDictationService._();
static final SpeechDictationService instance = SpeechDictationService._();
final stt.SpeechToText _speech = stt.SpeechToText();
DictationResultCallback? _onResult;
DictationStatusCallback? _onStatusChanged;
DictationErrorCallback? _onError;
bool get isListening => _speech.isListening;
Future<bool> initialize({
required DictationStatusCallback onStatusChanged,
required DictationErrorCallback onError,
}) async {
_onStatusChanged = onStatusChanged;
_onError = onError;
if (_speech.isAvailable) return true;
return _speech.initialize(
onStatus: _handleStatus,
onError: _handleError,
debugLogging: kDebugMode,
options: [stt.SpeechToText.androidNoBluetooth],
);
}
Future<void> listen({required DictationResultCallback onResult}) async {
_onResult = onResult;
final localeId = await _preferredSpanishLocaleId();
await _speech.listen(
onResult: (result) =>
_onResult?.call(result.recognizedWords, result.finalResult),
listenOptions: stt.SpeechListenOptions(
cancelOnError: true,
partialResults: true,
listenMode: stt.ListenMode.dictation,
autoPunctuation: true,
pauseFor: const Duration(seconds: 8),
listenFor: const Duration(minutes: 2),
localeId: localeId,
),
);
}
Future<void> stop() async {
await _speech.stop();
// Android puede entregar el resultado final poco después de informar que
// dejó de escuchar. Esta breve espera permite conservar esas últimas
// palabras antes de cerrar el receptor.
await Future<void>.delayed(const Duration(milliseconds: 300));
_onResult = null;
_onStatusChanged?.call(false);
}
Future<void> endSession() async {
_onResult = null;
_onStatusChanged = null;
_onError = null;
if (_speech.isAvailable) {
await _speech.cancel();
}
}
void _handleStatus(String status) {
if (status == stt.SpeechToText.listeningStatus) {
_onStatusChanged?.call(true);
} else if (status == stt.SpeechToText.doneStatus ||
status == stt.SpeechToText.notListeningStatus) {
_onStatusChanged?.call(false);
}
}
void _handleError(SpeechRecognitionError error) {
_onResult = null;
_onError?.call(error.errorMsg);
_onStatusChanged?.call(false);
}
Future<String?> _preferredSpanishLocaleId() async {
try {
final locales = await _speech.locales();
for (final locale in locales) {
final normalized = locale.localeId.toLowerCase().replaceAll('_', '-');
if (normalized == 'es-cl') return locale.localeId;
}
for (final locale in locales) {
if (locale.localeId.toLowerCase().startsWith('es')) {
return locale.localeId;
}
}
} catch (_) {
// El reconocedor usará el idioma predeterminado del dispositivo.
}
return null;
}
}

1
linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View file

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "ot_movil")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "cl.otmovil.ot_movil")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
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()
# 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_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
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)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
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.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
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.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# 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/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter 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.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <printing/printing_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,26 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
printing
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
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}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# 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 application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

6
linux/runner/main.cc Normal file
View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "SMoya Gasfiter");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "SMoya Gasfiter");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

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