commit 0bc65b6e84b43bae7f2c42bb10eb8541be3b3319 Author: Komek Hayytnazarov Date: Mon Feb 27 12:23:37 2023 +0500 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..ec98142 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# 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: d79295af24c3ed621c33713ecda14ad196fd9c31 + channel: stable + +project_type: app diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1ccf579 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "adaptix", + "birzha", + "prefs", + "Resizer" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fb0aeb --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# birzha + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. +# birzha diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..c374854 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,276 @@ +# include: package:lint/analysis_options.yaml +analyzer: + # strong-mode: + # Will become the default once non-nullable types land + # https://github.com/dart-lang/sdk/issues/31410#issuecomment-510683629 + # implicit-casts: false + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: warning + # treat missing returns as a warning (not a hint) + missing_return: warning + todo: ignore +# Rules are in the same order (alphabetically) as documented at http://dart-lang.github.io/linter/lints +# and https://github.com/dart-lang/linter/blob/master/example/all.yaml +linter: + rules: + # Prevents accidental return type changes which results in a breaking API change. + # Enforcing return type makes API changes visible in a diff + # pedantic: enabled + # http://dart-lang.github.io/linter/lints/always_declare_return_types.html + - always_declare_return_types + + # Single line `if`s are fine as recommended in Effective Dart "DO format your code using dartfmt" + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/always_put_control_body_on_new_line.html + # - always_put_control_body_on_new_line + + # Flutter widgets always put a Key as first optional parameter which breaks this rule. + # Also violates other orderings like matching the class fields or alphabetically. + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/always_declare_return_types.html + # - always_put_required_named_parameters_first + + # All non nullable named parameters should be and annotated with @required. + # This allows API consumers to get warnings via lint rather than a crash a runtime. + # Might become obsolete with Non-Nullable types + # pedantic: enabled + # http://dart-lang.github.io/linter/lints/always_require_non_null_named_parameters.html + - always_require_non_null_named_parameters + + # Since dart 2.0 dart is a sound language, specifying types is not required anymore. + # `var foo = 10;` is enough information for the compiler to make foo a int. + # Violates Effective Dart "AVOID type annotating initialized local variables". + # Makes code unnecessarily complex https://github.com/dart-lang/linter/issues/1620 + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/always_specify_types.html + # - always_specify_types + + # Protect against unintentionally overriding superclass members + # pedantic: enabled + # http://dart-lang.github.io/linter/lints/annotate_overrides.html + - annotate_overrides + + # All methods should define a return type. dynamic is no exception. + # Violates Effective Dart "PREFER annotating with dynamic instead of letting inference fail" + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_annotating_with_dynamic.html + # - avoid_annotating_with_dynamic + + # A leftover from dart1, should be deprecated + # pedantic: disabled + # - https://github.com/dart-lang/linter/issues/1401 + # http://dart-lang.github.io/linter/lints/avoid_as.html + # - avoid_as + + # Highlights boolean expressions which can be simplified + # http://dart-lang.github.io/linter/lints/avoid_bool_literals_in_conditional_expressions.html + - avoid_bool_literals_in_conditional_expressions + + # There are no strong arguments to enable this rule because it is very strict. Catching anything is useful + # and common even if not always the most correct thing to do. + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_catches_without_on_clauses.html + # - avoid_catches_without_on_clauses + + # Errors aren't for catching but to prevent prior to runtime + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_catching_errors.html + - avoid_catching_errors + + # Can usually be replaced with an extension + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_classes_with_only_static_members.html + # - avoid_classes_with_only_static_members + + # Never accidentally use dynamic invocations + # Dart SDK: unreleased • (Linter vnull) + # https://dart-lang.github.io/linter/lints/avoid_dynamic_calls.html + # avoid_dynamic_calls + + # Only useful when targeting JS + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_double_and_int_checks.html + # - avoid_double_and_int_checks + + # Prevents accidental empty else cases. See samples in documentation + # pedantic: enabled + # http://dart-lang.github.io/linter/lints/avoid_empty_else.html + - avoid_empty_else + + # It is expected that mutable objects which override hash & equals shouldn't be used as keys for hashmaps. + # This one use case doesn't make all hash & equals implementations for mutable classes bad. + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_equals_and_hash_code_on_mutable_classes.html + # - avoid_equals_and_hash_code_on_mutable_classes + + # Use different quotes instead of escaping + # Dart SDK: >= 2.8.0-dev.11.0 • (Linter v0.1.111) + # https://dart-lang.github.io/linter/lints/avoid_escaping_inner_quotes.html + - avoid_escaping_inner_quotes + + # Prevents unnecessary allocation of a field + # pedantic: disabled + # http://dart-lang.github.io/linter/lints/avoid_field_initializers_in_const_classes.html + - avoid_field_initializers_in_const_classes + + # Prevents allocating a lambda and allows return/break/continue control flow statements inside the loop + # http://dart-lang.github.io/linter/lints/avoid_function_literals_in_foreach_calls.html + - avoid_function_literals_in_foreach_calls + + # Don't break value types by implementing them + # http://dart-lang.github.io/linter/lints/avoid_implementing_value_types.html + - avoid_implementing_value_types + + # Removes redundant `= null;` + # https://dart-lang.github.io/linter/lints/avoid_init_to_null.html + - avoid_init_to_null + + # Only useful when targeting JS + # Warns about too large integers when compiling to JS + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_js_rounded_ints.html + # - avoid_js_rounded_ints + + # Null checks aren't required in ==() operators + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_null_checks_in_equality_operators.html + - avoid_null_checks_in_equality_operators + + # Good APIs don't use ambiguous boolean parameters. Instead use named parameters + # https://dart-lang.github.io/linter/lints/avoid_positional_boolean_parameters.html + - avoid_positional_boolean_parameters + + # Don't call print in production code + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_print.html + - avoid_print + + # Always prefer function references over typedefs. + # Jumping twice in code to see the signature of a lambda sucks. This is different from the flutter analysis_options + # https://dart-lang.github.io/linter/lints/avoid_private_typedef_functions.html + - avoid_private_typedef_functions + + # Don't explicitly set defaults + # Dart SDK: >= 2.8.0-dev.1.0 • (Linter v0.1.107) + # https://dart-lang.github.io/linter/lints/avoid_redundant_argument_values.html + - avoid_redundant_argument_values + + # package or relative? Let's end the discussion and use package everywhere. + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_relative_lib_imports.html + - avoid_relative_lib_imports + + # Not recommended to break dartdoc but besides that there is no reason to continue with bad naming + # https://dart-lang.github.io/linter/lints/avoid_renaming_method_parameters.html + # - avoid_renaming_method_parameters + + # Setters always return void, therefore defining void is redundant + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_return_types_on_setters.html + - avoid_return_types_on_setters + + # Especially with Non-Nullable types on the horizon, `int?` is fine. + # There are plenty of valid reasons to return null. + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_returning_null.html + # - avoid_returning_null + + # Don't use `Future?`, therefore never return null instead of a Future. + # Will become obsolete one Non-Nullable types land + # https://dart-lang.github.io/linter/lints/avoid_returning_null_for_future.html + - avoid_returning_null_for_future + + # Use empty returns, don't show off with you knowledge about dart internals. + # https://dart-lang.github.io/linter/lints/avoid_returning_null_for_void.html + - avoid_returning_null_for_void + + # Hinting you forgot about the cascade operator. But too often you did this on purpose. + # There are plenty of valid reasons to return this. + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_returning_this.html + # - avoid_returning_this + + # Prevents logical inconsistencies. It's good practice to define getters for all existing setters. + # https://dart-lang.github.io/linter/lints/avoid_setters_without_getters.html + - avoid_setters_without_getters + + # Don't reuse a type parameter when on with the same name already exists in the same scope + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_shadowing_type_parameters.html + - avoid_shadowing_type_parameters + + # A single cascade operator can be replaced with a normal method call + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_single_cascade_in_expression_statements.html + - avoid_single_cascade_in_expression_statements + + # Might cause frame drops because of synchronous file access on mobile, especially on older phones with slow storage. + # There are no known measurements sync access does *not* drop frames. + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_slow_async_io.html + # - avoid_slow_async_io + + # Don't use .toString() in production code which might be minified + # Dart SDK: >= 2.10.0-144.0.dev • (Linter v0.1.119) + # https://dart-lang.github.io/linter/lints/avoid_type_to_string.html + - avoid_type_to_string + + # Don't use a parameter names which can be confused with a types (i.e. int, bool, num, ...) + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/avoid_types_as_parameter_names.html + - avoid_types_as_parameter_names + + # Adding the type is not required, but sometimes improves readability. Therefore removing it doesn't always help + # https://dart-lang.github.io/linter/lints/avoid_types_on_closure_parameters.html + # - avoid_types_on_closure_parameters + + # Containers without parameters have no effect and can be removed + # https://dart-lang.github.io/linter/lints/avoid_unnecessary_containers.html + - avoid_unnecessary_containers + + # Unused parameters should be removed + # https://dart-lang.github.io/linter/lints/avoid_unused_constructor_parameters.html + - avoid_unused_constructor_parameters + + # TODO double check + # For async functions use `Future` as return value, not `void` + # This allows usage of the await keyword and prevents operations from running in parallel. + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/avoid_void_async.html + - avoid_void_async + + # Flutter mobile only: Web packages aren't available in mobile flutter apps + # https://dart-lang.github.io/linter/lints/avoid_web_libraries_in_flutter.html + - avoid_web_libraries_in_flutter + + # Use the await keyword only for futures. There is nothing to await in synchronous code + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/await_only_futures.html + - await_only_futures + + # Follow the style guide and use UpperCamelCase for extensions + # pedantic: enabled + # https://dart-lang.github.io/linter/lints/camel_case_extensions.html + - camel_case_extensions + + # Follow the style guide and use UpperCamelCase for class names and typedefs + # https://dart-lang.github.io/linter/lints/camel_case_types.html + - camel_case_types + + # Prevents leaks and code executing after their lifecycle. + # Discussion https://github.com/passsy/dart-lint/issues/4 + # + # pedantic: disabled + # https://dart-lang.github.io/linter/lints/cancel_subscriptions.html + - cancel_subscriptions + + # The cascade syntax is weird and you shouldn't be forced to use it. + # False positives: + # https://github.com/dart-lang/linter/issues/1589 + # + # https://dart-lang.github.io/linter/lints/cascade_invocations.html + # - cascade_invocations + + # Don't cast T? to T. Use ! instead + # Dart SDK: >= 2.11.0-182.0.dev • (Linter v0.1.120 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..0e64246 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,12 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +/gradle/wrapper/gradle-6.7-all.zip +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..9b3aa2b --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.tpsadvertising.digital.birzha" + minSdkVersion 20 + targetSdkVersion 31 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..80766fb --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..096d4f5 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/tpsadvertising/digital/birzha/MainActivity.kt b/android/app/src/main/kotlin/com/tpsadvertising/digital/birzha/MainActivity.kt new file mode 100644 index 0000000..f86abd3 --- /dev/null +++ b/android/app/src/main/kotlin/com/tpsadvertising/digital/birzha/MainActivity.kt @@ -0,0 +1,6 @@ +package com.tpsadvertising.digital.birzha + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..fe755b9 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..0862c45 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..ffa595e Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..3fe6b2e --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..6119b8f Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..4f7bd9b Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..c3502e2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..ffa595e Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..3fe6b2e --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..7b136a2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..55f6995 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..ac79776 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..850deff Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..089ae54 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..411ccd3 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..a937a0b --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..80766fb --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/upload-keystore.jks b/android/app/upload-keystore.jks new file mode 100644 index 0000000..f409c55 Binary files /dev/null and b/android/app/upload-keystore.jks differ diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..7603b16 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/assets/fonts/AppBarIcons.ttf b/assets/fonts/AppBarIcons.ttf new file mode 100644 index 0000000..21ae244 Binary files /dev/null and b/assets/fonts/AppBarIcons.ttf differ diff --git a/assets/fonts/Settings_Icons.ttf b/assets/fonts/Settings_Icons.ttf new file mode 100644 index 0000000..65867f4 Binary files /dev/null and b/assets/fonts/Settings_Icons.ttf differ diff --git a/assets/fonts/TabNavIcons.ttf b/assets/fonts/TabNavIcons.ttf new file mode 100644 index 0000000..29f28a8 Binary files /dev/null and b/assets/fonts/TabNavIcons.ttf differ diff --git a/assets/fonts/svgs.ttf b/assets/fonts/svgs.ttf new file mode 100644 index 0000000..8ba7b79 Binary files /dev/null and b/assets/fonts/svgs.ttf differ diff --git a/assets/icon.jpg b/assets/icon.jpg new file mode 100644 index 0000000..9ff3602 Binary files /dev/null and b/assets/icon.jpg differ diff --git a/assets/icons/home_screen/build.svg b/assets/icons/home_screen/build.svg new file mode 100644 index 0000000..245a443 --- /dev/null +++ b/assets/icons/home_screen/build.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/home_screen/chemical.svg b/assets/icons/home_screen/chemical.svg new file mode 100644 index 0000000..3c48bb7 --- /dev/null +++ b/assets/icons/home_screen/chemical.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/home_screen/machine.svg b/assets/icons/home_screen/machine.svg new file mode 100644 index 0000000..f4bb36d --- /dev/null +++ b/assets/icons/home_screen/machine.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/home_screen/shirt.svg b/assets/icons/home_screen/shirt.svg new file mode 100644 index 0000000..4a015e4 --- /dev/null +++ b/assets/icons/home_screen/shirt.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/profile_screen/cart.svg b/assets/icons/profile_screen/cart.svg new file mode 100644 index 0000000..0c97ce1 --- /dev/null +++ b/assets/icons/profile_screen/cart.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/profile_screen/exit.svg b/assets/icons/profile_screen/exit.svg new file mode 100644 index 0000000..5c4078f --- /dev/null +++ b/assets/icons/profile_screen/exit.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/profile_screen/message.svg b/assets/icons/profile_screen/message.svg new file mode 100644 index 0000000..17e5611 --- /dev/null +++ b/assets/icons/profile_screen/message.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/profile_screen/phone_not_verified.svg b/assets/icons/profile_screen/phone_not_verified.svg new file mode 100644 index 0000000..f6b30ec --- /dev/null +++ b/assets/icons/profile_screen/phone_not_verified.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/profile_screen/phone_verified.svg b/assets/icons/profile_screen/phone_verified.svg new file mode 100644 index 0000000..58f6de6 --- /dev/null +++ b/assets/icons/profile_screen/phone_verified.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/profile_screen/popup_history.svg b/assets/icons/profile_screen/popup_history.svg new file mode 100644 index 0000000..ccf424e --- /dev/null +++ b/assets/icons/profile_screen/popup_history.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/profile_screen/profile.svg b/assets/icons/profile_screen/profile.svg new file mode 100644 index 0000000..5267e70 --- /dev/null +++ b/assets/icons/profile_screen/profile.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/settings_screen/confidentials.svg b/assets/icons/settings_screen/confidentials.svg new file mode 100644 index 0000000..df6d8e0 --- /dev/null +++ b/assets/icons/settings_screen/confidentials.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/settings_screen/contact.svg b/assets/icons/settings_screen/contact.svg new file mode 100644 index 0000000..d53ab5c --- /dev/null +++ b/assets/icons/settings_screen/contact.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/settings_screen/feedback.svg b/assets/icons/settings_screen/feedback.svg new file mode 100644 index 0000000..b8544d0 --- /dev/null +++ b/assets/icons/settings_screen/feedback.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/settings_screen/lang.svg b/assets/icons/settings_screen/lang.svg new file mode 100644 index 0000000..c5be39d --- /dev/null +++ b/assets/icons/settings_screen/lang.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/appBarIcon.svg b/assets/images/appBarIcon.svg new file mode 100644 index 0000000..4c3a2c2 --- /dev/null +++ b/assets/images/appBarIcon.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/assets/images/map.jpg b/assets/images/map.jpg new file mode 100644 index 0000000..64288cf Binary files /dev/null and b/assets/images/map.jpg differ diff --git a/assets/images/product1.jpg b/assets/images/product1.jpg new file mode 100644 index 0000000..f092d14 Binary files /dev/null and b/assets/images/product1.jpg differ diff --git a/assets/images/unauth.svg b/assets/images/unauth.svg new file mode 100644 index 0000000..7cafd60 --- /dev/null +++ b/assets/images/unauth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..567473e --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,117 @@ +PODS: + - DKImagePickerController/Core (4.3.3): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.3) + - DKImagePickerController/PhotoGallery (4.3.3): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.3) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - path_provider_ios (0.0.1): + - Flutter + - SDWebImage (5.12.5): + - SDWebImage/Core (= 5.12.5) + - SDWebImage/Core (5.12.5) + - shared_preferences_ios (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - SwiftyGif (5.4.3) + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - wakelock (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + - wakelock (from `.symlinks/plugins/wakelock/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - FMDB + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" + wakelock: + :path: ".symlinks/plugins/wakelock/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + DKImagePickerController: 72fd378f244cef3d27288e0aebf217a4467e4012 + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f + +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c + +COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d1d56e4 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,556 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F4E55937107BA5306BB95CAA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 505142890B9BAEB822322951 /* Pods_Runner.framework */; }; +/* End PBXBuildFile 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2324F60B1D9EECDFBF46B475 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4D2B1C1844A116A91D1F2ADC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 505142890B9BAEB822322951 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5280C790285D926E005DF18B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 6F1D9381F9FE45A06B6099F8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4E55937107BA5306BB95CAA /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 88445B605A3ADB0F86AB262B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 505142890B9BAEB822322951 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + E24870FF2A93B633776EC7B4 /* Pods */, + 88445B605A3ADB0F86AB262B /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 5280C790285D926E005DF18B /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + E24870FF2A93B633776EC7B4 /* Pods */ = { + isa = PBXGroup; + children = ( + 6F1D9381F9FE45A06B6099F8 /* Pods-Runner.debug.xcconfig */, + 2324F60B1D9EECDFBF46B475 /* Pods-Runner.release.xcconfig */, + 4D2B1C1844A116A91D1F2ADC /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5D77B75D4239A29236376CB3 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E7953A05086AEC43D00BCA7C /* [CP] Embed Pods Frameworks */, + ); + 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 = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 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 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 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; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5D77B75D4239A29236376CB3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + 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"; + }; + E7953A05086AEC43D00BCA7C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 9.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; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQPXL8D953; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tpsadvertising.digital.birzha; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 9.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; + 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; + 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 = 9.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; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQPXL8D953; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tpsadvertising.digital.birzha; + 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; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQPXL8D953; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tpsadvertising.digital.birzha; + 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 */ + 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 */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..3faacd1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..09bd487 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..ea39d6a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..92a0f1e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..13b5424 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..5766096 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..f7c50e0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..ea39d6a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..e393ff9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..eb4439e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..eb4439e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..98d0f4e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..8a6cab6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..679e660 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..ffccfb8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..d48df87 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Birzha + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + birzha + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + NSPhotoLibraryUsageDescription + User can upload product images. + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..903def2 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..4e6692e --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/lib/components/TextInputCustom.dart b/lib/components/TextInputCustom.dart new file mode 100644 index 0000000..ec40840 --- /dev/null +++ b/lib/components/TextInputCustom.dart @@ -0,0 +1,127 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/services/textMetaData.dart'; + +class TextInputCustom extends StatefulWidget { + final double? fontSize; + final EdgeInsets? padding; + final EdgeInsets? margin; + final TextEditingController controller; + final TextInputMetaData fieldStandard; + final bool? showPassword; + final void Function(String?)? onChanged; + final Widget? suffix; + final Widget? prefix; + + TextInputCustom({ + Key? key, + this.showPassword, + this.fontSize, + this.padding, + this.suffix, + this.prefix, + this.margin, + required this.fieldStandard, + this.onChanged, + required this.controller, + }) : super(key: key); + + @override + _TextInputCustomState createState() => _TextInputCustomState(); +} + +class _TextInputCustomState extends State { + late FocusNode _focusNode; + bool hasFocus = false; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode() + ..addListener(() { + setState(() { + hasFocus = _focusNode.hasFocus; + }); + }); + } + + Widget? suffix() { + return widget.suffix ?? (widget.fieldStandard.pickerMode != null ? Icon(Icons.arrow_drop_down) : null); + } + + @override + Widget build(BuildContext context) { + final double textSize = 14.adaptedPx(); + final border = OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 0.9.adaptedPx(), + ), + ); + + return Container( + margin: widget.margin ?? EdgeInsets.symmetric(vertical: 12.adaptedPx()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: widget.fieldStandard.pickerMode == null + ? null + : () async { + var extracted = await widget.fieldStandard.pickerMode!(context); + if (extracted != null) widget.controller.text = extracted.toString(); + }, + child: Row( + children: [ + Expanded( + child: TextFormField( + readOnly: widget.fieldStandard.readOnly, + cursorColor: Theme.of(context).accentColor, + keyboardType: widget.fieldStandard.type, + focusNode: _focusNode, + controller: widget.controller, + autofocus: widget.fieldStandard.autoFocus, + onChanged: widget.onChanged, + cursorHeight: 15.2.adaptedPx(), + style: TextStyle( + fontSize: widget.fontSize ?? textSize * 0.9, fontWeight: FontWeight.w500, color: Theme.of(context).textTheme.bodyText2?.color), + validator: (i) => widget.fieldStandard.validation.validate(i ?? ""), + inputFormatters: widget.fieldStandard.formatters, + obscureText: widget.fieldStandard.password && widget.showPassword == false, + decoration: InputDecoration( + isDense: true, + contentPadding: widget.padding ?? EdgeInsets.symmetric(horizontal: 15.adaptedPx(), vertical: 14.adaptedPx()), + errorMaxLines: 2, + suffixIcon: suffix(), + prefixIcon: widget.prefix, + disabledBorder: border, + enabled: widget.fieldStandard.pickerMode == null, + border: border, + enabledBorder: border, + filled: widget.fieldStandard.filled, + fillColor: widget.fieldStandard.fillColor, + focusedBorder: border.copyWith(borderSide: BorderSide(color: Theme.of(context).accentColor)), + focusedErrorBorder: border.copyWith(borderSide: BorderSide(color: Theme.of(context).accentColor)), + hintText: widget.fieldStandard.hint, + labelText: widget.fieldStandard.label, + errorStyle: TextStyle(color: Theme.of(context).errorColor), + errorBorder: border.copyWith(borderSide: BorderSide(color: Colors.redAccent)), + focusColor: Colors.red, + hintStyle: TextStyle( + color: Colors.grey.shade500, + ), + labelStyle: TextStyle( + color: hasFocus ? Theme.of(context).accentColor : Colors.grey.shade400, + ), + ), + ), + ), + ], + ), + ), + // widget.showBottomInfo ? widget.fieldStandard.bottomInfo ?? SizedBox.shrink() : SizedBox.shrink() + ], + ), + ); + } +} diff --git a/lib/components/abstractForm.dart b/lib/components/abstractForm.dart new file mode 100644 index 0000000..17d9177 --- /dev/null +++ b/lib/components/abstractForm.dart @@ -0,0 +1,216 @@ +import 'package:birzha/components/verifyPhoneDialog.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/TextInputCustom.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/button.dart'; +import 'package:birzha/components/icon.dart'; +import 'package:birzha/models/settings/theme.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../core/manager/manager.dart'; +import '../models/user/userManager.dart'; +import 'indicator.dart'; + +mixin AbstractFormState on State { + String get title; + List get inputs; + List controllers = []; + List get before => []; + List get after => []; + Map get editedData => { + for (var i = 0; i < inputs.length; i++) + keyAfterFilter(inputs[i].key, controllers[i].text): valueAfterFilter( + inputs[i].key, + controllers[i].text, + ), + }; + String get buttonLabel; + bool get buttonIsLoading; + bool get shrinkWrap => false; + Alignment get listViewAlignment => Alignment.topCenter; + ScrollPhysics? get physics => null; + String keyAfterFilter(String key, String input); + String valueAfterFilter(String key, String input); + final GlobalKey formKey = GlobalKey(); + bool showPassword = false; + void act(); + void updateScreen(); + Positioned? get button => null; + bool get includeBackButton => true; + + Widget textField(TextInputMetaData meta, TextEditingController controller) { + return TextInputCustom( + fieldStandard: meta, + showPassword: showPassword, + suffix: suffix(meta), + controller: controller, + ); + } + + Widget? suffix(TextInputMetaData meta) { + if (meta.key == 'user_type') + return Icon( + Icons.arrow_drop_down, + size: 18.adaptedPx(), + ); + else if (meta.key == 'email' && meta.showSuffix) + return AppUserManager.of(context).dataSync.isEmailVerified + ? Container( + // width: 60, + child: SvgPicture.asset( + 'assets/icons/profile_screen/phone_verified.svg', + fit: BoxFit.scaleDown, + ), + ) + : ManagerSelector( + selector: (context, manager) => manager.getStatusByKey('verify_mail'), + shouldRebuild: (previous, next) => previous != next, + onUpdate: () { + updateScreen(); + }, + builder: (_, value) => GestureDetector( + onTap: () async { + debugPrint("verifyMail"); + AppUserManager.of(context).verifyMail(context); + }, + child: Container( + // width: 60, + child: value == TaskStatus.Loading + ? Indicator(size: 0.25.adaptedPx()) + : SvgPicture.asset( + 'assets/icons/profile_screen/phone_not_verified.svg', + fit: BoxFit.scaleDown, + ), + ), + ), + ); + else if (meta.key == 'username' && meta.showSuffix) + return AppUserManager.of(context).dataSync.isPhoneVerified + ? SvgPicture.asset( + 'assets/icons/profile_screen/phone_verified.svg', + fit: BoxFit.scaleDown, + ) + : GestureDetector( + onTap: () async { + debugPrint("openVerifyPhoneDialog"); + AppUserManager.of(context).sendSmsCode(context, () async { + await showDialog( + context: context, + builder: (_) => VerifyPhoneDialog(), + ); + updateScreen(); + }); + }, + child: SvgPicture.asset( + 'assets/icons/profile_screen/phone_not_verified.svg', + fit: BoxFit.scaleDown, + ), + ); + else if (meta.key == 'password' || meta.key == 'password_confirmation') + return AppIconButton( + icon: Icon(showPassword ? Icons.visibility_off : Icons.visibility), + onTap: () { + setState(() { + showPassword = !showPassword; + }); + }, + size: 18.adaptedPx(), + ); + + return null; + } + + void launchControllers() { + controllers = List.generate(inputs.length, (index) => TextEditingController()); + } + + void goBack() { + Navigator.of(context).pop(); + } + + Widget body(BuildContext context) => Form( + key: formKey, + child: Stack( + children: [ + Positioned.fill( + child: Align( + alignment: listViewAlignment, + child: ListView( + shrinkWrap: shrinkWrap, + physics: physics, + padding: EdgeInsets.symmetric(vertical: 10.adaptedPx(), horizontal: AppConstants.horizontalPadding(context)), + children: [ + ...before, + for (var i = 0; i < inputs.length; i++) Material(color: Colors.transparent, child: textField(inputs[i], controllers[i])), + ...after, + SizedBox( + height: 100.adaptedPx(), + ), + SizedBox(height: Adaptix.systemPadding.bottom) + ], + ), + ), + ), + button ?? + Positioned( + bottom: -1.adaptedPx(), + height: 70.adaptedPx(), + left: 0, + right: 0, + child: Container( + alignment: Alignment.bottomCenter, + padding: EdgeInsets.only( + top: 3.adaptedPx(), + bottom: 18.adaptedPx(), + left: AppConstants.horizontalPadding(context), + right: AppConstants.horizontalPadding(context)), + child: SizedBox( + width: 100.orientationWidth, + child: Row( + children: [ + Expanded( + child: MyButton( + text: buttonLabel.toUpperCase(), + inProgress: buttonIsLoading, + onTap: () { + if (formKey.currentState?.validate() ?? false) act(); + }, + height: 40.adaptedPx(), + ), + ) + ], + ), + ), + decoration: BoxDecoration(color: Colors.white, gradient: AppTheme.gradient(context)), + ), + ) + ], + ), + ); + + @override + void initState() { + super.initState(); + launchControllers(); + } + + @override + Widget build(BuildContext context) { + return BaseWidget( + appBar: BaseAppBar( + title: title, + goBack: () { + goBack(); + }, + includeBack: includeBackButton, + ), + body: Container( + color: Theme.of(context).backgroundColor, + child: body(context), + ), + ); + } +} diff --git a/lib/components/actionIcons/searchicon.dart b/lib/components/actionIcons/searchicon.dart new file mode 100644 index 0000000..0ae3f46 --- /dev/null +++ b/lib/components/actionIcons/searchicon.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/icon.dart'; +import 'package:birzha/constants.dart'; + +class SearchIcon extends StatelessWidget { + const SearchIcon({Key? key, this.iconColor}) : super(key: key); + + final Color? iconColor; + + @override + Widget build(BuildContext context) { + return AppIconButton( + icon: Icon( + CupertinoIcons.search, + color: iconColor ?? Colors.white, + ), + onTap: () {}, + size: AppConstants.appBarIconSize, + ); + } +} diff --git a/lib/components/appBarIcons.dart b/lib/components/appBarIcons.dart new file mode 100644 index 0000000..8c1b40f --- /dev/null +++ b/lib/components/appBarIcons.dart @@ -0,0 +1,26 @@ +/// Flutter icons MyFlutterApp +/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: MyFlutterApp +/// fonts: +/// - asset: fonts/MyFlutterApp.ttf +/// +/// +/// +import 'package:flutter/widgets.dart'; + +class AppBarIcons { + AppBarIcons._(); + + static const _kFontFam = 'MyFlutterApp'; + static const String? _kFontPkg = null; + + static const IconData info = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData search = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/components/askQuantity.dart b/lib/components/askQuantity.dart new file mode 100644 index 0000000..90570df --- /dev/null +++ b/lib/components/askQuantity.dart @@ -0,0 +1,73 @@ +import 'package:birzha/components/TextInputCustom.dart'; +import 'package:birzha/components/dialog.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:birzha/services/validator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AskQuantityDialog extends StatefulWidget { + AskQuantityDialog({Key? key, required this.initialQuantity}) : super(key: key); + + final double initialQuantity; + + @override + __AskQuantityDialogState createState() => __AskQuantityDialogState(); +} + +class __AskQuantityDialogState extends State { + late final TextEditingController _controller; + final GlobalKey _form = GlobalKey(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialQuantity.toString()); + } + + @override + Widget build(BuildContext context) { + return AppDialog( + title: 'quantitySumm'.translation, + child: Form( + key: _form, + child: Row( + children: [ + Expanded( + child: TextInputCustom( + fieldStandard: TextInputMetaData( + name: 'enterQuantity'.translation, + label: 'enterQuantity'.translation, + validation: Validation(conditions: [ + (inp) { + return inp.isEmpty || (double.tryParse(inp.trim())) == null ? 'formatError'.translation : null; + } + ]), + formatters: [ + FilteringTextInputFormatter.allow(RegExp('[0-9.]')), + ], + key: 'quantity'), + controller: _controller), + ), + ], + )), + actions: [ + Expanded( + child: Center( + child: GestureDetector( + onTap: () { + if (_form.currentState?.validate() ?? false) Navigator.of(context).pop(double.tryParse(_controller.text)); + }, + child: Text( + MaterialLocalizations.of(context).continueButtonLabel.characters.first + + MaterialLocalizations.of(context).continueButtonLabel.substring(1).toLowerCase(), + style: TextStyle(color: Theme.of(context).accentColor, fontWeight: FontWeight.bold, fontSize: 12.99.adaptedPx()), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/components/baseWidget.dart b/lib/components/baseWidget.dart new file mode 100644 index 0000000..e904651 --- /dev/null +++ b/lib/components/baseWidget.dart @@ -0,0 +1,202 @@ +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import 'package:birzha/components/icon.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:birzha/screens/settings/contact.dart'; + +import '../core/adaptix/adaptix.dart'; +import '../new/screens/news/screen.dart'; +import '../screens/settings/settingsScreen.dart'; + +class BaseWidget extends StatelessWidget { + const BaseWidget({ + Key? key, + this.bottom, + required this.appBar, + required this.body, + this.color, + }) : super(key: key); + + final BaseAppBar appBar; + final Widget body; + final Widget? bottom; + final Color? color; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: appBar, + body: body, + backgroundColor: color ?? Theme.of(context).scaffoldBackgroundColor, + bottomNavigationBar: bottom, + ); + } +} + +class BaseAppBar extends StatelessWidget implements PreferredSizeWidget { + BaseAppBar({ + Key? key, + this.customChild, + this.after = const [], + this.title, + required this.goBack, + this.includeBack = true, + this.color = ThemeColor.mainColor, + }) : super(key: key); + + final Widget? customChild; + final List after; + final String? title; + final void Function() goBack; + final Color? color; + final bool includeBack; + + factory BaseAppBar.home(BuildContext context, VoidCallback goBack) { + debugPrint('BaseAppBar.home'); + List navigators = [ + new MyNavigator(id: 1, name: "searchShort".translation, path: "search"), + new MyNavigator(id: 2, name: "settings".translation, path: "settings"), + new MyNavigator(id: 3, name: "contact".translation, path: "contact"), + ]; + return BaseAppBar( + goBack: goBack, + color: Theme.of(context).brightness == Brightness.light ? Theme.of(context).accentColor : Theme.of(context).appBarTheme.backgroundColor, + customChild: Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: Row( + children: [ + AppIconButton( + size: AppConstants.appBarIconSize, + onTap: () { + debugPrint('asd'); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => NewsScreen())); + }, + icon: Icon( + Icons.newspaper_rounded, + color: ThemeColor.white, + ), + ), + Spacer(), + SvgPicture.asset( + 'assets/images/appBarIcon.svg', + height: AppConstants.clearAppBarHeight * 0.75, + width: AppConstants.clearAppBarHeight * 0.88, + ), + Container( + width: MediaQuery.of(context).size.width * 0.39, + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + icon: Icon( + Icons.more_vert_rounded, + color: ThemeColor.white, + size: 22.adaptedPx(), + ), + items: navigators.map( + (MyNavigator value) { + return DropdownMenuItem( + value: value, + child: Text( + value.name, + overflow: TextOverflow.ellipsis, + // style: AppTheme.selectedTxtStyle, + ), + ); + }, + ).toList(), + onChanged: (newValue) { + if (newValue!.path == 'search') { + Tabnavigator.maybeOf(context)?.changePage(1); + } else if (newValue.path == 'settings') { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => SettingsScreen()), + ); + } else if (newValue.path == 'contact') { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => ContactScreen()), + ); + } + }, + ), + ), + ), + ], + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(AppConstants.appBarHeight); + + @override + Widget build(BuildContext context) { + return Container( + height: preferredSize.height, + padding: EdgeInsets.only(top: Adaptix.systemPadding.top), + color: color ?? Theme.of(context).appBarTheme.backgroundColor, + alignment: Alignment.centerLeft, + child: Material( + color: Colors.transparent, + child: Center( + child: customChild ?? + Padding( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + child: Row( + children: [ + if (includeBack) + AppIconButton( + icon: Icon( + CupertinoIcons.back, + color: ThemeColor.white, //Theme.of(context).appBarTheme.iconTheme?.color, + ), + onTap: goBack, + size: AppConstants.appBarIconSize), + SizedBox( + width: 10.adaptedPx(), + ), + Expanded( + child: Text( + title ?? "", + style: AppBarTheme.of(context).titleTextStyle?.copyWith( + fontWeight: FontWeight.w500, + color: ThemeColor.white, //Theme.of(context).accentTextTheme.headline2?.color, + ), + ), + ), + SizedBox( + width: 10.adaptedPx(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + for (var option in after) ...[SizedBox(width: 1.5.orientationWidth), option] + ], + ) + ], + ), + ), + ), + ), + ); + } +} + +class MyNavigator { + int id; + String name; + String path; + MyNavigator({ + required this.id, + required this.name, + required this.path, + }); +} diff --git a/lib/components/button.dart b/lib/components/button.dart new file mode 100644 index 0000000..27fd9aa --- /dev/null +++ b/lib/components/button.dart @@ -0,0 +1,90 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/indicator.dart'; + +class MyButton extends StatefulWidget { + MyButton({ + Key? key, + this.onDisabled, + this.color, + this.isDisabled = false, + this.onTap, + this.inProgress, + this.padding, + this.borderRadius, + this.text, + this.height, + this.indicatorSize, + this.textSpan, + }) : super(key: key); + + final void Function()? onTap; + final EdgeInsets? padding; + final String? text; + final bool? inProgress; + final double? height; + final Widget? textSpan; + final bool isDisabled; + final void Function()? onDisabled; + final Color? color; + final double? indicatorSize; + final double? borderRadius; + + @override + _MyButtonState createState() => _MyButtonState(); +} + +class _MyButtonState extends State with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + Color? _backColor; + Color? _color; + + if (widget.color != null) { + bool isDark = Theme.of(context).brightness == Brightness.dark; + _color = isDark ? widget.color : Theme.of(context).textTheme.button?.color; + _backColor = isDark ? Theme.of(context).buttonColor : widget.color; + } else { + _color = Theme.of(context).textTheme.button?.color; + _backColor = Theme.of(context).buttonColor; + } + + return Opacity( + opacity: widget.isDisabled ? 0.5 : 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 20.adaptedPx()), + child: Material( + color: _backColor, + child: InkWell( + onTap: () { + if (!(widget.inProgress ?? false) && widget.onTap != null && !widget.isDisabled) { + widget.onTap!(); + } else if (widget.isDisabled && widget.onDisabled != null) {} + }, + child: Container( + height: widget.height, + padding: widget.padding ?? EdgeInsets.symmetric(vertical: 5.adaptedPx(), horizontal: 15.adaptedPx()), + child: AnimatedSize( + duration: Duration(milliseconds: 200), + vsync: this, + child: (widget.inProgress ?? false) + ? Center( + child: Indicator( + size: widget.indicatorSize ?? 0.4.adaptedPx(), + color: Colors.white, + )) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.textSpan == null + ? Text('${widget.text}', style: TextStyle(color: _color, fontSize: 12.99.adaptedPx(), fontWeight: FontWeight.w500)) + : widget.textSpan! + ], + ), + ), + ), + ), + )), + ); + } +} diff --git a/lib/components/categoryBuilder.dart b/lib/components/categoryBuilder.dart new file mode 100644 index 0000000..b88c55a --- /dev/null +++ b/lib/components/categoryBuilder.dart @@ -0,0 +1,104 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/categories/products/pure.dart'; +import 'package:birzha/screens/productsScreen.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class CategoryBuilder extends StatefulWidget { + const CategoryBuilder({Key? key, required this.category}) : super(key: key); + + final ProductsPureCategory category; + + @override + _CategoryBuilderState createState() => _CategoryBuilderState(); +} + +class _CategoryBuilderState extends State { + bool _isPressed = false; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.ease, + padding: EdgeInsets.all(_isPressed ? 8.adaptedPx() : 0), + child: LayoutBuilder( + builder: (context, constraints) => PhysicalModel( + color: Theme.of(context).accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(constraints.biggest.height * 0.04), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() { + _isPressed = false; + }); + Navigator.of(context, rootNavigator: false) + .push(MaterialPageRoute(builder: (_) => ProductsScreen(route: 'category', category: widget.category.toRemote()))); + }, + borderRadius: BorderRadius.circular(constraints.biggest.height * 0.04), + onTapCancel: () { + setState(() { + _isPressed = false; + }); + }, + onTapDown: (details) { + setState(() { + _isPressed = true; + }); + }, + child: Align( + alignment: Alignment.center, + child: Flex( + direction: Axis.vertical, + children: [ + Spacer( + flex: 3, + ), + Expanded( + flex: 12, + child: ConstrainedBox( + constraints: BoxConstraints.expand(), + child: FittedBox( + child: CachedNetworkImage( + imageUrl: widget.category.icon, + placeholder: (_, __) { + return Icon( + Icons.category, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ); + }, + errorWidget: (_, __, ___) { + return Icon( + Icons.category, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ); + }, + ), + ), + ), + ), + Spacer(flex: 2), + Expanded( + flex: 6, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: constraints.biggest.width * 0.1), + child: Text(widget.category.name, + textScaleFactor: 1, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline2?.copyWith(fontSize: constraints.biggest.height * 0.074, height: 1.6)), + ), + ), + Spacer(flex: 3) + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/categoryNameWidget.dart b/lib/components/categoryNameWidget.dart new file mode 100644 index 0000000..01baa52 --- /dev/null +++ b/lib/components/categoryNameWidget.dart @@ -0,0 +1,36 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class CategoryNameWidget extends StatelessWidget { + final String categoryName; + final TextStyle? textStyle; + + const CategoryNameWidget({ + Key? key, + required this.categoryName, + this.textStyle, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 4.adaptedPx(), + height: 29.adaptedPx(), + color: Theme.of(context).accentColor, + ), + SizedBox(width: 10.adaptedPx()), + Text( + categoryName, + style: textStyle ?? + Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.bold, + fontSize: AppConstants.h1FontSize, + ), + ), + ], + ); + } +} diff --git a/lib/components/contactInfo.dart b/lib/components/contactInfo.dart new file mode 100644 index 0000000..0cb0569 --- /dev/null +++ b/lib/components/contactInfo.dart @@ -0,0 +1,59 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class ContactInfo extends StatelessWidget { + final IconData icon; + final String mainText; + final String description; + + const ContactInfo({ + Key? key, + required this.icon, + required this.mainText, + required this.description, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40.adaptedPx(), + height: 40.adaptedPx(), + child: Icon(icon, + size: 25.adaptedPx(), + color: Theme.of(context).chipTheme.backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).accentColor, + borderRadius: BorderRadius.circular(30), + ), + ), + SizedBox(width: 8.adaptedPx()), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + mainText, + style: + Theme.of(context).accentTextTheme.headline2!.copyWith( + fontSize: 16.adaptedPx(), + fontWeight: FontWeight.bold, + ), + ), + Text( + description, + style: Theme.of(context).primaryTextTheme.headline2, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/components/customCardWidget.dart b/lib/components/customCardWidget.dart new file mode 100644 index 0000000..4f46cf4 --- /dev/null +++ b/lib/components/customCardWidget.dart @@ -0,0 +1,78 @@ +import 'package:birzha/components/settingsIcons.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class CustomCardWidget extends StatelessWidget { + final String name; + final Widget? nextSign; + final void Function()? onTap; + final Icon? icon; + final BorderRadius? borderRadius; + final Widget? trailing; + + CustomCardWidget({ + Key? key, + required this.name, + this.nextSign, + this.onTap, + this.icon, + this.trailing, + this.borderRadius, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).cardColor, + borderRadius: borderRadius ?? BorderRadius.circular(5.adaptedPx()), + child: InkWell( + onTap: onTap, + borderRadius: borderRadius ?? BorderRadius.circular(5.adaptedPx()), + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + vertical: 26.adaptedPx(), + ), + child: Row( + children: [ + SizedBox(width: 13.adaptedPx()), + if (icon != null) + Icon( + icon!.icon, + size: 24.adaptedPx(), + color: icon?.color ?? Theme.of(context).accentColor, + ), + SizedBox(width: 12.adaptedPx()), + Expanded( + child: Text(name), + ), + SizedBox( + width: 5.adaptedPx(), + ), + if (trailing != null) ...[ + trailing!, + SizedBox( + width: 5.adaptedPx(), + ), + ], + Container( + child: + Icon(SettingsIcons.next_sign, + color: Theme.of(context).accentColor, + size: 18.adaptedPx()), + width: 19.adaptedPx(), + height: 19.adaptedPx(), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).accentColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(20.adaptedPx()), + ), + ), + SizedBox(width: 10.adaptedPx()), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/dialog.dart b/lib/components/dialog.dart new file mode 100644 index 0000000..bfa0f97 --- /dev/null +++ b/lib/components/dialog.dart @@ -0,0 +1,73 @@ +import 'package:birzha/components/categoryNameWidget.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class AppDialog extends StatelessWidget { + const AppDialog({ + Key? key, + this.title, + required this.child, + this.actions = const [], + }) : super(key: key); + + final String? title; + final Widget child; + final List actions; + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: Container( + padding: EdgeInsets.all( + 14.adaptedPx(), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CategoryNameWidget( + categoryName: title ?? "", + ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Icon( + Icons.cancel_outlined, + color: Theme.of(context).accentColor, + size: 22.adaptedPx(), + ), + ), + ], + ), + SizedBox( + height: 10.adaptedPx(), + ), + Flexible( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [child], + ), + ), + ), + SizedBox( + height: 10.adaptedPx(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: actions, + ) + ], + ), + ), + ); + } +} diff --git a/lib/components/fakeSearchBar.dart b/lib/components/fakeSearchBar.dart new file mode 100644 index 0000000..94a331c --- /dev/null +++ b/lib/components/fakeSearchBar.dart @@ -0,0 +1,90 @@ +import 'dart:math'; + +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/categories/products/search.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/icon.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/productsScreen.dart'; + +class FakeSearchBar extends StatelessWidget { + const FakeSearchBar({Key? key, this.fromHome = false, required this.route}) : super(key: key); + + final bool fromHome; + final String route; + + @override + Widget build(BuildContext context) { + final scaleFactor = min(1.35, MediaQuery.of(context).textScaleFactor); + return Hero( + tag: Tabnavigator.currentRoute(context) + '/$route', + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints.loose(Size(double.infinity, AppConstants.clearAppBarHeight)), + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + Navigator.of(context, rootNavigator: false).push( + CupertinoPageRoute( + builder: (_) => ProductsScreen( + route: route, + category: ProductsSearch(), + ), + ), + ); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + height: AppConstants.clearAppBarHeight, + child: Row( + children: [ + if (!fromHome) ...[ + AppIconButton( + icon: Icon( + CupertinoIcons.back, + color: Theme.of(context).accentColor, + ), + onTap: () { + Tabnavigator.backDispatcher(context); + }, + size: AppConstants.appBarIconSize), + SizedBox( + width: 12.adaptedPx(), + ) + ], + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: AppConstants.clearAppBarHeight * 0.18), + child: Container( + decoration: + BoxDecoration(color: Theme.of(context).dividerColor.withOpacity(0.06), borderRadius: BorderRadius.circular(5.adaptedPx())), + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 15.adaptedPx()), + child: Row( + children: [ + Expanded(child: Text('search'.translation, textScaleFactor: scaleFactor, style: TextStyle(fontWeight: FontWeight.w300))), + Icon( + CupertinoIcons.search, + color: Theme.of(context).accentColor, + size: AppConstants.appBarIconSize * 0.8, + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/icon.dart b/lib/components/icon.dart new file mode 100644 index 0000000..ae12c0b --- /dev/null +++ b/lib/components/icon.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class AppIconButton extends StatelessWidget { + const AppIconButton({ + Key? key, + required this.icon, + required this.size, + this.onTap, + this.highlightColor, + this.splashColor + }) : super(key: key); + + final Icon icon; + final double size; + final Color? highlightColor; + final Color? splashColor; + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: icon, + color: Colors.grey.shade400, + onPressed: (){ + if(onTap != null) + onTap!(); + }, + padding: EdgeInsets.zero, + splashRadius: size*1.1, + constraints: BoxConstraints(), + iconSize: size, + highlightColor: highlightColor ?? Colors.grey.withOpacity(0.1), + splashColor:splashColor ?? Theme.of(context).accentColor.withOpacity(0.1), + ); + } +} \ No newline at end of file diff --git a/lib/components/imagePlaceHolder.dart b/lib/components/imagePlaceHolder.dart new file mode 100644 index 0000000..40fd7b1 --- /dev/null +++ b/lib/components/imagePlaceHolder.dart @@ -0,0 +1,19 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class AppImagePlaceholder extends StatelessWidget { + const AppImagePlaceholder({Key? key, this.size}) : super(key: key); + + final double? size; + + @override + Widget build(BuildContext context) { + return Center( + child: Icon( + Icons.image, + size: size ?? 45.adaptedPx(), + color: Theme.of(context).accentColor, + ), + ); + } +} diff --git a/lib/components/indicator.dart b/lib/components/indicator.dart new file mode 100644 index 0000000..ecc1c9a --- /dev/null +++ b/lib/components/indicator.dart @@ -0,0 +1,19 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +class Indicator extends StatelessWidget { + final double? size; + final Color? color; + + Indicator({this.size, this.color}); + + @override + Widget build(BuildContext context) { + return Transform.scale( + scale: size ?? 1.5.adaptedPx(), + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(color ?? Theme.of(context).accentColor), + ), + ); + } +} diff --git a/lib/components/localizationOverride.dart b/lib/components/localizationOverride.dart new file mode 100644 index 0000000..7a4df0f --- /dev/null +++ b/lib/components/localizationOverride.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:birzha/models/tk_intl.dart'; + +class LocalizationOverride extends StatefulWidget { + const LocalizationOverride({Key? key, required this.builder}) : super(key: key); + + final Widget Function(BuildContext) builder; + + @override + _LocalizationOverrideState createState() => _LocalizationOverrideState(); +} + +class _LocalizationOverrideState extends State with WidgetsBindingObserver { + @override + void didChangeDependencies() { + WidgetsBinding.instance?.addObserver(this); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Localizations.override( + context: context, + locale: SettingsModel.of( + context, + ).language, + delegates: [ + AppLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + TkMaterialLocalizations.delegate + ], + child: Builder( + builder: (innerContext) { + return widget.builder(innerContext); + }, + ), + ); + } +} diff --git a/lib/components/postBuilderFactory.dart b/lib/components/postBuilderFactory.dart new file mode 100644 index 0000000..25fe1c8 --- /dev/null +++ b/lib/components/postBuilderFactory.dart @@ -0,0 +1,33 @@ +import 'package:birzha/components/productBuilder.dart'; +import 'package:birzha/models/chatroom/chatroom.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/transactions/transaction.dart'; +import 'package:birzha/screens/personalCabinet/messages/messages.dart'; +import 'package:birzha/screens/personalCabinet/topUp/topUpHistory.dart'; +import 'package:flutter/material.dart'; + +class PostBuilderFactory extends StatelessWidget { + const PostBuilderFactory({ Key? key, required this.post }) : super(key: key); + + final Post post; + + @override + Widget build(BuildContext context) { + + if(post is Product) + return ProductBuilder(product: post as Product); + + else if(post is Transaction) + return TopUpHistoryCard( + transaction: post as Transaction, + ); + + else if(post is Chatroom) + return MessagesCard( + chatroom: post as Chatroom, + ); + + return Container(); + } +} \ No newline at end of file diff --git a/lib/components/postlist.dart b/lib/components/postlist.dart new file mode 100644 index 0000000..d3543e3 --- /dev/null +++ b/lib/components/postlist.dart @@ -0,0 +1,79 @@ +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/postBuilderFactory.dart'; +import 'package:birzha/components/refreshButton.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../core/lazyload/lazyload.dart'; + +class PostList extends StatefulWidget { + const PostList({Key? key, required this.category, required this.fetchController, this.contentPadding, this.before = const [], this.after = const []}) + : super(key: key); + + final Serializer category; + final FetchController fetchController; + final EdgeInsets? contentPadding; + final List before; + final List after; + + @override + _PostListState createState() => _PostListState(); +} + +class _PostListState extends State { + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: widget.fetchController, + child: LazyLoadView( + data: (page, context) => widget.category.getPosts(context, page), + loaderWidget: SliverFillRemaining( + child: Center(child: Indicator(size: 0.7.adaptedPx())), + ), + loadMoreWidget: SliverToBoxAdapter( + child: Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + alignment: Alignment.center, + child: Row( + children: [ + Expanded( + child: Center(child: Indicator(size: 0.6.adaptedPx())), + ) + ], + ), + ), + ), + before: [...widget.before], + after: [...widget.after], + fetchController: widget.fetchController, + contentPadding: widget.contentPadding ?? EdgeInsets.zero, + errorWidget: (refresh, error) { + return SliverFillRemaining( + child: Center( + child: RefreshButton( + onTap: refresh, + size: 38.adaptedPx(), + ), + ), + ); + }, + gridDelegate: widget.category.gridDelegate(context), + errorOnLoadMoreWidget: (refresh, error) { + return Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + child: RefreshButton( + onTap: refresh, + size: 25.adaptedPx(), + ), + ); + }, + pageFactor: AppConstants.pageLimit, + needPagination: widget.category.needPagination, + itemBuilder: (context, model, index) => PostBuilderFactory(post: model)), + ); + } +} diff --git a/lib/components/productBuilder.dart b/lib/components/productBuilder.dart new file mode 100644 index 0000000..93b32da --- /dev/null +++ b/lib/components/productBuilder.dart @@ -0,0 +1,166 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/details.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../core/lazyload/lazyload.dart'; + +class ProductBuilder extends StatelessWidget { + final Product product; + + const ProductBuilder({ + Key? key, + required this.product, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + var shouldRefresh = await Navigator.of(context, rootNavigator: false).push(MaterialPageRoute(builder: (_) => ProductDetails(product: product))); + if (shouldRefresh is bool && shouldRefresh) { + try { + Provider.of>(context, listen: false).refresh(); + } catch (e) { + print(e); + } + } + }, + child: LayoutBuilder(builder: (context, constraints) { + Widget title(String name) { + return Expanded( + flex: 3, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + name, + textScaleFactor: 1, + maxLines: 1, + style: Theme.of(context).primaryTextTheme.headline3?.copyWith( + fontSize: constraints.maxHeight * 0.034, + ), + ), + ), + ); + } + + Widget value(String name, [Color? color]) { + return Expanded( + flex: 3, + child: Text(name, + textScaleFactor: 1, + maxLines: 1, + style: + Theme.of(context).accentTextTheme.headline2?.copyWith(fontSize: constraints.maxHeight * 0.034, color: color, fontWeight: FontWeight.bold)), + ); + } + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor, width: 0.8.adaptedPx()), + ), + padding: EdgeInsets.symmetric(horizontal: constraints.maxWidth * 0.07, vertical: constraints.maxHeight * 0.03), + child: Flex( + direction: Axis.vertical, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('lotNumber'.translation, + textScaleFactor: 1, + maxLines: 1, + style: Theme.of(context).primaryTextTheme.headline3?.copyWith( + fontSize: constraints.maxHeight * 0.04, + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Text(product.id.toString(), + textScaleFactor: 1, + maxLines: 1, + style: Theme.of(context).accentTextTheme.headline2?.copyWith( + fontSize: constraints.maxHeight * 0.04, + )), + ), + ), + ], + ), + SizedBox( + height: constraints.maxHeight * 0.04, + ), + Container( + child: CachedNetworkImage( + width: constraints.maxWidth, + fit: BoxFit.cover, + height: constraints.maxWidth / (167 / 92), + placeholder: (context, child) { + return Center( + child: SizedBox( + height: constraints.maxWidth / (167 / 92), + child: Icon( + Icons.image, + color: Theme.of(context).accentColor, + size: constraints.maxHeight * 0.1, + ), + ), + ); + }, + errorWidget: (context, error, stackTrace) { + return Center( + child: SizedBox( + height: constraints.maxWidth / (167 / 92), + child: Icon( + Icons.broken_image, + color: Theme.of(context).accentColor, + size: constraints.maxHeight * 0.1, + ), + ), + ); + }, + imageUrl: product.mainImage, + ), + ), + SizedBox( + height: constraints.maxHeight * 0.02, + ), + Text( + product.name + '\n\n', + textScaleFactor: 1, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: Theme.of(context).accentTextTheme.headline2?.copyWith( + fontWeight: FontWeight.bold, + fontSize: constraints.maxHeight * 0.038, + ), + ), + SizedBox( + height: constraints.maxHeight * 0.01, + ), + title('productAmount'.translation), + Spacer(flex: 2), + value(product.quantityFormatted + " " + product.unit), + Spacer(flex: 2), + title('startingPrice'.translation), + Spacer(flex: 2), + value(product.priceFormatted), + Spacer(flex: 2), + title('expiryDate'.translation), + Spacer(flex: 2), + value(product.expiryDateFormatted), + Spacer(), + if (product is MyProduct) ...[ + value((product as MyProduct).statusLabel, (product as MyProduct).statusColor), + ] + ], + ), + ); + }), + ); + } +} diff --git a/lib/components/radio.dart b/lib/components/radio.dart new file mode 100644 index 0000000..373bd12 --- /dev/null +++ b/lib/components/radio.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +class RadioItem{ + late final String name; + late final T value; + late final T groupValue; + + RadioItem({required this.name,required this.value,required this.groupValue}); + +} + +///Widget that draw a beautiful checkbox rounded. Provided with animation if wanted +class RadioButton extends StatefulWidget { + const RadioButton({ + Key? key, + required this.checkedColor, + required this.uncheckedColor, + required this.value, + required this.onChange, + required this.borderColor, + required this.size, + required this.borderWidth, + required this.animationDuration, + required this.groupValue, + }) : + super(key: key); + + //Define borderWidth + + final double borderWidth; + + ///Define the color that is shown when Widgets is checked + final Color checkedColor; + + ///Define the color that is shown when Widgets is unchecked + final Color uncheckedColor; + + ///Define the border of the widget + final Color borderColor; + + ///Define the size of the checkbox + final double size; + + ///Define the duration of the animation. If any + final Duration animationDuration; + + // value of item + + final dynamic value; + + // onchange of value + + final Function(dynamic) onChange; + + //groupValue of radios + + final dynamic groupValue; + + @override + _RadioButtonState createState() => _RadioButtonState(); +} + +class _RadioButtonState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + widget.onChange(widget.value); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.size / 2), + child: AnimatedContainer( + duration: widget.animationDuration, + height: widget.size, + width: widget.size, + decoration: BoxDecoration( + color: widget.value == widget.groupValue ? widget.checkedColor : widget.uncheckedColor, + border: Border.all( + color: widget.value == widget.groupValue? widget.checkedColor: widget.borderColor, + width: widget.borderWidth + ), + borderRadius: BorderRadius.circular(widget.size / 2), + ), + child: Container() + ), + ), + ); + } +} diff --git a/lib/components/refreshButton.dart b/lib/components/refreshButton.dart new file mode 100644 index 0000000..b2c835f --- /dev/null +++ b/lib/components/refreshButton.dart @@ -0,0 +1,20 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/icon.dart'; + +class RefreshButton extends StatelessWidget { + const RefreshButton({Key? key, required this.onTap, this.size}) : super(key: key); + + final void Function() onTap; + final double? size; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Center( + child: AppIconButton(icon: Icon(Icons.refresh), onTap: onTap, size: size ?? 39.adaptedPx()), + ), + ); + } +} diff --git a/lib/components/searchBar.dart b/lib/components/searchBar.dart new file mode 100644 index 0000000..c05f86a --- /dev/null +++ b/lib/components/searchBar.dart @@ -0,0 +1,161 @@ +import 'package:birzha/components/icon.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class ActualSearchBar extends StatefulWidget { + const ActualSearchBar({Key? key, required this.onSubmit, required this.route}) : super(key: key); + + final void Function(String) onSubmit; + final String route; + + @override + _ActualSearchBarState createState() => _ActualSearchBarState(); +} + +class _ActualSearchBarState extends State { + late FocusNode _focusNode; + late TextEditingController _controller; + String _word = ''; + String _previousWord = ''; + + String get word => _word; + + set word(String s) { + if (s != word) { + _previousWord = _word; + _word = s; + } + } + + void _focusListener() { + setState(() {}); + } + + void _search() { + if (_previousWord != _word) { + setState(() { + _previousWord = _word; + }); + widget.onSubmit(_word); + } + } + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _controller = TextEditingController(); + _focusNode.addListener(_focusListener); + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) async { + await Future.delayed(Duration(milliseconds: 300)); + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Hero( + tag: widget.route, + child: Material( + child: Container( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + child: Row( + children: [ + ...[ + AppIconButton( + icon: Icon( + CupertinoIcons.back, + color: Theme.of(context).accentColor, + ), + onTap: () { + Tabnavigator.backDispatcher(context); + }, + size: AppConstants.appBarIconSize), + SizedBox( + width: 15.adaptedPx(), + ) + ], + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: AppConstants.clearAppBarHeight * 0.2), + child: Container( + decoration: BoxDecoration(color: Theme.of(context).dividerColor.withOpacity(0.06), borderRadius: BorderRadius.circular(5.adaptedPx())), + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 15.adaptedPx()), + child: Row( + children: [ + Expanded( + child: Center( + child: TextField( + controller: _controller, + onChanged: (newWord) { + setState(() { + word = newWord; + }); + }, + focusNode: _focusNode, + autofocus: false, + onSubmitted: (_) => _search(), + style: Theme.of(context).textTheme.bodyText2, + cursorColor: Theme.of(context).accentColor, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(left: 10.adaptedPx()), + isDense: true, + hintText: 'search'.translation, + ), + ), + )), + AppIconButton( + onTap: () { + if (!_focusNode.hasFocus) + _focusNode.requestFocus(); + else + _search(); + }, + icon: Icon( + CupertinoIcons.search, + color: Theme.of(context).accentColor, + ), + size: AppConstants.appBarIconSize * 0.8, + ), + if (_focusNode.hasFocus) ...[ + SizedBox(width: 10.adaptedPx()), + AppIconButton( + onTap: () { + setState(() { + _controller.text = ''; + word = ''; + _search(); + _focusNode.unfocus(); + }); + }, + icon: Icon( + CupertinoIcons.clear, + color: Theme.of(context).accentColor, + ), + size: AppConstants.appBarIconSize * 0.8, + ) + ] + ], + ), + ), + ), + ), + ], + )), + ), + ); + } +} diff --git a/lib/components/settingsIcons.dart b/lib/components/settingsIcons.dart new file mode 100644 index 0000000..f1a7881 --- /dev/null +++ b/lib/components/settingsIcons.dart @@ -0,0 +1,29 @@ +/// Flutter icons Settings_Icons +/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: Settings_Icons +/// fonts: +/// - asset: fonts/Settings_Icons.ttf +/// +/// +/// +import 'package:flutter/widgets.dart'; + +class SettingsIcons { + SettingsIcons._(); + + static const _kFontFam = 'Settings_Icons'; + static const String? _kFontPkg = null; + + static const IconData language = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData feedback = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData next_sign = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData contact = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData confidentials = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/components/slider.dart b/lib/components/slider.dart new file mode 100644 index 0000000..306efe0 --- /dev/null +++ b/lib/components/slider.dart @@ -0,0 +1,96 @@ +import 'dart:ui'; +import 'package:birzha/components/imagePlaceHolder.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/screens/photo.dart'; + +class PostImageSlider extends StatefulWidget { + PostImageSlider({Key? key, this.images = const [], this.fit, this.height}) : super(key: key); + + final List images; + final BoxFit? fit; + final double? height; + + @override + __SliderState createState() => __SliderState(); +} + +class __SliderState extends State { + List imageUrls = []; + int page = 0; + + @override + void initState() { + super.initState(); + imageUrls = [ + for (var url in widget.images) url, + ]..remove(''); + } + + @override + Widget build(BuildContext context) { + var bottomBarHeight = 20.adaptedPx(); + var circleHeight = 8.adaptedPx(); + + return Container( + height: widget.height ?? 320.adaptedPx(), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill(child: Container(color: Theme.of(context).cardColor)), + PageView( + children: [ + for (var image in imageUrls) + GestureDetector( + onTap: () { + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (_) => PhotoScreen(imageUrls[page]))); + }, + child: CachedNetworkImage( + imageUrl: image, + fit: widget.fit ?? BoxFit.contain, + errorWidget: (context, url, error) => AppImagePlaceholder(), + placeholder: (_, __) => AppImagePlaceholder(), + alignment: Alignment.topCenter, + ), + ) + ], + onPageChanged: (_p) { + setState(() { + page = _p; + }); + }, + ), + Positioned( + bottom: AppConstants.horizontalPadding(context), + child: ClipRRect( + borderRadius: BorderRadius.circular(circleHeight), + child: Container( + height: bottomBarHeight, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + ), + padding: EdgeInsets.symmetric(horizontal: circleHeight / 2), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Row( + children: List.generate(imageUrls.length, (index) { + bool isSelected = index == page; + return AnimatedContainer( + duration: Duration(milliseconds: 300), + decoration: BoxDecoration(color: Colors.white.withOpacity(isSelected ? 1 : 0.7), shape: BoxShape.circle), + height: circleHeight * (isSelected ? 1 : 0.7), + width: circleHeight * (isSelected ? 1 : 0.7), + margin: EdgeInsets.symmetric(horizontal: circleHeight / 3), + ); + })), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/tab_nav_icons.dart b/lib/components/tab_nav_icons.dart new file mode 100644 index 0000000..ec6d975 --- /dev/null +++ b/lib/components/tab_nav_icons.dart @@ -0,0 +1,28 @@ +/// Flutter icons CustomIcons +/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: CustomIcons +/// fonts: +/// - asset: fonts/CustomIcons.ttf +/// +/// +/// +import 'package:flutter/widgets.dart'; + +class TabNavIcons { + TabNavIcons._(); + + static const _kFontFam = 'TabNavIcons'; + static const String? _kFontPkg = null; + + static const IconData news = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData home = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData category = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/components/tabview.dart b/lib/components/tabview.dart new file mode 100644 index 0000000..05a20dc --- /dev/null +++ b/lib/components/tabview.dart @@ -0,0 +1,218 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:google_nav_bar/google_nav_bar.dart'; +import 'package:provider/provider.dart'; + +class TabView extends StatefulWidget { + TabView({required this.items}); + + final List items; + + @override + _TabViewState createState() => _TabViewState(); +} + +class _TabViewState extends State { + late final List> navigatorKeys; + late final PageController controller; + + Future pop(BuildContext cntxt) async { + final tabnavigator = Provider.of(cntxt, listen: false); + bool canPop = await navigatorKeys[tabnavigator.index].currentState?.maybePop() ?? true; + if (canPop) { + return false; + } else if (tabnavigator.stack.length > 1) { + setState(() { + tabnavigator._removefromStack(); + changePage(tabnavigator.stack.last); + }); + return false; + } else if (tabnavigator.stack.length == 1 && tabnavigator.index != 0) { + setState(() { + tabnavigator._removefromStack(); + changePage(0); + }); + return false; + } + return true; + } + + void changePage(int index) { + controller.jumpToPage(index); + } + + void onPageChange(BuildContext cntxt, int index) { + Provider.of(cntxt, listen: false)._setIndex(index); + } + + @override + void initState() { + navigatorKeys = [for (var item in widget.items) LabeledGlobalKey(item.initialRouteName)]; + controller = PageController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => Tabnavigator( + currentRouteName: (index) { + return widget.items[index].initialRouteName; + }, + changePage: changePage, + pop: pop, + rootNavContext: context), + builder: (context, _) => WillPopScope( + onWillPop: () => pop(context), + child: Consumer( + builder: (context, tabnav, __) { + return Scaffold( + bottomNavigationBar: Container( + color: Theme.of(context).bottomNavigationBarTheme.backgroundColor ?? Colors.white, + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context) / 2).copyWith(bottom: Adaptix.systemPadding.bottom), + child: GNav( + tabBorderRadius: 20.adaptedPx(), + curve: Curves.easeInCubic, + duration: Duration(milliseconds: 280), + selectedIndex: tabnav.index, + backgroundColor: Colors.transparent, + gap: 10.adaptedPx(), + color: Theme.of(context).accentColor, + activeColor: Theme.of(context).accentColor, + iconSize: 18.7.adaptedPx(), + textStyle: Theme.of(context).textTheme.headline2?.copyWith( + color: Theme.of(context).accentColor, + fontSize: AppConstants.h2FontSize * 0.93, + // body text uly headlinedan kichi + fontWeight: FontWeight.w500, + height: 1.3), + tabBackgroundColor: Theme.of(context).bottomNavigationBarTheme.selectedItemColor ?? Colors.grey.shade300, + tabMargin: EdgeInsets.symmetric(vertical: 8.adaptedPx()), + padding: EdgeInsets.symmetric(horizontal: 10.adaptedPx(), vertical: 13.2.adaptedPx()), + onTabChange: (index) { + onPageChange(context, index); + changePage(index); + }, + tabs: [ + for (var item in widget.items) + + /// those items are integrated from each single tabconfig from widget.items + GButton( + icon: item.iconData, + text: item.routeLabel, + active: true, + ), + ], + ), + ), + body: Column( + children: [ + //will create as much Navigators as the length of widget.items.length + Expanded( + child: PageView.builder( + itemCount: widget.items.length, + onPageChanged: (newPageIndex) { + onPageChange(context, newPageIndex); + }, + controller: controller, + itemBuilder: (_, index) => Navigator( + initialRoute: widget.items[index].initialRouteName, + key: navigatorKeys[index], + onGenerateInitialRoutes: (_, __) { + return [ + //each first screen of each item in widget.items + MaterialPageRoute(builder: widget.items[index].firstScreen), + ]; + }, + ), + ), + ), + ], + ), + ); + }, + ), + ), + ); + } +} + +///describes each gbutton, each tab and e.t.c -> creaet list of items in [TabView] constructor +class TabConfigs { + final int index; + final Widget Function(BuildContext) firstScreen; + final String routeLabel; + final IconData iconData; + final String initialRouteName; + + TabConfigs({ + required this.index, + required this.firstScreen, + required this.routeLabel, + required this.iconData, + required this.initialRouteName, + }); +} + +class Tabnavigator extends ChangeNotifier { + int index = 0; + Set stack = {0}; + late final Future Function(BuildContext cntxt) pop; + late final String Function(int) currentRouteName; + late final void Function(int) _changePage; + late final BuildContext rootNavContext; + + Tabnavigator({required void Function(int) changePage, required this.currentRouteName, required this.pop, required this.rootNavContext}) { + _changePage = changePage; + } + + Map _routeSettingsTable = {}; + + void changePage(int page, [dynamic payload]) { + _routeSettingsTable[page] = payload; + _changePage(page); + notifyListeners(); + } + + dynamic getPayloadOf(int page) { + return _routeSettingsTable[page]; + } + + void _setIndex(int i) { + index = i; + stack.remove(i); + stack.add(i); + notifyListeners(); + } + + void _removefromStack() { + _routeSettingsTable.remove(stack.last); + var listFromStack = [...stack]..removeLast(); + stack = {...listFromStack}; + index = stack.last; + notifyListeners(); + } + + static Tabnavigator? maybeOf(BuildContext context) { + try { + return Provider.of(context, listen: false); + } catch (e) {} + } + + static backDispatcher(BuildContext context) { + if (Tabnavigator.maybeOf(context) == null) + Navigator.of(context).pop(); + else + Tabnavigator.maybeOf(context)!.pop(context); + } + + static String currentRoute(BuildContext context) { + var nav = Tabnavigator.maybeOf(context); + if (nav == null) { + return '/root'; + } else { + return nav.currentRouteName(nav.index); + } + } +} diff --git a/lib/components/unauthenticatedWidget.dart b/lib/components/unauthenticatedWidget.dart new file mode 100644 index 0000000..40f0a7e --- /dev/null +++ b/lib/components/unauthenticatedWidget.dart @@ -0,0 +1,51 @@ +import 'package:birzha/components/button.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/screens/auth/login.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +import '../core/manager/manager.dart'; + +class UnAuthenticated extends StatefulWidget { + const UnAuthenticated({Key? key}) : super(key: key); + @override + _UnAuthenticatedState createState() => _UnAuthenticatedState(); +} + +class _UnAuthenticatedState extends State { + TaskStatus status = TaskStatus.None; + final GlobalKey formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.center, + width: 50.orientationWidth, + height: 200.adaptedPx(), + child: SvgPicture.asset('assets/images/unauth.svg'), + ), + Text( + 'needToEnter'.translation, + style: TextStyle(fontSize: 17.adaptedPx(), fontWeight: FontWeight.w300, color: Theme.of(context).accentColor), + ), + SizedBox( + height: 20.adaptedPx(), + ), + MyButton( + text: 'login'.translation, + inProgress: false, + onTap: () { + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (_) => LoginScreen())); + }, + height: 40.adaptedPx(), + ), + ], + ), + ); + } +} diff --git a/lib/components/verifyPhoneDialog.dart b/lib/components/verifyPhoneDialog.dart new file mode 100644 index 0000000..2fdf99d --- /dev/null +++ b/lib/components/verifyPhoneDialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:birzha/components/TextInputCustom.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:birzha/services/validator.dart'; + +import '../constants.dart'; +import '../core/manager/manager.dart'; +import '../models/user/userManager.dart'; +import '../services/modals.dart'; +import 'button.dart'; + +class VerifyPhoneDialog extends StatefulWidget { + @override + __VerifyPhoneDialogState createState() => __VerifyPhoneDialogState(); +} + +class __VerifyPhoneDialogState extends State with ManagerObserverMixin { + late final TextEditingController _controller; + final GlobalKey _form = GlobalKey(); + + TaskStatus status = TaskStatus.None; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: Container( + padding: EdgeInsets.all(14.adaptedPx()), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 10.adaptedPx(), + ), + Text( + "phoneConfirmation".translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h1FontSize, + ), + ), + SizedBox( + height: 15.adaptedPx(), + ), + Text( + "smsSentInfo".translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.normal, + fontSize: AppConstants.h1FontSize, + ), + textAlign: TextAlign.center, + ), + SizedBox( + height: 15.adaptedPx(), + ), + Form( + key: _form, + child: Row( + children: [ + Expanded( + child: TextInputCustom( + fieldStandard: TextInputMetaData( + name: 'enterSmsCode'.translation, + label: 'enterSmsCode'.translation, + type: TextInputType.number, + autoFocus: true, + validation: Validation(conditions: [ + (inp) { + return inp.isEmpty || (int.tryParse(inp.trim())) == null ? 'formatError'.translation : null; + } + ]), + formatters: [ + LengthLimitingTextInputFormatter(4), + FilteringTextInputFormatter.digitsOnly, + ], + key: 'sms_code', + ), + controller: _controller, + ), + ), + ], + ), + ), + SizedBox( + height: 8.adaptedPx(), + ), + Center( + child: ManagerSelector( + selector: (context, manager) => manager.getStatusByKey('sms_verify'), + shouldRebuild: (previous, next) => previous != next, + onUpdate: () { + if (mounted && AppUserManager.of(context).getStatusByKey('sms_verify') == TaskStatus.Success) { + Navigator.of(context).pop(true); + } + }, + builder: (_, value) => MyButton( + text: 'confirm'.translation, + inProgress: value == TaskStatus.Loading, + onTap: () { + AppUserManager.of(context).checkCode(context, _controller.text); + }, + height: 40.adaptedPx(), + ), + ), + ), + ], + ), + ), + ); + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_keyForTask); + } + + @override + bool shouldUpdateListener(TaskStatus old, TaskStatus newOne) { + return old != newOne; + } + + @override + void updateListener() { + if (mounted) + setState(() { + status = AppUserManager.of(context).getStatusByKey(_keyForTask); + if (status == TaskStatus.Success) showSnackBar(context, content: 'dataIsUpdated'.translation); + }); + } +} + +const _keyForTask = 'sms_verify'; diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..9a9607a --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,86 @@ +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; + +const kBannerAssetRatio = 1161 / 500; +const kProductAspectRatio = 189 / 357; + +typedef VoidWithParam = void Function(T); + +abstract class AppConstants { + static const appColor = Color.fromRGBO(0, 49, 151, 1); + static const backgroundColor = Colors.white; + static const kDefaultLanguage = 'tk'; + static const scaffoldColorLight = const Color.fromRGBO(242, 245, 255, 1); + static const supprtedLocales = [const Locale('en', 'US'), const Locale('ru'), const Locale('tk')]; + static double get appBarHeight => clearAppBarHeight + Adaptix.systemPadding.top; + static double get clearAppBarHeight => 50.adaptedPx(); + static double get appBarIconSize => clearAppBarHeight / 2.3; + static double get appBarFontSize => clearAppBarHeight * 0.29; + static double get b2FontSize => 13.2.adaptedPx(); + static double get b3FontSize => 12.95.adaptedPx(); + static double get h2FontSize => 14.adaptedPx(); + static double get h1FontSize => 16.adaptedPx(); + static double get h3FontSize => 13.8.adaptedPx(); + static const int pageLimit = 20; + static int categoryAxisCount(BuildContext context) { + return Resizer(small: 2, large: 3, xlarge: 3, totalWidth: MediaQuery.of(context).size.width).value; + } + + static double horizontalPadding(BuildContext context) => MediaQuery.of(context).size.width * 0.04; + static double verticalPadding(BuildContext context) => 20.adaptedPx(); + + static int productAxisCount(BuildContext context) { + return Resizer(small: 2, large: 3, xlarge: 3, totalWidth: MediaQuery.of(context).size.width).value; + } + + static SliverGridDelegate productsGridDelegate(BuildContext context) { + return SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: kProductAspectRatio, + mainAxisSpacing: horizontalPadding(context), + crossAxisSpacing: horizontalPadding(context) / 2, + crossAxisCount: Resizer(small: 2, medium: 2, large: 3, xlarge: 4, totalWidth: MediaQuery.of(context).size.width).value, + ); + } + + static double productCarouselHeight(BuildContext context) { + final fullWidth = MediaQuery.of(context).size.width; + final divided = fullWidth / productAxisCount(context); + final clearWidth = divided - ((horizontalPadding(context) / 2) * 1.8); + return (clearWidth / kProductAspectRatio) / 1.07; + } + + static Map htmlContentStyle(BuildContext context) => { + '*': Style( + fontSize: FontSize(13.1.adaptedPx()), + alignment: Alignment.centerLeft, + textAlign: TextAlign.start, + margin: EdgeInsets.all(0), + lineHeight: LineHeight.number(1.2), + fontFamily: 'Segoe', + ), + 'p': Style(margin: EdgeInsets.only(bottom: 12.adaptedPx())), + 'a': Style(color: Theme.of(context).accentColor) + }; + + static Map htmlCustomRenderer(BuildContext context) => { + 'a': (elementContext, child) { + final String? text = elementContext.tree.children.first.toString().replaceAll('"', ''); + final String? url = elementContext.tree.attributes['href']; + return TextSpan( + children: [ + TextSpan( + text: '$text', + style: TextStyle(color: Theme.of(context).accentColor, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer() + ..onTap = () { + linkLauncher(url ?? ""); + }) + ], + ); + } + }; + static const passwordReset = 'https://tmex.gov.tm/ru/password-reset'; +} diff --git a/lib/core/adaptix/adaptix.dart b/lib/core/adaptix/adaptix.dart new file mode 100644 index 0000000..39277c7 --- /dev/null +++ b/lib/core/adaptix/adaptix.dart @@ -0,0 +1,48 @@ +library adaptix; + +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:nested/nested.dart'; +import 'package:provider/provider.dart'; +part 'models/sizing.dart'; +part 'models/resizer.dart'; +part 'widgets/initializer.dart'; + +abstract class Adaptix { + static EdgeInsets get systemPadding => _Sizing.systemPadding; + static double get systemAspectRatio => _Sizing.aspectRatio; + + static double xSmallScreenScaleFactor = 1.0; + static double smallScreenScaleFactor = 1.1; + static double mediumScreenScaleFactor = 1.15; + static double largeScreenScaleFactor = 1.2; + static double xLargeScreenScaleFactor = 1.25; + static double commonScaleFactor = 1.0; + + ///use under the context of [SizeInitializer] to notify widgets with constant constructors + static void depend(BuildContext context) { + try { + Provider.of<_SizingNotifierAdapter>(context, listen: true); + } catch (e) { + throw FlutterError('Use method Adaptix.depend only inside build methods under SizeInitializer widget\' context'); + } + } +} + +extension SizingExtendsion on num { + double get orientationWidth => _Sizing._blocksizeHorizontal * this; + + double get orientationHeight => _Sizing._blockSizeVertical * this; + + double adaptedPx() { + return this * + Resizer( + xSmall: Adaptix.xSmallScreenScaleFactor * Adaptix.commonScaleFactor, + small: Adaptix.smallScreenScaleFactor * Adaptix.commonScaleFactor, + medium: Adaptix.mediumScreenScaleFactor * Adaptix.commonScaleFactor, + large: Adaptix.largeScreenScaleFactor * Adaptix.commonScaleFactor, + xlarge: Adaptix.xLargeScreenScaleFactor * Adaptix.commonScaleFactor, + totalWidth: _Sizing._screenWidth) + .value; + } +} diff --git a/lib/core/adaptix/models/resizer.dart b/lib/core/adaptix/models/resizer.dart new file mode 100644 index 0000000..9c78423 --- /dev/null +++ b/lib/core/adaptix/models/resizer.dart @@ -0,0 +1,41 @@ +part of adaptix; + +class Resizer{ + + late final T _xSmall; + late final T _small; + late final T _medium; + late final T _large; + late final T _xlarge; + late final double totalWidth; + late final T value; + + T _invoke(){ + if(totalWidth <= 380 ){ + return _xSmall; + } + else if(totalWidth>380 && totalWidth <= 414){ + return _small; + } + else if(totalWidth>414 && totalWidth<=600){ + return _medium; + } + else if(totalWidth>600 && totalWidth<=800){ + return _large; + } + else{ + return _xlarge; + } + } + + Resizer({T? xSmall ,required T small, T? medium, T? large, T? xlarge,required this.totalWidth }): + _xSmall = xSmall ?? small, + _small = small, + _medium = medium ?? small, + _large = large ?? small, + _xlarge = xlarge ?? small + { + value = _invoke(); + } + +} \ No newline at end of file diff --git a/lib/core/adaptix/models/sizing.dart b/lib/core/adaptix/models/sizing.dart new file mode 100644 index 0000000..52c1275 --- /dev/null +++ b/lib/core/adaptix/models/sizing.dart @@ -0,0 +1,37 @@ +part of adaptix; + +_SizingNotifierAdapter _adapter = _SizingNotifierAdapter(); + +class _SizingNotifierAdapter with ChangeNotifier{ + + void setSizing(BoxConstraints constraints, Orientation orientation){ + _Sizing().._init(constraints, orientation); + notifyListeners(); + } + +} + +class _Sizing{ + static late double _screenWidth; + static late double _screenHeight; + static late double _blocksizeHorizontal = 0; + static late double _blockSizeVertical = 0; + static late double aspectRatio; + static late EdgeInsets systemPadding; + + void _init( BoxConstraints constraints, Orientation orientation ){ + if( orientation == Orientation.portrait ){ + _screenWidth = constraints.maxWidth; + _screenHeight = constraints.maxHeight; + } + else{ + _screenWidth = constraints.maxHeight; + _screenHeight = constraints.maxWidth; + } + _blocksizeHorizontal = _screenWidth/100; + _blockSizeVertical = _screenHeight/100; + + systemPadding = MediaQueryData.fromWindow(window).padding; + aspectRatio = _blockSizeVertical / _blocksizeHorizontal; + } +} \ No newline at end of file diff --git a/lib/core/adaptix/widgets/initializer.dart b/lib/core/adaptix/widgets/initializer.dart new file mode 100644 index 0000000..ba66383 --- /dev/null +++ b/lib/core/adaptix/widgets/initializer.dart @@ -0,0 +1,23 @@ +part of adaptix; + +class SizeInitializer extends SingleChildStatelessWidget { + const SizeInitializer({required this.builder}) : super(key: const Key('sizeInit')); + + final Widget Function(BuildContext context) builder; + + @override + Widget buildWithChild(BuildContext context, _) { + return OrientationBuilder( + builder: (context, orientation) { + return LayoutBuilder( + builder: (context, constraints) { + return ChangeNotifierProvider.value( + value: _adapter..setSizing(constraints, orientation), + builder: (_, __) => builder(context), + ); + }, + ); + }, + ); + } +} diff --git a/lib/core/lazyload/lazyload.dart b/lib/core/lazyload/lazyload.dart new file mode 100644 index 0000000..10042ff --- /dev/null +++ b/lib/core/lazyload/lazyload.dart @@ -0,0 +1,7 @@ +library lazyload; + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +part 'widget.dart'; +part 'streamControl.dart'; diff --git a/lib/core/lazyload/streamControl.dart b/lib/core/lazyload/streamControl.dart new file mode 100644 index 0000000..990d6a6 --- /dev/null +++ b/lib/core/lazyload/streamControl.dart @@ -0,0 +1,41 @@ +part of lazyload; + +mixin _StreamControlledMixin on State{ + Future Function() get asyncAction; + StreamSubscription? channel; + bool isLoading = false; + void start(BuildContext context){} + void done(BuildContext context){} + void error(BuildContext context, dynamic error){} + void onDataRecived(BuildContext context, D data){} + + void cancel(){ + channel?.cancel(); + } + + void connect(BuildContext ctx){ + cancel(); + this.setState(() { + start(ctx); + isLoading = true; + this.channel = asyncAction().asStream() + .listen((event){ + this.onDataRecived(ctx, event); + }) + ..onDone((){ + done(ctx); + if(mounted) + this.setState(() { + isLoading = false; + }); + }) + ..onError((e){ + print('error from generics $e'); + error(ctx, e); + }) + ; + }); + } + + +} \ No newline at end of file diff --git a/lib/core/lazyload/widget.dart b/lib/core/lazyload/widget.dart new file mode 100644 index 0000000..a941277 --- /dev/null +++ b/lib/core/lazyload/widget.dart @@ -0,0 +1,308 @@ +part of lazyload; + +typedef PaginatedData = Future> Function(int page, BuildContext context); +typedef _GetterDelegate = T Function(); + +class FetchController with ChangeNotifier { + _GetterDelegate _refresh = () {}; + _GetterDelegate _currentPage = () => 0; + _GetterDelegate _isLoading = () => true; + _GetterDelegate _isError = () => false; + + void _update() { + if (SchedulerBinding.instance != null) + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + notifyListeners(); + }); + else + notifyListeners(); + } + + set _setRefresh(_GetterDelegate refreshDelegate) { + _refresh = refreshDelegate; + } + + set _setCurrentPage(_GetterDelegate currentPageDelegate) { + _currentPage = currentPageDelegate; + } + + set _setIsLoading(_GetterDelegate loadingDelegate) { + _isLoading = loadingDelegate; + } + + set _setIsError(_GetterDelegate isErrorDelegate) { + _isError = isErrorDelegate; + } + + void _init({ + required _GetterDelegate refreshDelegate, + required _GetterDelegate pageDelegate, + required _GetterDelegate loadingDelegate, + required _GetterDelegate isErrorDelegate, + }) { + _setRefresh = refreshDelegate; + _setCurrentPage = pageDelegate; + _setIsLoading = loadingDelegate; + _setIsError = isErrorDelegate; + _update(); + } + + @visibleForTesting + void init({ + required _GetterDelegate refreshDelegate, + required _GetterDelegate pageDelegate, + required _GetterDelegate loadingDelegate, + required _GetterDelegate isErrorDelegate, + }) { + _init(refreshDelegate: refreshDelegate, pageDelegate: pageDelegate, loadingDelegate: loadingDelegate, isErrorDelegate: isErrorDelegate); + } + + @override + void dispose() { + if (SchedulerBinding.instance == null) + super.dispose(); + else + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + super.dispose(); + }); + } + + void refresh() { + _refresh(); + } + + bool get isLoading => _isLoading(); + bool get isError => _isError(); + int get currentPage => _currentPage(); +} + +class LazyLoadView extends StatefulWidget { + LazyLoadView( + {Key? key, + required this.data, + this.fetchController, + this.before = const [], + this.after = const [], + this.contentPadding = const EdgeInsets.all(0), + required this.loaderWidget, + required this.loadMoreWidget, + required this.errorWidget, + required this.errorOnLoadMoreWidget, + this.scrollPhysics, + required this.itemBuilder, + this.pageFactor = 10, + this.gridDelegate, + this.emptyWidget = const SliverToBoxAdapter(), + this.needBottomSpace = true, + this.needPagination = true, + this.overridePullToRefresh, + this.disableScrollOnLoad = true, + this.scrollController}) + : super(key: key); + + final FetchController? fetchController; + final PaginatedData data; + final List before; + final List after; + final EdgeInsets contentPadding; + final Widget loaderWidget; + final Widget loadMoreWidget; + final SliverGridDelegate? gridDelegate; + final Widget Function(void Function() closure, dynamic e) errorWidget; + final Widget Function(void Function() closure, dynamic e) errorOnLoadMoreWidget; + final ScrollPhysics? scrollPhysics; + final Widget Function(BuildContext context, T model, int index) itemBuilder; + final int pageFactor; + final Widget emptyWidget; + final bool needBottomSpace; + final bool needPagination; + final void Function()? overridePullToRefresh; + final ScrollController? scrollController; + final bool disableScrollOnLoad; + + @override + LazyLoadViewState createState() => LazyLoadViewState(); +} + +class LazyLoadViewState<_T> extends State> with _StreamControlledMixin, List<_T>> { + late int _pagefactor; + List<_T> _data = []; + bool _isError = false; + StreamSubscription? _channel; + bool get _isErrorOnLoadMore => _data.isNotEmpty && _isError; + bool get _isLoadingMore => _data.isNotEmpty && isLoading; + late ScrollController controller; + dynamic errorTrace; + bool performingFrame = false; + + List<_T> get data => [..._data]; + + int get _page { + return (_data.length / _pagefactor).ceil(); + } + + @override + Future Function() get asyncAction => () => widget.data(_page + 1, context); + + @override + start(_) { + setState(() { + _isError = false; + widget.fetchController?._update(); + }); + } + + @override + done(_) { + widget.fetchController?._update(); + } + + @override + onDataRecived(cntxt, dataRecieved) { + var finalData = dataRecieved as List<_T>; + setState(() { + _data.addAll(finalData); + widget.fetchController?._update(); + }); + } + + @override + error(_, err) { + setState(() { + _isError = true; + errorTrace = err; + widget.fetchController?._update(); + }); + } + + @override + void cancel() { + widget.fetchController?._update(); + _channel?.cancel(); + } + + @override + get channel => _channel; + + @override + set channel(v) { + _channel = v + ?..onDone(() async { + if (_data.isNotEmpty && widget.needPagination && !performingFrame) await Future.delayed(Duration(milliseconds: 570)); + if (mounted) { + setState(() { + if (performingFrame) performingFrame = false; + isLoading = false; + widget.fetchController?._update(); + }); + } + }); + } + + void _scrollListener() { + if ((controller.offset >= controller.position.maxScrollExtent && widget.needPagination)) { + _loadMore(); + } + } + + void _loadMore() async { + if (_data.length == _pagefactor * _page && !isLoading && !_isLoadingMore && !_isError) { + setState(() { + widget.fetchController?._update(); + connect(context); + }); + } + } + + void refresh() { + setState(() { + _data.clear(); + isLoading = true; + performingFrame = true; + widget.fetchController?._update(); + }); + connect(context); + } + + void tryAgain() { + widget.fetchController?._update(); + connect(context); + } + + @override + void dispose() { + super.dispose(); + cancel(); + isLoading = false; + if (widget.scrollController == null) controller.dispose(); + channel?.cancel(); + } + + int _getPage() => _page; + bool _getIsLoading() => isLoading; + bool _getIsError() => _isError; + + @override + void initState() { + super.initState(); + _pagefactor = widget.pageFactor; + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + if (widget.fetchController != null) { + widget.fetchController?._init(refreshDelegate: refresh, pageDelegate: _getPage, loadingDelegate: _getIsLoading, isErrorDelegate: _getIsError); + } + }); + controller = (widget.scrollController ?? ScrollController())..addListener(_scrollListener); + connect(context); + } + + @override + Widget build(BuildContext context) { + return Container( + child: RefreshIndicator( + onRefresh: () async { + if (widget.overridePullToRefresh == null) + refresh(); + else + widget.overridePullToRefresh!(); + }, + child: CustomScrollView( + physics: isLoading && !_isLoadingMore && widget.disableScrollOnLoad ? NeverScrollableScrollPhysics() : widget.scrollPhysics, + controller: controller, + slivers: [ + if (isLoading && _data.isEmpty) + widget.loaderWidget + else if (_isError && _data.isEmpty) + widget.errorWidget(refresh, errorTrace) + else if (_data.isEmpty) + widget.emptyWidget + else if (widget.gridDelegate != null) + SliverPadding( + padding: widget.contentPadding, + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate((_, index) => widget.itemBuilder(context, _data[index], index), childCount: _data.length), + gridDelegate: widget.gridDelegate!), + ) + else + SliverPadding( + padding: widget.contentPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) => widget.itemBuilder(context, _data[index], index), childCount: _data.length), + ), + ), + if (_isLoadingMore) widget.loadMoreWidget else if (_isErrorOnLoadMore) widget.errorOnLoadMoreWidget(tryAgain, errorTrace) + ] + ..insertAll(0, widget.before) + ..addAll([if (!isLoading && !_isLoadingMore) ...widget.after]) + ..addAll([ + if (widget.needBottomSpace) + SliverToBoxAdapter( + child: SizedBox( + height: (MediaQuery.of(context).size.height * 0.1) / 2, + ), + ) + ]), + ), + ), + ); + } +} diff --git a/lib/core/manager/manager.dart b/lib/core/manager/manager.dart new file mode 100644 index 0000000..b2092f1 --- /dev/null +++ b/lib/core/manager/manager.dart @@ -0,0 +1,19 @@ +library manager; + +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; +import 'package:provider/provider.dart'; +export 'manager.dart' show Manager; +export 'widgets/manager_selector.dart'; +part 'models/manager_model.dart'; +part 'models/task.dart'; +part 'widgets/manager_builder.dart'; +part 'models/paginated_manager.dart'; +part 'widgets/paginated_collection_builder.dart'; +part 'models/manager_state.dart'; +part 'models/manager_observer_mixin.dart'; + +const _kPaginatedTaskKey = 'pagino'; diff --git a/lib/core/manager/models/manager_model.dart b/lib/core/manager/models/manager_model.dart new file mode 100644 index 0000000..4649167 --- /dev/null +++ b/lib/core/manager/models/manager_model.dart @@ -0,0 +1,127 @@ +part of manager; + +abstract class Manager extends ChangeNotifier { + late BehaviorSubject value; + late Model _value; + + Model get dataSync => _value; + + Model transformer(Model newModel) { + return newModel; + } + + final Map?> _tasks = {}; + final Map _listeners = {}; + + Task? getTaskByKey(String id) => _tasks[id] == null + ? null + : Task(computation: _tasks[id]!.computation, key: _tasks[id]!.key) + ?.._stateController = _tasks[id]!._stateController + .._creationDate = _tasks[id]!._creationDate + .._subscriptionToFuture = _tasks[id]!._subscriptionToFuture; + + Stream>? taskStateOnly(String key) => _tasks[key]?.state; + + Stream> taskStateWithLatestValue(String key) => + CombineLatestStream.combine2, Model, + ManagerState>(_tasks[key]?.state ?? const Stream.empty(), + value, (a, b) => ManagerState(state: b, taskResult: a)); + + Future addTask(Task newTask, {bool shouldStart = true}) async { + try { + await _tasks[newTask.key]?._cancelInnerFutureSubscription(); + await _listeners[newTask.key]?.cancel(); + } catch (e) { + if (kDebugMode) { + print('Error in managers addtask function of $Model: $e'); + } + } + if (_tasks[newTask.key] == null) { + _tasks[newTask.key] = newTask; + } else { + _tasks[newTask.key]!.computation = newTask.computation; + _tasks[newTask.key]!._creationDate = DateTime.now(); + } + if (shouldStart) { + _tasks[newTask.key]!._register(); + _listeners[newTask.key] = _tasks[newTask.key]!.state.listen((event) { + if (event.status == TaskStatus.Success && event.value != null) { + value.add(transformer(event.value!)); + } + listenerCallBack(event, newTask.key); + }); + } + notifyListeners(); + } + + Future startTask(String taskID) async { + if (_tasks[taskID] != null) { + try { + await _tasks[taskID]?._cancelInnerFutureSubscription(); + await _listeners[taskID]?.cancel(); + } catch (e) { + if (kDebugMode) { + print('Error in managers startTask function of $Model: $e'); + } + } + _tasks[taskID]!._register(); + _tasks[taskID]!._creationDate = DateTime.now(); + _listeners[taskID] = _tasks[taskID]!.state.listen((event) { + listenerCallBack(event, taskID); + if (event.status == TaskStatus.Success && event.value != null) { + value.add(transformer(event.value!)); + } + }); + notifyListeners(); + } + } + + void refreshIfError(String key) async { + await _tasks[key]?._cancelInnerFutureSubscription(); + _tasks[key]?._register(); + } + + Stream> _destroy() async* { + for (var key in _tasks.keys) { + yield Future(() async { + await _listeners[key]?.cancel(); + await _tasks[key]?._destroy(); + _tasks[key] = null; + _listeners[key] = null; + }); + } + } + + Future destroy() async { + await for (var process in _destroy()) { + try { + await process; + // ignore: empty_catches + } catch (e) {} + } + notifyListeners(); + } + + Future destroyTask(String taskId) async { + await _listeners[taskId]?.cancel(); + await _tasks[taskId]?._destroy(); + _listeners[taskId] = null; + _tasks[taskId] = null; + notifyListeners(); + } + + void valueListener(Model newValue) { + _value = newValue; + notifyListeners(); + } + + void listenerCallBack(TaskResult result, String taskKey) {} + + static Duration globalTaskTimeOut = const Duration(minutes: 1); + + Manager(Model initialData) + : value = BehaviorSubject.seeded(initialData), + _value = initialData { + value.listen(valueListener); + } +} diff --git a/lib/core/manager/models/manager_observer_mixin.dart b/lib/core/manager/models/manager_observer_mixin.dart new file mode 100644 index 0000000..7b79014 --- /dev/null +++ b/lib/core/manager/models/manager_observer_mixin.dart @@ -0,0 +1,37 @@ +part of manager; + +mixin ManagerObserverMixin on State{ + + void updateListener(); + void _updateListener(){ + var _newVal = selector(context, Provider.of(context, listen: false)); + if(shouldUpdateListener(_oldVal, _newVal)){ + updateListener(); + } + _oldVal = _newVal; + } + + late M manager; + + V selector(BuildContext context, M someManager); + + late V _oldVal; + + bool shouldUpdateListener(V oldManager, V newManager); + + @mustCallSuper + @override + void initState() { + super.initState(); + manager = Provider.of(context, listen: false); + _oldVal = selector(context, Provider.of(context, listen: false)); + manager.addListener(_updateListener); + } + + @mustCallSuper + @override + void dispose(){ + manager.removeListener(_updateListener); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/core/manager/models/manager_state.dart b/lib/core/manager/models/manager_state.dart new file mode 100644 index 0000000..0b7377f --- /dev/null +++ b/lib/core/manager/models/manager_state.dart @@ -0,0 +1,10 @@ +part of manager; + +class ManagerState{ + + final TaskResult taskResult; + final Model state; + + ManagerState({required this.state,required this.taskResult}); + +} \ No newline at end of file diff --git a/lib/core/manager/models/paginated_manager.dart b/lib/core/manager/models/paginated_manager.dart new file mode 100644 index 0000000..c66b592 --- /dev/null +++ b/lib/core/manager/models/paginated_manager.dart @@ -0,0 +1,51 @@ +part of manager; + +Future delay() => Future.delayed(const Duration(microseconds: 100)); + +class Pagination { + final Set data; + final int page; + Pagination({this.data = const {}, this.page = 0}); +} + +abstract class PaginatedManager extends Manager> { + int get perPage; + Future> Function(int page) get computatiion; + + void _paginate() { + if (_value.data.length == perPage * _value.page) { + addTask(Task( + computation: () => computatiion(_value.page + 1), + key: _kPaginatedTaskKey)); + } + } + + void refresh() async { + value.add(Pagination()); + await addTask(Task( + computation: () async { + return Pagination(); + }, + key: _kPaginatedTaskKey)); + _paginate(); + } + + @override + transformer(newModel) { + late Pagination valueCopy = + Pagination(data: {..._value.data}, page: _value.page); + if (newModel.page == 0) { + valueCopy = newModel; + } else { + var merged = {...valueCopy.data, ...newModel.data}; + valueCopy = Pagination( + data: merged, page: max(1, (merged.length / perPage).truncate())); + } + return valueCopy; + } + + PaginatedManager({Pagination? initialData}) + : super(initialData ?? Pagination()) { + refresh(); + } +} diff --git a/lib/core/manager/models/task.dart b/lib/core/manager/models/task.dart new file mode 100644 index 0000000..199f20f --- /dev/null +++ b/lib/core/manager/models/task.dart @@ -0,0 +1,88 @@ +part of manager; + +enum TaskStatus { + // ignore: constant_identifier_names + Loading, + // ignore: constant_identifier_names + Error, + // ignore: constant_identifier_names + Success, + // ignore: constant_identifier_names + None +} + +class TaskResult { + final TaskStatus status; + final Model? value; + final Exception? errorTrace; + + TaskResult({this.value, required this.status, this.errorTrace}); +} + +class Task { + final String key; + late DateTime _creationDate; + // ignore: prefer_final_fields + BehaviorSubject> _stateController = BehaviorSubject(); + Future Function() computation; + Stream> get state => _stateController; + late StreamSubscription _subscriptionToFuture; + + @visibleForTesting + DateTime get timeStamp => _creationDate; + + void _register() { + if (!_stateController.isClosed) { + _stateController.add(TaskResult(status: TaskStatus.Loading)); + var _future = Future(() async { + try { + var newModel = await computation().timeout(Manager.globalTaskTimeOut); + return newModel; + } on TimeoutException catch (_) { + throw Exception('time exceeded'); + } + }); + _subscriptionToFuture = _future.asStream().listen((event) { + if (!_stateController.isClosed) { + _stateController + .add(TaskResult(status: TaskStatus.Success, value: event)); + } + }) + ..onError((e) { + var trace = e is Exception ? e : null; + if (kDebugMode) { + print('Error <$e> in Task:<$key>'); + } + if (!_stateController.isClosed) { + _stateController.add(TaskResult( + status: TaskStatus.Error, errorTrace: trace)); + } + }); + } + } + + Future _cancelInnerFutureSubscription() { + _stateController.add(TaskResult(status: TaskStatus.Loading)); + return _subscriptionToFuture.cancel(); + } + + Future _destroy() async { + _stateController.add(TaskResult(status: TaskStatus.None)); + await _subscriptionToFuture.cancel(); + return _stateController.close(); + } + + @override + bool operator ==(other) { + return other is Task && + hashCode == other.hashCode && + other._creationDate.isAtSameMomentAs(_creationDate); + } + + @override + int get hashCode => key.hashCode; + + Task({required this.computation, required this.key}) { + _creationDate = DateTime.now(); + } +} diff --git a/lib/core/manager/widgets/manager_builder.dart b/lib/core/manager/widgets/manager_builder.dart new file mode 100644 index 0000000..38277dd --- /dev/null +++ b/lib/core/manager/widgets/manager_builder.dart @@ -0,0 +1,23 @@ +part of manager; + +class TaskObserverBuilder, Model> extends StatelessWidget { + const TaskObserverBuilder({Key? key,required this.taskKey,required this.builder}) : super(key: key); + + final String taskKey; + final Widget Function(BuildContext context, TaskStatus status, Model model, void Function() refresh) builder; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, manager, child) { + return StreamBuilder>( + stream: manager.taskStateWithLatestValue(taskKey), + builder: (context, snapshot) { + final TaskStatus status = snapshot.data?.taskResult.status ?? TaskStatus.Loading; + return builder(context, status, manager.dataSync, () => manager.refreshIfError(taskKey)); + }, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/core/manager/widgets/manager_selector.dart b/lib/core/manager/widgets/manager_selector.dart new file mode 100644 index 0000000..74f1f58 --- /dev/null +++ b/lib/core/manager/widgets/manager_selector.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import '../manager.dart'; + +class ManagerSelector extends StatefulWidget { + const ManagerSelector({Key? key, required this.selector, required this.shouldRebuild, required this.onUpdate, required this.builder}) : super(key: key); + + final V Function(BuildContext, M) selector; + final bool Function(V, V) shouldRebuild; + final VoidCallback onUpdate; + final Widget Function(BuildContext, V) builder; + + @override + _ManagerSelectorState createState() => _ManagerSelectorState(); +} + +class _ManagerSelectorState extends State> with ManagerObserverMixin, M, V> { + @override + Widget build(BuildContext context) { + return widget.builder(context, selector(context, manager)); + } + + @override + V selector(BuildContext context, M someManager) { + return widget.selector(context, someManager); + } + + @override + bool shouldUpdateListener(V oldVal, V newVal) { + return widget.shouldRebuild(oldVal, newVal); + } + + @override + void updateListener() { + widget.onUpdate(); + if (mounted) { + setState(() {}); + } + } +} diff --git a/lib/core/manager/widgets/paginated_collection_builder.dart b/lib/core/manager/widgets/paginated_collection_builder.dart new file mode 100644 index 0000000..4ecdd1e --- /dev/null +++ b/lib/core/manager/widgets/paginated_collection_builder.dart @@ -0,0 +1,204 @@ +part of manager; + +class PaginatedCollectionBuilder, Model> + extends StatefulWidget { + const PaginatedCollectionBuilder( + {Key? key, + this.before = const [], + this.after = const [], + this.contentPadding = const EdgeInsets.all(0), + required this.loaderWidget, + required this.loadMoreWidget, + required this.errorWidget, + required this.errorOnLoadMoreWidget, + this.scrollPhysics, + required this.itemBuilder, + this.gridDelegate, + this.filter, + this.emptyWidget = const SliverToBoxAdapter(), + this.needbottomSpace = true}) + : super(key: key); + + final List before; + final List after; + final EdgeInsets contentPadding; + final Widget loaderWidget; + final Widget loadMoreWidget; + final List Function(List m)? filter; + final SliverGridDelegate? gridDelegate; + final Widget Function(void Function() closure) errorWidget; + final Widget Function(void Function() closure) errorOnLoadMoreWidget; + final ScrollPhysics? scrollPhysics; + final Widget Function(BuildContext context, Model model, int index) + itemBuilder; + final Widget emptyWidget; + final bool needbottomSpace; + + @override + _PaginatedCollectionBuilderState createState() => + _PaginatedCollectionBuilderState(); +} + +class _PaginatedCollectionBuilderState<_T extends PaginatedManager<_Model>, + _Model> extends State> + with + ManagerObserverMixin, _T, + Task>?> { + // ignore: unused_field + int _page = 1; + List<_Model> _secretData = []; + + List<_Model> get _data { + return _secretData; + } + + set _data(List<_Model> other) { + _secretData = [ + if (widget.filter != null) ...widget.filter!([...other]) else ...other + ]; + } + + bool get _isError => _status == TaskStatus.Error; + StreamSubscription? _channel; + TaskStatus _status = TaskStatus.Loading; + bool get isLoading => _status == TaskStatus.Loading; + bool get _isErrorOnLoadMore => _data.isNotEmpty && _isError; + bool get _isLoadingMore => _data.isNotEmpty && isLoading; + late ScrollController controller; + + void _scrollListener() { + if ((controller.offset >= controller.position.maxScrollExtent)) { + _loadMore(); + } + } + + void _loadMore() async { + if (!isLoading && !_isLoadingMore && !_isError) { + setState(() { + Provider.of<_T>(context, listen: false)._paginate(); + }); + } + } + + void tryAgain() { + Provider.of<_T>(context, listen: false)._paginate(); + } + + void refresh() { + Provider.of<_T>(context, listen: false).refresh(); + } + + void initializeData() async { + setState(() { + _data = [...Provider.of<_T>(context, listen: false).dataSync.data]; + _page = Provider.of<_T>(context, listen: false).dataSync.page; + }); + } + + @override + selector(_, man) { + return man.getTaskByKey(_kPaginatedTaskKey); + } + + @override + shouldUpdateListener(prev, next) { + return prev != next; + } + + @override + updateListener() { + _channel = Provider.of<_T>(context, listen: false) + .taskStateWithLatestValue(_kPaginatedTaskKey) + .listen((event) async { + if (mounted) { + final newManagerState = event.state; + final _taskStatus = event.taskResult.status; + setState(() { + _status = _taskStatus; + }); + if (_taskStatus == TaskStatus.Success) { + setState(() { + _data = [...newManagerState.data]; + _page = newManagerState.page; + if (_data.isEmpty) { + controller.jumpTo(1); + } + }); + } + } + }); + } + + @override + void initState() { + super.initState(); + initializeData(); + controller = ScrollController(); + controller.addListener(_scrollListener); + updateListener(); + } + + @override + void dispose() { + _channel?.cancel(); + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + refresh(); + }, + child: CustomScrollView( + physics: isLoading && !_isLoadingMore + ? const NeverScrollableScrollPhysics() + : widget.scrollPhysics, + controller: controller, + slivers: [ + if (isLoading && _data.isEmpty) + widget.loaderWidget + else if (_isError && _data.isEmpty) + widget.errorWidget(refresh) + else if (_data.isEmpty) + widget.emptyWidget + else if (widget.gridDelegate != null) + SliverPadding( + padding: widget.contentPadding, + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate( + (_, index) => + widget.itemBuilder(context, _data[index], index), + childCount: _data.length), + gridDelegate: widget.gridDelegate!), + ) + else + SliverPadding( + padding: widget.contentPadding, + sliver: SliverList( + delegate: SliverChildListDelegate(List.generate( + _data.length, + (index) => + widget.itemBuilder(context, _data[index], index))), + ), + ), + if (_isLoadingMore) + widget.loadMoreWidget + else if (_isErrorOnLoadMore) + widget.errorOnLoadMoreWidget(tryAgain) + ] + ..insertAll(0, widget.before) + ..addAll(widget.after) + ..addAll([ + if (widget.needbottomSpace) + SliverToBoxAdapter( + child: SizedBox( + height: (MediaQuery.of(context).size.height * 0.1) / 2, + ), + ) + ]), + ), + ); + } +} diff --git a/lib/core/orm/models/dao.dart b/lib/core/orm/models/dao.dart new file mode 100644 index 0000000..fe6b118 --- /dev/null +++ b/lib/core/orm/models/dao.dart @@ -0,0 +1,10 @@ +import '../orm.dart'; + +abstract class OrmDao { + Future apply(List records); + Future delete(List records); + Future> read(); + String get name; + Future getByKey(String fieldName, PKeyType key); + Future clear(); +} diff --git a/lib/core/orm/models/database/database.dart b/lib/core/orm/models/database/database.dart new file mode 100644 index 0000000..e48050d --- /dev/null +++ b/lib/core/orm/models/database/database.dart @@ -0,0 +1,96 @@ +import '../dao.dart'; +import '../../orm.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/timestamp.dart'; +import 'package:sembast/utils/value_utils.dart'; +import 'stub.dart' if (dart.library.io) 'mobile.dart' if (dart.library.html) 'web.dart'; + +const kCreatedTimeStampField = 'created_at'; + +typedef ConstructorDelegate = T Function(Map data); +typedef TransactionDelegate = Future Function(Transaction transaction); + +class OrmDataBase implements OrmDao { + OrmDataBase({required this.name, required this.constructorDelegate}); + + final String name; + final ConstructorDelegate constructorDelegate; + StoreRef get _getTable => intMapStoreFactory.store(name); + + @override + Future apply(List records, {bool withTimeStamp = false}) { + var store = _getTable; + return _makeTransaction((trans) async { + for (int i = 0; i < records.length; i++) { + var innerData = {...records[i].jSON}; + if (withTimeStamp && innerData[kCreatedTimeStampField] == null) innerData[kCreatedTimeStampField] = Timestamp.fromDateTime(DateTime.now()); + var postKey = await store.findFirst(trans, finder: Finder(filter: Filter.equals(records[i].primaryKeyField, records[i].primaryKey))); + if (postKey == null) + await store.add(trans, innerData); + else + await store.update(trans, innerData, finder: Finder(filter: Filter.equals(records[i].primaryKeyField, records[i].primaryKey))); + } + }); + } + + Future deleteWhere({Filter? filter}) { + var store = _getTable; + return _makeTransaction((trans) async { + await store.delete(trans, finder: Finder(filter: filter)); + }); + } + + @override + Future delete(List records) { + var store = _getTable; + return _makeTransaction((trans) async { + for (int i = 0; i < records.length; i++) { + var postKey = await store.findFirst(trans, finder: Finder(filter: Filter.equals(records[i].primaryKeyField, records[i].primaryKey))); + if (postKey != null) await store.record(postKey.key).delete(trans); + } + }); + } + + @override + Future> read({int? offset, int? limit, Filter? filter, bool getAll = false}) async { + var store = _getTable; + var finder = Finder(offset: getAll ? null : offset, limit: getAll ? null : limit, filter: filter); + + var list = await store.find(_database!, finder: finder); + return list.map((item) => constructorDelegate(cloneMap(item.value))).toList(); + } + + @override + Future getByKey(String fieldName, PKeyType key) async { + var store = _getTable; + var item = await store.findFirst(_database!, finder: Finder(filter: Filter.equals(fieldName, key))); + return item == null ? null : constructorDelegate(cloneMap(item.value)); + } + + @override + Future clear() { + var store = _getTable; + return store.delete(_database!); + } + + static Future get _localPath => getAppDbPath(); + + static Future _makeTransaction(TransactionDelegate action) async { + await _database!.transaction(action); + } + + static Future _launch() async { + String nameOfDataBase = 'ormDb'; + String path = await _localPath; + DatabaseFactory postFactory = appDataBaseFactory(); + return postFactory.openDatabase('$path/$nameOfDataBase'); + } + + static Database? _database; + + static Future init() async { + await _database?.close(); + _database = null; + _database = await _launch(); + } +} diff --git a/lib/core/orm/models/database/mobile.dart b/lib/core/orm/models/database/mobile.dart new file mode 100644 index 0000000..527f9d2 --- /dev/null +++ b/lib/core/orm/models/database/mobile.dart @@ -0,0 +1,11 @@ +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; + +DatabaseFactory appDataBaseFactory() { + return databaseFactoryIo; +} + +Future getAppDbPath() async { + return (await getApplicationDocumentsDirectory()).path; +} diff --git a/lib/core/orm/models/database/stub.dart b/lib/core/orm/models/database/stub.dart new file mode 100644 index 0000000..4014c1b --- /dev/null +++ b/lib/core/orm/models/database/stub.dart @@ -0,0 +1,10 @@ +import 'package:sembast/sembast.dart'; + +DatabaseFactory appDataBaseFactory() { + throw UnimplementedError('OrmDatabase has no factory for current platform'); +} + +Future getAppDbPath() { + throw UnimplementedError( + 'OrmDatabase has no database path for current platform'); +} diff --git a/lib/core/orm/models/database/web.dart b/lib/core/orm/models/database/web.dart new file mode 100644 index 0000000..d7a84a9 --- /dev/null +++ b/lib/core/orm/models/database/web.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/sembast_web.dart'; + +DatabaseFactory appDataBaseFactory() { + return databaseFactoryWeb; +} + +Future getAppDbPath() { + return SynchronousFuture('web_storage'); +} diff --git a/lib/core/orm/orm.dart b/lib/core/orm/orm.dart new file mode 100644 index 0000000..429a0d9 --- /dev/null +++ b/lib/core/orm/orm.dart @@ -0,0 +1,34 @@ +library orm; + +import 'package:sembast/timestamp.dart'; +import 'models/database/database.dart'; +export 'models/database/database.dart' hide kCreatedTimeStampField; +export 'package:sembast/sembast.dart' show Filter; + +abstract class Orm { + Map jSON; + String get primaryKeyField; + + PKeyType get primaryKey => jSON[primaryKeyField] as PKeyType; + + bool operator ==(other) { + return (other is Orm && other.hashCode == this.hashCode); + } + + int get hashCode => primaryKey.hashCode; + + @override + String toString() { + return jSON.toString(); + } + + Orm(this.jSON); +} + +mixin CopyAbleMixin { + T get copy; +} + +abstract class CreationTime implements Orm { + Timestamp? get createdAt => jSON[kCreatedTimeStampField]; +} diff --git a/lib/countryCodes.dart b/lib/countryCodes.dart new file mode 100644 index 0000000..477d100 --- /dev/null +++ b/lib/countryCodes.dart @@ -0,0 +1,244 @@ +const countryCodes = [ + {"country_name": "Turkmenistan", "dial_code": "+993"}, + {"country_name": "Afghanistan", "dial_code": "+93"}, + {"country_name": "Aland Islands", "dial_code": "+358"}, + {"country_name": "Albania", "dial_code": "+355"}, + {"country_name": "Algeria", "dial_code": "+213"}, + {"country_name": "AmericanSamoa", "dial_code": "+1684"}, + {"country_name": "Andorra", "dial_code": "+376"}, + {"country_name": "Angola", "dial_code": "+244"}, + {"country_name": "Anguilla", "dial_code": "+1264"}, + {"country_name": "Antarctica", "dial_code": "+672"}, + {"country_name": "Antigua and Barbuda", "dial_code": "+1268"}, + {"country_name": "Argentina", "dial_code": "+54"}, + {"country_name": "Armenia", "dial_code": "+374"}, + {"country_name": "Aruba", "dial_code": "+297"}, + {"country_name": "Australia", "dial_code": "+61"}, + {"country_name": "Austria", "dial_code": "+43"}, + {"country_name": "Azerbaijan", "dial_code": "+994"}, + {"country_name": "Bahamas", "dial_code": "+1242"}, + {"country_name": "Bahrain", "dial_code": "+973"}, + {"country_name": "Bangladesh", "dial_code": "+880"}, + {"country_name": "Barbados", "dial_code": "+1246"}, + {"country_name": "Belarus", "dial_code": "+375"}, + {"country_name": "Belgium", "dial_code": "+32"}, + {"country_name": "Belize", "dial_code": "+501"}, + {"country_name": "Benin", "dial_code": "+229"}, + {"country_name": "Bermuda", "dial_code": "+1441"}, + {"country_name": "Bhutan", "dial_code": "+975"}, + {"country_name": "Bolivia, Plurinational State of", "dial_code": "+591"}, + {"country_name": "Bosnia and Herzegovina", "dial_code": "+387"}, + {"country_name": "Botswana", "dial_code": "+267"}, + {"country_name": "Brazil", "dial_code": "+55"}, + {"country_name": "British Indian Ocean Territory", "dial_code": "+246"}, + {"country_name": "Brunei Darussalam", "dial_code": "+673"}, + {"country_name": "Bulgaria", "dial_code": "+359"}, + {"country_name": "Burkina Faso", "dial_code": "+226"}, + {"country_name": "Burundi", "dial_code": "+257"}, + {"country_name": "Cambodia", "dial_code": "+855"}, + {"country_name": "Cameroon", "dial_code": "+237"}, + {"country_name": "Canada", "dial_code": "+1"}, + {"country_name": "Cape Verde", "dial_code": "+238"}, + {"country_name": "Cayman Islands", "dial_code": "+ 345"}, + {"country_name": "Central African Republic", "dial_code": "+236"}, + {"country_name": "Chad", "dial_code": "+235"}, + {"country_name": "Chile", "dial_code": "+56"}, + {"country_name": "China", "dial_code": "+86"}, + {"country_name": "Christmas Island", "dial_code": "+61"}, + {"country_name": "Cocos (Keeling) Islands", "dial_code": "+61"}, + {"country_name": "Colombia", "dial_code": "+57"}, + {"country_name": "Comoros", "dial_code": "+269"}, + {"country_name": "Congo", "dial_code": "+242"}, + {"country_name": "Congo, The Democratic Republic of the Congo", "dial_code": "+243"}, + {"country_name": "Cook Islands", "dial_code": "+682"}, + {"country_name": "Costa Rica", "dial_code": "+506"}, + {"country_name": "Cote d'Ivoire", "dial_code": "+225"}, + {"country_name": "Croatia", "dial_code": "+385"}, + {"country_name": "Cuba", "dial_code": "+53"}, + {"country_name": "Cyprus", "dial_code": "+357"}, + {"country_name": "Czech Republic", "dial_code": "+420"}, + {"country_name": "Denmark", "dial_code": "+45"}, + {"country_name": "Djibouti", "dial_code": "+253"}, + {"country_name": "Dominica", "dial_code": "+1767"}, + {"country_name": "Dominican Republic", "dial_code": "+1849"}, + {"country_name": "Ecuador", "dial_code": "+593"}, + {"country_name": "Egypt", "dial_code": "+20"}, + {"country_name": "El Salvador", "dial_code": "+503"}, + {"country_name": "Equatorial Guinea", "dial_code": "+240"}, + {"country_name": "Eritrea", "dial_code": "+291"}, + {"country_name": "Estonia", "dial_code": "+372"}, + {"country_name": "Ethiopia", "dial_code": "+251"}, + {"country_name": "Falkland Islands (Malvinas)", "dial_code": "+500"}, + {"country_name": "Faroe Islands", "dial_code": "+298"}, + {"country_name": "Fiji", "dial_code": "+679"}, + {"country_name": "Finland", "dial_code": "+358"}, + {"country_name": "France", "dial_code": "+33"}, + {"country_name": "French Guiana", "dial_code": "+594"}, + {"country_name": "French Polynesia", "dial_code": "+689"}, + {"country_name": "Gabon", "dial_code": "+241"}, + {"country_name": "Gambia", "dial_code": "+220"}, + {"country_name": "Georgia", "dial_code": "+995"}, + {"country_name": "Germany", "dial_code": "+49"}, + {"country_name": "Ghana", "dial_code": "+233"}, + {"country_name": "Gibraltar", "dial_code": "+350"}, + {"country_name": "Greece", "dial_code": "+30"}, + {"country_name": "Greenland", "dial_code": "+299"}, + {"country_name": "Grenada", "dial_code": "+1473"}, + {"country_name": "Guadeloupe", "dial_code": "+590"}, + {"country_name": "Guam", "dial_code": "+1671"}, + {"country_name": "Guatemala", "dial_code": "+502"}, + {"country_name": "Guernsey", "dial_code": "+44"}, + {"country_name": "Guinea", "dial_code": "+224"}, + {"country_name": "Guinea-Bissau", "dial_code": "+245"}, + {"country_name": "Guyana", "dial_code": "+595"}, + {"country_name": "Haiti", "dial_code": "+509"}, + {"country_name": "Holy See (Vatican City State)", "dial_code": "+379"}, + {"country_name": "Honduras", "dial_code": "+504"}, + {"country_name": "Hong Kong", "dial_code": "+852"}, + {"country_name": "Hungary", "dial_code": "+36"}, + {"country_name": "Iceland", "dial_code": "+354"}, + {"country_name": "India", "dial_code": "+91"}, + {"country_name": "Indonesia", "dial_code": "+62"}, + {"country_name": "Iran, Islamic Republic of Persian Gulf", "dial_code": "+98"}, + {"country_name": "Iraq", "dial_code": "+964"}, + {"country_name": "Ireland", "dial_code": "+353"}, + {"country_name": "Isle of Man", "dial_code": "+44"}, + {"country_name": "Israel", "dial_code": "+972"}, + {"country_name": "Italy", "dial_code": "+39"}, + {"country_name": "Jamaica", "dial_code": "+1876"}, + {"country_name": "Japan", "dial_code": "+81"}, + {"country_name": "Jersey", "dial_code": "+44"}, + {"country_name": "Jordan", "dial_code": "+962"}, + {"country_name": "Kazakhstan", "dial_code": "+77"}, + {"country_name": "Kenya", "dial_code": "+254"}, + {"country_name": "Kiribati", "dial_code": "+686"}, + {"country_name": "Korea, Democratic People's Republic of Korea", "dial_code": "+850"}, + {"country_name": "Korea, Republic of South Korea", "dial_code": "+82"}, + {"country_name": "Kuwait", "dial_code": "+965"}, + {"country_name": "Kyrgyzstan", "dial_code": "+996"}, + {"country_name": "Laos", "dial_code": "+856"}, + {"country_name": "Latvia", "dial_code": "+371"}, + {"country_name": "Lebanon", "dial_code": "+961"}, + {"country_name": "Lesotho", "dial_code": "+266"}, + {"country_name": "Liberia", "dial_code": "+231"}, + {"country_name": "Libyan Arab Jamahiriya", "dial_code": "+218"}, + {"country_name": "Liechtenstein", "dial_code": "+423"}, + {"country_name": "Lithuania", "dial_code": "+370"}, + {"country_name": "Luxembourg", "dial_code": "+352"}, + {"country_name": "Macao", "dial_code": "+853"}, + {"country_name": "Macedonia", "dial_code": "+389"}, + {"country_name": "Madagascar", "dial_code": "+261"}, + {"country_name": "Malawi", "dial_code": "+265"}, + {"country_name": "Malaysia", "dial_code": "+60"}, + {"country_name": "Maldives", "dial_code": "+960"}, + {"country_name": "Mali", "dial_code": "+223"}, + {"country_name": "Malta", "dial_code": "+356"}, + {"country_name": "Marshall Islands", "dial_code": "+692"}, + {"country_name": "Martinique", "dial_code": "+596"}, + {"country_name": "Mauritania", "dial_code": "+222"}, + {"country_name": "Mauritius", "dial_code": "+230"}, + {"country_name": "Mayotte", "dial_code": "+262"}, + {"country_name": "Mexico", "dial_code": "+52"}, + {"country_name": "Micronesia, Federated States of Micronesia", "dial_code": "+691"}, + {"country_name": "Moldova", "dial_code": "+373"}, + {"country_name": "Monaco", "dial_code": "+377"}, + {"country_name": "Mongolia", "dial_code": "+976"}, + {"country_name": "Montenegro", "dial_code": "+382"}, + {"country_name": "Montserrat", "dial_code": "+1664"}, + {"country_name": "Morocco", "dial_code": "+212"}, + {"country_name": "Mozambique", "dial_code": "+258"}, + {"country_name": "Myanmar", "dial_code": "+95"}, + {"country_name": "Namibia", "dial_code": "+264"}, + {"country_name": "Nauru", "dial_code": "+674"}, + {"country_name": "Nepal", "dial_code": "+977"}, + {"country_name": "Netherlands", "dial_code": "+31"}, + {"country_name": "Netherlands Antilles", "dial_code": "+599"}, + {"country_name": "New Caledonia", "dial_code": "+687"}, + {"country_name": "New Zealand", "dial_code": "+64"}, + {"country_name": "Nicaragua", "dial_code": "+505"}, + {"country_name": "Niger", "dial_code": "+227"}, + {"country_name": "Nigeria", "dial_code": "+234"}, + {"country_name": "Niue", "dial_code": "+683"}, + {"country_name": "Norfolk Island", "dial_code": "+672"}, + {"country_name": "Northern Mariana Islands", "dial_code": "+1670"}, + {"country_name": "Norway", "dial_code": "+47"}, + {"country_name": "Oman", "dial_code": "+968"}, + {"country_name": "Pakistan", "dial_code": "+92"}, + {"country_name": "Palau", "dial_code": "+680"}, + {"country_name": "Palestinian Territory, Occupied", "dial_code": "+970"}, + {"country_name": "Panama", "dial_code": "+507"}, + {"country_name": "Papua New Guinea", "dial_code": "+675"}, + {"country_name": "Paraguay", "dial_code": "+595"}, + {"country_name": "Peru", "dial_code": "+51"}, + {"country_name": "Philippines", "dial_code": "+63"}, + {"country_name": "Pitcairn", "dial_code": "+872"}, + {"country_name": "Poland", "dial_code": "+48"}, + {"country_name": "Portugal", "dial_code": "+351"}, + {"country_name": "Puerto Rico", "dial_code": "+1939"}, + {"country_name": "Qatar", "dial_code": "+974"}, + {"country_name": "Romania", "dial_code": "+40"}, + {"country_name": "Russia", "dial_code": "+7"}, + {"country_name": "Rwanda", "dial_code": "+250"}, + {"country_name": "Reunion", "dial_code": "+262"}, + {"country_name": "Saint Barthelemy", "dial_code": "+590"}, + {"country_name": "Saint Helena, Ascension and Tristan Da Cunha", "dial_code": "+290"}, + {"country_name": "Saint Kitts and Nevis", "dial_code": "+1869"}, + {"country_name": "Saint Lucia", "dial_code": "+1758"}, + {"country_name": "Saint Martin", "dial_code": "+590"}, + {"country_name": "Saint Pierre and Miquelon", "dial_code": "+508"}, + {"country_name": "Saint Vincent and the Grenadines", "dial_code": "+1784"}, + {"country_name": "Samoa", "dial_code": "+685"}, + {"country_name": "San Marino", "dial_code": "+378"}, + {"country_name": "Sao Tome and Principe", "dial_code": "+239"}, + {"country_name": "Saudi Arabia", "dial_code": "+966"}, + {"country_name": "Senegal", "dial_code": "+221"}, + {"country_name": "Serbia", "dial_code": "+381"}, + {"country_name": "Seychelles", "dial_code": "+248"}, + {"country_name": "Sierra Leone", "dial_code": "+232"}, + {"country_name": "Singapore", "dial_code": "+65"}, + {"country_name": "Slovakia", "dial_code": "+421"}, + {"country_name": "Slovenia", "dial_code": "+386"}, + {"country_name": "Solomon Islands", "dial_code": "+677"}, + {"country_name": "Somalia", "dial_code": "+252"}, + {"country_name": "South Africa", "dial_code": "+27"}, + {"country_name": "South Sudan", "dial_code": "+211"}, + {"country_name": "South Georgia and the South Sandwich Islands", "dial_code": "+500"}, + {"country_name": "Spain", "dial_code": "+34"}, + {"country_name": "Sri Lanka", "dial_code": "+94"}, + {"country_name": "Sudan", "dial_code": "+249"}, + {"country_name": "Suriname", "dial_code": "+597"}, + {"country_name": "Svalbard and Jan Mayen", "dial_code": "+47"}, + {"country_name": "Swaziland", "dial_code": "+268"}, + {"country_name": "Sweden", "dial_code": "+46"}, + {"country_name": "Switzerland", "dial_code": "+41"}, + {"country_name": "Syrian Arab Republic", "dial_code": "+963"}, + {"country_name": "Taiwan", "dial_code": "+886"}, + {"country_name": "Tajikistan", "dial_code": "+992"}, + {"country_name": "Tanzania, United Republic of Tanzania", "dial_code": "+255"}, + {"country_name": "Thailand", "dial_code": "+66"}, + {"country_name": "Timor-Leste", "dial_code": "+670"}, + {"country_name": "Togo", "dial_code": "+228"}, + {"country_name": "Tokelau", "dial_code": "+690"}, + {"country_name": "Tonga", "dial_code": "+676"}, + {"country_name": "Trinidad and Tobago", "dial_code": "+1868"}, + {"country_name": "Tunisia", "dial_code": "+216"}, + {"country_name": "Turkey", "dial_code": "+90"}, + {"country_name": "Turks and Caicos Islands", "dial_code": "+1649"}, + {"country_name": "Tuvalu", "dial_code": "+688"}, + {"country_name": "Uganda", "dial_code": "+256"}, + {"country_name": "Ukraine", "dial_code": "+380"}, + {"country_name": "United Arab Emirates", "dial_code": "+971"}, + {"country_name": "United Kingdom", "dial_code": "+44"}, + {"country_name": "United States", "dial_code": "+1"}, + {"country_name": "Uruguay", "dial_code": "+598"}, + {"country_name": "Uzbekistan", "dial_code": "+998"}, + {"country_name": "Vanuatu", "dial_code": "+678"}, + {"country_name": "Venezuela, Bolivarian Republic of Venezuela", "dial_code": "+58"}, + {"country_name": "Vietnam", "dial_code": "+84"}, + {"country_name": "Virgin Islands, British", "dial_code": "+1284"}, + {"country_name": "Virgin Islands, U.S.", "dial_code": "+1340"}, + {"country_name": "Wallis and Futuna", "dial_code": "+681"}, + {"country_name": "Yemen", "dial_code": "+967"}, + {"country_name": "Zambia", "dial_code": "+260"}, + {"country_name": "Zimbabwe", "dial_code": "+263"} +]; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..a45dd74 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,191 @@ +{ + "yourMessage": " Your message...", + "similarProducts": "Similar products", + "nsm": "NSM", + "buyNow": "Buy Now", + "uploadReceipt": "Upload Receipt", + "payOnline": "Pay Online", + "sendMessage": "Send message", + "dataIsUpdated": "Data is updated", + "personalCabinet": "Personal cabinet", + "addPost": "Add Post", + "iu_company": "User company", + "iu_about": "User about info", + "phone_error": "Enter a valid phone number", + "email_error": "Enter a valid email address", + "username": "Username", + "noAccount": "No Account?", + "yourPassword": "Your password", + "yourLogin": "Your login", + "yourEmail": "Your email address", + "link": "link", + "loginText": "If you have forgotten your username or password, please restore it from the @ .", + "expiryDate": "Expiry Date", + "sellerContact": "Seller contact", + "address": "Address", + "realAddress": "744000, Turkmenistan Ashgabat city, Archabil avenue 52", + "mail": "E-mail", + "telephone": "Telephone", + "lotNumber": "Lot", + "productNumber": "Product number", + "productMark": "Product Mark", + "productAmount": "Product amount", + "startingPrice": "Starting price", + "producer": "Producer", + "producerCountry": "Country of origin", + "description": "Description", + "unit": "Unit", + "paymentTerms": "Terms of payment", + "deliveryTerms": "Terms of delivery", + "station": "Station", + "packing": "Packing", + "english": "English", + "russian": "Russian", + "turkmen": "Turkmen", + "localeLabel": "English", + "backendCode": "en", + "save_changes": "Save changes", + "email": "Email address", + "message": "Messages", + "top_up_history": "Top-up history", + "personal_data": "Personal data", + "news_feed": "News", + "feedback": "Feedback", + "privacy_policy": "Privacy Policy", + "contact": "Contact details", + "home": "Home", + "category": "Categories", + "favourites": "Wishlist", + "settings": "Settings", + "login": "Login", + "phone": "Phone number", + "password": "Password", + "or": "or", + "register": "Register", + "first_name": "Name", + "last_name": "Surname", + "lang": "Language", + "phoneValidator": "Enter the phone correctly", + "empty": "The field must not be empty", + "forgot": "Forgot Password?", + "search": "Search goods by a keyword", + "cart": "Cart", + "addToCart": "Add to Cart", + "addedToCart": "Added to Cart!", + "removedFromCart": "Removed from Cart!", + "inCart": "Already in Cart", + "allPrice": "Total price", + "wantDelete": "Do you really want to delete the item?", + "hit": "Top goods", + "newGoods": "New products", + "emptyPosts": "No data", + "order": "Create an order", + "account": "Account", + "otherException": "Error occured, try later", + "password_confirmation": "Confirm password", + "password_confirmation_error": "Passwords do not match", + "profile": "Profile", + "unauthenticated": "Your session is exprired, please re-login", + "needToEnter": "Please enter your account", + "logout": "Log out", + "logging_out": "Logging out...", + "sure_log_out": "Do you really want to log out?", + "date": "Date", + "status": "Status", + "products": "Products", + "smsCodeSent": "Sms code is sent to the phone", + "codeLabel": "Code", + "passwordError": "Password must contain at least 6 symbols", + "verify": "Verification", + "sendingCode": "Sending code...", + "sendCode": "Send again", + "rules": "the User Agreement", + "agree": "If you agree with conditions of @ click 'CONTINUE'", + "about": "About us", + "quantitySumm": "Summ", + "enterQuantity": "Enter quantity", + "formatError": "Invalid format", + "tapToLoadMore": "Load more", + "similiarProducts": "Related products", + "dialCode": "Dial code", + "verificationMailSent": "Please, check your email. Follow the verification link we've sent.", + "verifyMail": "Verify your email", + "verifyPhone": "Verify your phon number", + "sms_code": "Sms code", + "check": "Verify", + "myProducts": "My products", + "firstStep": "Step 1", + "addPosts": "Add product", + "addPostName": "Title", + "mark": "Mark", + "manufacturer": "Manufacturer", + "marketType": "Market type", + "country": "Country", + "inMarket": "Internal", + "outMarket": "External", + "addPostDescription": "Description", + "price": "Price", + "place": "Place", + "quantity": "Quantity", + "measure": "Measure", + "currency": "Currency", + "paymentTerm": "Payment term", + "deliveryTerm": "Delivery term", + "secondStep": "Step 2", + "packaging": "Packaging", + "yes": "Yes", + "no": "No", + "notANumber": "Not a number", + "selectImages": "Select images", + "localImages": "New images", + "selectAtLeastImage": "Select at least 1 image", + "statusNote": "Notes", + "draft": "Draft", + "approved": "Approved", + "denied": "Denied", + "published": "Sent", + "none": "Unknown", + "edit": "Edit", + "approve": "Send for revision", + "loadedImages": "Uploaded images", + "legalizationNumber": "Legalization Number", + "newPassword": "New password", + "newPasswordConfirmation": "Confirm new password", + "phoneConfirmation": "Confirmation", + "smsSentInfo": "An SMS with a code will be sent to the number you indicated, enter it below", + "enterSmsCode": "Enter sms code", + "confirm": "Confirm", + "verifyPhoneWarning": "Confirm your phone number by clicking on the button", + "topUpBalance": "Top up balance", + "selectPaymentMethod": "Select payment method", + "bankTransfer": "Bank transfer", + "selectFile": "Select file", + "upload": "Upload", + "fileNotSelected": "File not selected", + "send": "Send", + "reminderInfo": "Before sending, make sure that the transfer was made correctly and you have a receipt", + "taxCode": "Tax code:", + "manatAccount": "Manat account:", + "corrAccount": "Correspondent account:", + "bankAddress": "Bank address:", + "selectBank": "Select bank", + "transferAmount": "Transfer amount", + "enterAmount": "enter amount", + "searchShort": "Search", + "newsNotFount": "News not found", + "errorOccurred": "Error occurred", + "registeredDate": "Registered date", + "filters": "Filters", + "selectCategory": "Select category", + "selectUnit": "Select unit", + "selectCurrency": "Select currency", + "selectPayment": "Select payment", + "selectSendType": "Select send type", + "selectCountry": "Select country", + "clear": "Clear", + "apply": "Apply", + "importPrice": "Import prices", + "quotes": "Quotes", + "notFound": "Not found", + "verifyEmailWarning": "Press icon to confirm email" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..3cb5bbd --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,191 @@ +{ + "yourMessage": " Ваше сообщение...", + "similarProducts": "Похожие товары", + "nsm": "ФИО", + "buyNow": "Купить сейчас", + "uploadReceipt": "Загрузить квитанцию", + "payOnline": "Оплатить онлайн", + "sendMessage": "Отправить оообщение", + "dataIsUpdated": "Данные обновлены", + "personalCabinet": "Персональные данные", + "addPost": "Добавить объявления", + "iu_company": "Компания-пользователь", + "iu_about": "Информация о пользователе", + "phone_error": "Введите действующий номер телефона", + "email_error": "Введите корректный электронный адрес", + "username": "Имя пользователя", + "noAccount": "Нет аккаунта?", + "yourPassword": "Ваш пароль", + "yourLogin": "Ваш логин", + "yourEmail": "Ваш электронный адрес", + "link": "ссылке", + "loginText": "Если вы забыли свой логин или пароль, просим вас восстановить его по @ .", + "expiryDate": "Дата окончания", + "sellerContact": "Контакты продавца", + "address": "Адрес", + "realAddress": "744000, Туркменистан г.Ашгабат, Арчабиль шаелы 52", + "mail": "Почта", + "telephone": "Телефон", + "lotNumber": "Лот:", + "productNumber": "Номер товара", + "productMark": "Марка товара", + "productAmount": "Количество товара", + "startingPrice": "Стартовая цена за единицу", + "producer": "Производитель", + "producerCountry": "Страна производителя", + "description": "Описание", + "unit": "Единица измерения", + "paymentTerms": "Условия оплаты", + "deliveryTerms": "Условия поставки", + "station": "Пункт", + "packing": "Упаковка", + "english": "Английский", + "russian": "Русский", + "turkmen": "Туркменский", + "localeLabel": "Русский", + "save_changes": "Сохранить изменения", + "backendCode": "ru", + "email": "Электронный адрес", + "message": "Сообщения", + "top_up_history": "История пополнений", + "personal_data": "Персональные данные", + "news_feed": "Новости", + "feedback": "Обратная связь", + "privacy_policy": "Политика конфиденциальности", + "contact": "Контакты", + "home": "Главная", + "category": "Категории", + "favourites": "Желания", + "settings": "Настройки", + "login": "Войти", + "phone": "Номер телефона", + "password": "Пароль", + "or": "или", + "register": "Создать", + "first_name": "Имя", + "last_name": "Фамилия", + "lang": "Язык приложения", + "phoneValidator": "Введите телефон правильно", + "empty": "Поле не должно быть пустым", + "forgot": "Забыли пароль?", + "search": "Поиск по названию товара", + "cart": "Корзина", + "addToCart": "Добавить в корзину", + "addedToCart": "Добавлено в корзину!", + "removedFromCart": "Удалено из корзины!", + "inCart": "Уже в корзине", + "allPrice": "Общая сумма", + "wantDelete": "Вы действительно хотите убрать товар из корзины?", + "hit": "Хиты продаж", + "newGoods": "Новые товары", + "emptyPosts": "Пусто", + "order": "Оформить заказ", + "account": "Аккаунт", + "otherException": "Возникла ошибка, попробуйте заново", + "password_confirmation": "Подтвердите пароль", + "password_confirmation_error": "Пароли не совпадают", + "profile": "Кабинет", + "unauthenticated": "Ваша сессия истекла, войдите заново в аккаунт", + "needToEnter": "Войдите в свой аккаунт", + "logout": "Выйти", + "logging_out": "Выходим из системы...", + "sure_log_out": "Вы действительно хотите выйти?", + "date": "Дата", + "status": "Статус", + "products": "Товары", + "smsCodeSent": "Код отправлен на ваш номер телефона", + "codeLabel": "Код", + "passwordError": "Пароль должен содержать не менее 6 символов", + "verify": "Верификация", + "sendingCode": "Отправка кода...", + "sendCode": "Отправить заново", + "rules": "Пользовательского соглашения", + "agree": "Если вы согласны с условиями @, нажмите 'ПРОДОЛЖИТЬ'", + "about": "О нас", + "quantitySumm": "Сумма", + "enterQuantity": "Введите сумму", + "formatError": "Неверный формат", + "tapToLoadMore": "Загрузить ещё", + "similiarProducts": "Рекомендуемые товары", + "dialCode": "Телефонный код", + "verificationMailSent": "Пожалуйста, проверьте вашу электронную почту. Мы отправили вам письмо со ссылкой на подтверждение", + "verifyMail": "Подтвердить почту", + "verifyPhone": "Подтвердить номер телефона", + "sms_code": "Код подтверждения", + "check": "Подтвердить", + "myProducts": "Мои товары", + "firstStep": "Шаг 1", + "addPosts": "Добавить товар", + "addPostName": "Название", + "mark": "Марка", + "manufacturer": "Изготовитель", + "marketType": "Тип рынка", + "country": "Страна", + "inMarket": "Внутренний", + "outMarket": "Внешний", + "addPostDescription": "Описание", + "price": "Цена", + "place": "Место", + "quantity": "Количество", + "measure": "Мера измерения", + "currency": "Валюта", + "paymentTerm": "Метод оплаты", + "deliveryTerm": "Метод доставки", + "secondStep": "Шаг 2", + "packaging": "Упаковка", + "yes": "Да", + "no": "Нет", + "notANumber": "Введите число", + "selectImages": "Выбрать картинки", + "localImages": "Новые картинки", + "selectAtLeastImage": "Выберите хотя бы 1 картинку", + "statusNote": "Примечания", + "draft": "Черновик", + "approved": "Одобрен", + "denied": "Отклонен", + "published": "Отправлен", + "none": "Неизвестно", + "edit": "Изменить", + "approve": "Отправить на проверку", + "loadedImages": "Загруженные картинки", + "legalizationNumber": "Номер легализации", + "newPassword": "Новый пароль", + "newPasswordConfirmation": "Подтвердите новый пароль", + "phoneConfirmation": "Подтверждение", + "smsSentInfo": "На номер который Вы указали придет смс с кодом, введите его ниже", + "enterSmsCode": "Введите код", + "confirm": "Подтвердить", + "verifyPhoneWarning": "Подтвердите Ваш номер телефона, нажав на кнопку", + "topUpBalance": "Пополнить баланс", + "selectPaymentMethod": "Выберите метод оплаты", + "bankTransfer": "Банковский перевод", + "selectFile": "Выбрать файл", + "upload": "Загрузить", + "fileNotSelected": "Ничего не выбранно", + "send": "Отправить", + "reminderInfo": "Перед отправкой убедитесь что перевод был осуществлен верно и у вас имеется чек", + "taxCode": "Налоговый код:", + "manatAccount": "Манатный счёт:", + "corrAccount": "Корреспондент счет:", + "bankAddress": "Адрес банка:", + "selectBank": "Выберите банк", + "transferAmount": "Сумма перевода", + "enterAmount": "введите сумму", + "searchShort": "Поиск", + "newsNotFount": "Новости не найдены", + "errorOccurred": "Возникла ошибка", + "registeredDate": "Дата регистрации", + "filters": "Фильтры", + "selectCategory": "Выбрать категорию", + "selectUnit": "Выберите единицу измерения", + "selectCurrency": "Выберите валюту", + "selectPayment": "Выберите платеж", + "selectSendType": "Выберите тип отправки", + "selectCountry": "Выберите страну", + "clear": "Очистить", + "apply": "Применить", + "importPrice": "Импортные цены", + "quotes": "Котировки", + "notFound": "Не найден", + "verifyEmailWarning": "Нажмите значок, чтобы подтвердить адрес электронной почты" +} \ No newline at end of file diff --git a/lib/l10n/app_tk.arb b/lib/l10n/app_tk.arb new file mode 100644 index 0000000..d184852 --- /dev/null +++ b/lib/l10n/app_tk.arb @@ -0,0 +1,191 @@ +{ + "yourMessage": " Siziň hatyňyz...", + "similarProducts": "Meňzeş harytlar", + "nsm": "AFA", + "buyNow": "Satyn almak", + "uploadReceipt": "Kwitansiýaňyzy ýükläň", + "payOnline": "Online töleg", + "sendMessage": "Ugratmak", + "dataIsUpdated": "Maglumatlar täzelendi", + "personalCabinet": "Şahsy otag", + "addPost": "Bildiriş goşmak", + "iu_company": "Ulanyjy kompaniýasy", + "iu_about": "Ulanyjy barada maglumat", + "phone_error": "Dogry telefon belgisini giriziň", + "email_error": "Dogry e-poçta salgysyny giriziň", + "username": "Ulanyjy ady", + "noAccount": "Akkaundyňyz ýokmy?", + "yourPassword": "Açar sözüňiz", + "yourLogin": "Siziň loginiňiz", + "yourEmail": "Siziň elektron salgyňyz", + "link": "salgy boýunça", + "loginText": "Ulanyjy adyňyzy ýa-da parolyňyzy ýatdan çykaran bolsaňyz, şu @ dikeltmegiňizi haýyş edýäris", + "expiryDate": "Möhleti", + "sellerContact": "Habarlaşmak üçin", + "address": "Adres", + "realAddress": "744000, Türkmenistan ş.Aşgabat, Arçabil şaýoly 52", + "mail": "E-mail", + "telephone": "Telefon", + "lotNumber": "Lot:", + "productNumber": "Harydyň belgisi", + "productMark": "Harydyň markasy", + "productAmount": "Harydyň möçberi", + "startingPrice": "Başlangyç bahasy", + "producer": "Öndüriji", + "producerCountry": "Öndürilen ýurdy", + "description": "Beýany", + "unit": "Birligi", + "paymentTerms": "Töleg şertleri", + "deliveryTerms": "Eltip bermegiň şertleri", + "station": "Ýeri", + "packing": "Gaplamak", + "english": "Iňlis", + "russian": "Rus", + "turkmen": "Türkmen", + "localeLabel": "Türkmen", + "save_changes": "Ýatda sakla", + "backendCode": "tm", + "email": "Elektron salgysy", + "message": "Hatlarym", + "top_up_history": "Pul geçirimleriň taryhy", + "personal_data": "Şahsy maglumat", + "news_feed": "Habarlar", + "feedback": "Hat ýazmak", + "privacy_policy": "Gizlinlik syýasaty", + "contact": "Habarlaşmak üçin", + "home": "Baş sahypa", + "category": "Kategoriýa", + "favourites": "Islegler", + "settings": "Sazlama", + "login": "Girmek", + "phone": "Telefon nomer", + "password": "Parol", + "or": "ýa-da", + "register": "Döretmek", + "first_name": "Adyňyz", + "last_name": "Familiýaňyz", + "lang": "Dili", + "phoneValidator": "Telefony dogry giriziň", + "empty": "Boş durmaly däl", + "forgot": "Paroly unutdyňyzmy?", + "search": "Harydy adyndan gözle", + "cart": "Sebet", + "addToCart": "Sebede goş", + "addedToCart": "Sebede goşuldy!", + "removedFromCart": "Sebetden pozuldy!", + "inCart": "Sebetde", + "allPrice": "Umumy bahasy", + "wantDelete": "Harydy çyndanam sebetden pozmak isleýärsiňizmi?", + "hit": "Iň köp satylan", + "newGoods": "Täze harytlar", + "emptyPosts": "Maglumat ýok", + "order": "Sargydy jemlemek", + "account": "Akkaunt", + "otherException": "Ýalnyş döredi, täzeden barlap görüň", + "password_confirmation": "Paroly gaýtalaň", + "password_confirmation_error": "Parollar gabat gelenok", + "profile": "Profil", + "unauthenticated": "Siziň sessiýaňyz gutardy, täzeden giriň", + "needToEnter": "Akkaundyňyza giriň", + "logout": "Ulgamdan çykmak", + "logging_out": "Ulgamdan çykýarys...", + "sure_log_out": "Ulgamdan çyndanam çykmakçy my?", + "date": "Senesi", + "status": "Status", + "products": "Harytlar", + "smsCodeSent": "Telefon nomeryňyza iberilen kody giriziň", + "codeLabel": "Kod", + "passwordError": "Parol azyndan 6 belgiden ybarat bolmaly", + "verify": "Sms barlag", + "sendingCode": "Kody iberýäris...", + "sendCode": "Täzeden iber", + "rules": "Ulanyjy şertnamasy", + "agree": "Eger @ bilen razy bolsaňyz, dowam ediň", + "about": "Biz barada", + "quantitySumm": "Jemi", + "enterQuantity": "Doldurmaly jemi", + "formatError": "Nädogry format", + "tapToLoadMore": "Ýene görkez", + "similiarProducts": "Bagly harytlar", + "dialCode": "Telefon kody", + "verificationMailSent": "Siziň e-mail salgyňyza hat ugradyldy. Hatyň içindäki ugradyjyny yzarlamagyňyzy haýyş edýäris.", + "verifyMail": "E-mail tassyklamak", + "verifyPhone": "Telefon belgini tassyklamak", + "sms_code": "Sms kod", + "check": "Barla", + "myProducts": "Harytlarym", + "firstStep": "Ädim 1", + "addPosts": "Harydy goşmak", + "addPostName": "Harydyň ady", + "mark": "Markasy", + "manufacturer": "Öndüriji", + "marketType": "Bazaryň görnüşi", + "country": "Ýurdy", + "inMarket": "Içki", + "outMarket": "Daşky", + "addPostDescription": "Mazmuny", + "price": "Baha", + "place": "Ýeri", + "quantity": "Mukdary", + "measure": "Ölçegi", + "currency": "Walýuta", + "paymentTerm": "Töleg usuly", + "deliveryTerm": "Eltmek usuly", + "secondStep": "Ädim 2", + "packaging": "Gaplamak", + "yes": "Hawa", + "no": "Ýok", + "notANumber": "Sany giriziň", + "selectImages": "Suratlary saýla", + "localImages": "Täze suratlar", + "selectAtLeastImage": "Azyndan 1 surat saýlaň", + "statusNote": "Bellikler", + "draft": "Garalama", + "approved": "Tassyklanan", + "denied": "Inkär edilen", + "published": "Ugradyldy", + "none": "Unknown", + "edit": "Üýtget", + "approve": "Barlaga ugrat", + "loadedImages": "Ýüklenen suratlar", + "legalizationNumber": "Kanunylaşdyrmagyň belgisi", + "newPassword": "Täze açar sözi", + "newPasswordConfirmation": "Täze açar sözüni tassyklaň", + "phoneConfirmation": "Tassyklaň", + "smsSentInfo": "Görkezilen belgä SMS kody iberiler. SMS koduny giriziň", + "enterSmsCode": "Kody giriziň", + "confirm": "Tassykla", + "verifyPhoneWarning": "Düwmä basyp, telefon belgiňizi tassyklaň", + "topUpBalance": "Hasaby dolduryň", + "selectPaymentMethod": "Töleg usulyny saýlaň", + "bankTransfer": "Bank arkaly töleg", + "selectFile": "Faýl saýlaň", + "upload": "Ýükle", + "fileNotSelected": "Faýl saýlanmady", + "send": "Ugrat", + "reminderInfo": "Ibermezden ozal, geçirişiň dogry edilendigine we kwitansiýaňyzyň bardygyna göz ýetiriň", + "taxCode": "Nalog kody:", + "manatAccount": "Manat hasaby:", + "corrAccount": "Habarçy hasaby:", + "bankAddress": "Bankyň adresi:", + "selectBank": "Banky saýlaň", + "transferAmount": "Pul geçirimiň mukdary", + "enterAmount": "mukdary girziň", + "searchShort": "Gözleg", + "newsNotFount": "Habar tapylmady", + "errorOccurred": "Näsazlyk ýüze çykdy", + "registeredDate": "Hasaba alnan senesi", + "filters": "Süzgüçler", + "selectCategory": "Kategoriýany saýlaň", + "selectUnit": "Birlik saýlaň", + "selectCurrency": "Walýuta saýlaň", + "selectPayment": "Töleg görnüşini saýlaň", + "selectSendType": "Ibermegiň görnüşini saýlaň", + "selectCountry": "Ýurt saýlaň", + "clear": "Arassala", + "apply": "Ýerine ýetir", + "importPrice": "Import bahalary", + "quotes": "Kotirowkalar", + "notFound": "Tapylmady", + "verifyEmailWarning": "Düwmä basyp, e-poçtaňyzy tassyklaň" +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..b4477f2 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,73 @@ +import 'package:birzha/models/products/composableProduct.dart'; +import 'package:birzha/models/settings/theme.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/primal.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'core/adaptix/adaptix.dart'; + +//homeCateogryId: -1 +//relatedProductsCategory id: -2 +//masterCategoey id: -3 +//transactionsSerializer id: -4 +//chatroomSerializer id: -5 +//search category id: -6 +//my products category id: -7 + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); // flutter packet loaded + final prefs = await SharedPreferences.getInstance(); // loads memory + await SettingsModel.initLocalization(prefs.getString('language') ?? kDefaultLanguage); + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => SettingsModel(prefs)), + ChangeNotifierProvider(create: (_) => AppUserManager(prefs)), + ChangeNotifierProvider.value(value: ComposableProduct()) + ], + child: MyApp(), + )); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + late Locale _locale; + + void setLocale(Locale locale) { + setState(() { + _locale = _locale; + }); + } + + @override + Widget build(BuildContext context) { + return SizeInitializer( + builder: (context) => GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: AppTheme.appLightTheme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + AppLocalizations.delegate, + ], + locale: Locale('ru', ''), + supportedLocales: [ + Locale('en', ''), + Locale('ru', ''), + Locale('tk', ''), + ], + home: Primal(), + ), + ); + } +} diff --git a/lib/models/attributes/attribute.dart b/lib/models/attributes/attribute.dart new file mode 100644 index 0000000..0b1bf85 --- /dev/null +++ b/lib/models/attributes/attribute.dart @@ -0,0 +1,72 @@ +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:birzha/services/validator.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:birzha/core/orm/orm.dart'; + +abstract class AttributeSelectableDelegate { + T? get groupValue; + Future dialogBuilder(BuildContext context); +} + +class SingleOptionSelectableDelegate extends AttributeSelectableDelegate { + SingleOptionSelectableDelegate(this.groupValue, this.options); + + @override + Future dialogBuilder(BuildContext context) { + return attributeSelector(context, groupValue, options); + } + + @override + final AttributeWithValueNameMixin? groupValue; + + final List options; +} + +mixin AttributeLabelMixin { + String get label; + String toQueryString(); +} + +mixin OptionedAttributeMixin on KeyIndexedAttributeMixin { + List get options; +} + +mixin SelectableAttributeMixin on KeyIndexedAttributeMixin { + AttributeSelectableDelegate selectDelegate(V? groupValue); + + TextInputMetaData metaData({required V? groupValue, required void Function(V?) onSelected}) { + return TextInputMetaData( + name: label, + label: label, + pickerMode: (context) async { + var option = await selectDelegate(groupValue).dialogBuilder(context); + onSelected(option); + if (option is AttributeWithValueNameMixin) { + return option.value.toString(); + } else if (option != null) { + return option.toString(); + } + }, + validation: Validation(conditions: [(inp) => inp.isEmpty ? 'empty'.translation : null]), + key: key); + } +} + +mixin KeyIndexedAttributeMixin on PostAttribute { + String get key; +} + +mixin AttributeWithValueNameMixin on PostAttribute { + String get value; +} + +abstract class PostAttribute extends Orm with AttributeLabelMixin { + PostAttribute(Map jSON) : super(jSON); + + @override + String toQueryString() { + return primaryKey.toString(); + } +} diff --git a/lib/models/attributes/category.dart b/lib/models/attributes/category.dart new file mode 100644 index 0000000..4bb10f7 --- /dev/null +++ b/lib/models/attributes/category.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/categories/products/pure.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; + +class CategoryOption extends PostAttribute with AttributeWithValueNameMixin { + final ProductsPureCategory category; + + CategoryOption._(this.category) : super(category.jSON); + + @override + String get label => category.name; + + @override + String get primaryKeyField => 'id'; + + @override + String get value => label; +} + +class CategoryAttribute extends PostAttribute + with KeyIndexedAttributeMixin, SelectableAttributeMixin, OptionedAttributeMixin { + CategoryAttribute._(this.options) : super({'key': 'category_id'}); + + static Future getAttribute() async { + var categories = await FutureGetList(baseUrl(path: kApiPath + '/categories', queryParameters: {}), parser: (response) { + final map = jsonDecode(response.body); + final products = map?['data'] as List; + return products.map((jsonData) => ProductsPureCategory(data: jsonData)).toList(); + }).fetch(); + return CategoryAttribute._(categories.map((e) => CategoryOption._(e)).toList()); + } + + @override + String get key => 'category_id'; + + @override + String get label => 'category'.translation; + + @override + String get primaryKeyField => 'key'; + + @override + selectDelegate(groupValue) { + return SingleOptionSelectableDelegate(groupValue, options); + } + + @override + final List options; +} diff --git a/lib/models/attributes/market_type.dart b/lib/models/attributes/market_type.dart new file mode 100644 index 0000000..bab2dc2 --- /dev/null +++ b/lib/models/attributes/market_type.dart @@ -0,0 +1,66 @@ +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +enum MarketType { inMarket, outMarket } + +class MarketTypeOption extends PostAttribute + with AttributeWithValueNameMixin { + final MarketType marketType; + + MarketTypeOption._(this.marketType) : super({'id': _idToSet(marketType)}); + + static String _idToSet(MarketType type) { + switch (type) { + case MarketType.inMarket: + return 'in'; + case MarketType.outMarket: + return 'out'; + } + } + + @override + String get label { + switch (marketType) { + case MarketType.inMarket: + return 'inMarket'.translation; + case MarketType.outMarket: + return 'outMarket'.translation; + } + } + + @override + String get primaryKeyField => 'id'; + + @override + String get value => label; +} + +class MarketTypeAttribute extends PostAttribute + with + KeyIndexedAttributeMixin, + SelectableAttributeMixin, + OptionedAttributeMixin { + MarketTypeAttribute._(this.options) : super({'key': 'market_type'}); + + static MarketTypeAttribute getAttribute() { + return MarketTypeAttribute._( + MarketType.values.map((e) => MarketTypeOption._(e)).toList()); + } + + @override + String get key => 'market_type'; + + @override + String get label => 'marketType'.translation; + + @override + String get primaryKeyField => 'key'; + + @override + selectDelegate(groupValue) { + return SingleOptionSelectableDelegate(groupValue, options); + } + + @override + final List options; +} diff --git a/lib/models/attributes/miscelaneous.dart b/lib/models/attributes/miscelaneous.dart new file mode 100644 index 0000000..647794f --- /dev/null +++ b/lib/models/attributes/miscelaneous.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; + +class MiscAttributeOption extends PostAttribute + with AttributeWithValueNameMixin, TranslatableMixin { + MiscAttributeOption({required Map data}) : super(data) { + configureTranslationModel(); + } + + @override + String get label => + translationModel?.translationOf('name', jSON['name'] ?? '') ?? + jSON['name'] ?? + ''; + + @override + String get primaryKeyField => 'id'; + + @override + String get value => label; +} + +class MiscAttribute extends PostAttribute + with + KeyIndexedAttributeMixin, + SelectableAttributeMixin, + OptionedAttributeMixin { + MiscAttribute._( + {required this.options, required String key, required String labelKey}) + : super({'key': key, 'label': labelKey}); + + static Future getAttrbute( + {required String key, required String labelKey, required Uri uri}) async { + var options = + await FutureGetList(uri, parser: (response) { + final map = jsonDecode(response.body); + final products = map?['data'] as List; + return products + .map( + (jsonData) => MiscAttributeOption(data: jsonData)) + .toList(); + }).fetch(); + return MiscAttribute._(options: options, key: key, labelKey: labelKey); + } + + static Future> getAttrbutes() async { + return [ + await MiscAttribute.getAttrbute( + key: 'country', + labelKey: 'country', + uri: baseUrl(path: kApiPath + '/countries')), + await MiscAttribute.getAttrbute( + key: 'measure_id', + labelKey: 'measure', + uri: baseUrl(path: kApiPath + '/measures')), + await MiscAttribute.getAttrbute( + key: 'currency_id', + labelKey: 'currency', + uri: baseUrl(path: kApiPath + '/currencies')), + await MiscAttribute.getAttrbute( + key: 'payment_term_id', + labelKey: 'paymentTerm', + uri: baseUrl( + path: kApiPath + '/terms', queryParameters: {'type': 'payment'})), + await MiscAttribute.getAttrbute( + key: 'delivery_term_id', + labelKey: 'deliveryTerm', + uri: baseUrl( + path: kApiPath + '/terms', + queryParameters: {'type': 'delivery'})), + ]; + } + + @override + String get key => primaryKey; + + @override + String get label => jSON['label'].toString().translation; + + @override + String get primaryKeyField => 'key'; + + @override + selectDelegate(groupValue) { + return SingleOptionSelectableDelegate(groupValue, options); + } + + @override + final List options; +} diff --git a/lib/models/attributes/packaging.dart b/lib/models/attributes/packaging.dart new file mode 100644 index 0000000..49e64a7 --- /dev/null +++ b/lib/models/attributes/packaging.dart @@ -0,0 +1,66 @@ +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +enum PackagingType { yes, no } + +class PackagingOption extends PostAttribute + with AttributeWithValueNameMixin { + final PackagingType marketType; + + PackagingOption._(this.marketType) : super({'id': _idToSet(marketType)}); + + static String _idToSet(PackagingType type) { + switch (type) { + case PackagingType.yes: + return 'yes'; + case PackagingType.no: + return 'no'; + } + } + + @override + String get label { + switch (marketType) { + case PackagingType.yes: + return 'yes'.translation; + case PackagingType.no: + return 'no'.translation; + } + } + + @override + String get primaryKeyField => 'id'; + + @override + String get value => label; +} + +class PackagingAttribute extends PostAttribute + with + KeyIndexedAttributeMixin, + SelectableAttributeMixin, + OptionedAttributeMixin { + PackagingAttribute._(this.options) : super({'key': 'packaging'}); + + static PackagingAttribute getAttribute() { + return PackagingAttribute._( + PackagingType.values.map((e) => PackagingOption._(e)).toList()); + } + + @override + String get key => 'packaging'; + + @override + String get label => 'packaging'.translation; + + @override + String get primaryKeyField => 'key'; + + @override + selectDelegate(groupValue) { + return SingleOptionSelectableDelegate(groupValue, options); + } + + @override + final List options; +} diff --git a/lib/models/categories/category.dart b/lib/models/categories/category.dart new file mode 100644 index 0000000..eebcbc3 --- /dev/null +++ b/lib/models/categories/category.dart @@ -0,0 +1,35 @@ +library category_lib; + +import 'dart:convert'; + +import 'package:birzha/models/categories/home.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/queries/query.dart'; +import 'package:birzha/new/utils/locale.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/orm/orm.dart'; + +part 'package:birzha/models/categories/pureCategory.dart'; +part 'package:birzha/models/categories/interfaces.dart'; +part 'package:birzha/models/categories/remoteCategory.dart'; + +/// This is the starting point of all categories in the app +/// Important things to know: RemoteCategories are unpredictable that's why they are assigned with ids (primary key) +/// Local categories such as [HomeCategory] are signed with negative id to predict their appeareance +/// In current architecture EVERY model that can give you a Future> is considered as category + +abstract class Serializer extends Orm with CopyAbleMixin { + Serializer(Map jSON) : super(jSON); + + Future> getPosts(BuildContext context, int page); + String get name; + SliverGridDelegate? gridDelegate(BuildContext context) => null; + + @override + String get primaryKeyField => 'id'; + + T postBuilderDelegate(Map data); + + bool get needPagination => true; +} diff --git a/lib/models/categories/home.dart b/lib/models/categories/home.dart new file mode 100644 index 0000000..7050e67 --- /dev/null +++ b/lib/models/categories/home.dart @@ -0,0 +1,44 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/queries/query.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; + +class HomeCategory extends RemoteCategory with AddQueryMixin { + HomeCategory() : super(data: {}); + + List subCategoryQueries = []; + + @override + int get primaryKey => -1; + + @override + String get name => 'newGoods'.translation; + + @override + SliverGridDelegate? gridDelegate(BuildContext context) { + return AppConstants.productsGridDelegate(context); + } + + @override + Uri postUri(BuildContext context, int page) { + debugPrint("eeeeeeeeeeeeeeeeeeee"); + return baseUrl(path: kApiPath + '/products', queryParameters: { + 'page': page.toString(), + 'custom_per_page': AppConstants.pageLimit.toString(), + }); + } + + /// Before the first page is loaded we get set of Subcategories turned into list of [SubCategoryQuery] + /// After the first page is loaded all subscribed widgets will be notified about that + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } + + @override + HomeCategory get copy => HomeCategory(); +} diff --git a/lib/models/categories/interfaces.dart b/lib/models/categories/interfaces.dart new file mode 100644 index 0000000..d827c98 --- /dev/null +++ b/lib/models/categories/interfaces.dart @@ -0,0 +1,57 @@ +part of category_lib; + +typedef GetSubCategoriesDelegate = Future> Function(BuildContext context, int page); + +mixin _GetRemotePostssMixin on Serializer { + Uri postUri(BuildContext context, int page); + + Future> _getAndParsePosts(BuildContext context, int page) async { + String locale = await getLocale(); + return FutureGetList(postUri(context, page), parser: (response) { + final map = jsonDecode(response.body); + final products = map?['data'] as List; + return products.map((data) => postBuilderDelegate(data)).toList(); + }).fetch({ + 'Accept': 'application/json', + 'locale': locale, + }); + } +} + +mixin SearchableMixin on AddQueryMixin { + void search(String word) { + applyQuery(SearchQuery(word)); + applyQuery(LocaleQuery()); + } +} + +mixin AddQueryMixin on Serializer { + Set _queries = {}; + + void resetQueries(Set newQueries) { + _queries = {...newQueries}; + } + + Set get queriesSetCopy => {..._queries}; + + void applyQuery(CategoryQuery categoryQuery) { + if (_queries.contains(categoryQuery)) { + _queries.remove(categoryQuery); + } + _queries.add(categoryQuery); + } + + void applyAllQueries(Set queriesNew) { + _queries = {...queriesNew, ..._queries}; + } + + void removeQuery(CategoryQuery query) { + _queries.remove(query); + } + + Map get queries => {for (var query in _queries) ...query.query}..removeWhere((key, value) => value.isEmpty); + + void removeWhere(bool Function(CategoryQuery) condition) { + _queries.removeWhere(condition); + } +} diff --git a/lib/models/categories/products/my_products.dart b/lib/models/categories/products/my_products.dart new file mode 100644 index 0000000..a0ddabd --- /dev/null +++ b/lib/models/categories/products/my_products.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; + +import '../../../new/utils/locale.dart'; + +class MyProducts extends RemoteCategory { + MyProducts() : super(data: {'id': -7}); + + @override + SliverGridDelegate? gridDelegate(BuildContext context) { + return AppConstants.productsGridDelegate(context); + } + + @override + get copy => MyProducts(); + + @override + String get name => 'myProducts'.translation; + + @override + postBuilderDelegate(Map data) { + return MyProduct(data: data); + } + + @override + Uri postUri(BuildContext context, int page) { + debugPrint("myProducts"); + return baseUrl( + path: kApiPath + '/my-products', + queryParameters: { + 'page': page.toString(), + 'custom_per_page': AppConstants.pageLimit.toString(), + }, + ); + } + + @override + Future> getPosts(BuildContext context, int page) async { + String locale = await getLocale(); + return FutureGetList(postUri(context, page), parser: (response) { + final map = jsonDecode(response.body); + final products = map?['data'] as List; + return products.map((data) => postBuilderDelegate(data)).toList(); + }).fetch( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer ${AppUserManager.of(context).dataSync.token}', + 'locale': locale, + }, + ); + } +} diff --git a/lib/models/categories/products/pure.dart b/lib/models/categories/products/pure.dart new file mode 100644 index 0000000..8ec75e8 --- /dev/null +++ b/lib/models/categories/products/pure.dart @@ -0,0 +1,20 @@ +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/categories/products/remote.dart'; +import 'package:birzha/models/products/product.dart'; + +class ProductsPureCategory extends PureAppCategory { + ProductsPureCategory({required Map data}) + : super(data: {...data}); + + ProductsRemoteCategory toRemote() { + return ProductsRemoteCategory.fromPureCategory(this); + } + + @override + get copy => ProductsPureCategory(data: {...jSON}); + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } +} diff --git a/lib/models/categories/products/remote.dart b/lib/models/categories/products/remote.dart new file mode 100644 index 0000000..6fd3348 --- /dev/null +++ b/lib/models/categories/products/remote.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/categories/products/pure.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/queries/query.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class MasterProductsRemoteCategory implements Serializer { + MasterProductsRemoteCategory(); + + Future> getSubCategories(BuildContext context, + {int page = 1}) async { + var categories = await FutureGetList( + baseUrl(path: kApiPath + '/categories', queryParameters: { + 'page': page.toString(), + 'per_page': AppConstants.pageLimit.toString() + }), parser: (response) { + final map = jsonDecode(response.body); + final products = map?['data'] as List; + return products + .map( + (jsonData) => ProductsPureCategory(data: jsonData)) + .toList(); + }).fetch(); + return categories; + } + + @override + int get primaryKey => -3; + + @override + String get name => 'news_feed'.translation; + + @override + get copy => MasterProductsRemoteCategory(); + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } + + @override + SliverGridDelegate? gridDelegate(BuildContext context) { + return AppConstants.productsGridDelegate(context); + } + + @override + Map jSON = {}; + + @override + Future> getPosts(BuildContext context, int page) { + return SynchronousFuture(const []); + } + + @override + bool get needPagination => false; + + @override + String get primaryKeyField => 'id'; +} + +class ProductsRemoteCategory extends RemoteCategory + with AddQueryMixin, SearchableMixin { + ProductsRemoteCategory.fromPureCategory(ProductsPureCategory pureCategory) + : super(data: {...pureCategory.jSON, 'name': pureCategory.name}) { + applyQuery(SubCategoryQuery(pureCategory)); + } + + @override + ProductsRemoteCategory get copy => ProductsRemoteCategory.fromPureCategory( + ProductsPureCategory(data: {...jSON})) + ..applyAllQueries({...queriesSetCopy}); + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } + + @override + SliverGridDelegate? gridDelegate(BuildContext context) { + return AppConstants.productsGridDelegate(context); + } + + @override + Uri postUri(BuildContext context, int page) { + debugPrint('adsdasdasdasdasdasdasdasdasdasd'); + return baseUrl(path: kApiPath + '/products', queryParameters: { + ...queries, + 'page': page.toString(), + 'custom_per_page': AppConstants.pageLimit.toString() + }); + } + + @override + String get name => jSON['name'] ?? ""; +} diff --git a/lib/models/categories/products/search.dart b/lib/models/categories/products/search.dart new file mode 100644 index 0000000..dc2931a --- /dev/null +++ b/lib/models/categories/products/search.dart @@ -0,0 +1,38 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/categories/products/remote.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; + +class ProductsSearch extends RemoteCategory + with AddQueryMixin, SearchableMixin + implements ProductsRemoteCategory { + ProductsSearch() : super(data: {'id': -6}); + + @override + get copy => ProductsSearch()..applyAllQueries(queriesSetCopy); + + @override + String get name => 'search'.translation; + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } + + @override + Uri postUri(BuildContext context, int page) { + return baseUrl(path: kApiPath + '/products', queryParameters: { + ...queries, + 'page': page.toString(), + 'custom_per_page': AppConstants.pageLimit.toString() + }); + } + + @override + SliverGridDelegate? gridDelegate(BuildContext context) { + return AppConstants.productsGridDelegate(context); + } +} diff --git a/lib/models/categories/pureCategory.dart b/lib/models/categories/pureCategory.dart new file mode 100644 index 0000000..9932cdf --- /dev/null +++ b/lib/models/categories/pureCategory.dart @@ -0,0 +1,21 @@ +part of category_lib; + +abstract class PureAppCategory extends Serializer + with TranslatableMixin { + PureAppCategory({required Map data}) : super(data) { + configureTranslationModel(); + } + + @override + Future> getPosts(BuildContext context, int page) async { + return const []; + } + + @override + String get name => + translationModel?.translationOf('name', _defaultName) ?? _defaultName; + + String get icon => jSON['icon'] ?? ""; + + String get _defaultName => jSON['name'] ?? ""; +} diff --git a/lib/models/categories/relatedProducts.dart b/lib/models/categories/relatedProducts.dart new file mode 100644 index 0000000..641fee4 --- /dev/null +++ b/lib/models/categories/relatedProducts.dart @@ -0,0 +1,65 @@ +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; + +class RelatedProductsCategory extends RemoteCategory { + RelatedProductsCategory.fromProduct(Product product) + : super(data: { + 'product': {...product.jSON}, + 'products': [] + }); + + Product get product => Product(data: {...jSON['product']}); + + List get _products => [ + for (Map p in jSON['products']) Product(data: {...p}) + ]; + + set _products(List products) { + jSON['products'] = [ + for (Product p in products) {...p.jSON} + ]; + } + + Map getProductsForDetails() { + return { + 'relatedProducts': [ + for (Product product in _products) {...product.jSON} + ] + }; + } + + @override + String get name => 'relatedProducts'.translation; + + @override + int get primaryKey => -2; + + @override + Future> getPosts(BuildContext context, int page) async { + var productsList = await super.getPosts(context, page); + _products = [...productsList]; + return [..._products]; + } + + @override + Uri postUri(BuildContext context, int page) { + debugPrint('ggggggggggggggggggggggggg'); + return baseUrl(path: kApiPath + '/products', queryParameters: { + 'product_id': product.id.toString(), + 'page': '1', + 'custom_per_page': '10' + }); + } + + @override + Product postBuilderDelegate(Map data) { + return Product(data: {...data}); + } + + @override + get copy => + RelatedProductsCategory.fromProduct(product).._products = [..._products]; +} diff --git a/lib/models/categories/remoteCategory.dart b/lib/models/categories/remoteCategory.dart new file mode 100644 index 0000000..34494d3 --- /dev/null +++ b/lib/models/categories/remoteCategory.dart @@ -0,0 +1,13 @@ +part of category_lib; + +abstract class RemoteCategory extends Serializer with _GetRemotePostssMixin { + RemoteCategory({required Map data}) : super(data); + + @override + Future> getPosts(BuildContext context, int page) { + return _getAndParsePosts(context, page); + } + + @override + RemoteCategory get copy; +} diff --git a/lib/models/chatroom/chatroom.dart b/lib/models/chatroom/chatroom.dart new file mode 100644 index 0000000..1edf8cc --- /dev/null +++ b/lib/models/chatroom/chatroom.dart @@ -0,0 +1,303 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/chatroom/message.dart'; +import 'package:birzha/models/exceptions/exception.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/core/orm/orm.dart'; +import 'package:provider/provider.dart'; + +class Chatroom extends Manager<_ChatroomData> implements _ChatroomData { + Set _availableLocalMessages = {}; + Set get _availableRemoteMessages => { + for (var data in jSON['messages'] ?? []) RemoteMessage(data: {...data}) + }; + + Set get messages => UnmodifiableSetView({..._availableLocalMessages, ..._availableRemoteMessages}); + + int get lastIndexOfLocalMessages => _lastMessageInBufferId; + + void _clearSentMessages() { + _availableLocalMessages.removeWhere((element) => element.status == MessagesStatus.sent); + } + + @override + get jSON => {...dataSync.jSON}; + + @override + set jSON(other) {} + + int _lastMessageInBufferId = -1; + + Chatroom({required Map data}) : super(_ChatroomData(data: {...data})); + + factory Chatroom.init({required Map data}) { + var newData = {...data}; + newData['id'] = data['chatroom_id']; + newData['messages'] = []; + return Chatroom(data: {...newData}); + } + + @override + int get primaryKey => jSON[primaryKeyField]; + + @override + String get primaryKeyField => 'id'; + + @override + Vendor? get sender => dataSync.sender; + + @override + String get subTitile => dataSync.subTitile; + + @override + String get title => dataSync.title; + + @override + int get unread => dataSync.unread; + + @override + bool get hasNewMessages => dataSync.hasNewMessages; + + @override + @protected + _ChatroomData get dataSync => super.dataSync; + + @override + @protected + get value => super.value; + + @override + @protected + set value(other) { + super.value = other; + } + + @override + void listenerCallBack(TaskResult<_ChatroomData?> result, String taskKey) { + _statusTable[taskKey] = result.status; + notifyListeners(); + } + + @override + Future destroyTask(String taskId) async { + _statusTable.remove(taskId); + return super.destroyTask(taskId); + } + + Map _statusTable = {}; + + TaskStatus getStatusByKey(String key) { + return _statusTable[key] ?? TaskStatus.None; + } + + void loadPastMessages(BuildContext context, [VoidCallback? onDone]) { + addTask(Task( + key: 'load', + computation: () async { + try { + var response = await _loadMoreRequest(AppUserManager.of(context).dataSync.token ?? "", dataSync, _howManyMessagesToSkip); + var decoded = jsonDecode(response.body); + var isSuccess = decoded['status_code'] == 200 || decoded['status_code'] == '200'; + if (!isSuccess) { + throw AppExceptions.recognizer(decoded); + } else { + var data = decoded['data']; + var newMessages = data['messages']; + List processed = []; + if (newMessages is Iterable) { + processed = [...newMessages]; + } else { + var mapMessages = newMessages as Map; + processed = [for (var key in mapMessages.keys) mapMessages[key]]; + } + if (onDone != null) { + onDone(); + } + return dataSync.copy..jSON['messages'] = [...jSON['messages'], ...processed.reversed]; + } + } catch (ex) { + if (onDone != null) { + onDone(); + } + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + })); + } + + void refresh(BuildContext context, [VoidCallback? onDone]) { + addTask(Task( + key: 'load', + computation: () async { + try { + var response = await _loadMoreRequest(AppUserManager.of(context).dataSync.token ?? "", dataSync, 0); + var decoded = jsonDecode(response.body); + var isSuccess = decoded['status_code'] == 200 || decoded['status_code'] == '200'; + if (!isSuccess) { + throw AppExceptions.recognizer(decoded); + } else { + var data = decoded['data']; + var newMessages = data['messages']; + List processed = []; + if (newMessages is Iterable) { + processed = [...newMessages]; + } else { + var mapMessages = newMessages as Map; + processed = [for (var key in mapMessages.keys) mapMessages[key]]; + } + if (onDone != null) { + onDone(); + } + _clearSentMessages(); + return dataSync.copy + ..jSON['messages'] = [ + ...processed.reversed, + ...jSON['messages'], + ]; + } + } catch (ex, trace) { + print(trace); + if (onDone != null) { + onDone(); + } + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + })); + } + + int get _howManyMessagesToSkip { + var sentMessages = messages.where((element) => element.status == MessagesStatus.sent); + return sentMessages.length; + } + + void retextMessage(BuildContext context, Message message) { + _availableLocalMessages.remove(message); + notifyListeners(); + textMessage(context, message.text); + } + + void textMessage(BuildContext context, String text) { + addTask(Task( + computation: () async { + var message = LocalMessage(date: DateTime.fromMillisecondsSinceEpoch(DateTime.now().millisecondsSinceEpoch), id: _lastMessageInBufferId, text: text); + try { + _lastMessageInBufferId--; + message.status = MessagesStatus.sending; + _availableLocalMessages = {message, ..._availableLocalMessages}; + notifyListeners(); + var response = await _sendMessageRequest( + message, + AppUserManager.of(context).dataSync.token ?? "", + dataSync, + ); + var decoded = jsonDecode(response.body); + var isSuccess = decoded['status_code'] == 200 || decoded['status_code'] == '200'; + if (!isSuccess) { + throw AppExceptions.recognizer(decoded); + } else { + message.status = MessagesStatus.sent; + return dataSync.copy..jSON['last_message'] = {...message.toRemoteMessage().jSON}; + } + } catch (ex) { + message.status = MessagesStatus.failed; + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'message' + (_lastMessageInBufferId).toString())); + } + + @protected + @override + _ChatroomData get copy => throw UnimplementedError(); + + static Chatroom of(BuildContext context, {bool listen = false}) { + return Provider.of(context, listen: listen); + } +} + +Future _sendMessageRequest(LocalMessage message, String token, _ChatroomData chatroom) { + return http.post(baseUrl(path: kApiPath + '/messages/' + chatroom.primaryKey.toString()), body: jsonEncode({'msg': message.text}), headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }); +} + +Future _loadMoreRequest(String token, _ChatroomData chatroom, int skip) { + return http.get(baseUrl(path: kApiPath + '/messages/chatroom/' + chatroom.primaryKey.toString() + '/load-more', queryParameters: {'skip': skip.toString()}), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }); +} + +class _ChatroomData extends Post with CopyAbleMixin<_ChatroomData> { + _ChatroomData({required Map data}) : super(data); + + Vendor? get sender { + if (jSON['vendor'] != null) { + return Vendor(data: {...jSON['vendor']}); + } + if (jSON['users'] is! Iterable) { + return null; + } + var users = jSON['users'] as Iterable; + if (users.isEmpty) { + return null; + } + + return Vendor(data: {...users.first}); + } + + String get title => (sender?.name.isEmpty ?? true) ? (sender?.phone ?? "") : sender!.name; + String get subTitile => jSON['last_message'] is! Map ? '' : RemoteMessage(data: {...jSON['last_message']}).text; + int get unread => jSON['count_unread_messages'] is! int ? 0 : jSON['count_unread_messages']; + bool get hasNewMessages => unread > 0; + + @override + _ChatroomData get copy => _ChatroomData(data: {...jSON}); +} + +class ChatroomSerializer extends RemoteCategory { + ChatroomSerializer() : super(data: {'id': -5}); + + @override + get copy => ChatroomSerializer(); + + @override + String get name => 'messages'.translation; + + @override + Chatroom postBuilderDelegate(Map data) => Chatroom(data: {...data}); + + @override + Uri postUri(BuildContext context, int page) { + return baseUrl(path: kApiPath + '/messages'); + } + + @override + Future> getPosts(BuildContext context, int page) { + return FutureGetList( + postUri(context, page), + parser: (response) { + var decoded = jsonDecode(response.body); + var chatrooms = decoded['data']['chatrooms'] as Iterable; + return [ + for (var chatroomRaw in chatrooms) Chatroom(data: {...chatroomRaw}) + ]; + }, + ).fetch({'Accept': 'application/json', 'Authorization': 'Bearer ${AppUserManager.of(context).dataSync.token}'}); + } + + @override + bool get needPagination => false; +} diff --git a/lib/models/chatroom/message.dart b/lib/models/chatroom/message.dart new file mode 100644 index 0000000..2503cc6 --- /dev/null +++ b/lib/models/chatroom/message.dart @@ -0,0 +1,79 @@ +import 'package:birzha/models/user/userManager.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:birzha/core/orm/orm.dart'; + +enum MessagesStatus { sending, sent, failed, none } + +abstract class Message { + String get text; + DateTime? get date; + int get id; + MessagesStatus get status; + bool isMyMessage(BuildContext context); +} + +class RemoteMessage extends Orm implements Message { + RemoteMessage({required Map data}) : super(data); + + @override + DateTime? get date => DateTime.tryParse(jSON["send_at"] ?? ""); + + @override + int get id => primaryKey; + + @override + String get primaryKeyField => 'id'; + + @override + String get text => jSON['message'] ?? ""; + + @override + MessagesStatus get status => MessagesStatus.sent; + + RemoteMessage._fromLocalMessage(int id, String text) : this(data: {'id': id, 'message': text}); + + @override + bool isMyMessage(context) => AppUserManager.of(context).dataSync.primaryKey == jSON['sender_id']; +} + +class LocalMessage implements Message { + LocalMessage({required this.id, required DateTime date, required this.text}) { + _date = date; + } + + MessagesStatus _status = MessagesStatus.none; + + late final DateTime _date; + + DateTime get date => _date.toLocal(); + + @override + final int id; + + @override + final String text; + + @override + bool operator ==(other) { + return other is LocalMessage && other.hashCode == hashCode; + } + + RemoteMessage toRemoteMessage() { + return RemoteMessage._fromLocalMessage(id, text); + } + + @override + int get hashCode => id.hashCode; + + @override + // ignore: unnecessary_getters_setters + MessagesStatus get status => _status; + + // ignore: unnecessary_getters_setters + set status(MessagesStatus other) { + _status = other; + } + + @override + bool isMyMessage(_) => true; +} diff --git a/lib/models/exceptions/exception.dart b/lib/models/exceptions/exception.dart new file mode 100644 index 0000000..4700e06 --- /dev/null +++ b/lib/models/exceptions/exception.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/auth/login.dart'; +import 'package:birzha/services/modals.dart'; + +abstract class AppExceptions implements Exception { + final String message; + AppExceptions({required this.message}); + + factory AppExceptions.recognizer(Map data) { + var message = data['error']; + if (message != null && message is Map) { + var translated = message['backendCode'.translation]; + return MessageException(translated); + } else + return OtherException(); + } + + static void exceptionHandler(BuildContext context, dynamic exception) { + if (exception is OtherException) { + showSnackBar(context, content: exception.message.translation); + } else if (exception is CartProblemException) + showSnackBar(context, + content: exception.message, duration: Duration(seconds: 4)); + else if (exception is UnAuthenticatedException) + Navigator.of(context, rootNavigator: true) + .push(MaterialPageRoute(builder: (_) => LoginScreen())); + else if (exception is MessageException) { + showSnackBar(context, content: exception.message); + } else { + showSnackBar(context, content: OtherException().message.translation); + } + } + + @override + String toString() { + return message; + } +} + +class OtherException extends AppExceptions { + OtherException() : super(message: 'otherException'); +} + +class MessageException extends AppExceptions { + MessageException(String message) : super(message: message); +} + +class UnAuthenticatedException extends MessageException { + UnAuthenticatedException() : super('unauthenticated'.translation); +} + +class CartProblemException extends MessageException { + CartProblemException() : super('cartProblem'.translation); +} diff --git a/lib/models/products/composable.dart b/lib/models/products/composable.dart new file mode 100644 index 0000000..72b4bb3 --- /dev/null +++ b/lib/models/products/composable.dart @@ -0,0 +1,25 @@ +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/core/orm/orm.dart'; + +enum PostCompositionMode { create, update } + +mixin ComposableMixin on Orm { + Map _attributeCache = {}; + + PostAttribute? getModelData(String key) { + return _attributeCache[key]; + } + + String getBasicFieldValue(String key) { + return jSON[key] ?? ""; + } + + void setBasicTypeField(String key, String value) { + jSON[key] = value; + } + + void setModelField(String key, PostAttribute model) { + _attributeCache[key] = model; + jSON[key] = model.primaryKey.toString(); + } +} diff --git a/lib/models/products/composableProduct.dart b/lib/models/products/composableProduct.dart new file mode 100644 index 0000000..214acd4 --- /dev/null +++ b/lib/models/products/composableProduct.dart @@ -0,0 +1,126 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/attributes/category.dart'; +import 'package:birzha/models/attributes/market_type.dart'; +import 'package:birzha/models/attributes/miscelaneous.dart'; +import 'package:birzha/models/attributes/packaging.dart'; +import 'package:birzha/models/products/composable.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/orm/orm.dart'; +import 'package:http/http.dart' as http; + +abstract class ImageModel extends Orm { + ImageModel._({required Map data}) : super(data); + + @override + String get primaryKeyField => 'path'; +} + +class LocalImageModel extends ImageModel { + LocalImageModel._fromPath(String path) : super._(data: {'path': path}); + LocalImageModel._fromModel({required Map data}) : super._(data: {...data}); +} + +class RemoteImageModel extends ImageModel { + RemoteImageModel._fromPath(String path, int id) : super._(data: {'path': path, 'id': id}); + RemoteImageModel._fromModel({required Map data}) : super._(data: {...data}); + + int? get id => jSON['id']; +} + +class ComposableProduct extends Orm with ComposableMixin, ChangeNotifier { + ComposableProduct._() : super({}); + + static ComposableProduct? _singletone; + + factory ComposableProduct() { + return _singletone ??= ComposableProduct._(); + } + + static Future getAttributes(BuildContext context, MyProduct? product) async { + List options = []; + if (ComposableProduct().attrbiutes.isEmpty) { + options = await _getAttributes(context); + } else { + options = await SynchronousFuture([...ComposableProduct().attrbiutes]); + } + ComposableProduct()._attributes = [...options]; + if (product != null) { + await ComposableProduct()._changeAttributes(context, product.primaryKey); + } + } + + List _attributes = []; + + List get attrbiutes => UnmodifiableListView(_attributes); + + Future _changeAttributes(BuildContext context, int productId) async { + var token = AppUserManager.of(context).dataSync.token; + var editedData = await http.get(baseUrl(path: kApiPath + '/my-products/$productId'), headers: {'Authorization': 'Bearer $token'}); + var decoded = jsonDecode(editedData.body); + jSON = {...decoded}; + print(jSON['old_img']); + jSON['old_img'] = [ + for (var item in ((decoded['old_img'] ?? []) as Iterable).where((element) => element['path'] is String && (element['path'] as String).isNotEmpty)) + RemoteImageModel._fromPath(item?['path'] ?? "", item['id']).jSON + ]; + for (var attribute in _attributes) { + var id = jSON[attribute.key]; + if (id != null) { + try { + var matchingOption = attribute.options.firstWhere((element) => element.primaryKey == id); + setModelField(attribute.key, matchingOption); + } catch (e) {} + } + } + saveProgress(decoded['id']); + } + + void saveProgress(int? id) { + jSON['productForEditing'] = id; + notifyListeners(); + } + + Map sendData() { + return {...jSON}; + } + + void removeImage(BuildContext context, ImageModel image) { + if (image is LocalImageModel) { + (jSON['new_img'] as List).removeWhere((element) => LocalImageModel._fromModel(data: element).primaryKey == image.primaryKey); + } else if (image is RemoteImageModel) { + AppUserManager.of(context).deleteImage(context, this, image, () { + try { + (jSON['old_img'] as List).removeWhere((element) => RemoteImageModel._fromModel(data: element).primaryKey == image.primaryKey); + } catch (e) {} + }); + } + notifyListeners(); + } + + void pickImages() async { + var result = await FilePicker.platform.pickFiles(type: FileType.custom, allowMultiple: true, withReadStream: true, allowedExtensions: ['jpg', 'png']); + if (result != null) { + jSON['new_img'] = [for (var image in result.files.where((element) => element.path != null)) LocalImageModel._fromPath(image.path!).jSON]; + notifyListeners(); + } + } + + List get localImages => [for (var item in jSON['new_img'] ?? []) LocalImageModel._fromModel(data: item)]; + + List get remoteImages => [for (var item in jSON['old_img'] ?? []) RemoteImageModel._fromPath(item['path'], item['id'])]; + + @override + String get primaryKeyField => 'productForEditing'; +} + +Future> _getAttributes(BuildContext context) async { + return [await CategoryAttribute.getAttribute(), ...await MiscAttribute.getAttrbutes(), MarketTypeAttribute.getAttribute(), PackagingAttribute.getAttribute()]; +} diff --git a/lib/models/products/my_product.dart b/lib/models/products/my_product.dart new file mode 100644 index 0000000..c7f55b7 --- /dev/null +++ b/lib/models/products/my_product.dart @@ -0,0 +1,70 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/material.dart'; + +enum MyProductStatus { approved, puiblished, draft, denied, none } + +class MyProduct extends Product { + MyProduct({required Map data}) : super(data: data); + + String get statusNotes => jSON['status_note'] ?? ""; + + bool get isAbleToEdit { + return status == MyProductStatus.draft || status == MyProductStatus.denied; + } + + Color get statusColor { + switch (status) { + case MyProductStatus.approved: + return Colors.green; + case MyProductStatus.puiblished: + return Colors.orange; + case MyProductStatus.draft: + return AppConstants.appColor; + case MyProductStatus.denied: + return Colors.redAccent; + case MyProductStatus.none: + return AppConstants.appColor; + } + } + + String get statusLabel { + switch (status) { + case MyProductStatus.approved: + return 'approved'.translation; + case MyProductStatus.puiblished: + return 'published'.translation; + case MyProductStatus.draft: + return 'draft'.translation; + case MyProductStatus.denied: + return 'denied'.translation; + case MyProductStatus.none: + return 'none'.translation; + } + } + + MyProductStatus get status { + switch (jSON['status']) { + case 'approved': + return MyProductStatus.approved; + case 'new': + return MyProductStatus.puiblished; + case 'draft': + return MyProductStatus.draft; + case 'denied': + return MyProductStatus.denied; + default: + return MyProductStatus.none; + } + } + + @override + get copy => MyProduct(data: {...jSON}); + + @override + get characteristics => [ + ProductCharacteristics('status'.translation, statusLabel), + ...super.characteristics + ]; +} diff --git a/lib/models/products/post.dart b/lib/models/products/post.dart new file mode 100644 index 0000000..a031ac3 --- /dev/null +++ b/lib/models/products/post.dart @@ -0,0 +1,31 @@ +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/translationModel.dart'; +import 'package:birzha/services/translationServices.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/orm/orm.dart'; + +abstract class PostCharacteristics { + String get translatedLabel; + String get value; +} + +mixin RemoteDetailsMixin on Post { + Future loadDetails(BuildContext context); +} + +mixin TranslatableMixin on Orm { + void configureTranslationModel() { + var translationsRaw = jSON['translations']; + if (translationsRaw != null && translationsRaw is Iterable) + (translationsRaw as List).removeWhere((element) => TranslationModel({...element}).primaryKey != 'backendCode'.translation); + } + + TranslationModel? get translationModel => getTranslationAccordingToLocale(translationsFromMap(jSON)); +} + +abstract class Post extends Orm { + Post(Map jSON) : super(jSON); + + @override + String get primaryKeyField => 'id'; +} diff --git a/lib/models/products/product.dart b/lib/models/products/product.dart new file mode 100644 index 0000000..c8caa87 --- /dev/null +++ b/lib/models/products/product.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'dart:math' as math; +import 'package:birzha/models/categories/relatedProducts.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:birzha/core/orm/orm.dart'; + +const _characLabelKeys = [ + 'productNumber', + 'productAmount', + 'productMark', + 'producer', + 'producerCountry', +]; + +const _characValueKeys = ['id', 'quantity', 'mark', 'manufacturer', 'country']; + +class ProductCharacteristics implements PostCharacteristics { + @override + final String translatedLabel; + + @override + final String value; + + ProductCharacteristics(this.translatedLabel, this.value); +} + +class Product extends Post with RemoteDetailsMixin, CopyAbleMixin, TranslatableMixin { + Product({required Map data}) : super(data) { + configureTranslationModel(); + } + + int get id => primaryKey; + + @protected + String get quantity => jSON['quantity'] ?? '0'; + + String get quantityFormatted => quantity.toString(); + + @protected + DateTime? get expiryDate => DateTime.tryParse(jSON['ends_at'] ?? ""); + + @protected + double get price => double.tryParse('${jSON["price"]}') ?? 0; + + String get expiryDateFormatted => safeValueDate(expiryDate); + + String get priceFormatted => priceFormatter(price); + + String get _defaultName => jSON['name'] ?? ""; + + String get _defaultDescription => jSON['description'] ?? ""; + + String get unit => jSON['unit']["name"] ?? ""; + + String get name { + return translationModel?.translationOf('name', _defaultName) ?? _defaultName; + } + + String get description { + return translationModel?.translationOf('description', _defaultDescription) ?? _defaultDescription; + } + + List get images => [ + for (var image in [...?jSON['images']]) image['path'] + ]..removeWhere((element) => element.isEmpty); + + String get mainImage => images.isEmpty ? '' : images.first; + + @override + Future loadDetails(context) async { + var user = AppUserManager.of(context).dataSync; + var data = await http.get(baseUrl(path: kApiPath + '/products/' + id.toString()), + headers: {if (user.isRegistered) 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json'}); + var result = jsonDecode(data.body); + var category = RelatedProductsCategory.fromProduct(this); + await category.getPosts(context, 1); + jSON = {...jSON, ...result, ...category.getProductsForDetails()}; + } + + List get relatedProducts => [ + for (var productRaw in [...?jSON['relatedProducts']]) Product(data: {...productRaw}) + ]; + + @override + Product get copy => Product(data: {...jSON}); + + List get characteristics => [ + ...[ + for (var i = 0; i < math.min(_characLabelKeys.length, _characValueKeys.length); i++) + ProductCharacteristics(_characLabelKeys[i].translation, jSON[_characValueKeys[i]]?.toString() ?? ""), + ]..insert(1, ProductCharacteristics('startingPrice'.translation, priceFormatted)) + ]; + + Vendor? get vendor { + if (jSON['vendor'] == null) { + return null; + } else { + return Vendor(data: {...jSON['vendor']}); + } + } +} + +/* { + id: 4, + deleted_at: null, + created_at: 2021-06-24 06:01:38, + updated_at: 2021-10-19 16:42:48, + name: in.m, + code: in.m, + translations: [ + { + locale: en, + model_id: 4, + attribute_data: + { + "name":"sq.m", + "code":"sq.m" + } + }, + { + locale: ru, + model_id: 4, + attribute_data: + { + "name":"кв.м", + "code":"кв.м" + } + } + ] +} */ \ No newline at end of file diff --git a/lib/models/queries/query.dart b/lib/models/queries/query.dart new file mode 100644 index 0000000..446214b --- /dev/null +++ b/lib/models/queries/query.dart @@ -0,0 +1,46 @@ +library query_lib; + +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +part 'package:birzha/models/queries/subCategoryQuery.dart'; + +int _suCategoryQueryHasCode = -1; + +/// This is a starting point of queries namespace +/// Important thins to know: All queries will be summed up into one Set of queries in [AddQueryMixin] of categories +/// That means - you may not have to identical queries in one Set (depends on the [CategoryQuery.hashCodeIdentificator]) +/// Some queries are forced to be single in one set: that's why I used negative constant [_suCategoryQueryHasCode] for [SubCategoryQuery] + +abstract class CategoryQuery { + final String name; + final String value; + + Map get query => {name: value}; + + @override + bool operator ==(other) { + return other is CategoryQuery && hashCode == other.hashCode; + } + + @override + int get hashCode => hashCodeIdentificator; + + int get hashCodeIdentificator; + + CategoryQuery({required this.name, required this.value}); +} + +class SearchQuery extends CategoryQuery { + SearchQuery(String word) : super(name: 'q', value: word); + + @override + int get hashCodeIdentificator => -6; +} + +class LocaleQuery extends CategoryQuery { + LocaleQuery() : super(name: 'locale', value: 'backendCode'.translation); + + @override + int get hashCodeIdentificator => -7; +} diff --git a/lib/models/queries/subCategoryQuery.dart b/lib/models/queries/subCategoryQuery.dart new file mode 100644 index 0000000..b931c31 --- /dev/null +++ b/lib/models/queries/subCategoryQuery.dart @@ -0,0 +1,11 @@ +part of query_lib; + +class SubCategoryQuery extends CategoryQuery{ + + PureAppCategory categoryReference; + + SubCategoryQuery(this.categoryReference) : super(name: 'category_id', value: categoryReference.primaryKey.toString()); + + @override + int get hashCodeIdentificator => _suCategoryQueryHasCode.hashCode; +} \ No newline at end of file diff --git a/lib/models/settings/settingsModel.dart b/lib/models/settings/settingsModel.dart new file mode 100644 index 0000000..eaa9b72 --- /dev/null +++ b/lib/models/settings/settingsModel.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const kDefaultLanguage = 'tk'; + +class SettingsModel extends ChangeNotifier { + String _language = kDefaultLanguage; + bool pushEnabled = true; + + static AppLocalizations? _localization; + static Future initLocalization(String l) async { + _localization = await AppLocalizations.delegate.load(Locale(l, l == 'en' ? 'US' : '')); + } + + Future setLanguage(String l) async { + _language = l; + await initLocalization(l); + var prefs = await SharedPreferences.getInstance(); + await prefs.setString('language', _language); + notifyListeners(); + } + + Locale get language { + switch (_language) { + case 'ru': + return const Locale('ru', ''); + case 'en': + return const Locale('en', 'US'); + case 'tk': + return const Locale('tk', ''); + default: + return const Locale('tk', ''); + } + } + + void launch(SharedPreferences prefs) { + _language = prefs.getString('language') ?? _language; + notifyListeners(); + } + + SettingsModel(SharedPreferences prefs) { + launch(prefs); + } + + static SettingsModel of(BuildContext context, [bool listen = false]) { + return Provider.of(context, listen: listen); + } +} + +extension LocalMessageExt on String { + String get translation { + return { + "yourMessage": SettingsModel._localization?.yourMessage, + "nsm": SettingsModel._localization?.nsm, + "buyNow": SettingsModel._localization?.buyNow, + "uploadReceipt": SettingsModel._localization?.uploadReceipt, + "payOnline": SettingsModel._localization?.payOnline, + "sendMessage": SettingsModel._localization?.sendMessage, + "dataIsUpdated": SettingsModel._localization?.dataIsUpdated, + "personalCabinet": SettingsModel._localization?.personalCabinet, + "addPost": SettingsModel._localization?.addPost, + "iu_company": SettingsModel._localization?.iu_company, + "iu_about": SettingsModel._localization?.iu_about, + "phone_error": SettingsModel._localization?.phone_error, + "email_error": SettingsModel._localization?.email_error, + "username": SettingsModel._localization?.username, + "noAccount": SettingsModel._localization?.noAccount, + "yourPassword": SettingsModel._localization?.yourPassword, + "yourLogin": SettingsModel._localization?.yourLogin, + "yourEmail": SettingsModel._localization?.yourEmail, + "link": SettingsModel._localization?.link, + "loginText": SettingsModel._localization?.loginText, + "expiryDate": SettingsModel._localization?.expiryDate, + "sellerContact": SettingsModel._localization?.sellerContact, + "address": SettingsModel._localization?.address, + "realAddress": SettingsModel._localization?.realAddress, + "mail": SettingsModel._localization?.mail, + "telephone": SettingsModel._localization?.telephone, + "lotNumber": SettingsModel._localization?.lotNumber, + "productNumber": SettingsModel._localization?.productNumber, + "productMark": SettingsModel._localization?.productMark, + "productAmount": SettingsModel._localization?.productAmount, + "startingPrice": SettingsModel._localization?.startingPrice, + "producer": SettingsModel._localization?.producer, + "producerCountry": SettingsModel._localization?.producerCountry, + "description": SettingsModel._localization?.description, + "unit": SettingsModel._localization?.unit, + "paymentTerms": SettingsModel._localization?.paymentTerms, + "deliveryTerms": SettingsModel._localization?.deliveryTerms, + "station": SettingsModel._localization?.station, + "packing": SettingsModel._localization?.packing, + "english": SettingsModel._localization?.english, + "russian": SettingsModel._localization?.russian, + "turkmen": SettingsModel._localization?.turkmen, + "localeLabel": SettingsModel._localization?.localeLabel, + "backendCode": SettingsModel._localization?.backendCode, + "save_changes": SettingsModel._localization?.save_changes, + "email": SettingsModel._localization?.email, + "message": SettingsModel._localization?.message, + "top_up_history": SettingsModel._localization?.top_up_history, + "personal_data": SettingsModel._localization?.personal_data, + "news_feed": SettingsModel._localization?.news_feed, + "feedback": SettingsModel._localization?.feedback, + "privacy_policy": SettingsModel._localization?.privacy_policy, + "contact": SettingsModel._localization?.contact, + "home": SettingsModel._localization?.home, + "category": SettingsModel._localization?.category, + "favourites": SettingsModel._localization?.favourites, + "settings": SettingsModel._localization?.settings, + "login": SettingsModel._localization?.login, + "phone": SettingsModel._localization?.phone, + "password": SettingsModel._localization?.password, + "or": SettingsModel._localization?.or, + "register": SettingsModel._localization?.register, + "first_name": SettingsModel._localization?.first_name, + "last_name": SettingsModel._localization?.last_name, + "lang": SettingsModel._localization?.lang, + "phoneValidator": SettingsModel._localization?.phoneValidator, + "empty": SettingsModel._localization?.empty, + "forgot": SettingsModel._localization?.forgot, + "search": SettingsModel._localization?.search, + 'cart': SettingsModel._localization?.cart, + 'addToCart': SettingsModel._localization?.addToCart, + 'addedToCart': SettingsModel._localization?.addedToCart, + "removedFromCart": SettingsModel._localization?.removedFromCart, + "inCart": SettingsModel._localization?.inCart, + "allPrice": SettingsModel._localization?.allPrice, + 'wantDelete': SettingsModel._localization?.wantDelete, + 'hit': SettingsModel._localization?.hit, + 'newGoods': SettingsModel._localization?.newGoods, + "emptyPosts": SettingsModel._localization?.emptyPosts, + "order": SettingsModel._localization?.order, + "account": SettingsModel._localization?.account, + "otherException": SettingsModel._localization?.otherException, + "password_confirmation": SettingsModel._localization?.password_confirmation, + "password_confirmation_error": SettingsModel._localization?.password_confirmation_error, + 'profile': SettingsModel._localization?.profile, + "unauthenticated": SettingsModel._localization?.unauthenticated, + "needToEnter": SettingsModel._localization?.needToEnter, + "logout": SettingsModel._localization?.logout, + "logging_out": SettingsModel._localization?.logging_out, + "sure_log_out": SettingsModel._localization?.sure_log_out, + "date": SettingsModel._localization?.date, + "status": SettingsModel._localization?.status, + "products": SettingsModel._localization?.products, + "smsCodeSent": SettingsModel._localization?.smsCodeSent, + "codeLabel": SettingsModel._localization?.codeLabel, + "passwordError": SettingsModel._localization?.passwordError, + "verify": SettingsModel._localization?.verify, + "sendingCode": SettingsModel._localization?.sendingCode, + "sendCode": SettingsModel._localization?.sendCode, + "rules": SettingsModel._localization?.rules, + "agree": SettingsModel._localization?.agree, + "about": SettingsModel._localization?.about, + "quantitySumm": SettingsModel._localization?.quantitySumm, + "enterQuantity": SettingsModel._localization?.enterQuantity, + "formatError": SettingsModel._localization?.formatError, + "tapToLoadMore": SettingsModel._localization?.tapToLoadMore, + "similiarProducts": SettingsModel._localization?.similiarProducts, + "dialCode": SettingsModel._localization?.dialCode, + "verificationMailSent": SettingsModel._localization?.verificationMailSent, + "verifyMail": SettingsModel._localization?.verifyMail, + "verifyPhone": SettingsModel._localization?.verifyPhone, + "sms_code": SettingsModel._localization?.sms_code, + "check": SettingsModel._localization?.check, + "myProducts": SettingsModel._localization?.myProducts, + "firstStep": SettingsModel._localization?.firstStep, + "addPosts": SettingsModel._localization?.addPosts, + "addPostName": SettingsModel._localization?.addPostName, + "mark": SettingsModel._localization?.mark, + "manufacturer": SettingsModel._localization?.manufacturer, + "marketType": SettingsModel._localization?.marketType, + "country": SettingsModel._localization?.country, + "inMarket": SettingsModel._localization?.inMarket, + "outMarket": SettingsModel._localization?.outMarket, + "addPostDescription": SettingsModel._localization?.addPostDescription, + "price": SettingsModel._localization?.price, + "place": SettingsModel._localization?.place, + "quantity": SettingsModel._localization?.quantity, + "measure": SettingsModel._localization?.measure, + "currency": SettingsModel._localization?.currency, + "paymentTerm": SettingsModel._localization?.paymentTerm, + "deliveryTerm": SettingsModel._localization?.deliveryTerm, + "secondStep": SettingsModel._localization?.secondStep, + "packaging": SettingsModel._localization?.packaging, + "yes": SettingsModel._localization?.yes, + "no": SettingsModel._localization?.no, + "notANumber": SettingsModel._localization?.notANumber, + "selectImages": SettingsModel._localization?.selectImages, + "localImages": SettingsModel._localization?.localImages, + "selectAtLeastImage": SettingsModel._localization?.selectAtLeastImage, + "statusNote": SettingsModel._localization?.statusNote, + "draft": SettingsModel._localization?.draft, + "approved": SettingsModel._localization?.approved, + "denied": SettingsModel._localization?.denied, + "published": SettingsModel._localization?.published, + "none": SettingsModel._localization?.none, + "edit": SettingsModel._localization?.edit, + "approve": SettingsModel._localization?.approve, + "loadedImages": SettingsModel._localization?.loadedImages, + "legalizationNumber": SettingsModel._localization?.legalizationNumber, + "newPassword": SettingsModel._localization?.newPassword, + "newPasswordConfirmation": SettingsModel._localization?.newPasswordConfirmation, + "phoneConfirmation": SettingsModel._localization?.phoneConfirmation, + "smsSentInfo": SettingsModel._localization?.smsSentInfo, + "enterSmsCode": SettingsModel._localization?.enterSmsCode, + "confirm": SettingsModel._localization?.confirm, + "verifyPhoneWarning": SettingsModel._localization?.verifyPhoneWarning, + "verifyEmailWarning": SettingsModel._localization?.verifyEmailWarning, + "topUpBalance": SettingsModel._localization?.topUpBalance, + "selectPaymentMethod": SettingsModel._localization?.selectPaymentMethod, + "bankTransfer": SettingsModel._localization?.bankTransfer, + "selectFile": SettingsModel._localization?.selectFile, + "upload": SettingsModel._localization?.upload, + "fileNotSelected": SettingsModel._localization?.fileNotSelected, + "send": SettingsModel._localization?.send, + "reminderInfo": SettingsModel._localization?.reminderInfo, + "taxCode": SettingsModel._localization?.taxCode, + "manatAccount": SettingsModel._localization?.manatAccount, + "corrAccount": SettingsModel._localization?.corrAccount, + "bankAddress": SettingsModel._localization?.bankAddress, + "selectBank": SettingsModel._localization?.selectBank, + "transferAmount": SettingsModel._localization?.transferAmount, + "enterAmount": SettingsModel._localization?.enterAmount, + "searchShort": SettingsModel._localization?.searchShort, + "newsNotFount": SettingsModel._localization?.newsNotFount, + "errorOccurred": SettingsModel._localization?.errorOccurred, + "registeredDate": SettingsModel._localization?.registeredDate, + "filters": SettingsModel._localization?.filters, + "selectCategory": SettingsModel._localization?.selectCategory, + "selectUnit": SettingsModel._localization?.selectUnit, + "selectCurrency": SettingsModel._localization?.selectCurrency, + "selectPayment": SettingsModel._localization?.selectPayment, + "selectSendType": SettingsModel._localization?.selectSendType, + "selectCountry": SettingsModel._localization?.selectCountry, + "clear": SettingsModel._localization?.clear, + "apply": SettingsModel._localization?.apply, + "importPrice": SettingsModel._localization?.importPrice, + "quotes": SettingsModel._localization?.quotes, + "notFound": SettingsModel._localization?.notFound, + }[this] ?? + this; + } +} diff --git a/lib/models/settings/theme.dart b/lib/models/settings/theme.dart new file mode 100644 index 0000000..fb53138 --- /dev/null +++ b/lib/models/settings/theme.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; + +abstract class AppTheme { + static final _darkTheme = ThemeData.dark(); + static final _lightTheme = ThemeData.light(); + + static LinearGradient gradient(BuildContext context) { + var isDark = false; + + return LinearGradient( + begin: FractionalOffset.topCenter, + end: FractionalOffset.bottomCenter, + colors: [ + if (isDark) ...[ + Color.fromRGBO(43, 43, 43, 0), + Theme.of(context).backgroundColor, + Color.fromRGBO(60, 60, 60, 1) + ] else ...[ + Colors.grey.shade200.withOpacity(0.03), + Colors.white, + Colors.white + ] + ], + stops: !isDark ? null : [0.0, 0.6, 1.0]); + } + + static get appLightTheme => _lightTheme.copyWith( + accentColor: AppConstants.appColor, + primaryTextTheme: _lightTheme.textTheme.copyWith( + headline2: _lightTheme.textTheme.headline2?.copyWith( + fontSize: AppConstants.h2FontSize, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + headline3: TextStyle(fontSize: AppConstants.h3FontSize, color: Colors.grey)), + accentTextTheme: _lightTheme.textTheme.copyWith( + headline2: _lightTheme.textTheme.headline2?.copyWith( + fontSize: AppConstants.h2FontSize, + color: AppConstants.appColor, + fontWeight: FontWeight.w500, + ), + ), + textTheme: _lightTheme.textTheme.copyWith( + bodyText2: _lightTheme.textTheme.bodyText2?.copyWith(fontSize: AppConstants.b2FontSize, color: Colors.grey.shade800), + button: TextStyle( + color: Colors.white, + ), + headline1: _lightTheme.textTheme.headline2?.copyWith(fontSize: AppConstants.h1FontSize, color: Colors.grey.shade700, fontWeight: FontWeight.w500), + headline2: _lightTheme.textTheme.headline2?.copyWith(fontSize: AppConstants.h2FontSize, color: Colors.grey.shade700, fontWeight: FontWeight.w500), + ), + chipTheme: _lightTheme.chipTheme.copyWith( + backgroundColor: Colors.white, + side: BorderSide(color: Colors.grey.shade300), + padding: EdgeInsets.symmetric(vertical: 4.adaptedPx(), horizontal: 3.adaptedPx()), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 10.adaptedPx(), + ), + ), + labelStyle: _lightTheme.chipTheme.labelStyle?.copyWith( + fontSize: AppConstants.b3FontSize, + ), + ), + colorScheme: ColorScheme.fromSwatch().copyWith(secondary: AppConstants.appColor), + appBarTheme: AppBarTheme( + color: Colors.white, + foregroundColor: AppConstants.appColor, + iconTheme: IconThemeData(color: AppConstants.appColor), + textTheme: TextTheme(), + titleTextStyle: TextStyle(fontSize: AppConstants.appBarFontSize, fontWeight: FontWeight.bold)), + iconTheme: IconThemeData(color: Colors.white), + cardTheme: CardTheme(color: Colors.grey.shade300), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.all( + 2.adaptedPx(), + ), + ), + textStyle: MaterialStateProperty.all(TextStyle(fontSize: AppConstants.b2FontSize)), + overlayColor: MaterialStateProperty.all(AppConstants.appColor.withOpacity(0.2)), + foregroundColor: MaterialStateProperty.all(AppConstants.appColor)), + ), + backgroundColor: Colors.white, + // backgroundColor: Color.fromRGBO(242, 245, 255, 1), + buttonColor: AppConstants.appColor, + scaffoldBackgroundColor: AppConstants.scaffoldColorLight, + bottomNavigationBarTheme: _bottomAppBarTheme.copyWith(backgroundColor: AppConstants.backgroundColor)); + + static get appDarkTheme => _darkTheme.copyWith( + accentColor: AppConstants.appColor, + brightness: Brightness.dark, + cardColor: const Color.fromRGBO(38, 38, 38, 1), + colorScheme: ColorScheme.fromSwatch().copyWith(secondary: AppConstants.appColor), + appBarTheme: _darkTheme.appBarTheme.copyWith( + backgroundColor: Color.fromRGBO(25, 25, 25, 1), titleTextStyle: TextStyle(fontSize: AppConstants.appBarFontSize, fontWeight: FontWeight.w500)), + chipTheme: _darkTheme.chipTheme.copyWith( + labelStyle: _darkTheme.chipTheme.labelStyle?.copyWith(fontSize: AppConstants.b3FontSize, color: AppConstants.appColor), + backgroundColor: Colors.black26, + padding: EdgeInsets.symmetric(vertical: 4.adaptedPx(), horizontal: 3.adaptedPx()), + side: BorderSide(color: Colors.grey.shade500), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.adaptedPx())), + ), + buttonColor: Colors.grey.shade900, + accentTextTheme: _darkTheme.textTheme.copyWith( + headline2: _darkTheme.textTheme.headline2?.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w500, + ), + ), + primaryTextTheme: _darkTheme.textTheme.copyWith( + headline2: _darkTheme.textTheme.headline2?.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w500, + ), + headline3: TextStyle(fontSize: AppConstants.h3FontSize, color: Colors.grey)), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.all(2.adaptedPx())), + textStyle: MaterialStateProperty.all(TextStyle(fontSize: AppConstants.b2FontSize)), + overlayColor: MaterialStateProperty.all(AppConstants.appColor.withOpacity(0.2)), + foregroundColor: MaterialStateProperty.all(AppConstants.appColor)), + ), + cardTheme: CardTheme( + color: Colors.grey.shade500, + ), + textTheme: _darkTheme.textTheme.copyWith( + bodyText2: _darkTheme.textTheme.bodyText2?.copyWith(fontSize: AppConstants.b2FontSize), + button: TextStyle(color: AppConstants.appColor), + headline2: _darkTheme.textTheme.headline2?.copyWith(fontSize: AppConstants.h2FontSize, fontWeight: FontWeight.w500), + headline1: _darkTheme.textTheme.headline2?.copyWith(fontSize: AppConstants.h1FontSize, fontWeight: FontWeight.w500), + ), + backgroundColor: Color.fromRGBO(29, 29, 29, 1), + snackBarTheme: SnackBarThemeData(backgroundColor: Colors.white), + scaffoldBackgroundColor: Color.fromRGBO(29, 29, 29, 1), + bottomNavigationBarTheme: _bottomAppBarTheme.copyWith(backgroundColor: Colors.grey.shade900, selectedItemColor: Color.fromRGBO(25, 25, 25, 1))); + + static const _bottomAppBarTheme = BottomNavigationBarThemeData( + selectedItemColor: Color.fromRGBO(229, 234, 244, 100), + ); +} diff --git a/lib/models/tk_intl.dart b/lib/models/tk_intl.dart new file mode 100644 index 0000000..df39649 --- /dev/null +++ b/lib/models/tk_intl.dart @@ -0,0 +1,577 @@ +import 'dart:async'; + +import 'package:intl/intl.dart' as intl; +import 'package:intl/date_symbols.dart' as intl; +import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +const _tkDatePatterns = {"d":"d","E":"ccc","EEEE":"cccc","LLL":"LLL","LLLL":"LLLL","M":"L","Md":"dd/MM","MEd":"EEE, dd/MM","MMM":"LLL","MMMd":"d MMM","MMMEd":"EEE, d MMM","MMMM":"LLLL","MMMMd":"d MMMM","MMMMEEEEd":"EEEE, d MMMM","QQQ":"QQQ","QQQQ":"QQQQ","y":"y","yM":"MM/y","yMd":"dd/MM/y","yMEd":"EEE, dd/MM/y","yMMM":"MMM y","yMMMd":"d MMM y","yMMMEd":"EEE, d MMM y","yMMMM":"MMMM y","yMMMMd":"d MMMM y","yMMMMEEEEd":"EEEE, d MMMM y","yQQQ":"QQQ y","yQQQQ":"QQQQ y","H":"HH","Hm":"HH:mm","Hms":"HH:mm:ss","j":"HH","jm":"HH:mm","jms":"HH:mm:ss","jmv":"HH:mm v","jmz":"HH:mm z","jz":"HH z","m":"m","ms":"mm:ss","s":"s","v":"v","z":"z","zzzz":"zzzz","ZZZZ":"ZZZZ"}; + + +const _tkSymbols = { + "NAME":"tk", + "ERAS":["BC","AD"], + "ERANAMES":["Before Christ","Anno Domini"], + "NARROWMONTHS":["Ý","F","M","A","M","I","I","A","S","O","N","D"], + "STANDALONENARROWMONTHS":["Ý","F","M","A","M","I","I","A","S","O","N","D"], + "MONTHS":["Ýanwar","Fewral","Mart","Aprel","Maý","Iýun","Iýul","Awgust","Sentýabr","Oktýabr","Noýabr","Dekabr"], + "STANDALONEMONTHS":["Ýanwar","Fewral","Mart","Aprel","Maý","Iýun","Iýul","Awgust","Sentýabr","Oktýabr","Noýabr","Dekabr"], + "SHORTMONTHS":["Ýan","Few","Mar","Apr","Maý","Iýu","Iýl","Awg","Sen","Okt","Noý","Dek"], + "STANDALONESHORTMONTHS":["Ýan","Few","Mar","Apr","Maý","Iýu","Iýl","Awg","Sen","Okt","Noý","Dek"], + "WEEKDAYS":["Ýekşenbe","Duşenbe","Sişenbe","Çarşenbe","Penşenbe","Anna","Şenbe"], + "STANDALONEWEEKDAYS":["Ýekşenbe","Duşenbe","Sişenbe","Çarşenbe","Penşenbe","Anna","Şenbe"], + "SHORTWEEKDAYS":["Ýek","Duş","Siş","Çar","Pen","Ann","Şen"], + "STANDALONESHORTWEEKDAYS":["Ýek","Duş","Siş","Çar","Pen","Ann","Şen"], + "NARROWWEEKDAYS":["Ý","D","S","Ç","P","A","Ş"], + "STANDALONENARROWWEEKDAYS":["Ý","D","S","Ç","P","A","Ş"], + "SHORTQUARTERS":["K1","K2","K3","K4"], + "QUARTERS":["1-nji kwartal","2-nji kwartal","3-nji kwartal","4-nji kwartal"], + "AMPMS":["am","pm"], + "DATEFORMATS":["EEEE, d MMMM y","d MMMM y","d MMM y","dd/MM/y"], + "TIMEFORMATS":["HH:mm:ss zzzz","HH:mm:ss z","HH:mm:ss","HH:mm"], + "AVAILABLEFORMATS":null, + "FIRSTDAYOFWEEK":0, + "WEEKENDRANGE":[6], + "FIRSTWEEKCUTOFFDAY":3, + "DATETIMEFORMATS":["{1}, {0}","{1}, {0}","{1}, {0}","{1}, {0}"]}; + + + class _TkMaterialLocalizationsDelegate + extends LocalizationsDelegate { + const _TkMaterialLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => locale.languageCode == 'tk'; + + @override + Future load(Locale locale) async { + final String localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + // The locale (in this case `nn`) needs to be initialized into the custom + // date symbols and patterns setup that Flutter uses. + date_symbol_data_custom.initializeDateFormattingCustom( + locale: localeName, + patterns: _tkDatePatterns, + symbols: intl.DateSymbols.deserializeFromMap(_tkSymbols), + ); + + return SynchronousFuture( + TkMaterialLocalizations( + localeName: localeName, + // The `intl` library's NumberFormat class is generated from CLDR data + // (see https://github.com/dart-lang/intl/blob/master/lib/number_symbols_data.dart). + // Unfortunately, there is no way to use a locale that isn't defined in + // this map and the only way to work around this is to use a listed + // locale's NumberFormat symbols. So, here we use the number formats + // for 'en_US' instead. + decimalFormat: intl.NumberFormat('#,##0.###', 'en_US'), + twoDigitZeroPaddedFormat: intl.NumberFormat('00', 'en_US'), + // DateFormat here will use the symbols and patterns provided in the + // `date_symbol_data_custom.initializeDateFormattingCustom` call above. + // However, an alternative is to simply use a supported locale's + // DateFormat symbols, similar to NumberFormat above. + fullYearFormat: intl.DateFormat('y', localeName), + compactDateFormat: intl.DateFormat('yMd', localeName), + shortDateFormat: intl.DateFormat('yMMMd', localeName), + mediumDateFormat: intl.DateFormat('EEE, MMM d', localeName), + longDateFormat: intl.DateFormat('EEEE, MMMM d, y', localeName), + yearMonthFormat: intl.DateFormat('MMMM y', localeName), + shortMonthDayFormat: intl.DateFormat('MMM d'), + ), + ); + } + + @override + bool shouldReload(_TkMaterialLocalizationsDelegate old) => false; +} +// #enddocregion Delegate + +/// A custom set of localizations for the 'nn' locale. In this example, only +/// the value for openAppDrawerTooltip was modified to use a custom message as +/// an example. Everything else uses the American English (en_US) messages +/// and formatting. +class TkMaterialLocalizations extends GlobalMaterialLocalizations { + const TkMaterialLocalizations({ + String localeName = 'tk', + required intl.DateFormat fullYearFormat, + required intl.DateFormat compactDateFormat, + required intl.DateFormat shortDateFormat, + required intl.DateFormat mediumDateFormat, + required intl.DateFormat longDateFormat, + required intl.DateFormat yearMonthFormat, + required intl.DateFormat shortMonthDayFormat, + required intl.NumberFormat decimalFormat, + required intl.NumberFormat twoDigitZeroPaddedFormat, + }) : super( + localeName: localeName, + fullYearFormat: fullYearFormat, + compactDateFormat: compactDateFormat, + shortDateFormat: shortDateFormat, + mediumDateFormat: mediumDateFormat, + longDateFormat: longDateFormat, + yearMonthFormat: yearMonthFormat, + shortMonthDayFormat: shortMonthDayFormat, + decimalFormat: decimalFormat, + twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat, + ); + +// #docregion Getters + @override + String get moreButtonTooltip => r'Giňişleýin'; + + @override + String get firstPageTooltip => 'Baş sahypa'; + + @override + String get lastPageTooltip => 'Soňky sahypa'; + + @override + String get aboutListTileTitleRaw => r'$applicationName hakda'; + + @override + String get alertDialogLabel => r'Üns beriň'; +// #enddocregion Getters + + @override + String get anteMeridiemAbbreviation => r'AM'; + + @override + String get backButtonTooltip => r'Dolan'; + + @override + String get cancelButtonLabel => r'ÖÇÜR'; + + @override + String get closeButtonLabel => r'ÝAPMAK'; + + @override + String get closeButtonTooltip => r'Ýapmak'; + + @override + String get collapsedIconTapHint => r'Giňelt'; + + @override + String get continueButtonLabel => r'DOWAM ET'; + + @override + String get copyButtonLabel => r'GÖÇÜR'; + + @override + String get cutButtonLabel => r'KES'; + + @override + String get deleteButtonTooltip => r'Poz'; + + @override + String get dialogLabel => r'Dialog'; + + @override + String get drawerLabel => r'Nawigasiýa menýusy'; + + @override + String get expandedIconTapHint => r'Kiçelt'; + + @override + String get hideAccountsLabel => r'Akkaundy gizle'; + + @override + String get licensesPageTitle => r'Lisenziýalar'; + + @override + String get modalBarrierDismissLabel => r'Öçür'; + + @override + String get nextMonthTooltip => r'Indiki aý'; + + @override + String get nextPageTooltip => r'Indiki sahypa'; + + @override + String get okButtonLabel => r'OK'; + + @override + // A custom drawer tooltip message. + String get openAppDrawerTooltip => r'Custom Navigation Menu Tooltip'; + +// #docregion Raw + @override + String get pageRowsInfoTitleRaw => r'$firstRow–$lastRow of $rowCount'; + + @override + String get pageRowsInfoTitleApproximateRaw => + r'$firstRow–$lastRow of about $rowCount'; +// #enddocregion Raw + + @override + String get pasteButtonLabel => r'GIRIZ'; + + @override + String get popupMenuLabel => r'Popup menýu'; + + @override + String get postMeridiemAbbreviation => r'PM'; + + @override + String get previousMonthTooltip => r'Öňki aý'; + + @override + String get previousPageTooltip => r'Öňki sahypa'; + + @override + String get refreshIndicatorSemanticLabel => r'Täzele'; + + @override + String? get remainingTextFieldCharacterCountFew => null; + + @override + String? get remainingTextFieldCharacterCountMany => null; + + @override + String get remainingTextFieldCharacterCountOne => r'1 harp galdy'; + + @override + String get remainingTextFieldCharacterCountOther => + r'$remainingCount harplar galdy'; + + @override + String? get remainingTextFieldCharacterCountTwo => null; + + @override + String get remainingTextFieldCharacterCountZero => r'Hiç harp galmady'; + + @override + String get reorderItemDown => r'Aşak geçir'; + + @override + String get reorderItemLeft => r'Çepe geçir'; + + @override + String get reorderItemRight => r'Saga geçir'; + + @override + String get reorderItemToEnd => r'Soňuna geçir'; + + @override + String get reorderItemToStart => r'Başyna geçir'; + + @override + String get reorderItemUp => r'Yokary geçir'; + + @override + String get rowsPerPageTitle => r'Sahypa başyna hatar:'; + + @override + ScriptCategory get scriptCategory => ScriptCategory.englishLike; + + @override + String get searchFieldLabel => r'Gözle'; + + @override + String get selectAllButtonLabel => r'HEMMESINI SAÝLA'; + + @override + String? get selectedRowCountTitleFew => null; + + @override + String? get selectedRowCountTitleMany => null; + + @override + String get selectedRowCountTitleOne => r'1 element saýlandy'; + + @override + String get selectedRowCountTitleOther => r'$selectedRowCount elementler saýlandy'; + + @override + String? get selectedRowCountTitleTwo => null; + + @override + String get selectedRowCountTitleZero => r'Hiç element saýlanmady'; + + @override + String get showAccountsLabel => r'Akkauntlary aç'; + + @override + String get showMenuTooltip => r'Menýuny aç'; + + @override + String get signedInLabel => r'Ulgamda'; + + @override + String get tabLabelRaw => r'$tabCount içinden $tabIndex tab'; + + @override + TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.h_colon_mm_space_a; + + @override + String get timePickerHourModeAnnouncement => r'Sagady saýlaň'; + + @override + String get timePickerMinuteModeAnnouncement => r'Minudy saýlaň'; + + @override + String get viewLicensesButtonLabel => r'LISENZIÝALARY GÖRKEZ'; + + @override + List get narrowWeekdays => + const ['Ý', 'D', 'S', 'Ç', 'P', 'A', 'Ş']; + + @override + int get firstDayOfWeekIndex => 0; + + static const LocalizationsDelegate delegate = + _TkMaterialLocalizationsDelegate(); + + @override + String get calendarModeButtonLabel => r'Senenama geç'; + + @override + String get dateHelpText => r'mm/dd/yyyy'; + + @override + String get dateInputLabel => r'Senesini giriziň'; + + @override + String get dateOutOfRangeLabel => r'Aralykdan geçdi.'; + + @override + String get datePickerHelpText => r'SENESINI SAÝLAŇ'; + + @override + String get dateRangeEndDateSemanticLabelRaw => r'Gutarýan sene $fullDate'; + + @override + String get dateRangeEndLabel => r'Gutarýan sene'; + + @override + String get dateRangePickerHelpText => 'SENE ARALYGY'; + + @override + String get dateRangeStartDateSemanticLabelRaw => 'Başlangyç sene \$fullDate'; + + @override + String get dateRangeStartLabel => 'Başlangyç sene'; + + @override + String get dateSeparator => '/'; + + @override + String get dialModeButtonLabel => 'Belgi saýlaýyja geçmek'; + + @override + String get inputDateModeButtonLabel => 'Girizmege geç'; + + @override + String get inputTimeModeButtonLabel => 'Tekst girizmege geç'; + + @override + String get invalidDateFormatLabel => 'Ýalňyş format.'; + + @override + String get invalidDateRangeLabel => 'Ýalňyş format.'; + + @override + String get invalidTimeLabel => 'Dogry wagty giriziň'; + + @override + String get licensesPackageDetailTextOther => '\$licenseCount licenses'; + + @override + String get saveButtonLabel => 'ÝATDA SAKLA'; + + @override + String get selectYearSemanticsLabel => 'Ýyly saýlaň'; + + @override + String get timePickerDialHelpText => 'WAGTY SAÝLAŇ'; + + @override + String get timePickerHourLabel => 'Sagat'; + + @override + String get timePickerInputHelpText => 'WAGTY GIRIZIŇ'; + + @override + String get timePickerMinuteLabel => 'Minut'; + + @override + String get unspecifiedDate => 'Sene'; + + @override + String get unspecifiedDateRange => 'Sene aralygy'; + + @override + String get keyboardKeyAlt => ""; + + @override + String get keyboardKeyAltGraph => ""; + + @override + String get keyboardKeyBackspace => ""; + + @override + String get keyboardKeyCapsLock => ""; + + @override + String get keyboardKeyChannelDown => ""; + + @override + String get keyboardKeyChannelUp => ""; + + @override + String get keyboardKeyControl => ""; + + @override + String get keyboardKeyDelete => ""; + + @override + String get keyboardKeyEisu => ""; + + @override + String get keyboardKeyEject => ""; + + @override + String get keyboardKeyEnd => ""; + + @override + String get keyboardKeyEscape => ""; + + @override + String get keyboardKeyFn => ""; + + @override + String get keyboardKeyHangulMode => ""; + + @override + String get keyboardKeyHanjaMode => ""; + + @override + String get keyboardKeyHankaku => ""; + + @override + String get keyboardKeyHiragana => ""; + + @override + String get keyboardKeyHiraganaKatakana => ""; + + @override + String get keyboardKeyHome => ""; + + @override + String get keyboardKeyInsert => ""; + + @override + String get keyboardKeyKanaMode => ""; + + @override + String get keyboardKeyKanjiMode => ""; + + @override + String get keyboardKeyKatakana => ""; + + @override + String get keyboardKeyMeta => ""; + + @override + String get keyboardKeyMetaMacOs => ""; + + @override + String get keyboardKeyMetaWindows => ""; + + @override + String get keyboardKeyNumLock => ""; + + @override + String get keyboardKeyNumpad0 => ""; + + @override + String get keyboardKeyNumpad1 => ""; + + @override + String get keyboardKeyNumpad2 => ""; + + @override + String get keyboardKeyNumpad3 => ""; + + @override + String get keyboardKeyNumpad4 => ""; + + @override + String get keyboardKeyNumpad5 => ""; + + @override + String get keyboardKeyNumpad6 => ""; + + @override + String get keyboardKeyNumpad7 => ""; + + @override + String get keyboardKeyNumpad8 => ""; + + @override + String get keyboardKeyNumpad9 => ""; + + @override + String get keyboardKeyNumpadAdd => ""; + + @override + String get keyboardKeyNumpadComma => ""; + + @override + String get keyboardKeyNumpadDecimal => ""; + + @override + String get keyboardKeyNumpadDivide => ""; + + @override + String get keyboardKeyNumpadEnter => ""; + + @override + String get keyboardKeyNumpadEqual => ""; + + @override + String get keyboardKeyNumpadMultiply => ""; + + @override + String get keyboardKeyNumpadParenLeft => ""; + + @override + String get keyboardKeyNumpadParenRight => ""; + + @override + String get keyboardKeyNumpadSubtract => ""; + + @override + String get keyboardKeyPageDown => ""; + + @override + String get keyboardKeyPageUp => ""; + + @override + String get keyboardKeyPower => ""; + + @override + String get keyboardKeyPowerOff => ""; + + @override + String get keyboardKeyPrintScreen => ""; + + @override + String get keyboardKeyRomaji => ""; + + @override + String get keyboardKeyScrollLock => ""; + + @override + String get keyboardKeySelect => ""; + + @override + String get keyboardKeySpace => ""; + + @override + String get keyboardKeyZenkaku => ""; + + @override + String get keyboardKeyZenkakuHankaku => ""; +} \ No newline at end of file diff --git a/lib/models/transactions/serializer.dart b/lib/models/transactions/serializer.dart new file mode 100644 index 0000000..e0cc698 --- /dev/null +++ b/lib/models/transactions/serializer.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/transactions/transaction.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/material.dart'; + +class TransactionSerializer extends RemoteCategory{ + + TransactionSerializer() : super( + data: { + 'id':-4, + } + ); + + + @override + Future> getPosts(BuildContext context, int page) async{ + var user = AppUserManager.of(context).dataSync; + var transactions = await FutureGetList( + postUri(context, page), + parser: (res){ + final map = jsonDecode(res.body); + final products = map?['data'] as List; + return products.map((data) => postBuilderDelegate(data)).toList(); + } + ).fetch({ + 'Authorization':'Bearer ${user.token}' + }); + return [...transactions]; + } + + @override + get copy => TransactionSerializer(); + + @override + String get name => 'top_up_history'.translation; + + @override + postBuilderDelegate(Map data) { + return Transaction(data: data); + } + + @override + Uri postUri(context, int page) { + return baseUrl( + path: kApiPath + '/transactions', + queryParameters: { + 'page':page.toString() + } + ); + } + +} \ No newline at end of file diff --git a/lib/models/transactions/transaction.dart b/lib/models/transactions/transaction.dart new file mode 100644 index 0000000..20407ce --- /dev/null +++ b/lib/models/transactions/transaction.dart @@ -0,0 +1,23 @@ +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/helpers.dart'; + +class Transaction extends Post { + Transaction({required Map data}) : super(data); + + double get _amountDouble => double.tryParse('${jSON["amount"]}') ?? 0.0; + + double get amountDouble => _amountDouble; + + double get _amount => double.tryParse('${jSON["amount"]}') ?? 0.0; + + String get amount => priceFormatter(_amount); + + DateTime? get _date => DateTime.tryParse('${jSON["updated_at"]}'); + + String get date => safeValueDate(_date); + + String get description => jSON['description'] ?? ""; + + String get stateName => jSON['state_${"backendCode".translation}']; +} diff --git a/lib/models/translationModel.dart b/lib/models/translationModel.dart new file mode 100644 index 0000000..e4eb057 --- /dev/null +++ b/lib/models/translationModel.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:birzha/core/orm/orm.dart'; + +class TranslationModel extends Orm { + TranslationModel(Map data) : super(data); + + String translationOf(String field, String defaultValue) { + return this.attributeData?[field] ?? defaultValue; + } + + @override + String get primaryKeyField => "locale"; + + Map? get attributeData => jSON['attribute_data'] == null || jSON['attribute_data'].toString().isEmpty ? null : jsonDecode(jSON['attribute_data']); +} diff --git a/lib/models/user/simpleUser.dart b/lib/models/user/simpleUser.dart new file mode 100644 index 0000000..8ff2738 --- /dev/null +++ b/lib/models/user/simpleUser.dart @@ -0,0 +1,10 @@ +part of user_lib; + +class SimpleUser extends User{ + + SimpleUser._(Map data) : super({...data}); + + @override + SimpleUser get copy => SimpleUser._({...jSON}); + +} \ No newline at end of file diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart new file mode 100644 index 0000000..c47b218 --- /dev/null +++ b/lib/models/user/user.dart @@ -0,0 +1,408 @@ +library user_lib; + +import 'dart:convert'; + +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:birzha/services/validator.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:birzha/core/orm/orm.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'simpleUser.dart'; + +abstract class User extends Orm with CopyAbleMixin { + User(Map jSON) : super(jSON); + + factory User.spawnFromPrefs(SharedPreferences prefs) { + var dataString = prefs.getString('user'); + Map? data; + try { + if (dataString != null) data = jsonDecode(dataString); + } catch (e) {} + + if (data == null) + return SampleUser(data: {}); + else { + debugPrint('sampleUser data: $data'); + return SimpleUser._(data); + } + } + + List get smsVerificationMetaData => [ + TextInputMetaData( + name: 'sms_code'.translation, + label: 'sms_code'.translation, + hint: 'XXXX', + formatters: [LengthLimitingTextInputFormatter(4), FilteringTextInputFormatter.digitsOnly], + validation: Validation(conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ]), + key: 'sms_code', + ) + ]; + + List get loginMetaData => [ + TextInputMetaData( + name: 'dialCode', + label: 'dialCode'.translation, + pickerMode: (context) async { + var dialCode = await showCountryCodesSheet(context); + if (dialCode == null) { + return null; + } else { + return dialCode.toString(); + } + }, + validation: Validation(conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ]), + key: 'dial_code'), + TextInputMetaData( + name: 'phone'.translation, + label: 'phone'.translation, + type: TextInputType.phone, + validation: Validation(conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ]), + key: 'username'), + TextInputMetaData( + name: 'password'.translation, + label: 'password'.translation, + password: true, + validation: Validation(conditions: [(_inp) => _inp.isEmpty ? 'empty'.translation : null]), + key: 'password', + ), + ]; + + List get registerMetaData => [ + TextInputMetaData( + name: 'yourEmail'.translation, + label: 'yourEmail'.translation, + type: TextInputType.emailAddress, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + (_inp) { + if (Validation.emailValidator(_inp)) + return null; + else + return 'email_error'.translation; + } + ], + ), + key: 'email', + ), + TextInputMetaData( + name: 'dialCode', + label: 'dialCode'.translation, + pickerMode: (context) async { + var dialCode = await showCountryCodesSheet(context); + if (dialCode == null) { + return null; + } else { + return dialCode.toString(); + } + }, + validation: Validation(conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ]), + key: 'dial_code'), + TextInputMetaData( + name: 'phone'.translation, + label: 'phone'.translation, + type: TextInputType.emailAddress, + validation: Validation( + conditions: [(_inp) => _inp.isEmpty ? 'empty'.translation : null], + ), + key: 'username', + ), + TextInputMetaData( + name: 'yourPassword'.translation, + label: 'yourPassword'.translation, + password: true, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'password', + ), + ]; + + List get updateMetaData => [ + // name + TextInputMetaData( + name: 'first_name'.translation, + label: 'first_name'.translation, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'name', + ), + + // surname + TextInputMetaData( + name: 'last_name'.translation, + label: 'last_name'.translation, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'surname', + ), + + //email + TextInputMetaData( + name: 'yourEmail'.translation, + label: 'yourEmail'.translation, + type: TextInputType.emailAddress, + showSuffix: true, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + (_inp) { + if (Validation.emailValidator(_inp)) + return null; + else + return 'email_error'.translation; + } + ], + ), + key: 'email', + ), + + // phone number + TextInputMetaData( + name: 'phone'.translation, + label: 'phone'.translation, + type: TextInputType.number, + readOnly: true, + filled: true, + showSuffix: true, + validation: Validation( + conditions: [], + ), + key: 'username', + ), + + // company + TextInputMetaData( + name: 'iu_company'.translation, + label: 'iu_company'.translation, + validation: Validation( + conditions: [ + // (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'company', + ), + + // legalization number + TextInputMetaData( + name: 'legalizationNumber'.translation, + label: 'legalizationNumber'.translation, + type: TextInputType.emailAddress, + validation: Validation( + conditions: [ + // (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'zip', + ), + + // new password confirmation + TextInputMetaData( + name: 'newPassword'.translation, + label: 'newPassword'.translation, + password: true, + validation: Validation( + conditions: [ + // (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'password', + ), + + // new password confirmation + TextInputMetaData( + name: 'newPasswordConfirmation'.translation, + label: 'newPasswordConfirmation'.translation, + password: true, + validation: Validation( + conditions: [ + // (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'password_confirmation', + ), + ]; + + List get addPostMetaData => [ + TextInputMetaData( + name: 'addPostName'.translation + ' (EN)', + label: 'addPostName'.translation + ' (EN)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'name_en'), + TextInputMetaData( + name: 'addPostName'.translation + ' (RU)', + label: 'addPostName'.translation + ' (RU)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'name_ru'), + TextInputMetaData( + name: 'addPostName'.translation + ' (TM)', + label: 'addPostName'.translation + ' (TM)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'name_tm'), + TextInputMetaData( + name: 'mark'.translation, + label: 'mark'.translation, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'mark'), + TextInputMetaData( + name: 'manufacturer'.translation, + label: 'manufacturer'.translation, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'manufacturer'), + ]; + + List get addMoreMetaData => [ + TextInputMetaData( + name: 'addPostDescription'.translation + ' (EN)', + label: 'addPostDescription'.translation + ' (EN)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'description_en'), + TextInputMetaData( + name: 'addPostDescription'.translation + ' (RU)', + label: 'addPostDescription'.translation + ' (RU)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'description_ru'), + TextInputMetaData( + name: 'addPostDescription'.translation + ' (TM)', + label: 'addPostDescription'.translation + ' (TM)', + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'description_tm'), + TextInputMetaData( + name: 'price'.translation, + label: 'price'.translation, + validation: Validation( + conditions: [ + (_inp) => double.tryParse(_inp) == null ? 'notANumber'.translation : null, + ], + ), + formatters: [FilteringTextInputFormatter.allow(RegExp('[0-9.]'))], + key: 'price'), + TextInputMetaData( + name: 'place'.translation, + label: 'place'.translation, + validation: Validation( + conditions: [ + (_inp) => _inp.isEmpty ? 'empty'.translation : null, + ], + ), + key: 'place'), + TextInputMetaData( + name: 'quantity'.translation, + label: 'quantity'.translation, + validation: Validation( + conditions: [ + (_inp) => double.tryParse(_inp) == null ? 'notANumber'.translation : null, + ], + ), + formatters: [FilteringTextInputFormatter.allow(RegExp('[0-9.]'))], + key: 'quantity'), + ]; + + String? get token => jSON['token']; + + bool get isRegistered => token != null; + + bool get isEmailVerified => isRegistered && boolParser(jSON['email_verified']); + + bool get isPhoneVerified => isRegistered && boolParser(jSON['phone_verified']); + + @override + int get primaryKey => jSON[primaryKeyField] ?? -1; + + @override + String get primaryKeyField => 'id'; + + List get keysAllowedToStore => [ + 'id', + 'name', + 'surname', + 'token', + 'username', + 'company', + 'zip', + 'email', + 'email_verified', + 'phone_verified', + ]; + + Map get storingData => {...jSON}..removeWhere( + (key, value) => !keysAllowedToStore.contains(key), + ); +} + +class SampleUser extends User { + SampleUser({required Map data}) : super(data); + + User castToRealUser() { + return SimpleUser._({...jSON}); + } + + @override + get copy => SampleUser(data: {...jSON}); +} + +class Vendor extends User { + Vendor({required Map data}) : super(data); + + String get name => jSON['name'] ?? ""; + String get surname => jSON['surname'] ?? ""; + String get email => jSON['email'] ?? ""; + String get phone => jSON['username'] ?? ""; + + @override + User get copy => Vendor(data: {...jSON}); +} diff --git a/lib/models/user/userManager.dart b/lib/models/user/userManager.dart new file mode 100644 index 0000000..7de709b --- /dev/null +++ b/lib/models/user/userManager.dart @@ -0,0 +1,629 @@ +import 'dart:convert'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/models/chatroom/chatroom.dart'; +import 'package:birzha/models/products/composableProduct.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/imageUpload.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:birzha/services/requests.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:http/http.dart' as http; +import 'package:birzha/models/exceptions/exception.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart' as picker; +import 'dart:async'; +import 'dart:io'; +import 'package:async/async.dart'; + +class AppUserManager extends Manager { + static AppUserManager of(BuildContext context, {bool listen = false}) { + return Provider.of(context, listen: listen); + } + + AppUserManager(SharedPreferences prefs) : super(User.spawnFromPrefs(prefs)); + + void checkCode(BuildContext context, String code) { + addTask( + Task( + computation: () async { + try { + var response = await _checkSmsCode(dataSync.token ?? "", code); + + var decoded = jsonDecode(response.body); + + if (decoded is Map) { + final success = decoded["status"]; + final message = decoded['message']?['backendCode'.translation]; + + if (!success && message != null && message is String) { + throw MessageException(message); + } else if (!success && message is! String) { + throw OtherException(); + } + } + + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + syncAccount(context); + }); + + return dataSync.copy; + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'sms_verify', + ), + ); + } + + void syncAccount(BuildContext context) { + addTask( + Task( + computation: () async { + try { + var newSample = dataSync.copy; + if (newSample.isRegistered) { + var userAccountResponse = await _getAccountRequest(newSample.token!); + var decoded = jsonDecode(userAccountResponse.body); + if (decoded['me'] != null) newSample.jSON = {...newSample.jSON, ...decoded['me']}; + } + return newSample; + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'sync', + ), + ); + } + + void sendSmsCode(BuildContext context, VoidCallback onSuccess) { + addTask( + Task( + computation: () async { + try { + debugPrint('dataSync.token: ${dataSync.token}'); + + var response = await _sendSmsCode(dataSync.token ?? ""); + + var decoded = jsonDecode(response.body); + bool success = decoded['result'] == 0 || decoded['result'] == '0'; + if (success) { + onSuccess(); + return dataSync.copy; + } else { + var message = decoded['message']?['backendCode'.translation]; + if (message != null && message is String) { + throw MessageException(message); + } else { + throw OtherException(); + } + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'send_sms'), + ); + } + + void verifyMail(BuildContext context) { + addTask(Task( + computation: () async { + try { + print('backendCode'.translation); + String? message; + var response = await _verifyEmailRequest(dataSync.token ?? ""); + var decoded = jsonDecode(response.body); + print(decoded); + if (decoded is String) { + message = decoded; + } + showSnackBar( + context, + content: message ?? 'verificationMailSent'.translation, + backgroundColor: Colors.blue, + textColor: Colors.white, + duration: const Duration(seconds: 10), + ); + return dataSync.copy; + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'verify_mail')); + } + + void login(BuildContext context, SampleUser user) { + addTask( + Task( + computation: () async { + try { + var response = await _loginRequest(user); + var decoded = jsonDecode(response.body); + if (decoded['user'] == null) + throw AppExceptions.recognizer(decoded); + else { + var newSample = SampleUser(data: {...decoded['user'], 'token': decoded['token']}); + if (newSample.isRegistered) { + var userAccountResponse = await _getAccountRequest(newSample.token!); + var decoded = jsonDecode(userAccountResponse.body); + if (decoded['me'] != null) newSample.jSON = {...newSample.jSON, ...decoded['me']}; + } + return newSample.castToRealUser(); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'login', + ), + ); + } + + void register(BuildContext context, SampleUser user) { + addTask(Task( + computation: () async { + try { + var response = await _registerRequest(user); + var decoded = jsonDecode(response.body); + if (decoded['user'] == null) + throw AppExceptions.recognizer(decoded); + else { + var newSample = SampleUser(data: {...decoded['user'], 'token': decoded['token']}); + if (newSample.isRegistered) { + var userAccountResponse = await _getAccountRequest(newSample.token!); + var decoded = jsonDecode(userAccountResponse.body); + if (decoded['me'] != null) newSample.jSON = {...newSample.jSON, ...decoded['me']}; + } + return newSample.castToRealUser(); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'register')); + } + + void logout() async { + addTask(Task( + computation: () { + return SynchronousFuture(SampleUser(data: {})); + }, + key: 'logout')); + } + + void update(BuildContext context, SampleUser user) { + addTask( + Task( + computation: () async { + try { + debugPrint('sample user $user'); + + var response = await _updateRequest(user, dataSync.token!); + + debugPrint('response: $response'); + + var decoded = jsonDecode(response.body); + debugPrint('decoded: $decoded'); + + if (decoded['me'] == null) + throw AppExceptions.recognizer(decoded); + else { + var newSample = SampleUser(data: { + ...dataSync.jSON, + ...decoded['me'], + }); + + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + syncAccount(context); + }); + + return newSample.castToRealUser(); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'update', + ), + ); + } + + void balanceUp(BuildContext context, String cardType, double amount) { + addTask( + Task( + computation: () async { + try { + var response = await _balanceUpRequest(dataSync.token!, cardType, amount); + var decoded = jsonDecode(response.body); + + var status = decoded['formUrl'] != null && decoded['formUrl'] is String; + if (!status) + throw AppExceptions.recognizer(decoded); + else + linkLauncher(decoded['formUrl']); + + return dataSync.copy; + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'balanceUp', + ), + ); + } + + void uploadBill(BuildContext context, picker.PlatformFile? file) { + addTask( + Task( + computation: () async { + try { + String? message; + if (file != null) { + debugPrint('file ${file.name}'); + message = await uploadImage( + baseUrl(path: kApiPath + '/balance_update'), + dataSync, + file, //fileResult!.files.first, + 'bank_file', + { + 'type': 'bank', + }, + onUploadProgressCallback: (a, b) { + debugPrint('a $a'); + debugPrint('b $b'); + }, + ); + } + + debugPrint('message $message'); + if (message != null) { + showSnackBar( + context, + content: message, + backgroundColor: Colors.blue, + textColor: Colors.white, + duration: const Duration(seconds: 3), + ); + } + + return dataSync.copy; + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'uploadBill'), + ); + } + + void buyFromSeller(BuildContext navigatorContext, Vendor vendor, void Function(Chatroom room) onSuccess) { + addTask(Task( + computation: () async { + try { + if (!dataSync.isRegistered) { + Tabnavigator.maybeOf(navigatorContext)?.changePage(2); + return dataSync.copy; + } + await Future.delayed(Duration(seconds: 3)); + var response = await _initChatRequest(dataSync.token ?? "", vendor); + var data = jsonDecode(response.body); + var isSuccess = data['data'] != null; + if (isSuccess) { + onSuccess(Chatroom.init(data: { + ...data['data'], + 'vendor': {...vendor.jSON} + })); + return dataSync.copy; + } else { + throw AppExceptions.recognizer(data); + } + } catch (ex) { + AppExceptions.exceptionHandler(navigatorContext, ex); + throw ex; + } + }, + key: 'buy')); + } + + void composeStep1(BuildContext context, List sendKeys) { + addTask(Task( + computation: () async { + try { + var composable = ComposableProduct(); + await Future.delayed(const Duration(seconds: 3)); + var response = await _postCompositionRequest(composable, dataSync.token ?? "", sendKeys); + var decoded = jsonDecode(response.body); + var success = decoded["status_code"] == 200 || decoded["status_code"] == '200'; + if (success) { + int? id = decoded['data']?['product']?['id']; + ComposableProduct().saveProgress(id); + return dataSync.copy; + } else { + throw AppExceptions.recognizer(decoded); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'compose_1')); + } + + void composeStep2(BuildContext context, List sendKeys) { + addTask(Task( + computation: () async { + try { + var composable = ComposableProduct(); + if ((composable.localImages.isEmpty && composable.primaryKey == null) || (composable.localImages.isEmpty && composable.remoteImages.isEmpty)) { + throw MessageException('selectAtLeastImage'.translation); + } + var response = await _postMoreRequest(composable, dataSync.token ?? "", sendKeys); + var byteArray = await response.stream.toBytes(); + var stringBody = utf8.decode(byteArray); + print(stringBody); + var decoded = jsonDecode(stringBody); + var success = decoded["status_code"] == 200 || decoded["status_code"] == '200'; + if (success) { + ComposableProduct().jSON.clear(); + return dataSync.copy; + } else { + throw AppExceptions.recognizer(decoded); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'compose_2')); + } + + void deletePost(BuildContext context, MyProduct product) { + addTask(Task( + computation: () async { + try { + var response = await _deletePost(product, dataSync.token ?? ""); + var decoded = jsonDecode(response.body); + var success = decoded["status_code"] == 200 || decoded["status_code"] == '200'; + if (success) { + return dataSync.copy; + } else { + throw AppExceptions.recognizer(decoded); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'delete_post')); + } + + void deleteImage(BuildContext context, ComposableProduct product, RemoteImageModel image, void Function() onSuccess) { + addTask(Task( + computation: () async { + var navContext = Navigator.of(context).context; + try { + var response = await _deleteImage(product, image, dataSync.token ?? ""); + print(response.body); + var decoded = jsonDecode(response.body); + var success = decoded["status_code"] == 200 || decoded["status_code"] == '200'; + if (success) { + onSuccess(); + return dataSync.copy; + } else { + throw AppExceptions.recognizer(decoded); + } + } catch (ex) { + AppExceptions.exceptionHandler(navContext, ex); + throw ex; + } + }, + key: 'delete_image')); + } + + void publishPost(BuildContext context, MyProduct product) { + addTask(Task( + computation: () async { + try { + var response = await _publish(product, dataSync.token ?? ""); + var decoded = jsonDecode(response.body); + var success = decoded["status_code"] == 200 || decoded["status_code"] == '200'; + if (success) { + return dataSync.copy; + } else { + throw AppExceptions.recognizer(decoded); + } + } catch (ex) { + AppExceptions.exceptionHandler(context, ex); + throw ex; + } + }, + key: 'publish_post')); + } + + Map _statusTable = {}; + + TaskStatus getStatusByKey(String key) { + return _statusTable[key] ?? TaskStatus.None; + } + + @override + Future destroyTask(String taskId) async { + _statusTable.remove(taskId); + return super.destroyTask(taskId); + } + + @override + Future valueListener(newValue) async { + super.valueListener(newValue); + var prefs = await SharedPreferences.getInstance(); + if (newValue.jSON.isEmpty) + prefs.remove('user'); + else + await prefs.setString('user', jsonEncode({...newValue.storingData})); + } + + @override + void listenerCallBack(TaskResult result, String taskKey) { + _statusTable[taskKey] = result.status; + notifyListeners(); + } +} + +Future _loginRequest(SampleUser user) { + return http.post( + baseUrl(path: 'api' + '/login'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: jsonEncode( + {...user.jSON}, + ), + ); +} + +Future _registerRequest(SampleUser user) { + return http.post( + baseUrl(path: 'api' + '/signup'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: jsonEncode( + {...user.jSON}, + ), + ); +} + +Future _updateRequest(SampleUser user, String token) { + return http.post( + baseUrl(path: 'api' + '/me'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonEncode( + {...user.jSON}, + ), + ); +} + +Future _postCompositionRequest(ComposableProduct composition, String token, List keys) { + return http.post( + baseUrl(path: kApiPath + '/products'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonEncode( + {...composition.jSON}..removeWhere((key, value) => !keys.contains(key)), + ), + ); +} + +Future _deletePost(MyProduct composition, String token) { + return http.delete(baseUrl(path: kApiPath + '/my-products/${composition.primaryKey}'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}); +} + +Future _deleteImage(ComposableProduct product, RemoteImageModel image, String token) { + return http.delete(baseUrl(path: kApiPath + '/products/${product.primaryKey}/image-delete/${image.id}'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}); +} + +Future _publish(MyProduct composition, String token) { + return http.post(baseUrl(path: kApiPath + '/products/${composition.primaryKey}/publish'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}); +} + +Future _postMoreRequest(ComposableProduct composition, String token, List keys) { + var multipart = http.MultipartRequest('POST', baseUrl(path: kApiPath + '/products/${composition.primaryKey}')); + var images = composition.localImages; + var files = []; + for (var image in images) { + var file = File(image.primaryKey); + var stream = http.ByteStream(DelegatingStream(file.openRead())); + var multipartFile = http.MultipartFile('new_img[]', stream, file.lengthSync(), filename: image.primaryKey.split('/').last); + files.add(multipartFile); + } + if (files.isNotEmpty) { + multipart.files.addAll(files); + } + multipart.fields.addAll({ + for (var key in composition.jSON.keys.where((element) => keys.contains(element)).where((element) => element != 'new_img')) + key: composition.jSON[key].toString(), + }); + for (var i = 0; i < composition.remoteImages.length; i++) { + multipart.fields['old_img[$i]'] = composition.remoteImages[i].primaryKey; + } + multipart.headers.addAll( + {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}, + ); + return multipart.send(); +} + +Future _balanceUpRequest(String token, String cardType, double amount) { + return http.post( + baseUrl(path: kApiPath + '/balance_update'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonEncode( + { + 'type': 'online', + 'amount': amount.toString(), + 'card_type': cardType, + }, + ), + ); +} + +Future _getAccountRequest(String token) { + return http.get( + baseUrl(path: 'api' + '/me'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); +} + +Future _initChatRequest(String token, Vendor vendor) { + return http.post( + baseUrl(path: kApiPath + '/messages/initialize-chatting/' + vendor.primaryKey.toString()), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}, + ); +} + +Future _verifyEmailRequest(String token) { + print(jsonEncode({'locale': 'backendCode'.translation})); + return http.post(baseUrl(path: kApiPath + '/send-email-verification-link'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}, + body: jsonEncode({'locale': 'backendCode'.translation})); +} + +Future _sendSmsCode(String token) { + return http.post(baseUrl(path: kApiPath + '/send-sms-code'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}); +} + +Future _checkSmsCode(String token, String code) { + return http.post(baseUrl(path: kApiPath + '/check-sms-code'), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'}, body: jsonEncode({"sms_code": code})); +} diff --git a/lib/new/api/a.rest b/lib/new/api/a.rest new file mode 100644 index 0000000..c6161a4 --- /dev/null +++ b/lib/new/api/a.rest @@ -0,0 +1,10 @@ +@url = https://tmex.gov.tm/api/v1/news?per_page=2&locale=tm +@contentType = application/json + + +### 1. get news +GET {{ url }} HTTP/1.1 + +### 1. get news +@url = https://tmex.gov.tm/api/v1/ +GET {{ url }} HTTP/1.1 \ No newline at end of file diff --git a/lib/new/api/bank.dart b/lib/new/api/bank.dart new file mode 100644 index 0000000..e5ed846 --- /dev/null +++ b/lib/new/api/bank.dart @@ -0,0 +1,60 @@ +import 'package:birzha/new/models/bank.dart'; +import 'package:birzha/new/models/bank_info.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../utils/constants.dart'; +import '../utils/http_util.dart'; + +class BankApi { + static String className = 'BankApi'; + static Future getBankInfo() async { + final String fnName = 'getBankInfo'; + + SharedPreferences prefs = await SharedPreferences.getInstance(); + + final String locale = prefs.getString('language') ?? 'tm'; + + try { + final Map params = { + 'locale': locale == 'tk' ? 'tm' : locale, + }; + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'api/v1/bank-info'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return BankInfoModel.fromJson(response); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + return null; + } + } + + static Future?> getBankTypes() async { + final String fnName = 'getBankTypes'; + + SharedPreferences prefs = await SharedPreferences.getInstance(); + + final String locale = prefs.getString('language') ?? 'tm'; + + try { + final Map params = { + 'locale': locale == 'tk' ? 'tm' : locale, + }; + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'api/v1/bank-types'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return BankType.listFromJson(response as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + return null; + } + } +} diff --git a/lib/new/api/news.dart b/lib/new/api/news.dart new file mode 100644 index 0000000..5e4d006 --- /dev/null +++ b/lib/new/api/news.dart @@ -0,0 +1,36 @@ +import 'package:birzha/new/models/news.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../utils/constants.dart'; +import '../utils/http_util.dart'; + +class NewsApi { + static String className = 'NewsApi'; + static Future> get(int page) async { + final String fnName = 'get'; + + SharedPreferences prefs = await SharedPreferences.getInstance(); + + final String locale = prefs.getString('language') ?? 'tm'; + + try { + final Map params = { + 'page': '$page', + 'per_page': '10', + 'locale': locale == 'tk' ? 'tm' : locale, + }; + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'api/v1/news'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return NewsModel.listFromJson(response['data'] as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + throw e; + } + } +} diff --git a/lib/new/api/sort.dart b/lib/new/api/sort.dart new file mode 100644 index 0000000..9df390c --- /dev/null +++ b/lib/new/api/sort.dart @@ -0,0 +1,124 @@ +import 'package:birzha/new/models/category_filter.dart'; +import 'package:birzha/new/models/export.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../models/import.dart'; +import '../models/sort_group.dart'; +import '../utils/constants.dart'; +import '../utils/http_util.dart'; +import '../utils/locale.dart'; + +class SortApi { + static String className = 'SortApi'; + + static String metaText = ''; + + static Future> getGroups(String type) async { + final String fnName = 'getGroups'; + + SharedPreferences prefs = await SharedPreferences.getInstance(); + + final String locale = prefs.getString('language') ?? 'tm'; + + try { + final Map params = { + 'type': '$type', + 'locale': locale == 'tk' ? 'tm' : locale, + }; + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'app/api/groups'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return SortGroupModel.listFromJson(response as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + return []; + } + } + + static Future> getExports(Map params) async { + final String fnName = 'getExports'; + + try { + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'app/api/exports'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + metaText = response['meta_text']; + + return ExportModel.listFromJson(response['exports'] as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + throw e; + } + } + + static Future> getImports(Map params) async { + final String fnName = 'getImports'; + + try { + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'app/api/imports'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return ImportModel.listFromJson(response['imports'] as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + throw e; + } + } + + static Future> getOtherFilters(Map params) async { + final String fnName = 'getOtherFilters'; + + try { + String locale = await getLocale(); + + params.addAll({ + 'locale': locale, + }); + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'app/api/other-filters'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return response.cast(); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + throw e; + } + } + + static Future> getCategoryFilters() async { + final String fnName = 'getCategoryFilters'; + + try { + String locale = await getLocale(); + + final Map params = { + 'locale': locale, + }; + + debugPrint('class: $className, method: $fnName , params: $params'); + + final String path = Constants.BASE_URL + 'app/api/categories'; + + final response = await HttpUtil().get(path: path, queryParameters: params); + + return CategoryFilterModel.listFromJson(response as List); + } catch (e) { + debugPrint('ERROR: class: $className, method: $fnName, error: $e '); + throw e; + } + } +} diff --git a/lib/new/global/form_field_decoration.dart b/lib/new/global/form_field_decoration.dart new file mode 100644 index 0000000..def6798 --- /dev/null +++ b/lib/new/global/form_field_decoration.dart @@ -0,0 +1,87 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../themes/colors.dart'; + +class MyTextFormField extends StatelessWidget { + final TextEditingController controller; + final String label; + final String? hintText; + final int maxLines; + final bool obscureText; + final TextInputType inputType; + final Widget? suffix; + final List? inputFormatters; + final Function(String value)? onChangeCallback; + + MyTextFormField({ + required this.controller, + required this.label, + this.hintText, + this.maxLines = 1, + this.obscureText = false, + this.inputType = TextInputType.text, + this.suffix, + this.inputFormatters, + this.onChangeCallback, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + left: 16.adaptedPx(), + right: 16.adaptedPx(), + ), + decoration: new BoxDecoration( + color: ThemeColor.colorEFF0F4, + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + obscureText: this.obscureText, + controller: this.controller, + maxLines: this.maxLines, + cursorColor: ThemeColor.cursorColor, + style: TextStyle(color: ThemeColor.black, fontSize: 16.adaptedPx()), + keyboardType: inputType, + inputFormatters: inputFormatters != null ? [...inputFormatters!] : [], + textInputAction: TextInputAction.done, + decoration: inputDecoration(), + validator: (value) => _validator(value), + onChanged: (value) => onChangeCallback != null ? onChangeCallback!(value) : null, + ), + ); + } + + InputDecoration inputDecoration() { + final txtStyle = new TextStyle( + color: ThemeColor.black.withOpacity(0.60), + fontSize: 16.adaptedPx(), + ); + return InputDecoration( + alignLabelWithHint: maxLines != 1, + hintText: hintText ?? this.label, + hintStyle: txtStyle, + // labelText: this.label, + labelStyle: txtStyle, + suffixIcon: this.suffix, + suffixIconConstraints: BoxConstraints(), + // floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + disabledBorder: InputBorder.none, + ); + } + + String? _validator(String? value) { + if (this.obscureText) { + //FIXME: add translation + return (value == null || value.trim().length < 6) ? 'min_6_char'.translation : null; + } + + return (value == null || value.trim().length == 0) ? '$label ' + 'is_required'.translation : null; + } +} diff --git a/lib/new/global/full_width_button.dart b/lib/new/global/full_width_button.dart new file mode 100644 index 0000000..4a6ac38 --- /dev/null +++ b/lib/new/global/full_width_button.dart @@ -0,0 +1,48 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; + +import '../themes/colors.dart'; + +class FullWidthButton extends StatelessWidget { + final String title; + final Color btnColor; + final Color txtColor; + final VoidCallback? callback; + + FullWidthButton({ + required this.title, + required this.callback, + this.txtColor = ThemeColor.white, + this.btnColor = ThemeColor.mainColor, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: 48.adaptedPx(), + child: TextButton( + style: ButtonStyle( + // elevation: MaterialStateProperty.all(5), + shape: MaterialStateProperty.all((RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)))), + // padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: MaterialStateProperty.all(this.btnColor), // <-- Button color + overlayColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.pressed)) + return this.btnColor == ThemeColor.mainColor ? ThemeColor.grey.withOpacity(0.50) : ThemeColor.mainColor.withOpacity(0.30); // <-- Splash color + return null; + }), + ), + onPressed: this.callback != null ? () => callback!() : null, + child: Text( + this.title, + style: new TextStyle( + fontSize: 14.adaptedPx(), + fontWeight: FontWeight.w500, + color: this.txtColor, + ), + ), + ), + ); + } +} diff --git a/lib/new/models/bank.dart b/lib/new/models/bank.dart new file mode 100644 index 0000000..a9662ae --- /dev/null +++ b/lib/new/models/bank.dart @@ -0,0 +1,16 @@ +class BankType { + late String name; + late String cardType; + + BankType({ + required this.name, + required this.cardType, + }); + + BankType.fromJson(Map json) { + name = json['name']; + cardType = json['api_val']; + } + + static List listFromJson(list) => List.from(list.map((x) => BankType.fromJson(x))); +} diff --git a/lib/new/models/bank_info.dart b/lib/new/models/bank_info.dart new file mode 100644 index 0000000..ebd9eff --- /dev/null +++ b/lib/new/models/bank_info.dart @@ -0,0 +1,23 @@ +class BankInfoModel { + late String taxCode; + late String bab; + late String manatAccount; + late String correspondentAccount; + late String bankAddress; + + BankInfoModel({ + required this.taxCode, + required this.bab, + required this.manatAccount, + required this.correspondentAccount, + required this.bankAddress, + }); + + BankInfoModel.fromJson(Map json) { + taxCode = json['tax_code']; + bab = json['bab']; + manatAccount = json['manat_account']; + correspondentAccount = json['correspondent_account']; + bankAddress = json['bank_address']; + } +} diff --git a/lib/new/models/category_filter.dart b/lib/new/models/category_filter.dart new file mode 100644 index 0000000..74dbc23 --- /dev/null +++ b/lib/new/models/category_filter.dart @@ -0,0 +1,16 @@ +class CategoryFilterModel { + late int id; + late String title; + + CategoryFilterModel({ + required this.id, + required this.title, + }); + + CategoryFilterModel.fromJson(Map json) { + id = json['id']; + title = json['title']; + } + + static List listFromJson(list) => List.from(list.map((x) => CategoryFilterModel.fromJson(x))); +} diff --git a/lib/new/models/export.dart b/lib/new/models/export.dart new file mode 100644 index 0000000..10bc484 --- /dev/null +++ b/lib/new/models/export.dart @@ -0,0 +1,55 @@ +class ExportModel { + late int id; + late int groupId; + late String type; + late String title; + late String country; + late String unit; + late String amount; + late String point; + late String place; + late String currency; + late String payment; + late String send; + late String seller; + late String price; + late String total; + + ExportModel({ + required this.id, + required this.groupId, + required this.type, + required this.title, + required this.country, + required this.unit, + required this.amount, + required this.point, + required this.place, + required this.currency, + required this.payment, + required this.send, + required this.seller, + required this.price, + required this.total, + }); + + ExportModel.fromJson(Map json) { + id = json['id']; + groupId = json['group_id']; + type = json['type']; + title = json['title']; + country = json['country']; + unit = json['unit']; + amount = json['amount'].toString(); + point = json['point']; + place = json['place']; + currency = json['currency']; + payment = json['payment']; + send = json['send']; + seller = json['seller']; + price = json['price'].toString(); + total = json['total'].toString(); + } + + static List listFromJson(list) => List.from(list.map((x) => ExportModel.fromJson(x))); +} diff --git a/lib/new/models/filter.dart b/lib/new/models/filter.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/new/models/import.dart b/lib/new/models/import.dart new file mode 100644 index 0000000..14cea60 --- /dev/null +++ b/lib/new/models/import.dart @@ -0,0 +1,43 @@ +class ImportModel { + late int id; + late int groupId; + late String title; + late String country; + late String unit; + late String currency; + late String price; + late String registeredAt; + late String locale; + late String createdAt; + late String updatedAt; + + ImportModel({ + required this.id, + required this.groupId, + required this.title, + required this.country, + required this.unit, + required this.currency, + required this.price, + required this.registeredAt, + required this.locale, + required this.createdAt, + required this.updatedAt, + }); + + ImportModel.fromJson(Map json) { + id = json['id']; + groupId = json['group_id']; + title = json['title']; + country = json['country']; + unit = json['unit']; + currency = json['currency']; + price = json['price']; + registeredAt = json['registered_at']; + locale = json['locale']; + createdAt = json['created_at']; + updatedAt = json['updated_at']; + } + + static List listFromJson(list) => List.from(list.map((x) => ImportModel.fromJson(x))); +} diff --git a/lib/new/models/news.dart b/lib/new/models/news.dart new file mode 100644 index 0000000..6056d5b --- /dev/null +++ b/lib/new/models/news.dart @@ -0,0 +1,51 @@ +class NewsModel { + late int id; + late String title; + late String publishedAt; + late List featuredImages; + late String contentHtml; + + NewsModel({ + required this.id, + required this.title, + required this.publishedAt, + required this.featuredImages, + required this.contentHtml, + }); + + NewsModel.fromJson(Map json) { + id = json['id']; + title = json['title']; + publishedAt = json['published_at']; + if (json['featured_images'] != null) { + featuredImages = []; + json['featured_images'].forEach((v) { + featuredImages.add(new FeaturedImages.fromJson(v)); + }); + } + contentHtml = json['content_html']; + } + + static List listFromJson(list) => List.from(list.map((x) => NewsModel.fromJson(x))); +} + +class FeaturedImages { + late int id; + late String diskName; + late String fileName; + late String path; + + FeaturedImages({ + required this.id, + required this.diskName, + required this.fileName, + required this.path, + }); + + FeaturedImages.fromJson(Map json) { + id = json['id']; + diskName = json['disk_name']; + fileName = json['file_name']; + path = json['path']; + } +} diff --git a/lib/new/models/sort_group.dart b/lib/new/models/sort_group.dart new file mode 100644 index 0000000..c7aa7b3 --- /dev/null +++ b/lib/new/models/sort_group.dart @@ -0,0 +1,28 @@ +class SortGroupModel { + late int id; + late String title; + late String type; + String? file; + int? isDefault; + String? hashId; + + SortGroupModel({ + required this.id, + required this.title, + required this.type, + this.file, + this.isDefault, + this.hashId, + }); + + SortGroupModel.fromJson(Map json) { + id = json['id']; + title = json['title']; + type = json['type']; + file = json['file']; + isDefault = json['is_default']; + hashId = json['hashid']; + } + + static List listFromJson(list) => List.from(list.map((x) => SortGroupModel.fromJson(x))); +} diff --git a/lib/new/screens/news/binding.dart b/lib/new/screens/news/binding.dart new file mode 100644 index 0000000..c3198f6 --- /dev/null +++ b/lib/new/screens/news/binding.dart @@ -0,0 +1,9 @@ +import 'package:birzha/new/screens/news/controller.dart'; +import 'package:get/get.dart'; + +class NewsBinding extends Bindings { + @override + void dependencies() { + Get.put(NewsController()); + } +} diff --git a/lib/new/screens/news/controller.dart b/lib/new/screens/news/controller.dart new file mode 100644 index 0000000..022f192 --- /dev/null +++ b/lib/new/screens/news/controller.dart @@ -0,0 +1,50 @@ +import 'package:birzha/new/api/news.dart'; +import 'package:birzha/new/models/news.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class NewsController extends GetxController with StateMixin>, ScrollMixin { + List repositories = []; + int page = 1; + bool getFirstData = false; + RxBool lastPage = false.obs; + @override + void onInit() { + getNews(); + super.onInit(); + } + + Future getNews() async { + await NewsApi.get(page).then( + (result) { + final bool emptyRepositories = result.isEmpty; + if (!getFirstData && emptyRepositories) { + change(null, status: RxStatus.empty()); + } else if (getFirstData && emptyRepositories) { + lastPage.value = true; + } else { + getFirstData = true; + repositories.addAll(result); + change(repositories, status: RxStatus.success()); + } + }, + onError: (err) { + change(null, status: RxStatus.error(err.toString())); + }, + ); + } + + @override + Future onEndScroll() async { + debugPrint('onEndScroll'); + if (!lastPage.value) { + page += 1; + await getNews(); + } + } + + @override + Future onTopScroll() async { + debugPrint('onTopScroll'); + } +} diff --git a/lib/new/screens/news/details_screen.dart b/lib/new/screens/news/details_screen.dart new file mode 100644 index 0000000..e9395b6 --- /dev/null +++ b/lib/new/screens/news/details_screen.dart @@ -0,0 +1,108 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../components/baseWidget.dart'; +import '../../../components/tabview.dart'; +import '../../../constants.dart'; +import '../../models/news.dart'; + +class NewsDetailScreen extends StatefulWidget { + final NewsModel news; + const NewsDetailScreen({ + Key? key, + required this.news, + }) : super(key: key); + + @override + _NewsDetailScreenState createState() => _NewsDetailScreenState(); +} + +class _NewsDetailScreenState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + final deviceHeight = MediaQuery.of(context).size.height; + return BaseWidget( + appBar: BaseAppBar( + title: 'news_feed'.translation, + goBack: () { + Tabnavigator.backDispatcher(context); + }, + ), + body: ListView( + children: [ + ZoomIn( + child: CachedNetworkImage( + imageUrl: widget.news.featuredImages.first.path, + height: deviceHeight * 0.25, + width: double.infinity, + errorWidget: (context, url, error) { + return Container( + height: deviceHeight * 0.25, + width: double.infinity, + alignment: Alignment.center, + child: Icon(Icons.image), + ); + }, + placeholder: (context, url) => Container( + height: deviceHeight * 0.20, + width: double.infinity, + alignment: Alignment.center, + child: Icon(Icons.broken_image), + ), + fit: BoxFit.cover, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: AppConstants.verticalPadding(context), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.news.title, + style: new TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16.adaptedPx(), + ), + ), + SizedBox(height: 8.adaptedPx()), + Text( + widget.news.publishedAt, + style: new TextStyle( + fontSize: 14.adaptedPx(), + color: Colors.black, + ), + ), + SizedBox(height: 8.adaptedPx()), + Html( + data: widget.news.contentHtml, + style: { + "body": Style( + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + fontSize: FontSize.large, + ) + }, + onLinkTap: (String? url, RenderContext context, Map attributes, _) async { + if (!await launch('$url')) throw 'Could not launch $url'; + }, + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/new/screens/news/screen.dart b/lib/new/screens/news/screen.dart new file mode 100644 index 0000000..1f4f854 --- /dev/null +++ b/lib/new/screens/news/screen.dart @@ -0,0 +1,181 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/screens/news/details_screen.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../components/baseWidget.dart'; +import '../../../components/indicator.dart'; +import '../../models/news.dart'; +import 'controller.dart'; + +class NewsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).chipTheme.backgroundColor, + appBar: BaseAppBar( + after: [], + goBack: () { + Navigator.of(context).pop(); + }, + title: 'news_feed'.translation, + ), + // appBar: BaseAppBar.home( + // context, + // () {}, + // ), + // color: ThemeColor.white, + body: GetX( + init: NewsController(), + builder: (nc) { + final bool isLastPage = nc.lastPage.value; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Expanded( + child: nc.obx( + (state) => state != null + ? RefreshIndicator( + onRefresh: nc.getNews, + color: ThemeColor.mainColor, + child: ListView.builder( + padding: EdgeInsets.only(top: 12), + shrinkWrap: true, + controller: nc.scroll, + itemCount: state.length + 1, + itemBuilder: (context, index) { + if (index < state.length) { + final NewsModel news = state[index]; + return InkWell( + onTap: () { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => NewsDetailScreen(news: news)), + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.adaptedPx(), + horizontal: 8.adaptedPx(), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: CachedNetworkImage( + imageUrl: news.featuredImages.first.path, + height: 70.adaptedPx(), + width: 120.adaptedPx(), + errorWidget: (context, url, error) { + return Container( + height: 70.adaptedPx(), + width: 120.adaptedPx(), + alignment: Alignment.center, + child: Icon(Icons.image), + ); + }, + placeholder: (context, url) => Container( + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: Colors.grey.withOpacity(0.50), + ), + ), + height: 70.adaptedPx(), + width: 120.adaptedPx(), + alignment: Alignment.center, + child: Icon( + Icons.broken_image, + color: Colors.grey, + ), + ), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Container( + padding: EdgeInsets.only(left: 10.adaptedPx()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + news.title, + style: TextStyle( + fontSize: 13.adaptedPx(), + color: Color(0xFF003197), + ), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 8), + Text( + news.publishedAt, + style: TextStyle( + fontSize: 12.adaptedPx(), + color: Colors.black, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } else if (index == state.length && !isLastPage) + return Padding( + padding: const EdgeInsets.only(top: 10, bottom: 40), + child: Center( + child: Indicator( + size: 0.6.adaptedPx(), + ), + ), + ); + else { + return SizedBox.shrink(); + } + }, + ), + ) + : Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onLoading: Center(child: Indicator(size: 0.7.adaptedPx())), + onEmpty: Center( + child: Text( + 'newsNotFount'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onError: (error) => Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/new/screens/quotes/controller.dart b/lib/new/screens/quotes/controller.dart new file mode 100644 index 0000000..cdcf4e0 --- /dev/null +++ b/lib/new/screens/quotes/controller.dart @@ -0,0 +1,29 @@ +// import 'package:flutter/rendering.dart'; +// import 'package:get/get.dart'; + +// import '../../api/sort.dart'; +// import '../../models/quotes.dart'; +// import 'state.dart'; + +// class QuotesController extends GetxController with StateMixin>, ScrollMixin { +// // final state = SortState(); + +// @override +// void onInit() { +// super.onInit(); +// } + +// @override +// Future onEndScroll() async { +// debugPrint('onEndScroll'); +// // if (!lastPage.value) { +// // page += 1; +// // await getNews(); +// // } +// } + +// @override +// Future onTopScroll() async { +// debugPrint('onTopScroll'); +// } +// } diff --git a/lib/new/screens/quotes/exports/controller.dart b/lib/new/screens/quotes/exports/controller.dart new file mode 100644 index 0000000..7561a7f --- /dev/null +++ b/lib/new/screens/quotes/exports/controller.dart @@ -0,0 +1,222 @@ +import 'package:birzha/new/utils/capitalize.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../api/sort.dart'; +import '../../../models/export.dart'; +import '../../../models/sort_group.dart'; +import '../../../utils/locale.dart'; +import 'state.dart'; + +class ExportsController extends GetxController with StateMixin>, ScrollMixin { + final exportState = ExportState(); + + @override + void onInit() { + debugPrint('ExportsController onInit'); + initialize(); + super.onInit(); + } + + /// reset pagination data + void resetPaginationParams() { + exportState.page = 1; + exportState.getFirstData = false; + exportState.lastPage.value = false; + exportState.repositories.clear(); + } + + /// dropdown change + void onSelectedSortGroupChanged(value) { + debugPrint('onSelectedSortGroupChanged $value'); + exportState.selectedGroup.value = value; + + resetPaginationParams(); + change(null, status: RxStatus.loading()); + getExports(); + } + + /// filter change + void onFilterChanged(value, filter) { + debugPrint('onFilterChanged $value, filter: $filter'); + if (filter == 'country') { + exportState.country.value = value; + } else if (filter == 'unit') { + exportState.unit.value = value; + } else if (filter == 'currency') { + exportState.currency.value = value; + } else if (filter == 'payment') { + exportState.payment.value = value; + } else if (filter == 'send') { + exportState.send.value = value; + } + + countAppliedFilters(); + } + + void onSelectedCategoryFilterChanged(value) { + debugPrint('onSelectedCategoryFilterChanged $value'); + exportState.selectedCategoryFilter.value = value; + + countAppliedFilters(); + } + + void countAppliedFilters() { + int count = 0; + if (exportState.selectedCategoryFilter.value != null) count++; + + if (exportState.country.value.isNotEmpty) count++; + + if (exportState.unit.value.isNotEmpty) count++; + + if (exportState.currency.value.isNotEmpty) count++; + + if (exportState.payment.value.isNotEmpty) count++; + + if (exportState.send.value.isNotEmpty) count++; + + exportState.appliedFilterCount.value = count; + + update(); + } + + void onApplyBtnTapped() { + resetPaginationParams(); + change(null, status: RxStatus.loading()); + getExports(); + } + + void onClearBtnTapped() { + exportState.selectedCategoryFilter.value = null; + exportState.country.value = ''; + exportState.unit.value = ''; + exportState.currency.value = ''; + exportState.payment.value = ''; + exportState.send.value = ''; + + countAppliedFilters(); + } + + Future initialize() async { + resetPaginationParams(); + await getSortGroups(); + getExports(); + } + + Future getSortGroups() async { + if (exportState.sortGroups.isNotEmpty) return; + + exportState.isLoading.value = true; + + final result = await SortApi.getGroups('export'); + if (result.isNotEmpty) { + exportState.selectedGroup.value = result.firstWhereOrNull((group) => group.isDefault == 1); + exportState.sortGroups.addAll(result); + } + + exportState.isLoading.value = false; + } + + Future getFilters() async { + exportState.isLoadingFilters.value = true; + getOtherFilters('country', exportState.countryFilters); + getOtherFilters('unit', exportState.unitFilters); + getOtherFilters('currency', exportState.currencyFilters); + getOtherFilters('payment', exportState.paymentFilters); + getOtherFilters('send', exportState.sendFilters); + exportState.isLoadingFilters.value = false; + } + + Future getExports() async { + final Map params = { + 'per_page': exportState.itemPerPage, + 'page': exportState.page, + 'locale': await getLocale(), + 'group': exportState.selectedGroup.value?.id, + 'payment': exportState.payment.value, + 'send': exportState.send.value, + 'country': exportState.country.value, + 'unit': exportState.unit.value, + 'currency': exportState.currency.value, + }; + + final categoryId = exportState.selectedCategoryFilter.value?.id.toString(); + + if (categoryId != null && categoryId.isNotEmpty) { + params.addAll({'category': categoryId}); + } + + await SortApi.getExports(params).then( + (result) { + exportState.metaText.value = SortApi.metaText; + final bool emptyRepositories = result.isEmpty; + if (!exportState.getFirstData && emptyRepositories) { + change(null, status: RxStatus.empty()); + } else if (exportState.getFirstData && emptyRepositories) { + exportState.lastPage.value = true; + } else { + exportState.getFirstData = true; + exportState.repositories.addAll(result); + + if (result.length < exportState.itemPerPage) { + exportState.lastPage.value = true; + } + + change(exportState.repositories, status: RxStatus.success()); + } + }, + onError: (err) { + change(null, status: RxStatus.error(err.toString())); + }, + ); + } + + Future getOtherFilters(String filter, List filterList) async { + final SortGroupModel? groupValue = exportState.selectedGroup.value ?? null; + + if (groupValue == null) { + // TODO: show error message + return; + } + + if (filterList.isNotEmpty) return; + + final result = await SortApi.getOtherFilters({ + 'group': groupValue.id, + 'model': groupValue.type.capitalizeFirstLetter(), + 'filter': filter, + }); + + if (result.isNotEmpty) filterList.addAll(result); + + update(); + } + + Future getCategoryFilters() async { + if (exportState.categoryFilters.isNotEmpty) return; + exportState.isLoadingCategory.value = true; + + final result = await SortApi.getCategoryFilters(); + if (result.isNotEmpty) { + exportState.categoryFilters.addAll(result); + } + + exportState.isLoadingCategory.value = false; + + update(); + } + + @override + Future onEndScroll() async { + debugPrint('onEndScroll'); + if (!exportState.lastPage.value) { + exportState.page += 1; + await getExports(); + } + } + + @override + Future onTopScroll() async { + debugPrint('onTopScroll'); + } +} diff --git a/lib/new/screens/quotes/exports/screen.dart b/lib/new/screens/quotes/exports/screen.dart new file mode 100644 index 0000000..5c24364 --- /dev/null +++ b/lib/new/screens/quotes/exports/screen.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +import '../../../../components/indicator.dart'; +import '../../../../constants.dart'; +import '../../../themes/colors.dart'; +import 'controller.dart'; +import 'widgets/expandable_card.dart'; +import 'widgets/filter_widget.dart'; + +class ExportsScreen extends StatelessWidget { + const ExportsScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + // vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + color: ThemeColor.white, + child: GetX( + init: ExportsController(), + builder: (exc) { + final bool isLastPage = exc.exportState.lastPage.value; + return exc.exportState.isLoading.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : CustomScrollView( + controller: exc.scroll, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: AppConstants.verticalPadding(context)), + child: Column( + children: [ + ExportFilterWidget(), + SizedBox(height: 16.adaptedPx()), + Text(exc.exportState.metaText.value), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox(height: 16.adaptedPx()), + ), + SliverToBoxAdapter( + child: exc.obx( + (state) => state != null + ? RefreshIndicator( + onRefresh: exc.getExports, + color: ThemeColor.mainColor, + child: ListView.builder( + physics: ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: state.length + 1, + itemBuilder: (context, index) { + if (index < state.length) { + return ExportExpandableCard(index: index + 1, model: state[index]); + } else if (index == state.length && !isLastPage) + return Center( + child: Indicator( + size: 0.6.adaptedPx(), + ), + ); + else { + return SizedBox.shrink(); + } + }, + ), + ) + : Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onLoading: Center(child: Indicator(size: 0.7.adaptedPx())), + onEmpty: Center( + child: Text( + 'notFound'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onError: (error) => Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/new/screens/quotes/exports/state.dart b/lib/new/screens/quotes/exports/state.dart new file mode 100644 index 0000000..c32076b --- /dev/null +++ b/lib/new/screens/quotes/exports/state.dart @@ -0,0 +1,46 @@ +import 'package:birzha/new/models/export.dart'; +import 'package:get/get.dart'; + +import '../../../models/category_filter.dart'; +import '../../../models/sort_group.dart'; + +class ExportState { + /// loadings + RxBool isLoading = false.obs; + RxBool isLoadingFilters = false.obs; + RxBool isLoadingCategory = false.obs; + + RxList sortGroups = [].obs; + + /// [selectedGroup] is import or export + Rxn selectedGroup = Rxn(); + + /// [metaText] displayed top of the list + RxString metaText = ''.obs; + + /// dropdown items + RxList categoryFilters = [].obs; + RxList countryFilters = [].obs; + RxList unitFilters = [].obs; + RxList currencyFilters = [].obs; + RxList paymentFilters = [].obs; + RxList sendFilters = [].obs; + + /// pagination data + List repositories = []; + int itemPerPage = 10; + int page = 1; + bool getFirstData = false; + RxBool lastPage = false.obs; + + /// filters + Rxn selectedCategoryFilter = Rxn(); + RxString country = ''.obs; + RxString unit = ''.obs; + RxString currency = ''.obs; + RxString payment = ''.obs; + RxString send = ''.obs; + + /// total applied filter + RxInt appliedFilterCount = 0.obs; +} diff --git a/lib/new/screens/quotes/exports/widgets/expandable_card.dart b/lib/new/screens/quotes/exports/widgets/expandable_card.dart new file mode 100644 index 0000000..ccb2d41 --- /dev/null +++ b/lib/new/screens/quotes/exports/widgets/expandable_card.dart @@ -0,0 +1,177 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; + +import '../../../../models/export.dart'; +import '../../../../themes/colors.dart'; + +class ExportExpandableCard extends StatelessWidget { + final ExportModel model; + final int index; + const ExportExpandableCard({ + Key? key, + required this.model, + required this.index, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget buildCollapsed() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CardTitle(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + children: [ + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'quantity'.translation + ':', text2: model.amount, text3: model.unit), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'price'.translation + ':', text2: model.price, text3: model.currency), + ], + ), + ), + ], + ), + ], + ); + } + + Widget buildExpanded() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CardTitle(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + children: [ + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'quantity'.translation + ':', text2: model.amount, text3: model.unit), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'price'.translation + ':', text2: model.price, text3: model.currency), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'paymentTerms'.translation + ':', text2: model.payment), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'deliveryTerms'.translation + ':', text2: model.send), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'station'.translation + ':', text2: model.point), + ], + ), + ), + ], + ), + ], + ); + } + + return ExpandableNotifier( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ScrollOnExpand( + child: Container( + decoration: BoxDecoration( + // color: Colors.grey, + border: Border.all( + color: ThemeColor.colorE2E2E2, + width: 1.5, + ), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expandable( + collapsed: buildCollapsed(), + expanded: buildExpanded(), + ), + // bottom expandable button + Center( + child: Container( + color: Colors.white, + child: Builder( + builder: (context) { + var controller = ExpandableController.of(context, required: true)!; + return InkWell( + onTap: controller.toggle, + child: Container( + decoration: new BoxDecoration( + color: ThemeColor.mainColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ), + width: 80.adaptedPx(), + child: Icon( + controller.expanded ? Icons.arrow_drop_up : Icons.arrow_drop_down, + size: 20.adaptedPx(), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget RowTextBuilder({required String text1, required String text2, String? text3}) { + return Row( + children: [ + Expanded( + flex: 2, + child: Text( + text1, + style: TextStyle( + color: ThemeColor.color717278, + fontSize: 12.adaptedPx(), + ), + ), + ), + Spacer(), + Expanded( + flex: 2, + child: Text( + '$text2 ${text3 ?? ''}', + textAlign: TextAlign.right, + style: TextStyle( + color: ThemeColor.black, + fontSize: 12.adaptedPx(), + ), + ), + ), + ], + ); + } + + Widget CardTitle() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + width: double.infinity, + decoration: new BoxDecoration( + color: ThemeColor.colorE5E5E5, + ), + child: Text( + "$index. ${model.title}", + style: TextStyle( + color: ThemeColor.color3A3A3A, + fontSize: 13.adaptedPx(), + ), + ), + ); + } +} diff --git a/lib/new/screens/quotes/exports/widgets/filter_dialog.dart b/lib/new/screens/quotes/exports/widgets/filter_dialog.dart new file mode 100644 index 0000000..82bb32b --- /dev/null +++ b/lib/new/screens/quotes/exports/widgets/filter_dialog.dart @@ -0,0 +1,235 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../../../components/button.dart'; +import '../../../../../components/categoryNameWidget.dart'; +import '../../../../../components/indicator.dart'; +import '../../../../models/category_filter.dart'; +import '../../../../models/sort_group.dart'; +import '../../../../themes/colors.dart'; +import '../../widgets/custom_dd.dart'; +import '../controller.dart'; + +final Widget divider = SizedBox(height: 10.adaptedPx()); + +class ExportFilterDialog extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GetBuilder( + init: ExportsController(), + builder: (ec) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + titlePadding: EdgeInsets.zero, + scrollable: true, + title: Container( + padding: const EdgeInsets.only(top: 24, bottom: 24, right: 8, left: 24), + decoration: new BoxDecoration( + // color: ThemeColor.color3A3A3A, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: Row( + children: [ + CategoryNameWidget( + categoryName: 'filters'.translation, + ), + Spacer(), + Material( + shape: CircleBorder(), + child: InkWell( + customBorder: CircleBorder(), + onTap: () => Get.back(), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration(shape: BoxShape.circle), + child: Icon( + Icons.close, + color: ThemeColor.mainColor, + size: 18.adaptedPx(), + ), + ), + ), + ), + ], + ), + ), + contentPadding: const EdgeInsets.all(16), + insetPadding: EdgeInsets.zero, + content: Builder( + builder: (context) { + final width = MediaQuery.of(context).size.width - 70; + final SortGroupModel? groupValue = ec.exportState.selectedGroup.value ?? null; + return groupValue != null + ? ec.exportState.isLoadingFilters.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : Container( + width: width > 0 ? width : double.infinity, + child: Column( + children: [ + ec.exportState.isLoadingCategory.value + ? Center( + child: Indicator( + size: 0.4.adaptedPx(), + ), + ) + : CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + value: ec.exportState.selectedCategoryFilter.value, + hint: Text('selectCategory'.translation), + items: ec.exportState.categoryFilters.map( + (CategoryFilterModel value) { + return DropdownMenuItem( + value: value, + child: Text(value.title), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onSelectedCategoryFilterChanged(newValue), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + hint: Text(ec.exportState.unit.value.isEmpty ? 'selectUnit'.translation : ec.exportState.unit.value), + items: ec.exportState.unitFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onFilterChanged(newValue, 'unit'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + hint: Text(ec.exportState.currency.value.isEmpty ? 'selectCurrency'.translation : ec.exportState.currency.value), + items: ec.exportState.currencyFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onFilterChanged(newValue, 'currency'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + hint: Text(ec.exportState.payment.value.isEmpty ? 'selectPayment'.translation : ec.exportState.payment.value), + items: ec.exportState.paymentFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onFilterChanged(newValue, 'payment'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + hint: Text(ec.exportState.send.value.isEmpty ? 'selectSendType'.translation : ec.exportState.send.value), + items: ec.exportState.sendFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onFilterChanged(newValue, 'send'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + hint: Text(ec.exportState.country.value.isEmpty ? 'selectCountry'.translation : ec.exportState.country.value), + items: ec.exportState.countryFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onFilterChanged(newValue, 'country'), + ), + ), + ), + SizedBox(height: 18.adaptedPx()), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: MyButton( + text: 'clear'.translation, + inProgress: false, + onTap: ec.onClearBtnTapped, + height: 40.adaptedPx(), + ), + ), + SizedBox(width: 20.adaptedPx()), + Expanded( + child: MyButton( + text: 'apply'.translation, + inProgress: false, + onTap: () { + debugPrint('apply tapped'); + ec.onApplyBtnTapped(); + Get.back(); + }, + height: 40.adaptedPx(), + ), + ), + ], + ), + ) + ], + ), + ) + : Text('errorOccurred'.translation); + }, + ), + ), + ); + } +} diff --git a/lib/new/screens/quotes/exports/widgets/filter_widget.dart b/lib/new/screens/quotes/exports/widgets/filter_widget.dart new file mode 100644 index 0000000..3bfe326 --- /dev/null +++ b/lib/new/screens/quotes/exports/widgets/filter_widget.dart @@ -0,0 +1,90 @@ +import 'package:badges/badges.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../../../constants.dart'; +import '../../../../models/sort_group.dart'; +import '../../../../themes/colors.dart'; +import '../controller.dart'; +import 'filter_dialog.dart'; + +class ExportFilterWidget extends StatelessWidget { + const ExportFilterWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetX( + init: ExportsController(), + builder: (ec) => Row( + children: [ + Material( + child: InkWell( + splashColor: Colors.grey.shade200, + onTap: () { + debugPrint('Filter Widget'); + ec.getCategoryFilters(); + ec.getFilters(); + Get.dialog(ExportFilterDialog()); + }, + child: Badge( + badgeColor: ThemeColor.mainColor, + padding: const EdgeInsets.all(6), + position: BadgePosition.topEnd( + top: -16, + ), + badgeContent: Text( + ec.exportState.appliedFilterCount.toString(), + style: TextStyle(color: Colors.white, fontSize: 11.adaptedPx()), + ), + child: Container( + height: 40.adaptedPx(), + width: 40.adaptedPx(), + // padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ThemeColor.mainColor, + ), + ), + child: Icon( + Icons.filter_list_alt, + size: 20.adaptedPx(), + color: ThemeColor.mainColor, + ), + ), + ), + ), + ), + Spacer(), + Container( + width: MediaQuery.of(context).size.width - AppConstants.horizontalPadding(context) - 40.adaptedPx() - 30.adaptedPx(), + height: 40.adaptedPx(), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: ThemeColor.mainColor), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: BorderRadius.circular(16), + value: ec.exportState.selectedGroup.value, + items: ec.exportState.sortGroups.map( + (SortGroupModel value) { + return DropdownMenuItem( + value: value, + child: Text(value.title), + ); + }, + ).toList(), + onChanged: (newValue) => ec.onSelectedSortGroupChanged(newValue), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/new/screens/quotes/imports/controller.dart b/lib/new/screens/quotes/imports/controller.dart new file mode 100644 index 0000000..394eaa2 --- /dev/null +++ b/lib/new/screens/quotes/imports/controller.dart @@ -0,0 +1,176 @@ +import 'package:birzha/new/models/import.dart'; +import 'package:birzha/new/utils/capitalize.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../api/sort.dart'; +import '../../../models/sort_group.dart'; +import '../../../utils/locale.dart'; +import 'state.dart'; + +class ImportsController extends GetxController with StateMixin>, ScrollMixin { + final importState = ImportState(); + + @override + void onInit() { + debugPrint('ExportsController onInit'); + initialize(); + super.onInit(); + } + + void resetPaginationParams() { + // reset pagination data + importState.page = 1; + importState.getFirstData = false; + importState.lastPage.value = false; + importState.repositories.clear(); + } + + void onSelectedSortGroupChanged(value) { + debugPrint('onSelectedSortGroupChanged $value'); + importState.selectedGroup.value = value; + + resetPaginationParams(); + change(null, status: RxStatus.loading()); + getImports(); + } + + void onFilterChanged(value, filter) { + debugPrint('onFilterChanged $value, filter: $filter'); + if (filter == 'country') { + importState.country.value = value; + } else if (filter == 'unit') { + importState.unit.value = value; + } else if (filter == 'currency') { + importState.currency.value = value; + } + + countAppliedFilters(); + } + + void countAppliedFilters() { + int count = 0; + + if (importState.country.value.isNotEmpty) count++; + + if (importState.unit.value.isNotEmpty) count++; + + if (importState.currency.value.isNotEmpty) count++; + + importState.appliedFilterCount.value = count; + + update(); + } + + void onApplyBtnTapped() { + resetPaginationParams(); + change(null, status: RxStatus.loading()); + getImports(); + } + + void onClearBtnTapped() { + importState.country.value = ''; + importState.unit.value = ''; + importState.currency.value = ''; + + countAppliedFilters(); + } + + Future initialize() async { + resetPaginationParams(); + await getSortGroups(); + getImports(); + } + + Future getSortGroups() async { + if (importState.sortGroups.isNotEmpty) return; + + importState.isLoading.value = true; + + final result = await SortApi.getGroups('import'); + if (result.isNotEmpty) { + importState.selectedGroup.value = result.firstWhereOrNull((group) => group.isDefault == 1); + importState.sortGroups.addAll(result); + } + + importState.isLoading.value = false; + } + + Future getFilters() async { + importState.isLoadingFilters.value = true; + getOtherFilters('country', importState.countryFilters); + getOtherFilters('unit', importState.unitFilters); + getOtherFilters('currency', importState.currencyFilters); + importState.isLoadingFilters.value = false; + } + + Future getImports() async { + final Map params = { + 'per_page': importState.itemPerPage, + 'page': importState.page, + 'locale': await getLocale(), + 'group': importState.selectedGroup.value?.id, + 'country': importState.country.value, + 'unit': importState.unit.value, + 'currency': importState.currency.value, + }; + + await SortApi.getImports(params).then( + (result) { + final bool emptyRepositories = result.isEmpty; + if (!importState.getFirstData && emptyRepositories) { + change(null, status: RxStatus.empty()); + } else if (importState.getFirstData && emptyRepositories) { + importState.lastPage.value = true; + } else { + importState.getFirstData = true; + importState.repositories.addAll(result); + + if (result.length < importState.itemPerPage) { + importState.lastPage.value = true; + } + + change(importState.repositories, status: RxStatus.success()); + } + }, + onError: (err) { + change(null, status: RxStatus.error(err.toString())); + }, + ); + } + + Future getOtherFilters(String filter, List filterList) async { + final SortGroupModel? groupValue = importState.selectedGroup.value ?? null; + + if (groupValue == null) { + // TODO: show error message + return; + } + + if (filterList.isNotEmpty) return; + + final result = await SortApi.getOtherFilters({ + 'group': groupValue.id, + 'model': groupValue.type.capitalizeFirstLetter(), + 'filter': filter, + }); + + if (result.isNotEmpty) filterList.addAll(result); + + update(); + } + + @override + Future onEndScroll() async { + debugPrint('onEndScroll'); + if (!importState.lastPage.value) { + importState.page += 1; + await getImports(); + } + } + + @override + Future onTopScroll() async { + debugPrint('onTopScroll'); + } +} diff --git a/lib/new/screens/quotes/imports/screen.dart b/lib/new/screens/quotes/imports/screen.dart new file mode 100644 index 0000000..5119fe6 --- /dev/null +++ b/lib/new/screens/quotes/imports/screen.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +import '../../../../components/indicator.dart'; +import '../../../../constants.dart'; +import '../../../themes/colors.dart'; +import 'controller.dart'; +import 'widgets/expandable_card.dart'; +import 'widgets/filter_widget.dart'; + +class ImportsScreen extends StatelessWidget { + const ImportsScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + color: ThemeColor.white, + child: GetX( + init: ImportsController(), + builder: (ic) { + final bool isLastPage = ic.importState.lastPage.value; + return ic.importState.isLoading.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : CustomScrollView( + controller: ic.scroll, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: AppConstants.verticalPadding(context)), + child: Column( + children: [ + ImportFilterWidget(), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox(height: 16.adaptedPx()), + ), + SliverToBoxAdapter( + child: ic.obx( + (state) => state != null + ? RefreshIndicator( + onRefresh: ic.getImports, + color: ThemeColor.mainColor, + child: ListView.builder( + physics: ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: state.length + 1, + itemBuilder: (context, index) { + if (index < state.length) { + return ImportExpandableCard(index: index + 1, model: state[index]); + } else if (index == state.length && !isLastPage) + return Center( + child: Indicator( + size: 0.6.adaptedPx(), + ), + ); + else { + return SizedBox.shrink(); + } + }, + ), + ) + : Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onLoading: Center(child: Indicator(size: 0.7.adaptedPx())), + onEmpty: Center( + child: Text( + 'notFound'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + onError: (error) => Center( + child: Text( + 'errorOccurred'.translation, + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/new/screens/quotes/imports/state.dart b/lib/new/screens/quotes/imports/state.dart new file mode 100644 index 0000000..0e0d46c --- /dev/null +++ b/lib/new/screens/quotes/imports/state.dart @@ -0,0 +1,34 @@ +import 'package:get/get.dart'; + +import '../../../models/import.dart'; +import '../../../models/sort_group.dart'; + +class ImportState { + RxBool isLoading = false.obs; + RxBool isLoadingFilters = false.obs; + + /// [sortGroups] dates that displayed in the dropdown + RxList sortGroups = [].obs; + + Rxn selectedGroup = Rxn(); + + /// pagination data + List repositories = []; + int itemPerPage = 10; + int page = 1; + bool getFirstData = false; + RxBool lastPage = false.obs; + + /// filter arrays + RxList countryFilters = [].obs; + RxList unitFilters = [].obs; + RxList currencyFilters = [].obs; + + /// filters + RxString country = ''.obs; + RxString unit = ''.obs; + RxString currency = ''.obs; + + /// total count for applied filters + RxInt appliedFilterCount = 0.obs; +} diff --git a/lib/new/screens/quotes/imports/widgets/expandable_card.dart b/lib/new/screens/quotes/imports/widgets/expandable_card.dart new file mode 100644 index 0000000..fa35e2c --- /dev/null +++ b/lib/new/screens/quotes/imports/widgets/expandable_card.dart @@ -0,0 +1,177 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/models/import.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; + +import '../../../../themes/colors.dart'; + +class ImportExpandableCard extends StatelessWidget { + final ImportModel model; + final int index; + const ImportExpandableCard({ + Key? key, + required this.model, + required this.index, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget buildCollapsed() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CardTitle(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + children: [ + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'price'.translation + ':', text2: model.price), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'currency'.translation + ':', text2: model.currency), + ], + ), + ), + ], + ), + ], + ); + } + + Widget buildExpanded() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CardTitle(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + children: [ + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'registeredDate'.translation + ':', text2: model.registeredAt), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'price'.translation + ':', text2: model.price), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'currency'.translation + ':', text2: model.currency), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'country'.translation + ':', text2: model.country), + SizedBox(height: 8.adaptedPx()), + RowTextBuilder(text1: 'unit'.translation + ':', text2: model.unit), + ], + ), + ), + ], + ), + ], + ); + } + + return ExpandableNotifier( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ScrollOnExpand( + child: Container( + decoration: BoxDecoration( + // color: Colors.grey, + border: Border.all( + color: ThemeColor.colorE2E2E2, + width: 1.5, + ), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expandable( + collapsed: buildCollapsed(), + expanded: buildExpanded(), + ), + // bottom expandable button + Center( + child: Container( + color: Colors.white, + child: Builder( + builder: (context) { + var controller = ExpandableController.of(context, required: true)!; + return InkWell( + onTap: controller.toggle, + child: Container( + decoration: new BoxDecoration( + color: ThemeColor.mainColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ), + width: 80.adaptedPx(), + child: Icon( + controller.expanded ? Icons.arrow_drop_up : Icons.arrow_drop_down, + size: 20.adaptedPx(), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget RowTextBuilder({required String text1, required String text2, String? text3}) { + return Row( + children: [ + Expanded( + flex: 2, + child: Text( + text1, + style: TextStyle( + color: ThemeColor.color717278, + fontSize: 12.adaptedPx(), + ), + ), + ), + Spacer(), + Expanded( + flex: 2, + child: Text( + '$text2 ${text3 ?? ''}', + textAlign: TextAlign.right, + style: TextStyle( + color: ThemeColor.black, + fontSize: 12.adaptedPx(), + ), + ), + ), + ], + ); + } + + Widget CardTitle() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + width: double.infinity, + decoration: new BoxDecoration( + color: ThemeColor.colorE5E5E5, + ), + child: Text( + "$index. ${model.title}", + style: TextStyle( + color: ThemeColor.color3A3A3A, + fontSize: 13.adaptedPx(), + ), + ), + ); + } +} diff --git a/lib/new/screens/quotes/imports/widgets/filter_dialog.dart b/lib/new/screens/quotes/imports/widgets/filter_dialog.dart new file mode 100644 index 0000000..a4b4d6c --- /dev/null +++ b/lib/new/screens/quotes/imports/widgets/filter_dialog.dart @@ -0,0 +1,173 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/screens/quotes/widgets/custom_dd.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../../../components/button.dart'; +import '../../../../../components/categoryNameWidget.dart'; +import '../../../../../components/indicator.dart'; +import '../../../../models/sort_group.dart'; +import '../../../../themes/colors.dart'; +import '../controller.dart'; + +final Widget divider = SizedBox(height: 10.adaptedPx()); + +class ImportFilterDialog extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GetBuilder( + init: ImportsController(), + builder: (ic) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + titlePadding: EdgeInsets.zero, + scrollable: true, + title: Container( + padding: const EdgeInsets.only(top: 24, bottom: 24, right: 8, left: 24), + decoration: new BoxDecoration( + // color: ThemeColor.color3A3A3A, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: Row( + children: [ + CategoryNameWidget( + categoryName: 'filters'.translation, + ), + Spacer(), + Material( + shape: CircleBorder(), + child: InkWell( + customBorder: CircleBorder(), + onTap: () => Get.back(), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration(shape: BoxShape.circle), + child: Icon( + Icons.close, + color: ThemeColor.mainColor, + size: 18.adaptedPx(), + ), + ), + ), + ), + ], + ), + ), + contentPadding: const EdgeInsets.all(16), + insetPadding: EdgeInsets.zero, + content: Builder( + builder: (context) { + final width = MediaQuery.of(context).size.width - 70; + final SortGroupModel? groupValue = ic.importState.selectedGroup.value ?? null; + return groupValue != null + ? ic.importState.isLoadingFilters.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : Container( + width: width > 0 ? width : double.infinity, + child: Column( + children: [ + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + // value: ac.state.unit.value, + hint: Text(ic.importState.unit.value.isEmpty ? 'selectUnit'.translation : ic.importState.unit.value), + items: ic.importState.unitFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ic.onFilterChanged(newValue, 'unit'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + // value: ac.state.unit.value, + hint: Text(ic.importState.currency.value.isEmpty ? 'selectCurrency'.translation : ic.importState.currency.value), + items: ic.importState.currencyFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ic.onFilterChanged(newValue, 'currency'), + ), + ), + ), + SizedBox(height: 12.adaptedPx()), + CustomDropDown( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + // value: ac.state.unit.value, + hint: Text(ic.importState.country.value.isEmpty ? 'selectCountry'.translation : ic.importState.country.value), + items: ic.importState.countryFilters.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }, + ).toList(), + onChanged: (newValue) => ic.onFilterChanged(newValue, 'country'), + ), + ), + ), + SizedBox(height: 18.adaptedPx()), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: MyButton( + text: 'clear'.translation, + inProgress: false, + onTap: ic.onClearBtnTapped, + height: 40.adaptedPx(), + ), + ), + SizedBox(width: 20.adaptedPx()), + Expanded( + child: MyButton( + text: 'apply'.translation, + inProgress: false, + onTap: () { + debugPrint('apply tapped'); + ic.onApplyBtnTapped(); + Get.back(); + }, + height: 40.adaptedPx(), + ), + ), + ], + ), + ) + ], + ), + ) + : Text('errorOccurred'.translation); + }, + ), + ), + ); + } +} diff --git a/lib/new/screens/quotes/imports/widgets/filter_widget.dart b/lib/new/screens/quotes/imports/widgets/filter_widget.dart new file mode 100644 index 0000000..97b0fe3 --- /dev/null +++ b/lib/new/screens/quotes/imports/widgets/filter_widget.dart @@ -0,0 +1,89 @@ +import 'package:badges/badges.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../../../constants.dart'; +import '../../../../models/sort_group.dart'; +import '../../../../themes/colors.dart'; +import '../controller.dart'; +import 'filter_dialog.dart'; + +class ImportFilterWidget extends StatelessWidget { + const ImportFilterWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetX( + init: ImportsController(), + builder: (ic) => Row( + children: [ + Material( + child: InkWell( + splashColor: Colors.grey.shade200, + onTap: () { + debugPrint('ImportFilterDialog Widget'); + ic.getFilters(); + Get.dialog(ImportFilterDialog()); + }, + child: Badge( + badgeColor: ThemeColor.mainColor, + padding: const EdgeInsets.all(6), + position: BadgePosition.topEnd( + top: -16, + ), + badgeContent: Text( + ic.importState.appliedFilterCount.toString(), + style: TextStyle(color: Colors.white, fontSize: 11.adaptedPx()), + ), + child: Container( + height: 40.adaptedPx(), + width: 40.adaptedPx(), + // padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ThemeColor.mainColor, + ), + ), + child: Icon( + Icons.filter_list_alt, + size: 20.adaptedPx(), + color: ThemeColor.mainColor, + ), + ), + ), + ), + ), + Spacer(), + Container( + width: MediaQuery.of(context).size.width - AppConstants.horizontalPadding(context) - 40.adaptedPx() - 30.adaptedPx(), + height: 40.adaptedPx(), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: ThemeColor.mainColor), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: BorderRadius.circular(16), + value: ic.importState.selectedGroup.value, + items: ic.importState.sortGroups.map( + (SortGroupModel value) { + return DropdownMenuItem( + value: value, + child: Text(value.title), + ); + }, + ).toList(), + onChanged: (newValue) => ic.onSelectedSortGroupChanged(newValue), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/new/screens/quotes/screen.dart b/lib/new/screens/quotes/screen.dart new file mode 100644 index 0000000..0e7f699 --- /dev/null +++ b/lib/new/screens/quotes/screen.dart @@ -0,0 +1,48 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/screens/quotes/imports/screen.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:flutter/material.dart'; + +import '../../../components/baseWidget.dart'; +import 'exports/screen.dart'; + +class QuotesScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BaseWidget( + appBar: BaseAppBar.home( + context, + () {}, + ), + color: ThemeColor.white, + body: DefaultTabController( + length: 2, + child: Scaffold( + backgroundColor: ThemeColor.white, + appBar: PreferredSize( + preferredSize: Size(double.infinity, 50.adaptedPx()), + child: AppBar( + elevation: 0.50, + bottom: TabBar( + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: ThemeColor.mainColor, + // indicatorWeight: 10, + tabs: [ + Tab(child: Text('quotes'.translation)), + Tab(child: Text('importPrice'.translation)), + ], + ), + ), + ), + body: TabBarView( + children: [ + ExportsScreen(), + ImportsScreen(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/new/screens/quotes/state.dart b/lib/new/screens/quotes/state.dart new file mode 100644 index 0000000..6b803ee --- /dev/null +++ b/lib/new/screens/quotes/state.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; + +import '../../models/category_filter.dart'; +import '../../models/sort_group.dart'; + +class SortState { + RxBool isLoading = false.obs; + RxList exportSortGroups = [].obs; + + /// [selectedExportSortGroup] is import or export + Rxn selectedExportSortGroup = Rxn(); + + RxList categoryFilters = [].obs; + RxList countryFilters = [].obs; + RxList unitFilters = [].obs; + RxList currencyFilters = [].obs; + RxList paymentFilters = [].obs; + RxList sendFilters = [].obs; + + /// filters + Rxn selectedCategoryFilter = Rxn(); + RxString country = ''.obs; + RxString unit = ''.obs; + RxString currency = ''.obs; + RxString payment = ''.obs; + RxString send = ''.obs; + + RxInt appliedFilterCount = 0.obs; +} diff --git a/lib/new/screens/quotes/widgets/custom_dd.dart b/lib/new/screens/quotes/widgets/custom_dd.dart new file mode 100644 index 0000000..6addcef --- /dev/null +++ b/lib/new/screens/quotes/widgets/custom_dd.dart @@ -0,0 +1,96 @@ +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/new/api/sort.dart'; +import 'package:flutter/material.dart'; + +import 'package:birzha/core/adaptix/adaptix.dart'; + +import '../../../../constants.dart'; +import '../../../themes/colors.dart'; + +class CustomDropDown extends StatelessWidget { + final Widget child; + const CustomDropDown({ + Key? key, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width - AppConstants.horizontalPadding(context) - 40.adaptedPx() - 30.adaptedPx(), + height: 40.adaptedPx(), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: ThemeColor.mainColor), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + child: child, + ); + } +} + +class CustomDropDown2 extends StatefulWidget { + final void Function(String?) onSelectionChanged; + final String filter; + final String value; + final List items; + const CustomDropDown2({ + Key? key, + required this.filter, + required this.value, + required this.items, + required this.onSelectionChanged, + }) : super(key: key); + + @override + State> createState() => _CustomDropDown2State(); +} + +class _CustomDropDown2State extends State> { + late var _selectedValue; + + @override + void initState() { + super.initState(); + _selectedValue = widget.value; + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width - AppConstants.horizontalPadding(context) - 40.adaptedPx() - 30.adaptedPx(), + height: 40.adaptedPx(), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: ThemeColor.mainColor), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: BorderRadius.circular(16), + hint: Text(_selectedValue), + // value: _selectedValue, + items: widget.items.map( + (String value) { + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }, + ).toList(), + onChanged: (newValue) { + debugPrint('newValue $newValue'); + setState(() { + if (newValue != null) _selectedValue = newValue; + widget.onSelectionChanged(newValue); + }); + }, + ), + ), + ); + } +} diff --git a/lib/new/screens/top_up_balance/bank_top_up_screen.dart b/lib/new/screens/top_up_balance/bank_top_up_screen.dart new file mode 100644 index 0000000..57c291b --- /dev/null +++ b/lib/new/screens/top_up_balance/bank_top_up_screen.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:birzha/components/actionIcons/searchicon.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/global/full_width_button.dart'; +import 'package:birzha/new/themes/colors.dart'; + +import '../../../components/button.dart'; +import '../../../components/categoryNameWidget.dart'; +import '../../../components/indicator.dart'; +import '../../../constants.dart'; +import '../../../core/manager/manager.dart'; +import '../../../models/user/userManager.dart'; +import 'controller.dart'; + +class BankTopUpBalance extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GetBuilder( + init: BalanceController(), + builder: (tb) => BaseWidget( + // color: Theme.of(context).scaffoldBackgroundColor, + color: ThemeColor.white, + appBar: BaseAppBar( + title: 'topUpBalance'.translation, + after: [SearchIcon()], + goBack: () { + Navigator.of(context, rootNavigator: false).pop(); + }, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: CategoryNameWidget( + categoryName: 'topUpBalance'.translation, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: "selectPaymentMethod".translation, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14.adaptedPx(), + ), + children: [ + new TextSpan( + text: ' *', + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.red, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + // vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: FullWidthButton( + callback: () {}, + title: 'payOnline'.translation, + btnColor: Color(0xFFF2F6FF), + txtColor: ThemeColor.mainColor, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: FullWidthButton( + callback: () {}, + title: "bankTransfer".translation, + ), + ), + + // info + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: tb.state.isLoadingBankInfo.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : tb.state.hasErrorBankInfo.value + ? Center(child: Text('errorOccurred'.translation)) + : Column( + children: [ + InfoCard( + header: 'taxCode'.translation, + content: tb.state.bankInfo.value?.taxCode ?? '', + ), + SizedBox(height: 8.adaptedPx()), + InfoCard( + header: 'manatAccount'.translation, + content: tb.state.bankInfo.value?.manatAccount ?? '', + ), + SizedBox(height: 8.adaptedPx()), + InfoCard( + header: 'corrAccount'.translation, + content: '21101934110100700005000', + ), + SizedBox(height: 8.adaptedPx()), + InfoCard( + header: 'bankAddress'.translation, + content: tb.state.bankInfo.value?.bankAddress ?? '', + ), + SizedBox(height: 8.adaptedPx()), + InfoCard( + header: 'МФО:', + content: tb.state.bankInfo.value?.bab ?? '', + ), + SizedBox(height: 8.adaptedPx()), + ], + ), + ), + + // upload file + Padding( + padding: EdgeInsets.only( + top: AppConstants.verticalPadding(context), + left: AppConstants.horizontalPadding(context), + right: AppConstants.horizontalPadding(context), + bottom: 8.adaptedPx(), + // vertical: AppConstants.verticalPadding(context), + // horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: "selectFile".translation, + style: new TextStyle( + color: Color(0xFFC9C9C9), + fontWeight: FontWeight.w500, + fontSize: 14.adaptedPx(), + ), + children: [ + new TextSpan( + text: ' *', + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.red, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric( + // vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Color(0xFFE5E5E5), + ), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + InkWell( + onTap: tb.uploadFile, + child: Container( + width: MediaQuery.of(context).size.width * 0.30, + height: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Color(0xFFE2E2E2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "upload".translation, + style: new TextStyle( + color: Colors.black, + fontSize: 14.adaptedPx(), + ), + ), + ), + ), + SizedBox(width: 12.adaptedPx()), + Expanded( + child: Center( + child: Text( + tb.state.file.value == null ? 'fileNotSelected'.translation : tb.state.file.value!.name, + style: new TextStyle( + color: Colors.black, + fontSize: 14.adaptedPx(), + ), + ), + ), + ) + ], + ), + ), + ), + ), + + SizedBox(height: 12.adaptedPx()), + + ManagerSelector( + onUpdate: () { + debugPrint('onUpdate'); + tb.resetFile(); + }, + selector: (context, manager) => manager.getStatusByKey('uploadBill'), + shouldRebuild: (prev, next) => prev != next, + builder: (context, status) => Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.40, + child: MyButton( + text: 'send'.translation, + borderRadius: 8.0, + // isDisabled: tb.topUpAmount.value < 100, + inProgress: status == TaskStatus.Loading, + onTap: () async { + debugPrint('uploadBill'); + AppUserManager.of(context).uploadBill(context, tb.state.file.value); + }, + height: 48.adaptedPx(), + ), + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: '* ', + style: new TextStyle( + color: Colors.red, + fontWeight: FontWeight.w500, + fontSize: 12.adaptedPx(), + ), + children: [ + new TextSpan( + text: 'reminderInfo'.translation, + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.black, + fontSize: 14.adaptedPx(), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class InfoCard extends StatelessWidget { + final String header; + final String content; + + const InfoCard({ + Key? key, + required this.header, + required this.content, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: Color(0xFFF2F6FF), + ), + ), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(16.adaptedPx()), + color: Color(0xFFF2F6FF), + child: Center( + child: Text( + header, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 14.adaptedPx(), + ), + ), + ), + ), + Container( + color: Colors.white, + padding: EdgeInsets.all(16.adaptedPx()), + child: Center( + child: Text( + content, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 14.adaptedPx(), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/new/screens/top_up_balance/controller.dart b/lib/new/screens/top_up_balance/controller.dart new file mode 100644 index 0000000..c0ec17e --- /dev/null +++ b/lib/new/screens/top_up_balance/controller.dart @@ -0,0 +1,93 @@ +import 'package:birzha/new/models/bank.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:file_picker/file_picker.dart' as picker; + +import '../../api/bank.dart'; +import 'state.dart'; + +class BalanceController extends GetxController { + final state = TopUpBalanceState(); + + @override + void onInit() { + getBankTypes(); + super.onInit(); + } + + @override + void onClose() { + state.balanceTopUpCtrl.dispose(); + super.onClose(); + } + + void onBankChange(BankType? model) { + debugPrint('onShippingRateChange'); + state.selectedBank.value = model; + update(); + } + + void onChange(String value) { + state.topUpAmount.value = double.tryParse(value.trim()) ?? 0.0; + update(); + } + + void resetFile() { + state.file.value = null; + update(); + } + + Future getBankInfo() async { + if (state.bankInfo.value != null) return; + + state.hasErrorBankInfo.value = false; + state.isLoadingBankInfo.value = true; + final result = await BankApi.getBankInfo(); + + if (result == null) { + state.hasErrorBankInfo.value = true; + state.isLoadingBankInfo.value = false; + update(); + return; + } + + state.bankInfo.value = result; + + state.isLoadingBankInfo.value = false; + update(); + } + + Future getBankTypes() async { + if (state.banks.isNotEmpty) return; + + state.hasErrorBankType.value = false; + state.isLoadingBankType.value = true; + final result = await BankApi.getBankTypes(); + + if (result == null) { + state.hasErrorBankType.value = true; + state.isLoadingBankType.value = false; + update(); + return; + } + + state.banks.addAll(result); + + state.selectedBank.value = state.banks.first; + + state.isLoadingBankType.value = false; + update(); + } + + Future uploadFile() async { + debugPrint('uploadFile'); + var fileResult = await picker.FilePicker.platform.pickFiles( + type: picker.FileType.custom, + allowedExtensions: ['pdf', 'png', 'jpg', 'jpeg', 'JPG', 'PNG'], + ); + if (fileResult?.files.isNotEmpty ?? false) { + state.file.value = fileResult?.files.first; + update(); + } + } +} diff --git a/lib/new/screens/top_up_balance/online_top_up_screen.dart b/lib/new/screens/top_up_balance/online_top_up_screen.dart new file mode 100644 index 0000000..bd09531 --- /dev/null +++ b/lib/new/screens/top_up_balance/online_top_up_screen.dart @@ -0,0 +1,239 @@ +import 'package:birzha/components/actionIcons/searchicon.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/new/global/full_width_button.dart'; +import 'package:birzha/new/models/bank.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../components/button.dart'; +import '../../../components/categoryNameWidget.dart'; +import '../../../components/indicator.dart'; +import '../../../constants.dart'; +import '../../../core/manager/manager.dart'; +import '../../../models/user/userManager.dart'; +import '../../global/form_field_decoration.dart'; +import 'bank_top_up_screen.dart'; +import 'controller.dart'; + +class OnlineTopUpBalance extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GetBuilder( + init: BalanceController(), + builder: (tb) => BaseWidget( + // color: Theme.of(context).scaffoldBackgroundColor, + color: ThemeColor.white, + appBar: BaseAppBar( + title: 'topUpBalance'.translation, + after: [SearchIcon()], + goBack: () { + Navigator.of(context).pop(); + }, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: CategoryNameWidget( + categoryName: 'topUpBalance'.translation, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: 'selectPaymentMethod'.translation, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14.adaptedPx(), + ), + children: [ + new TextSpan( + text: ' *', + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.red, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + // vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: FullWidthButton( + callback: () {}, + title: 'payOnline'.translation, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: FullWidthButton( + callback: () { + tb.getBankInfo(); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => BankTopUpBalance())); + }, + title: 'bankTransfer'.translation, + btnColor: Color(0xFFF2F6FF), + txtColor: ThemeColor.mainColor, + ), + ), + SizedBox(height: 16.adaptedPx()), + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: 'selectBank'.translation, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14.adaptedPx(), + ), + children: [ + new TextSpan( + text: ' *', + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.red, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ), + tb.state.isLoadingBankType.value + ? Center(child: Indicator(size: 0.7.adaptedPx())) + : tb.state.hasErrorBankType.value + ? Center(child: Text('errorOccurred'.translation)) + : Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: 8.adaptedPx(), + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: ThemeColor.colorE2E2E2), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + borderRadius: BorderRadius.circular(16), + value: tb.state.selectedBank.value, + items: tb.state.banks.map( + (BankType value) { + return DropdownMenuItem( + value: value, + child: Text( + value.name, + // style: AppTheme.selectedTxtStyle, + ), + ); + }, + ).toList(), + onChanged: (newValue) => tb.onBankChange(newValue), + ), + ), + ), + ), + SizedBox(height: 16.adaptedPx()), + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + ), + child: RichText( + text: new TextSpan( + text: 'transferAmount'.translation, + style: new TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14.adaptedPx(), + ), + children: [ + new TextSpan( + text: ' *', + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.red, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ), + Padding( + // padding: const EdgeInsets.all(8), + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: 8.adaptedPx(), + ), + child: MyTextFormField( + label: 'transferAmount'.translation, + controller: tb.state.balanceTopUpCtrl, + hintText: 'enterAmount'.translation, + onChangeCallback: (value) => tb.onChange(value), + ), + ), + ManagerSelector( + onUpdate: () {}, + selector: (context, manager) => manager.getStatusByKey('balanceUp'), + shouldRebuild: (prev, next) => prev != next, + builder: (context, status) => Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.40, + child: MyButton( + text: 'send'.translation, + borderRadius: 8.0, + isDisabled: tb.state.topUpAmount.value < 1, + inProgress: status == TaskStatus.Loading, + onTap: () async { + debugPrint('balanceUp'); + if (tb.state.topUpAmount.value >= 1) + AppUserManager.of(context).balanceUp( + context, + tb.state.selectedBank.value!.cardType, + tb.state.topUpAmount.value, + ); + }, + height: 48.adaptedPx(), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/new/screens/top_up_balance/state.dart b/lib/new/screens/top_up_balance/state.dart new file mode 100644 index 0000000..cdf32ed --- /dev/null +++ b/lib/new/screens/top_up_balance/state.dart @@ -0,0 +1,31 @@ +import 'package:birzha/new/models/bank.dart'; +import 'package:birzha/new/models/bank_info.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:file_picker/file_picker.dart' as picker; + +class TopUpBalanceState { + // List banks = [ + // new BankType( name: 'Altyn asyr karty', cardType: 'halkbank'), + // new BankType( name: 'Rysgal bank', cardType: 'rysgal'), + // new BankType( name: 'Senagat bank', cardType: 'senagat'), + // ]; + + // bank type + RxBool hasErrorBankType = false.obs; + RxBool isLoadingBankType = false.obs; + RxList banks = [].obs; + + TextEditingController balanceTopUpCtrl = new TextEditingController(); + RxDouble topUpAmount = 0.0.obs; + + Rxn file = Rxn(); + + Rxn selectedBank = Rxn(); + + // Bank info + RxBool hasErrorBankInfo = false.obs; + RxBool isLoadingBankInfo = false.obs; + + Rxn bankInfo = Rxn(); +} diff --git a/lib/new/themes/colors.dart b/lib/new/themes/colors.dart new file mode 100644 index 0000000..a3ad4a5 --- /dev/null +++ b/lib/new/themes/colors.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ThemeColor { + static const Color mainColor = Color(0xFF003197); + static const Color grey = Colors.grey; + static const Color white = Color(0xFFFFFFFF); + static const Color colorEFF0F4 = Color(0xFFEFF0F4); + static const Color colorE2E2E2 = Color(0xFFE2E2E2); + static const Color colorE5E5E5 = Color(0xFFE5E5E5); + static const Color color717278 = Color(0xFF717278); + static const Color color3A3A3A = Color(0xFF73A3A3A); + static const Color colorC9C9C9 = Color(0xFFC9C9C9); + static const Color black = Color(0xFF000000); + static const Color cursorColor = Colors.grey; +} diff --git a/lib/new/utils/capitalize.dart b/lib/new/utils/capitalize.dart new file mode 100644 index 0000000..26ad4b7 --- /dev/null +++ b/lib/new/utils/capitalize.dart @@ -0,0 +1,5 @@ +extension StringExtension on String { + String capitalizeFirstLetter() { + return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}"; + } +} diff --git a/lib/new/utils/constants.dart b/lib/new/utils/constants.dart new file mode 100644 index 0000000..d2181ca --- /dev/null +++ b/lib/new/utils/constants.dart @@ -0,0 +1,3 @@ +class Constants { + static const BASE_URL = 'https://tmex.gov.tm/'; +} diff --git a/lib/new/utils/http_util.dart b/lib/new/utils/http_util.dart new file mode 100644 index 0000000..031fbf8 --- /dev/null +++ b/lib/new/utils/http_util.dart @@ -0,0 +1,328 @@ +import 'dart:async'; + +import 'package:birzha/models/user/userManager.dart'; +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'constants.dart'; + +class HttpUtil { + static HttpUtil _instance = HttpUtil._internal(); + + factory HttpUtil() => _instance; + + late Dio dio; + CancelToken cancelToken = new CancelToken(); + + HttpUtil._internal() { + BaseOptions options = new BaseOptions( + baseUrl: Constants.BASE_URL, + connectTimeout: 100000, + receiveTimeout: 500000, + headers: {}, + contentType: 'application/json; charset=utf-8', + ); + + dio = new Dio(options); + + // CookieJar cookieJar = CookieJar(); + // dio.interceptors.add(CookieManager(cookieJar)); + + dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + // Do something before request is sent + return handler.next(options); //continue + // If you want to complete the request and return some custom data, + // you can resolve a Response object `handler.resolve(response)`. + // In this way, the request will be terminated, the upper then will be called, + // and the data returned in then will be your custom response. + // If you want to terminate the request and trigger an error, + // you can return a `DioError` object, such as `handler.reject(error)`, + // In this way, the request will be aborted and an exception will be triggered, + // and the upper catchError will be called + }, + onResponse: (response, handler) { + // Do something with response data + return handler.next(response); // continue + // If you want to terminate the request and trigger an error, you can reject a `DioError` object, such as `handler.reject(error)`, + // In this way, the request will be aborted and an exception will be triggered, and the upper-level catchError will be called. + }, + onError: (DioError e, handler) { + // Do something with response error + ErrorEntity eInfo = createErrorEntity(e); + + switch (eInfo.code) { + case 401: // No permission to log in again + // navigateToLoginScreen(); + // setLoginStatus(false); + break; + default: + } + return handler.next(e); //continue + }, + )); + } + +/* + + Error unified processing + */ + // error message + ErrorEntity createErrorEntity(DioError error) { + switch (error.type) { + case DioErrorType.cancel: + return ErrorEntity(code: -1, message: 'Request cancellation'); + case DioErrorType.connectTimeout: + return ErrorEntity(code: -1, message: 'Connection timed out'); + case DioErrorType.sendTimeout: + return ErrorEntity(code: -1, message: 'Request timed out'); + case DioErrorType.receiveTimeout: + return ErrorEntity(code: -1, message: 'Response timeout'); + case DioErrorType.response: + { + try { + int? errCode = error.response?.statusCode; + // String errMsg = error.response.statusMessage; + // return ErrorEntity(code: errCode, message: errMsg); + switch (errCode) { + case 400: + return ErrorEntity(code: errCode, message: 'Request syntax error'); + case 401: + return ErrorEntity(code: errCode, message: 'Permission denied'); + case 403: + return ErrorEntity(code: errCode, message: 'Server refused to execute'); + case 404: + return ErrorEntity(code: errCode, message: 'Can not reach server'); + case 405: + return ErrorEntity(code: errCode, message: 'Request method is forbidden'); + case 500: + return ErrorEntity(code: errCode, message: 'Server internal error'); + case 502: + return ErrorEntity(code: errCode, message: 'Invalid request'); + case 503: + return ErrorEntity(code: errCode, message: 'Server hung up'); + case 505: + return ErrorEntity(code: errCode, message: 'Does not support HTTP protocol request'); + default: + { + // return ErrorEntity(code: errCode, message: 'Unknown error'); + return ErrorEntity( + code: errCode, + message: error.response?.statusMessage, + ); + } + } + } on Exception catch (_) { + // showSnack('error'.tr, 'internet_conn_err'.tr, SnackType.ERROR); + return ErrorEntity(code: -1, message: 'Unknown error'); + } + } + default: + { + return ErrorEntity(code: -1, message: error.message); + } + } + } + + /* + Cancel Request + The same cancel token can be used for multiple requests. When a cancel token is cancelled, + all requests using the cancel token will be cancelled. So the parameters are optional + */ + void cancelRequests(CancelToken token) { + token.cancel('cancelled'); + } + + Future> getAuthorizationHeader() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + Map headers; + String? accessToken = prefs.getString('token'); + headers = { + 'Authorization': 'Bearer $accessToken', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }; + return headers; + } + + /// restful get + /// refresh false + /// noCache true + /// list false + /// cacheKey + /// cacheDisk + Future get({ + required String path, + Map? queryParameters, + Options? options, + bool refresh = false, + bool noCache = false, + bool list = false, + String cacheKey = '', + bool cacheDisk = false, + }) async { + Options requestOptions = options ?? Options(); + if (requestOptions.extra == null) { + requestOptions.extra = Map(); + } + requestOptions.extra!.addAll({ + 'refresh': refresh, + 'noCache': noCache, + 'list': list, + 'cacheKey': cacheKey, + 'cacheDisk': cacheDisk, + }); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = await getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + + var response = await dio.get(path, queryParameters: queryParameters, options: requestOptions, cancelToken: cancelToken); + return response.data; + } + + /// restful post + Future post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = await getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + var response = await dio.post( + path, + data: data, + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + } + + /// restful put + Future put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = await getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + var response = await dio.put( + path, + data: data, + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + } + + /// restful patch + Future patch( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = await getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + var response = await dio.patch( + path, + data: data, + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + } + + /// restful delete + Future delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = await getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + var response = await dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + } + + /// restful post form +/*Future postForm( + String path, { + required Map data, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = getAuthorizationHeader(); + requestOptions.headers!.addAll(authorization); + var response = await dio.post( + path, + data: FormData.fromMap(data), + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + }*/ + + /// restful post Stream +/*Future postStream( + String path, { + dynamic data, + int dataLength = 0, + Map? queryParameters, + Options? options, + }) async { + Options requestOptions = options ?? Options(); + requestOptions.headers = requestOptions.headers ?? {}; + Map? authorization = getAuthorizationHeader(); + if (authorization != null) { + requestOptions.headers!.addAll(authorization); + } + requestOptions.headers!.addAll({ + Headers.contentLengthHeader: dataLength.toString(), + }); + var response = await dio.post( + path, + data: Stream.fromIterable(data.map((e) => [e])), + queryParameters: queryParameters, + options: requestOptions, + cancelToken: cancelToken, + ); + return response.data; + } */ +} + +// Exception handling +class ErrorEntity implements Exception { + int? code; + String? message; + + ErrorEntity({this.code, this.message}); + + @override + String toString() { + if (message == null) return 'Exception'; + return 'Exception: code $code, $message'; + } +} diff --git a/lib/new/utils/locale.dart b/lib/new/utils/locale.dart new file mode 100644 index 0000000..72a48a6 --- /dev/null +++ b/lib/new/utils/locale.dart @@ -0,0 +1,9 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +Future getLocale() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final String locale = prefs.getString('language') ?? 'tm'; + if (locale == 'tk') return 'tm'; + + return locale; +} diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart new file mode 100644 index 0000000..598885a --- /dev/null +++ b/lib/screens/auth/login.dart @@ -0,0 +1,158 @@ +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/auth/register.dart'; +import 'package:birzha/screens/first_page.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter/scheduler.dart'; + +class LoginScreen extends StatefulWidget { + LoginScreen({Key? key}) : super(key: key); + + @override + _LoginScreenState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State with AbstractFormState, ManagerObserverMixin { + SampleUser sampleUser = SampleUser(data: {}); + + TaskStatus status = TaskStatus.None; + + @override + bool get includeBackButton => false; + + @override + List get before => [ + Text( + 'login'.translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.bold, + fontSize: AppConstants.h1FontSize, + ), + ), + SizedBox(height: 5.adaptedPx()), + Container( + margin: EdgeInsets.symmetric(vertical: 15.adaptedPx()), + child: Text.rich(TextSpan(children: [ + for (var word in 'loginText'.translation.split(' ')) + word == '@' + ? (TextSpan( + style: Theme.of(context).primaryTextTheme.bodyText2!.copyWith( + color: Theme.of(context).accentColor, + ), + text: 'link'.translation + ' ', + recognizer: TapGestureRecognizer() + ..onTap = () { + linkLauncher(AppConstants.passwordReset); + })) + : TextSpan(text: word + ' '), + ])), + ) + ]; + + @override + List get after => [ + SizedBox( + height: 15.adaptedPx(), + ), + Row( + children: [ + Text('noAccount'.translation, style: TextStyle(fontWeight: FontWeight.w300)), + SizedBox(width: 5.adaptedPx()), + GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => RegisterScreen()), + ); + }, + child: Text( + 'register'.translation, + style: TextStyle(color: Theme.of(context).accentColor, fontWeight: FontWeight.w600), + ), + ) + ], + ), + SizedBox( + height: 15.adaptedPx(), + ) + ]; + + @override + List get inputs => sampleUser.loginMetaData; + + @override + String get title => ''; + + @override + String get buttonLabel => 'login'.translation; + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String valueAfterFilter(String key, String input) { + if (key == 'dial_code') { + return input.split(' ').last; + } + return input; + } + + @override + void act() { + sampleUser.jSON = {...editedData}; + AppUserManager.of(context).login(context, sampleUser); + } + + @override + void dispose() { + super.dispose(); + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + manager.destroyTask(_keyForTask); + }); + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_keyForTask); + } + + @override + bool shouldUpdateListener(TaskStatus old, TaskStatus newOne) { + return old != newOne; + } + + @override + void updateListener() { + if (mounted) + setState(() { + status = AppUserManager.of(context).getStatusByKey(_keyForTask); + if (status == TaskStatus.Success) { + Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => FirstPage()), (route) => false); + } + }); + } + + @override + void initState() { + super.initState(); + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + } +} + +const _keyForTask = 'login'; diff --git a/lib/screens/auth/logout.dart b/lib/screens/auth/logout.dart new file mode 100644 index 0000000..9fa9755 --- /dev/null +++ b/lib/screens/auth/logout.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/models/exceptions/exception.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/first_page.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; + +class Logout extends StatefulWidget { + Logout({Key? key}) : super(key: key); + + @override + _LogoutState createState() => _LogoutState(); +} + +class _LogoutState extends State with ManagerObserverMixin?> { + StreamSubscription? _subscription; + + @override + selector(BuildContext context, AppUserManager someManager) { + return someManager.getTaskByKey('logout'); + } + + @override + bool shouldUpdateListener(oldVal, newVal) { + return oldVal != newVal; + } + + @override + void updateListener() async { + await _subscription?.cancel(); + _subscription = AppUserManager.of(context).taskStateOnly('logout')?.listen((event) { + var navigatorContext = Navigator.of(context, rootNavigator: true).context; + if (event.status == TaskStatus.Success && mounted) { + Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => FirstPage()), (route) => false); + } else if (event.status == TaskStatus.Error) { + AppExceptions.exceptionHandler(navigatorContext, event.errorTrace); + Navigator.of(context).pop(); + } + }); + } + + @override + void initState() { + super.initState(); + manager.logout(); + updateListener(); + } + + @override + void dispose() { + _subscription?.cancel().then((value) { + manager.destroyTask('logout'); + }); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Indicator(), + ), + SizedBox( + height: 30.adaptedPx(), + ), + Text( + 'logging_out'.translation, + style: TextStyle(fontSize: 17.adaptedPx(), fontWeight: FontWeight.w300, color: Theme.of(context).accentColor), + ) + ], + ), + ), + ); + } +} diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart new file mode 100644 index 0000000..4f1e3a2 --- /dev/null +++ b/lib/screens/auth/register.dart @@ -0,0 +1,109 @@ +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/first_page.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; + +class RegisterScreen extends StatefulWidget { + RegisterScreen({Key? key}) : super(key: key); + + @override + _RegisterScreenState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State + with AbstractFormState, ManagerObserverMixin { + SampleUser sampleUser = SampleUser(data: {}); + + TaskStatus status = TaskStatus.None; + + @override + void initState() { + super.initState(); + } + + @override + List get before => [ + Text( + 'register'.translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.bold, + fontSize: AppConstants.h1FontSize, + ), + ), + SizedBox(height: 5.adaptedPx()), + ]; + + @override + List get after => []; + + @override + List get inputs => sampleUser.registerMetaData; + + @override + String get title => ''; + + @override + String get buttonLabel => 'register'.translation; + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String valueAfterFilter(String key, String input) { + if (key == 'dial_code') { + return input.split(' ').last; + } + + return input; + } + + @override + void act() { + sampleUser.jSON = {...editedData}; + AppUserManager.of(context).register(context, sampleUser); + } + + @override + void dispose() { + super.dispose(); + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_keyForTask); + } + + @override + bool shouldUpdateListener(TaskStatus old, TaskStatus newOne) { + return old != newOne; + } + + @override + void updateListener() { + if (mounted) + setState(() { + status = AppUserManager.of(context).getStatusByKey(_keyForTask); + if (status == TaskStatus.Success) { + Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => FirstPage()), (route) => false); + } + }); + } +} + +const _keyForTask = 'register'; diff --git a/lib/screens/auth/smsVerification.dart b/lib/screens/auth/smsVerification.dart new file mode 100644 index 0000000..13c7c23 --- /dev/null +++ b/lib/screens/auth/smsVerification.dart @@ -0,0 +1,99 @@ +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:birzha/core/manager/manager.dart'; + +class SmsVerificationScreen extends StatefulWidget { + const SmsVerificationScreen({Key? key}) : super(key: key); + + @override + _SmsVerificationScreenState createState() => _SmsVerificationScreenState(); +} + +class _SmsVerificationScreenState extends State + with AbstractFormState, ManagerObserverMixin { + TaskStatus status = TaskStatus.None; + + late User user; + + @override + List get before => [ + Text( + 'smsCodeSent'.translation, + ), + ]; + + @override + void initState() { + user = AppUserManager.of(context).dataSync; + super.initState(); + } + + @override + void act() { + var code = editedData.values.first; + AppUserManager.of(context).checkCode(context, code); + } + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String get buttonLabel => 'check'.translation; + + @override + List get inputs => user.smsVerificationMetaData; + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String get title => 'verifyPhone'.translation; + + @override + String valueAfterFilter(String key, String input) { + return input; + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_keyForTask); + } + + @override + bool shouldUpdateListener(TaskStatus oldVal, TaskStatus newVal) { + return oldVal != newVal; + } + + @override + void updateListener() { + if (mounted) + setState(() { + status = AppUserManager.of(context).getStatusByKey(_keyForTask); + if (status == TaskStatus.Success && mounted) { + Navigator.of(context).pop(); + } + }); + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + } + + @override + void dispose() { + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + manager.destroyTask(_keyForTask); + }); + super.dispose(); + } +} + +const _keyForTask = 'sms_verify'; diff --git a/lib/screens/auth/update.dart b/lib/screens/auth/update.dart new file mode 100644 index 0000000..814cdff --- /dev/null +++ b/lib/screens/auth/update.dart @@ -0,0 +1,142 @@ +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; + +class UpdateScreen extends StatefulWidget { + UpdateScreen({Key? key}) : super(key: key); + + @override + _UpdateScreenState createState() => _UpdateScreenState(); +} + +class _UpdateScreenState extends State with AbstractFormState, ManagerObserverMixin { + SampleUser sampleUser = SampleUser(data: {}); + + TaskStatus status = TaskStatus.None; + + @override + Widget textField(TextInputMetaData meta, TextEditingController controller) { + final bool isPhoneVerified = AppUserManager.of(context).dataSync.isPhoneVerified; + final bool isEmailVerified = AppUserManager.of(context).dataSync.isEmailVerified; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + super.textField(meta, controller), + if (meta.key == 'username' && !isPhoneVerified) + new RichText( + text: new TextSpan( + text: '*', + style: new TextStyle(color: Colors.red), + children: [ + new TextSpan( + text: 'verifyPhoneWarning'.translation, + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.black, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + if (meta.key == 'email' && !isEmailVerified) + new RichText( + text: new TextSpan( + text: '*', + style: new TextStyle(color: Colors.red), + children: [ + new TextSpan( + text: 'verifyEmailWarning'.translation, + style: new TextStyle( + fontWeight: FontWeight.w400, + color: Colors.black, + fontSize: 12.adaptedPx(), + ), + ), + ], + ), + ), + ], + ); + } + + @override + List get before => []; + + @override + List get after => []; + + @override + List get inputs => sampleUser.updateMetaData; + + @override + String get title => 'personal_data'.translation; + + @override + String get buttonLabel => 'save_changes'.translation; + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String valueAfterFilter(String key, String input) { + return input; + } + + @override + void act() { + sampleUser.jSON = {...editedData}..removeWhere((key, value) => value.isEmpty); + AppUserManager.of(context).update(context, sampleUser); + } + + @override + void initState() { + super.initState(); + launchControllers(); + } + + @override + void launchControllers() { + super.launchControllers(); + for (var i = 0; i < controllers.length; i++) controllers[i].text = AppUserManager.of(context).dataSync.jSON[inputs[i].key] ?? ""; + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_keyForTask); + } + + @override + bool shouldUpdateListener(TaskStatus old, TaskStatus newOne) { + return old != newOne; + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + launchControllers(); + setState(() {}); + } + + @override + void updateListener() { + if (mounted) + setState(() { + status = AppUserManager.of(context).getStatusByKey(_keyForTask); + if (status == TaskStatus.Success) showSnackBar(context, content: 'dataIsUpdated'.translation); + }); + } +} + +const _keyForTask = 'update'; diff --git a/lib/screens/details.dart b/lib/screens/details.dart new file mode 100644 index 0000000..42b70ad --- /dev/null +++ b/lib/screens/details.dart @@ -0,0 +1,409 @@ +import 'dart:ui'; + +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/button.dart'; +import 'package:birzha/components/categoryNameWidget.dart'; +import 'package:birzha/components/contactInfo.dart'; +import 'package:birzha/components/dialog.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/productBuilder.dart'; +import 'package:birzha/components/refreshButton.dart'; +import 'package:birzha/components/slider.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/personalCabinet/addPost/step1.dart'; +import 'package:birzha/screens/personalCabinet/messages/personalChat.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/streamFetchService.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:birzha/core/manager/manager.dart'; + +class ProductDetails extends StatefulWidget { + final Product product; + + const ProductDetails({ + Key? key, + required this.product, + }) : super(key: key); + + @override + _ProductDetailsState createState() => _ProductDetailsState(); +} + +class _ProductDetailsState extends State with StreamControlledMixin { + late Product product; + bool _isError = false; + + @override + void initState() { + super.initState(); + product = widget.product.copy; + connect(context); + } + + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).chipTheme.backgroundColor, + appBar: BaseAppBar( + goBack: () { + Tabnavigator.backDispatcher(context); + }, + title: product.name, + ), + body: isLoading + ? Center( + child: Indicator(size: 0.7.adaptedPx()), + ) + : _isError + ? RefreshButton( + onTap: () { + connect(context); + }, + ) + : ListView( + children: [ + if (product.images.isNotEmpty) + PostImageSlider( + images: [...product.images], + fit: BoxFit.cover, + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: CategoryNameWidget(categoryName: product.name), + ), + SizedBox( + height: 25.adaptedPx(), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + if (product is MyProduct && (product as MyProduct).statusNotes.isNotEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 25.adaptedPx(), horizontal: 15.adaptedPx()), + width: 30.adaptedPx(), + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 8.adaptedPx(), right: 8.adaptedPx(), bottom: 12.adaptedPx()), + child: Text( + 'statusNote'.translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.w600, + fontSize: AppConstants.h1FontSize, + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 8.adaptedPx(), right: 8.adaptedPx(), bottom: 12.adaptedPx()), + child: Text((product as MyProduct).statusNotes)), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 25.adaptedPx(), horizontal: 15.adaptedPx()), + width: 30.adaptedPx(), + color: Theme.of(context).chipTheme.backgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 8.adaptedPx(), right: 8.adaptedPx(), bottom: 12.adaptedPx()), + child: Text( + 'description'.translation, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.w600, + fontSize: AppConstants.h1FontSize, + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 8.adaptedPx(), right: 8.adaptedPx(), bottom: 12.adaptedPx()), + child: Html( + data: product.description, + customRender: AppConstants.htmlCustomRenderer(context), + style: AppConstants.htmlContentStyle(context), + )), + ], + ), + ), + for (var i = 0; i < product.characteristics.length; i++) + DetailsScreenRow( + name: product.characteristics[i].translatedLabel, + value: product.characteristics[i].value, + backgroundColor: i.isOdd ? Theme.of(context).chipTheme.backgroundColor : null, + ), + if (product.relatedProducts.isNotEmpty) + SizedBox( + height: 25.adaptedPx(), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + if (product.vendor != null && product is! MyProduct) + Padding( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: MyButton( + text: 'sellerContact'.translation, + color: Colors.green.shade600, + onTap: () { + showDialog( + context: context, + builder: (context) { + return _ContactDialog( + vendor: product.vendor!, + ); + }); + }, + height: 40.adaptedPx(), + ), + ), + SizedBox(width: AppConstants.horizontalPadding(context) / 2), + if (AppUserManager.of(context, listen: true).dataSync != product.vendor) + Expanded( + child: ManagerSelector( + selector: (context, manager) => manager.getStatusByKey('buy'), + onUpdate: () {}, + shouldRebuild: (prev, next) => prev != next, + builder: (context, taskStatus) => MyButton( + text: 'buyNow'.translation, + onTap: () { + AppUserManager.of(context).buyFromSeller(Navigator.of(context).context, product.vendor!, (chatroom) { + if (mounted) + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute( + builder: (context) => PersonalChat(chatroom: chatroom), + )); + }); + }, + inProgress: taskStatus == TaskStatus.Loading, + height: 40.adaptedPx(), + ), + ), + ), + ], + ), + ) + else if (product is MyProduct && (product as MyProduct).isAbleToEdit) ...[ + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding( + context, + )), + child: Row( + children: [ + Expanded( + child: ManagerSelector( + selector: (contex, manager) => manager.getStatusByKey('delete_post'), + shouldRebuild: (previous, next) => previous != next, + onUpdate: () { + if (mounted && AppUserManager.of(context).getStatusByKey('delete_post') == TaskStatus.Success) { + Navigator.of(context).pop(true); + } + }, + builder: (context, value) => MyButton( + text: caitalize(MaterialLocalizations.of(context).deleteButtonTooltip), + color: Colors.redAccent, + inProgress: value == TaskStatus.Loading, + onTap: () { + AppUserManager.of(context).deletePost(context, product as MyProduct); + }, + height: 40.adaptedPx(), + ), + ), + ), + SizedBox(width: AppConstants.horizontalPadding(context) / 2), + Expanded( + child: MyButton( + text: 'edit'.translation, + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AddBasicInformationScreen(forEditing: product as MyProduct), + )); + }, + height: 40.adaptedPx(), + ), + ), + ], + ), + ), + SizedBox( + height: 10.adaptedPx(), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding( + context, + )), + child: Row( + children: [ + Expanded( + child: ManagerSelector( + selector: (contex, manager) => manager.getStatusByKey('publish_post'), + shouldRebuild: (previous, next) => previous != next, + onUpdate: () { + if (mounted && AppUserManager.of(context).getStatusByKey('publish_post') == TaskStatus.Success) { + Navigator.of(context).pop(true); + } + }, + builder: (context, value) => MyButton( + text: 'approve'.translation, + color: Colors.green, + inProgress: value == TaskStatus.Loading, + onTap: () { + AppUserManager.of(context).publishPost(context, product as MyProduct); + }, + height: 40.adaptedPx(), + ), + ), + ), + ], + ), + ) + ], + if (product.relatedProducts.isNotEmpty) + Padding( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: AppConstants.verticalPadding(context), + ), + child: CategoryNameWidget(categoryName: 'similiarProducts'.translation), + ), + if (product.relatedProducts.isNotEmpty) + Container( + height: AppConstants.productCarouselHeight(context), + width: MediaQuery.of(context).size.width, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + itemBuilder: (context, index) { + return AspectRatio(aspectRatio: kProductAspectRatio, child: ProductBuilder(product: product.relatedProducts[index])); + }, + separatorBuilder: (_, __) { + return SizedBox(width: AppConstants.horizontalPadding(context) / 2); + }, + itemCount: product.relatedProducts.length), + ), + SizedBox(height: 20.adaptedPx()) + ], + ), + ); + } + + @override + Future Function() get asyncAction => () => product.loadDetails(context); + + @override + void onDataRecived(BuildContext context, void data) { + setState(() {}); + } + + @override + void error(BuildContext context, error) { + setState(() { + _isError = true; + }); + } + + @override + void start(BuildContext context) { + setState(() { + _isError = false; + }); + } + + @override + void dispose() { + cancel(); + super.dispose(); + } +} + +class _ContactDialog extends StatelessWidget { + const _ContactDialog({Key? key, required this.vendor}) : super(key: key); + + final Vendor vendor; + + @override + Widget build(BuildContext context) { + return AppDialog( + title: 'sellerContact'.translation, + child: Column( + children: [ + SizedBox(height: 10.adaptedPx()), + ContactInfo(icon: Icons.person_outline, mainText: 'nsm'.translation + ':', description: [vendor.name, vendor.surname].join(' ')), + SizedBox(height: 25.adaptedPx()), + ContactInfo(icon: Icons.phone_outlined, mainText: 'telephone'.translation + ':', description: vendor.phone), + SizedBox(height: 25.adaptedPx()), + ContactInfo(icon: Icons.mail_outlined, mainText: 'mail'.translation + ':', description: vendor.email), + ], + ), + ); + } +} + +class DetailsScreenRow extends StatelessWidget { + final String name; + final Color? backgroundColor; + final Widget? nextSign; + final Icon? icon; + final String value; + + const DetailsScreenRow({ + Key? key, + required this.name, + this.backgroundColor, + this.nextSign, + this.icon, + required this.value, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + child: InkWell( + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 26.adaptedPx(), horizontal: 20.adaptedPx()), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text(name, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.w400, + )), + SizedBox( + width: 10.adaptedPx(), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Text( + value, + style: Theme.of(context).accentTextTheme.headline2!.copyWith(fontWeight: FontWeight.w400), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/first_page.dart b/lib/screens/first_page.dart new file mode 100644 index 0000000..1d0ddef --- /dev/null +++ b/lib/screens/first_page.dart @@ -0,0 +1,18 @@ +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/auth/login.dart'; +import 'package:birzha/screens/primal.dart'; +import 'package:flutter/material.dart'; + +class FirstPage extends StatelessWidget { + const FirstPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint('FirstPage'); + if (AppUserManager.of(context).dataSync.isRegistered) { + return Primal(); + } else { + return LoginScreen(); + } + } +} diff --git a/lib/screens/home/homeScreen.dart b/lib/screens/home/homeScreen.dart new file mode 100644 index 0000000..cef941c --- /dev/null +++ b/lib/screens/home/homeScreen.dart @@ -0,0 +1,124 @@ +import 'dart:ui'; + +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/categoryNameWidget.dart'; +import 'package:birzha/components/imagePlaceHolder.dart'; +import 'package:birzha/components/postlist.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/lazyload/lazyload.dart'; +import 'package:birzha/models/categories/home.dart'; +import 'package:birzha/models/products/product.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class HomeScreen extends StatefulWidget { + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State with AutomaticKeepAliveClientMixin { + FetchController controller = FetchController(); + var category = HomeCategory(); + + @override + void initState() { + debugPrint('HomeScreen.initState'); + super.initState(); + SchedulerBinding.instance?.addPostFrameCallback((timeStamp) { + controller.refresh(); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return BaseWidget( + appBar: BaseAppBar.home( + context, + () {}, + ), + color: Theme.of(context).backgroundColor, + body: PostList( + category: category, + before: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: AppConstants.verticalPadding(context), + horizontal: AppConstants.horizontalPadding(context), + ), + child: CategoryNameWidget(categoryName: category.name), + ), + ) + ], + contentPadding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context)), + fetchController: controller, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} + +class HomeScreenButton extends StatelessWidget { + final String? name; + final Color? color; + final void Function() onTap; + final String? iconLink; + + const HomeScreenButton({ + Key? key, + this.iconLink, + required this.name, + this.color, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.adaptedPx(), vertical: 10.adaptedPx()), + child: InkWell( + onTap: onTap, + hoverColor: Theme.of(context).cardColor, // alternative color dividerColor; + borderRadius: BorderRadius.circular(15.adaptedPx()), + child: Container( + decoration: BoxDecoration(color: Theme.of(context).accentColor.withOpacity(0.1), borderRadius: BorderRadius.circular(10.adaptedPx())), + padding: EdgeInsets.symmetric( + horizontal: 10.adaptedPx(), + vertical: 13.2.adaptedPx(), + ), + child: Row( + children: [ + SizedBox(width: 13.adaptedPx()), + if (iconLink != null) + CachedNetworkImage( + height: 35.adaptedPx(), + width: 35.adaptedPx(), + imageUrl: iconLink!, + placeholder: (_, __) => AppImagePlaceholder(size: 30.adaptedPx()), + errorWidget: (_, __, ___) => AppImagePlaceholder(size: 30.adaptedPx()), + ), + SizedBox(width: 15.adaptedPx()), + Expanded( + child: Text( + name!, + style: Theme.of(context).accentTextTheme.headline2!.copyWith(fontWeight: FontWeight.bold, fontSize: AppConstants.h2FontSize * 0.95), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/masterCategoryScreen.dart b/lib/screens/masterCategoryScreen.dart new file mode 100644 index 0000000..ff7bce4 --- /dev/null +++ b/lib/screens/masterCategoryScreen.dart @@ -0,0 +1,85 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/categoryBuilder.dart'; +import 'package:birzha/components/fakeSearchBar.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/refreshButton.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/products/pure.dart'; +import 'package:birzha/models/categories/products/remote.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/lazyload/lazyload.dart'; + +class MasterCategoryScreen extends StatefulWidget { + const MasterCategoryScreen({Key? key}) : super(key: key); + + @override + _MasterCategoryScreenState createState() => _MasterCategoryScreenState(); +} + +class _MasterCategoryScreenState extends State with AutomaticKeepAliveClientMixin { + final category = MasterProductsRemoteCategory(); + + @override + Widget build(BuildContext context) { + super.build(context); + return BaseWidget( + appBar: BaseAppBar( + color: ThemeColor.white, + goBack: () {}, + customChild: FakeSearchBar( + route: 'category', + ), + ), + color: Theme.of(context).backgroundColor, + body: LazyLoadView( + data: (page, context) => category.getSubCategories(context, page: page), + loaderWidget: SliverFillRemaining( + child: Center(child: Indicator(size: 0.7.adaptedPx())), + ), + loadMoreWidget: SliverToBoxAdapter( + child: Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + alignment: Alignment.center, + child: Row( + children: [ + Expanded( + child: Center(child: Indicator(size: 0.6.adaptedPx())), + ) + ], + ), + ), + ), + contentPadding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context) / 1.2, vertical: 15.adaptedPx()), + errorWidget: (refresh, error) { + return SliverFillRemaining( + child: Center( + child: RefreshButton( + onTap: refresh, + size: 38.adaptedPx(), + ), + ), + ); + }, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: AppConstants.categoryAxisCount(context), + childAspectRatio: 1, + mainAxisSpacing: AppConstants.horizontalPadding(context) / 1.2, + crossAxisSpacing: AppConstants.horizontalPadding(context) / 1.2), + errorOnLoadMoreWidget: (refresh, error) { + return Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + child: RefreshButton( + onTap: refresh, + size: 25.adaptedPx(), + ), + ); + }, + needPagination: false, + itemBuilder: (context, model, index) => CategoryBuilder(category: model))); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/screens/personalCabinet/addPost/step1.dart b/lib/screens/personalCabinet/addPost/step1.dart new file mode 100644 index 0000000..33c10a7 --- /dev/null +++ b/lib/screens/personalCabinet/addPost/step1.dart @@ -0,0 +1,205 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/refreshButton.dart'; +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/products/composableProduct.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/personalCabinet/addPost/step2.dart'; +import 'package:birzha/services/streamFetchService.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; + +class AddBasicInformationScreen extends StatefulWidget { + const AddBasicInformationScreen({Key? key, required this.forEditing}) : super(key: key); + + final MyProduct? forEditing; + + @override + State createState() => _AddBasicInformationScreenState(); +} + +class _AddBasicInformationScreenState extends State + with AbstractFormState, StreamControlledMixin, ManagerObserverMixin { + TaskStatus status = TaskStatus.None; + + late final User user; + + bool isError = false; + + Map selectedAttrbiutes = {}; + + @override + body(BuildContext context) { + if (isLoading) + return Center( + child: Indicator( + size: 0.7.adaptedPx(), + ), + ); + else if (isError) + return Center( + child: Container( + height: 120.adaptedPx(), + child: RefreshButton( + onTap: () { + connect(context); + }, + ), + ), + ); + return super.body(context); + } + + @override + bool get shrinkWrap => isLoading; + + @override + void launchControllers([bool force = false]) { + if (force) { + var product = ComposableProduct(); + controllers = List.generate(inputs.length, (index) => TextEditingController(text: "")); + if (product.primaryKey != null) { + for (var i = 0; i < controllers.length; i++) { + var controller = controllers[i]; + var input = inputs[i]; + if (input.pickerMode == null) { + controller.text = product.jSON[input.key]?.toString() ?? ""; + } else if (product.getModelData(input.key) is AttributeWithValueNameMixin) { + var value = product.getModelData(input.key) as AttributeWithValueNameMixin; + selectedAttrbiutes[input.key] = value; + controller.text = value.label.toString(); + } + } + } + } else { + super.launchControllers(); + } + } + + @override + void dispose() { + cancel(); + super.dispose(); + } + + @override + void initState() { + user = AppUserManager.of(context).dataSync.copy; + connect(context); + super.initState(); + } + + @override + void act() { + for (int i = 0; i < inputs.length; i++) { + ComposableProduct().setBasicTypeField(inputs[i].key, controllers[i].text); + } + for (var attributeKey in selectedAttrbiutes.keys) { + ComposableProduct().setModelField(attributeKey, selectedAttrbiutes[attributeKey]!); + } + + var product = ComposableProduct(); + + AppUserManager.of(context).composeStep1(context, [...inputs.map((el) => el.key), if (product.primaryKey != null) product.primaryKeyField.toString()]); + } + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String get buttonLabel => MaterialLocalizations.of(context).saveButtonLabel; + + @override + List get inputs => [ + ...user.addPostMetaData, + for (var attribute in ComposableProduct().attrbiutes.whereType().where((element) => _attributeKeys.contains(element.key))) + attribute.metaData( + groupValue: selectedAttrbiutes[attribute.key], + onSelected: (option) { + if (option is AttributeWithValueNameMixin && mounted) { + setState(() { + selectedAttrbiutes[attribute.key] = option; + }); + } + }), + ]; + + @override + void start(BuildContext context) { + setState(() { + isError = false; + }); + super.start(context); + } + + @override + void error(BuildContext context, error) { + print(error); + setState(() { + isError = true; + }); + super.error(context, error); + } + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String get title => 'firstStep'.translation; + + @override + String valueAfterFilter(String key, String input) { + return input; + } + + @override + Future Function() get asyncAction => () => ComposableProduct.getAttributes(context, widget.forEditing); + + @override + void onDataRecived(BuildContext context, data) { + if (mounted) { + setState(() {}); + launchControllers(true); + } + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_taskStatusKey); + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + } + + @override + bool shouldUpdateListener(TaskStatus oldVal, TaskStatus newVal) { + return oldVal != newVal; + } + + @override + void updateListener() { + if (mounted) { + setState(() { + status = AppUserManager.of(context).getStatusByKey(_taskStatusKey); + if (status == TaskStatus.Success) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AddMoreInformationScreen( + forEditing: widget.forEditing, + ))); + } + }); + } + } +} + +const _taskStatusKey = 'compose_1'; +const _attributeKeys = ['category_id', 'market_type', 'country']; diff --git a/lib/screens/personalCabinet/addPost/step2.dart b/lib/screens/personalCabinet/addPost/step2.dart new file mode 100644 index 0000000..4438fce --- /dev/null +++ b/lib/screens/personalCabinet/addPost/step2.dart @@ -0,0 +1,358 @@ +import 'dart:io'; + +import 'package:birzha/components/abstractForm.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/refreshButton.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/products/composableProduct.dart'; +import 'package:birzha/models/products/my_product.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/personalCabinet/my_products.dart'; +import 'package:birzha/services/streamFetchService.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AddMoreInformationScreen extends StatefulWidget { + const AddMoreInformationScreen({Key? key, required this.forEditing}) : super(key: key); + + final MyProduct? forEditing; + + @override + State createState() => _AddBasicInformationScreenState(); +} + +class _AddBasicInformationScreenState extends State + with AbstractFormState, StreamControlledMixin, ManagerObserverMixin { + TaskStatus status = TaskStatus.None; + + late final User user; + + bool isError = false; + + Map selectedAttrbiutes = {}; + + Widget galleryBuilder(String title, List images) { + return Container( + margin: EdgeInsets.symmetric(vertical: 15.adaptedPx()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).accentTextTheme.headline2, + ), + Container( + height: 100.adaptedPx(), + width: MediaQuery.of(context).size.width, + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + child: ListView.builder( + itemCount: images.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return LayoutBuilder( + builder: (context, constraints) { + var image = images[index]; + var iconSize = 25.adaptedPx(); + var imageBuilder = image is LocalImageModel + ? Image.file(File(image.primaryKey), + height: constraints.maxHeight - iconSize, width: constraints.maxHeight - iconSize, fit: BoxFit.cover) + : CachedNetworkImage( + imageUrl: image.primaryKey, + height: constraints.maxHeight - iconSize, + width: constraints.maxHeight - iconSize, + errorWidget: (context, url, error) { + return Container( + height: constraints.maxHeight - iconSize, + width: constraints.maxHeight - iconSize, + alignment: Alignment.center, + child: Icon(Icons.image), + ); + }, + placeholder: (context, url) => Container( + height: constraints.maxHeight - iconSize, + width: constraints.maxHeight - iconSize, + alignment: Alignment.center, + child: Icon(Icons.broken_image), + ), + fit: BoxFit.cover, + ); + return Stack( + children: [ + Container( + margin: EdgeInsets.only(top: iconSize / 2, right: iconSize / 2), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.adaptedPx()), + child: imageBuilder, + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () { + ComposableProduct().removeImage(context, image); + }, + child: Icon( + CupertinoIcons.clear_circled_solid, + color: Theme.of(context).accentColor, + size: iconSize, + ), + ), + ), + ], + ); + }, + ); + }, + )) + ], + ), + ); + } + + @override + body(BuildContext context) { + if (isLoading) + return Center( + child: Indicator( + size: 0.7.adaptedPx(), + ), + ); + else if (isError) + return Center( + child: Container( + height: 120.adaptedPx(), + child: RefreshButton( + onTap: () { + connect(context); + }, + ), + ), + ); + return super.body(context); + } + + @override + bool get shrinkWrap => isLoading; + + @override + void launchControllers([bool force = false]) { + if (force) { + var product = ComposableProduct(); + controllers = List.generate(inputs.length, (index) => TextEditingController(text: "")); + if (product.primaryKey != null) { + for (var i = 0; i < controllers.length; i++) { + var controller = controllers[i]; + var input = inputs[i]; + if (input.pickerMode == null) { + controller.text = product.jSON[input.key]?.toString() ?? ""; + } else if (product.getModelData(input.key) is AttributeWithValueNameMixin) { + var value = product.getModelData(input.key) as AttributeWithValueNameMixin; + selectedAttrbiutes[input.key] = value; + controller.text = value.label.toString(); + } + } + } + } else { + super.launchControllers(); + } + } + + @override + void initState() { + user = AppUserManager.of(context).dataSync.copy; + connect(context); + super.initState(); + } + + @override + void dispose() { + cancel(); + super.dispose(); + } + + @override + void updateScreen() { + debugPrint('updateScreen'); + } + + @override + void act() { + for (int i = 0; i < inputs.length; i++) { + ComposableProduct().setBasicTypeField(inputs[i].key, controllers[i].text); + } + for (var attributeKey in selectedAttrbiutes.keys) { + ComposableProduct().setModelField(attributeKey, selectedAttrbiutes[attributeKey]!); + } + + var product = ComposableProduct(); + + AppUserManager.of(context).composeStep2(context, [...inputs.map((el) => el.key), if (product.primaryKey != null) product.primaryKeyField.toString()]); + } + + @override + bool get buttonIsLoading => status == TaskStatus.Loading; + + @override + String get buttonLabel => MaterialLocalizations.of(context).saveButtonLabel; + + @override + List get inputs => [ + ...user.addMoreMetaData, + for (var attribute in ComposableProduct().attrbiutes.whereType().where((element) => _attributeKeys.contains(element.key))) + attribute.metaData( + groupValue: selectedAttrbiutes[attribute.key], + onSelected: (option) { + if (option is AttributeWithValueNameMixin && mounted) { + setState(() { + selectedAttrbiutes[attribute.key] = option; + }); + } + }), + ]; + + @override + void start(BuildContext context) { + setState(() { + isError = false; + }); + super.start(context); + } + + @override + void error(BuildContext context, error) { + print(error); + setState(() { + isError = true; + }); + super.error(context, error); + } + + @override + String keyAfterFilter(String key, String input) { + return key; + } + + @override + String get title => 'secondStep'.translation; + + @override + String valueAfterFilter(String key, String input) { + return input; + } + + @override + Future Function() get asyncAction => () => ComposableProduct.getAttributes(context, widget.forEditing); + + @override + void onDataRecived(BuildContext context, data) { + if (mounted) { + setState(() {}); + launchControllers(true); + } + } + + @override + TaskStatus selector(BuildContext context, AppUserManager someManager) { + return someManager.getStatusByKey(_taskStatusKey); + } + + @override + bool shouldUpdateListener(TaskStatus oldVal, TaskStatus newVal) { + return oldVal != newVal; + } + + @override + void updateListener() { + if (mounted) { + setState(() { + status = AppUserManager.of(context).getStatusByKey(_taskStatusKey); + if (status == TaskStatus.Success) { + Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => MyProductsScreen()), (route) => route.isFirst); + } + }); + } + } + + @override + List get after => [ + GestureDetector( + onTap: ComposableProduct().pickImages, + child: Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + border: Border.all(color: Theme.of(context).accentColor, width: 0.7.adaptedPx()), + borderRadius: BorderRadius.circular(10.adaptedPx())), + padding: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + alignment: Alignment.center, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add, + size: 20.adaptedPx(), + color: Theme.of(context).accentColor, + ), + SizedBox( + width: 5.adaptedPx(), + ), + Text( + 'selectImages'.translation, + style: Theme.of(context).accentTextTheme.headline2, + ) + ], + ), + ), + ), + ), + Selector( + selector: (context, product) => product.localImages.length, + shouldRebuild: (previous, next) => previous != next, + builder: (context, value, child) { + if (value == 0) { + return Container(); + } else { + return galleryBuilder('localImages'.translation, ComposableProduct().localImages); + } + }, + ), + Selector( + selector: (_, manager) => manager.getStatusByKey('delete_image'), + shouldRebuild: (previous, next) => previous != next, + builder: (context, value, child) { + if (value == TaskStatus.Loading) { + return Container( + height: 100.adaptedPx(), + alignment: Alignment.center, + child: Indicator( + size: 0.7.adaptedPx(), + ), + ); + } + return Selector( + selector: (context, product) => product.remoteImages.length, + shouldRebuild: (previous, next) => previous != next, + builder: (context, value, child) { + if (value == 0) { + return Container(); + } else { + return galleryBuilder('loadedImages'.translation, ComposableProduct().remoteImages); + } + }, + ); + }, + ), + ]; +} + +const _taskStatusKey = 'compose_2'; +const _attributeKeys = ['measure_id', 'currency_id', 'payment_term_id', 'delivery_term_id', 'packaging']; diff --git a/lib/screens/personalCabinet/messages/messages.dart b/lib/screens/personalCabinet/messages/messages.dart new file mode 100644 index 0000000..a186426 --- /dev/null +++ b/lib/screens/personalCabinet/messages/messages.dart @@ -0,0 +1,97 @@ +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/postlist.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/chatroom/chatroom.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/personalCabinet/messages/personalChat.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/lazyload/lazyload.dart'; +import 'package:provider/provider.dart'; + +class Messages extends StatefulWidget { + @override + _MessagesState createState() => _MessagesState(); +} + +class _MessagesState extends State { + ChatroomSerializer serializer = ChatroomSerializer(); + FetchController _fetchController = FetchController(); + + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).chipTheme.backgroundColor, + appBar: BaseAppBar( + title: 'message'.translation, + goBack: () { + Navigator.of(context).pop(); + }, + ), + body: PostList(category: serializer, fetchController: _fetchController)); + } +} + +class MessagesCard extends StatelessWidget { + final Chatroom chatroom; + + const MessagesCard({ + Key? key, + required this.chatroom, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: chatroom, + builder: (context, val) => Consumer( + builder: (context, room, __) => InkWell( + onTap: () { + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (_) => PersonalChat(chatroom: chatroom))); + }, + child: Container( + decoration: BoxDecoration(border: Border(bottom: BorderSide(width: 0.9.adaptedPx(), color: Theme.of(context).dividerColor))), + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context), vertical: 20.adaptedPx()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + chatroom.title, + style: Theme.of(context).textTheme.headline1!.copyWith( + color: AppConstants.appColor, + fontWeight: FontWeight.bold, + ), + ), + if (chatroom.hasNewMessages) + Container( + child: Text( + chatroom.unread.toString(), + style: Theme.of(context).primaryTextTheme.headline3, + ), + width: 21.adaptedPx(), + height: 21.adaptedPx(), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.adaptedPx()), + ), + ), + ], + ), + SizedBox(height: 10.adaptedPx()), + Text( + chatroom.subTitile, + style: + TextStyle(color: Theme.of(context).primaryTextTheme.headline2!.color, fontSize: Theme.of(context).primaryTextTheme.headline2!.fontSize), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/personalCabinet/messages/personalChat.dart b/lib/screens/personalCabinet/messages/personalChat.dart new file mode 100644 index 0000000..e0ea198 --- /dev/null +++ b/lib/screens/personalCabinet/messages/personalChat.dart @@ -0,0 +1,244 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/components/TextInputCustom.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/icon.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/chatroom/chatroom.dart'; +import 'package:birzha/models/chatroom/message.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/textMetaData.dart'; +import 'package:birzha/services/validator.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:provider/provider.dart'; + +class PersonalChat extends StatefulWidget { + final Chatroom chatroom; + + PersonalChat({required this.chatroom}); + + @override + _PersonalChatState createState() => _PersonalChatState(); +} + +class _PersonalChatState extends State { + TextEditingController _textEditingController = TextEditingController(); + + ScrollController controller = ScrollController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + widget.chatroom.refresh(context); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: widget.chatroom, + child: BaseWidget( + appBar: BaseAppBar( + goBack: () { + Navigator.of(context, rootNavigator: false).pop(); + }, + title: widget.chatroom.title, + ), + body: Column( + children: [ + ManagerSelector( + selector: (context, manager) => manager.getStatusByKey('load'), + shouldRebuild: (prev, newValue) => prev != newValue, + onUpdate: () async { + setState(() {}); + }, + builder: (context, status) => status != TaskStatus.Loading + ? Container() + : SizedBox( + height: 3.adaptedPx(), + width: MediaQuery.of(context).size.width, + child: LinearProgressIndicator( + minHeight: 3.adaptedPx(), + )), + ), + Expanded( + child: Selector( + selector: (context, chatroom) => chatroom.lastIndexOfLocalMessages, + shouldRebuild: (prev, next) { + return prev != next; + }, + builder: (context, messages, __) => CustomScrollView( + controller: controller, + reverse: true, + slivers: [ + SliverList( + delegate: SliverChildListDelegate([for (var message in Chatroom.of(context).messages) _MessageBuilder(message: message)]), + ), + SliverToBoxAdapter( + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + ManagerSelector( + selector: (context, manager) => manager.getStatusByKey('load'), + shouldRebuild: (prev, newValue) => prev != newValue, + onUpdate: () async { + setState(() {}); + }, + builder: (context, status) => status == TaskStatus.Loading + ? Container() + : Padding( + padding: EdgeInsets.symmetric(vertical: 15.adaptedPx()), + child: GestureDetector( + onTap: () { + widget.chatroom.loadPastMessages(context, () async { + await Future.delayed(const Duration(milliseconds: 200)); + if (mounted) + controller.animateTo(controller.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), curve: Curves.ease); + }); + }, + child: PhysicalModel( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(15.adaptedPx()), + elevation: 5.adaptedPx(), + shadowColor: Colors.black.withOpacity(0.3), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15.adaptedPx()), + alignment: Alignment.center, + height: 36.adaptedPx(), + child: Text( + 'tapToLoadMore'.translation, + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + ), + ), + ), + ]), + ), + ], + ), + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context), vertical: 5.adaptedPx()).copyWith(bottom: 10.adaptedPx()), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + ), + child: Row( + children: [ + Expanded( + child: TextInputCustom( + fieldStandard: + TextInputMetaData(hint: 'yourMessage'.translation, name: 'message', validation: Validation(conditions: []), key: 'message'), + controller: _textEditingController), + ), + SizedBox( + width: 15.adaptedPx(), + ), + PhysicalModel( + color: Theme.of(context).accentColor, + elevation: 3.adaptedPx(), + shape: BoxShape.circle, + child: SizedBox( + width: 45.adaptedPx(), + height: 45.adaptedPx(), + child: AppIconButton( + onTap: () { + widget.chatroom.textMessage(context, _textEditingController.text); + _textEditingController.clear(); + }, + icon: Icon( + Icons.send, + color: Colors.white, + ), + size: 20.adaptedPx()), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _MessageBuilder extends StatelessWidget { + const _MessageBuilder({Key? key, required this.message}) : super(key: key); + + final Message message; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, chatroom) => chatroom.messages.lookup(message)?.status ?? MessagesStatus.none, + shouldRebuild: (prev, next) { + return prev != next; + }, + builder: (context, status, ___) => Container( + margin: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + padding: !message.isMyMessage(context) ? EdgeInsets.only(left: 10.adaptedPx()) : EdgeInsets.only(right: 10.adaptedPx()), + child: Row( + mainAxisAlignment: !message.isMyMessage(context) ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: Material( + color: Colors.transparent, + child: Row( + mainAxisAlignment: !message.isMyMessage(context) ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if (status == MessagesStatus.sending) + Icon(Icons.arrow_circle_up, size: 22.adaptedPx(), color: Theme.of(context).accentColor) + else if (status == MessagesStatus.failed) + AppIconButton( + onTap: () { + Provider.of(context, listen: false).retextMessage(context, message); + }, + icon: Icon(Icons.warning, color: Colors.red), + size: 22.adaptedPx(), + ), + Flexible( + child: Column(crossAxisAlignment: !message.isMyMessage(context) ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 11, horizontal: 16), + decoration: BoxDecoration( + color: message.isMyMessage(context) ? Theme.of(context).accentColor : Colors.green.shade600, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(21.adaptedPx()), + topRight: Radius.circular(21.adaptedPx()), + bottomLeft: !message.isMyMessage(context) ? Radius.circular(4.adaptedPx()) : Radius.circular(21.adaptedPx()), + bottomRight: !message.isMyMessage(context) ? Radius.circular(21.adaptedPx()) : Radius.circular(4.adaptedPx()), + ), + ), + child: Text( + message.text, + style: Theme.of(context).primaryTextTheme.bodyText2!.copyWith( + fontSize: 15, + color: Theme.of(context).backgroundColor, + ), + ), + ), + SizedBox(height: 5.adaptedPx()), + if (message.date != null) + Text( + safeValueDate(message.date), + style: TextStyle(fontSize: 11.99.adaptedPx()), + ) + ])), + ], + ), + ), + ), + ], + )), + ); + } +} diff --git a/lib/screens/personalCabinet/my_products.dart b/lib/screens/personalCabinet/my_products.dart new file mode 100644 index 0000000..21f06ef --- /dev/null +++ b/lib/screens/personalCabinet/my_products.dart @@ -0,0 +1,32 @@ +import 'package:birzha/models/categories/products/my_products.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/productsScreen.dart'; +import 'package:flutter/material.dart'; + +import '../../components/baseWidget.dart'; + +class MyProductsScreen extends StatefulWidget { + const MyProductsScreen({Key? key}) : super(key: key); + + @override + State createState() => _MyProductsScreenState(); +} + +class _MyProductsScreenState extends State { + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).chipTheme.backgroundColor, + appBar: BaseAppBar( + title: 'myProducts'.translation, + goBack: () { + Navigator.of(context).pop(); + }, + ), + body: ProductsScreen(category: MyProducts(), route: 'auth'), + ); + } + // Widget build(BuildContext context) { + // return ProductsScreen(category: MyProducts(), route: 'auth'); + // } +} diff --git a/lib/screens/personalCabinet/personalCabinet.dart b/lib/screens/personalCabinet/personalCabinet.dart new file mode 100644 index 0000000..b64a1a2 --- /dev/null +++ b/lib/screens/personalCabinet/personalCabinet.dart @@ -0,0 +1,245 @@ +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/customCardWidget.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/components/unauthenticatedWidget.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/manager/manager.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/userManager.dart'; +import 'package:birzha/screens/auth/smsVerification.dart'; +import 'package:birzha/screens/auth/update.dart'; +import 'package:birzha/screens/first_page.dart'; +import 'package:birzha/screens/personalCabinet/addPost/step1.dart'; +import 'package:birzha/screens/personalCabinet/messages/messages.dart'; +import 'package:birzha/screens/personalCabinet/my_products.dart'; +import 'package:birzha/screens/personalCabinet/topUp/topUpHistory.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../new/screens/top_up_balance/online_top_up_screen.dart'; + +class PersonalCabinet extends StatefulWidget { + @override + _PersonalCabinetState createState() => _PersonalCabinetState(); +} + +class _PersonalCabinetState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return BaseWidget( + appBar: BaseAppBar( + goBack: () { + Tabnavigator.maybeOf(context)?.pop(context); + }, + title: 'personalCabinet'.translation, + ), + body: !AppUserManager.of(context, listen: true).dataSync.isRegistered + ? Center( + child: UnAuthenticated(), + ) + : Selector( + selector: (context, user) => + user.getStatusByKey('sync') == TaskStatus.Loading || + user.getStatusByKey('verify_mail') == TaskStatus.Loading || + user.getStatusByKey('send_sms') == TaskStatus.Loading, + shouldRebuild: (prev, next) => prev != next, + builder: (context, isLoading, __) => isLoading + ? Center( + child: Indicator( + size: 0.7.adaptedPx(), + ), + ) + : RefreshIndicator( + onRefresh: () { + AppUserManager.of(context).syncAccount(context); + return Future.value(null); + }, + child: ListView( + physics: AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: AppConstants.verticalPadding(context), + ), + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(5.adaptedPx()), + child: Column( + children: [ + // personal data + CustomCardWidget( + name: 'personal_data'.translation, + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdateScreen())); + }, + icon: Icon(Icons.person_outline), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + + //top up history + CustomCardWidget( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => TopUpHistory())); + }, + name: 'top_up_history'.translation, + icon: Icon(Icons.inbox_outlined, color: Theme.of(context).accentColor), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + + //top up balance + CustomCardWidget( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => OnlineTopUpBalance())); + }, + name: 'topUpBalance'.translation, + icon: Icon( + Icons.account_balance_wallet_outlined, + color: Theme.of(context).accentColor, + ), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + + // message + CustomCardWidget( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Messages(), + ), + ); + }, + name: 'message'.translation, + icon: Icon(Icons.message_outlined), + borderRadius: BorderRadius.zero, + ), + ], + ), + ), + + SizedBox(height: 20.adaptedPx()), + + ClipRRect( + borderRadius: BorderRadius.circular(5.adaptedPx()), + child: Column( + children: [ + // add product + CustomCardWidget( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => AddBasicInformationScreen(forEditing: null)), + ); + }, + name: 'addPosts'.translation, + icon: Icon(Icons.add), + borderRadius: BorderRadius.zero, + ), + + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + + // my products + CustomCardWidget( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => MyProductsScreen()), + ); + }, + name: 'myProducts'.translation, + icon: Icon(Icons.article_outlined), + borderRadius: BorderRadius.zero, + ), + ], + ), + ), + + SizedBox(height: 20.adaptedPx()), + + /* ClipRRect( + borderRadius: BorderRadius.circular(5.adaptedPx()), + child: Column( + children: [ + // verify mail + if (!AppUserManager.of(context).dataSync.isEmailVerified) ...[ + CustomCardWidget( + onTap: () { + AppUserManager.of(context).verifyMail(context); + }, + name: 'verifyMail'.translation, + icon: Icon(Icons.email_outlined), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ) + ], + + // verify phone + if (!AppUserManager.of(context).dataSync.isPhoneVerified) + CustomCardWidget( + onTap: () { + var navContext = Navigator.of(context, rootNavigator: true).context; + AppUserManager.of(context).sendSmsCode(context, () { + Navigator.of(navContext).push(MaterialPageRoute(builder: (_) => SmsVerificationScreen())); + }); + }, + name: 'verifyPhone'.translation, + icon: Icon(Icons.smartphone_outlined), + borderRadius: BorderRadius.zero, + ), + ], + ), + ), */ + + SizedBox(height: 20.adaptedPx()), + // logout + CustomCardWidget( + onTap: () async { + var isSure = await yesOrNoDialog(context, content: 'sure_log_out'.translation); + var nav = Navigator.of(context, rootNavigator: true).context; + if (isSure && mounted) { + AppUserManager.of(context).logout(); + await Future.delayed(const Duration(milliseconds: 200)); + Navigator.of(nav, rootNavigator: true).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => FirstPage()), (route) => false); + } + }, + name: 'logout'.translation, + icon: Icon(Icons.exit_to_app_outlined, color: Colors.red), + ), + ], + ), + ), + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/screens/personalCabinet/topUp/topUpHistory.dart b/lib/screens/personalCabinet/topUp/topUpHistory.dart new file mode 100644 index 0000000..664326c --- /dev/null +++ b/lib/screens/personalCabinet/topUp/topUpHistory.dart @@ -0,0 +1,162 @@ +import 'package:badges/badges.dart'; +import 'package:birzha/components/actionIcons/searchicon.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/postlist.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/core/lazyload/lazyload.dart'; +import 'package:birzha/models/products/post.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/transactions/serializer.dart'; +import 'package:birzha/models/transactions/transaction.dart'; +import 'package:flutter/material.dart'; + +class TopUpHistory extends StatefulWidget { + @override + _TopUpHistoryState createState() => _TopUpHistoryState(); +} + +class _TopUpHistoryState extends State { + TransactionSerializer serializer = TransactionSerializer(); + FetchController _fetchController = FetchController(); + + @override + void dispose() { + _fetchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).scaffoldBackgroundColor, + appBar: BaseAppBar( + title: 'top_up_history'.translation, + after: [SearchIcon()], + goBack: () { + Navigator.of(context, rootNavigator: false).pop(); + }, + ), + body: PostList( + category: serializer, + contentPadding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context), vertical: AppConstants.verticalPadding(context) / 2), + before: [ + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 10.adaptedPx(), horizontal: AppConstants.horizontalPadding(context)), + sliver: SliverList( + delegate: SliverChildListDelegate.fixed( + [ + /* ManagerSelector( + onUpdate: () {}, + selector: (context, manager) => manager.getStatusByKey('balanceUp'), + shouldRebuild: (prev, next) => prev != next, + builder: (context, status) => MyButton( + text: 'payOnline'.translation, + inProgress: status == TaskStatus.Loading, + onTap: () async { + var amount = await showDialog( + context: context, + builder: (context) => AskQuantityDialog(initialQuantity: 1), + ); + if (amount is double) AppUserManager.of(context).balanceUp(context, amount); + }, + height: 40.adaptedPx(), + ), + ), + SizedBox(height: 12), + ManagerSelector( + onUpdate: () {}, + selector: (context, manager) => manager.getStatusByKey('uploadBill'), + shouldRebuild: (prev, next) => prev != next, + builder: (context, status) => MyButton( + text: 'uploadReceipt'.translation, + inProgress: status == TaskStatus.Loading, + onTap: () async { + AppUserManager.of(context).uploadBill(context); + }, + height: 40.adaptedPx(), + ), + ), + SizedBox(height: 12), */ + ], + ), + ), + ) + ], + fetchController: _fetchController), + ); + } +} + +class TopUpHistoryCard extends StatelessWidget { + final Transaction transaction; + + TopUpHistoryCard({Key? key, required this.transaction}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 15.adaptedPx()), + child: InkWell( + borderRadius: BorderRadius.circular(10.adaptedPx()), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(10.adaptedPx()), + ), + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: AppConstants.verticalPadding(context), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '#' + transaction.primaryKey.toString(), + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + color: Theme.of(context).accentColor, + ), + ), + Text( + transaction.date, + style: Theme.of(context).primaryTextTheme.headline2, + ), + ], + ), + SizedBox( + height: 20.adaptedPx(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Badge( + position: BadgePosition.topStart(top: 0, start: 0), + animationDuration: Duration(milliseconds: 300), + badgeColor: transaction.amountDouble > 0 ? Colors.green : Colors.red, + child: Row(children: [ + SizedBox(width: 20.adaptedPx()), + Text( + transaction.stateName, + style: Theme.of(context).primaryTextTheme.headline2, + ), + ]), + ), + Spacer(), + Text( + transaction.amount, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).accentColor, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/photo.dart b/lib/screens/photo.dart new file mode 100644 index 0000000..0532b9d --- /dev/null +++ b/lib/screens/photo.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; +import 'package:birzha/components/imagePlaceHolder.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:birzha/components/indicator.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; + +class PhotoScreen extends StatefulWidget{ + final String image; + PhotoScreen(this.image); + + @override + _PhotoScreenState createState()=> _PhotoScreenState(); + +} + +class _PhotoScreenState extends State{ + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + final double paddingTop = MediaQueryData.fromWindow(window).padding.top; + final double verticalMarginTitle = + MediaQuery.of(context).size.width * (32 / 452); + + return Container( + color: Colors.white, + child: Stack( + children: [ + Hero( + tag: 'detailedPhoto', + child: Center( + child: PhotoView( + errorBuilder: (_, __, ___) => AppImagePlaceholder(), + loadingBuilder: (_, chunk) => Container( + color: Colors.white, + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric( + vertical: verticalMarginTitle), + alignment: Alignment.center, + child: Indicator(size: 0.7.adaptedPx(),color: Theme.of(context).accentColor,) + )), + imageProvider: CachedNetworkImageProvider( + widget.image + ), + backgroundDecoration: BoxDecoration( + color: Colors.transparent + ), + ), + ), + ), + OrientationBuilder( + builder: (cntxt, orientation){ + + double height = (orientation == Orientation.portrait? + MediaQuery.of(context).size.height: MediaQuery.of(context).size.width)*0.049; + + return Container( + height: height, + alignment: Alignment.centerRight, + width: MediaQuery.of(context).size.width*0.98, + margin: EdgeInsets.only(top: paddingTop), + child: FittedBox( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => Navigator.of(cntxt).pop(), + child: Icon( + CupertinoIcons.clear_fill, + color: Theme.of(context).accentColor, + size:30.adaptedPx(), + ), + ), + ) + ) + ); + }, + ) + ], + )); + } +} \ No newline at end of file diff --git a/lib/screens/primal.dart b/lib/screens/primal.dart new file mode 100644 index 0000000..19cab4f --- /dev/null +++ b/lib/screens/primal.dart @@ -0,0 +1,53 @@ +import 'package:birzha/components/localizationOverride.dart'; +import 'package:birzha/components/tab_nav_icons.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/home/homeScreen.dart'; +import 'package:birzha/screens/masterCategoryScreen.dart'; +import 'package:birzha/screens/personalCabinet/personalCabinet.dart'; +import 'package:flutter/material.dart'; + +import '../new/screens/quotes/screen.dart'; + +class Primal extends StatelessWidget { + const Primal({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LocalizationOverride( + builder: (context) { + return TabView( + items: [ + TabConfigs( + index: 0, + firstScreen: (_) => HomeScreen(), + routeLabel: 'home'.translation, + iconData: TabNavIcons.home, + initialRouteName: '/home', + ), + TabConfigs( + index: 1, + firstScreen: (_) => MasterCategoryScreen(), + routeLabel: 'category'.translation, + iconData: TabNavIcons.category, + initialRouteName: '/category'), + TabConfigs( + index: 2, + firstScreen: (_) => QuotesScreen(), + routeLabel: 'quotes'.translation, + iconData: TabNavIcons.news, + initialRouteName: '/quotations', + ), + TabConfigs( + index: 3, + firstScreen: (_) => PersonalCabinet(), + routeLabel: 'profile'.translation, + iconData: TabNavIcons.user, + initialRouteName: '/user', + ), + ], + ); + }, + ); + } +} diff --git a/lib/screens/productsScreen.dart b/lib/screens/productsScreen.dart new file mode 100644 index 0000000..5961928 --- /dev/null +++ b/lib/screens/productsScreen.dart @@ -0,0 +1,71 @@ +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/postlist.dart'; +import 'package:birzha/components/searchBar.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/categories/category.dart'; +import 'package:birzha/new/themes/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/lazyload/lazyload.dart'; +import 'package:birzha/models/products/post.dart'; + +class ProductsScreen extends StatefulWidget { + const ProductsScreen({Key? key, required this.category, required this.route, this.headerToolBar = const []}) : super(key: key); + + final RemoteCategory category; + final String route; + final List headerToolBar; + + @override + _ProductsScreenState createState() => _ProductsScreenState(); +} + +class _ProductsScreenState extends State { + FetchController controller = FetchController(); + + late RemoteCategory primal; + + @override + void initState() { + super.initState(); + primal = widget.category.copy; + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BaseWidget( + appBar: BaseAppBar( + color: ThemeColor.white, + goBack: () { + Tabnavigator.backDispatcher(context); + }, + after: [...widget.headerToolBar], + customChild: primal is! SearchableMixin + ? null + : ActualSearchBar( + onSubmit: (word) { + (primal as SearchableMixin).search(word); + setState(() { + controller.refresh(); + }); + }, + route: Tabnavigator.currentRoute(context) + '/${widget.route}'), + title: primal.name, + ), + body: Container( + color: Theme.of(context).backgroundColor, + child: PostList( + contentPadding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context), vertical: AppConstants.verticalPadding(context)), + before: [], + category: primal, + fetchController: controller), + ), + ); + } +} diff --git a/lib/screens/settings/contact.dart b/lib/screens/settings/contact.dart new file mode 100644 index 0000000..41653fa --- /dev/null +++ b/lib/screens/settings/contact.dart @@ -0,0 +1,137 @@ +import 'package:birzha/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; + +class ContactScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BaseWidget( + color: Theme.of(context).chipTheme.backgroundColor, + appBar: BaseAppBar( + after: [], + goBack: () { + Navigator.of(context).pop(); + }, + title: 'contact'.translation, + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: AppConstants.horizontalPadding(context), vertical: AppConstants.verticalPadding(context)), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 20.adaptedPx()), + Container( + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), color: Theme.of(context).accentColor), + width: 50.adaptedPx(), + height: 50.adaptedPx(), + child: Icon( + Icons.phone_in_talk_outlined, + size: 30.adaptedPx(), + color: Theme.of(context).accentIconTheme.color, + ), + ), + Padding( + padding: EdgeInsets.only(top: 20.adaptedPx(), bottom: 10.adaptedPx()), + child: Text( + 'telephone'.translation + ':', + style: Theme.of(context).accentTextTheme.headline2!.copyWith( + fontSize: AppConstants.h1FontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + '+99312446015', + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w400, + ), + ), + Padding( + padding: EdgeInsets.only(top: 10.adaptedPx(), bottom: 30.adaptedPx()), + child: Text( + '+99312446016', + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w400, + ), + ), + ), + Container( + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), color: Theme.of(context).accentColor), + width: 50.adaptedPx(), + height: 50.adaptedPx(), + child: Icon( + Icons.mail_outline, + size: 30.adaptedPx(), + color: Theme.of(context).accentIconTheme.color, + ), + ), + Padding( + padding: EdgeInsets.only(top: 20.adaptedPx(), bottom: 10.adaptedPx()), + child: Text( + 'mail'.translation + ':', + style: Theme.of(context).accentTextTheme.headline2!.copyWith( + fontSize: AppConstants.h1FontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + 'info@exchange.gov.tm', + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w400, + ), + ), + Padding( + padding: EdgeInsets.only(top: 10.adaptedPx(), bottom: 30.adaptedPx()), + child: Text( + 'brokers@exchange.gov.tm', + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w400, + ), + ), + ), + Container( + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), color: Theme.of(context).accentColor), + width: 50.adaptedPx(), + height: 50.adaptedPx(), + child: Icon( + Icons.map_outlined, + size: 30.adaptedPx(), + color: Theme.of(context).accentIconTheme.color, + ), + ), + Padding( + padding: EdgeInsets.only(top: 20.adaptedPx(), bottom: 10.adaptedPx()), + child: Text( + 'address'.translation + ':', + style: Theme.of(context).accentTextTheme.headline2!.copyWith( + fontSize: AppConstants.h1FontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + 'realAddress'.translation, + textAlign: TextAlign.center, + style: Theme.of(context).primaryTextTheme.headline2!.copyWith( + fontSize: AppConstants.h2FontSize, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings/feedback.dart b/lib/screens/settings/feedback.dart new file mode 100644 index 0000000..82f5ef8 --- /dev/null +++ b/lib/screens/settings/feedback.dart @@ -0,0 +1,59 @@ +// import 'package:birzha/components/abstractForm.dart'; +// import 'package:birzha/models/user/user.dart'; +// import 'package:birzha/services/textMetaData.dart'; +// import 'package:flutter/material.dart'; +// import 'package:manager/manager.dart'; +// import 'package:birzha/models/settings/settingsModel.dart'; + + +// class FeedbackScreen extends StatefulWidget { +// FeedbackScreen({Key? key}) : super(key: key); + +// @override +// _FeedbackScreenState createState() => _FeedbackScreenState(); +// } + +// class _FeedbackScreenState extends State +// with AbstractFormState { +// SampleUser sampleUser = SampleUser(data: {}); + +// TaskStatus status = TaskStatus.None; + +// @override +// List get before => []; + +// @override +// List get after => []; + +// @override +// List get inputs => sampleUser.feedbackMetaData; + +// @override +// String get title => 'feedback'.translation; + +// @override +// String get buttonLabel => 'sendMessage'.translation; + +// @override +// bool get buttonIsLoading => false; + +// @override +// String keyAfterFilter(String key, String input) { +// return key; +// } + +// @override +// String valueAfterFilter(String key, String input) { +// return input; +// } + +// @override +// void act() {} + +// @override +// void dispose() { +// super.dispose(); +// } +// } + +// // const _keyForTask = 'register'; diff --git a/lib/screens/settings/settingsScreen.dart b/lib/screens/settings/settingsScreen.dart new file mode 100644 index 0000000..55635a1 --- /dev/null +++ b/lib/screens/settings/settingsScreen.dart @@ -0,0 +1,109 @@ +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:birzha/components/baseWidget.dart'; +import 'package:birzha/components/customCardWidget.dart'; +import 'package:birzha/components/tabview.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/screens/primal.dart'; +import 'package:birzha/screens/settings/contact.dart'; +import 'package:birzha/services/helpers.dart'; +import 'package:birzha/services/modals.dart'; +import 'package:flutter/material.dart'; + +class SettingsScreen extends StatefulWidget { + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return BaseWidget( + appBar: BaseAppBar( + title: 'settings'.translation, + goBack: () { + Tabnavigator.backDispatcher(context); + }, + ), + body: ListView( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context), + vertical: AppConstants.verticalPadding(context), + ), + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(5.adaptedPx()), + child: Column( + children: [ + CustomCardWidget( + name: 'lang'.translation, + onTap: () async { + var a = await showLanguagePicker(context); + if (a != null) { + await SettingsModel.of(context).setLanguage(a); + Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => Primal()), + (route) => false, + ); + } + }, + icon: Icon(Icons.language_outlined), + trailing: Container( + margin: EdgeInsets.only(right: 3.adaptedPx()), + child: Text( + 'localeLabel'.translation, + style: TextStyle(fontSize: AppConstants.b3FontSize, color: Theme.of(context).dividerColor.withOpacity(0.3)), + ), + ), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + CustomCardWidget( + onTap: () { + linkLauncher('https://tmex.gov.tm/ru/politika-konfidencialnosti'); + }, + name: 'privacy_policy'.translation, + icon: Icon(Icons.policy_outlined, color: Theme.of(context).accentColor), + borderRadius: BorderRadius.zero, + ), + Divider( + indent: 16.adaptedPx(), + endIndent: 15.adaptedPx(), + height: 0.adaptedPx(), + thickness: 0.9.adaptedPx(), + ), + CustomCardWidget( + onTap: () { + linkLauncher('https://tmex.gov.tm/ru/contact-us'); + }, + name: 'feedback'.translation, + icon: Icon(Icons.feedback_outlined), + borderRadius: BorderRadius.zero, + ), + ], + ), + ), + SizedBox(height: 10.adaptedPx()), + CustomCardWidget( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ContactScreen()), + ); + }, + name: 'contact'.translation, + icon: Icon(Icons.contact_mail_outlined), + ), + ], + ), + ); + } +} diff --git a/lib/services/helpers.dart b/lib/services/helpers.dart new file mode 100644 index 0000000..fc5d80c --- /dev/null +++ b/lib/services/helpers.dart @@ -0,0 +1,45 @@ +import 'package:url_launcher/url_launcher.dart'; + +String priceFormatter(double price) { + return price.toStringAsFixed(2) + ' TMT'; +} + +String safeValueDate(DateTime? dateTime) { + if (dateTime == null) return ''; + final String year = dateTime.year.toString(), + month = dateTime.month.toString().length < 2 + ? '0${dateTime.month}' + : '${dateTime.month.toString()}', + day = dateTime.day.toString().length < 2 + ? '0${dateTime.day}' + : '${dateTime.day.toString()}', + hour = dateTime.hour.toString().length < 2 + ? '0${dateTime.hour}' + : '${dateTime.hour.toString()}', + minute = dateTime.minute.toString().length < 2 + ? '0${dateTime.minute}' + : '${dateTime.minute.toString()}', + processed = [year, month, day].reversed.join('/') + ' $hour:$minute'; + return processed; +} + +void linkLauncher(String link) async { + if (await canLaunch(link)) launch(link); +} + +bool boolParser(dynamic value) { + return value != null && + value != '0' && + value != false && + value != 0 && + value != 'false'; +} + +String caitalize(String word) { + if (word.length <= 1) { + return word.toUpperCase(); + } else { + var first = word[0]; + return first.toUpperCase() + word.substring(1, word.length); + } +} diff --git a/lib/services/imageUpload.dart b/lib/services/imageUpload.dart new file mode 100644 index 0000000..f6087de --- /dev/null +++ b/lib/services/imageUpload.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:async/async.dart'; +import 'package:path/path.dart'; +import 'package:birzha/models/exceptions/exception.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/user/user.dart'; + +typedef void OnUploadProgressCallback(int sentBytes, int totalBytes); + +Future uploadImage(Uri uri, User user, PlatformFile image, String attributeName, Map body, + {required OnUploadProgressCallback onUploadProgressCallback}) async { + var multipartRequest = _MultipartRequest("POST", uri, onProgress: onUploadProgressCallback); + var stream = http.ByteStream(DelegatingStream(File(image.path!).openRead())); + var length = image.size; + var multipartFile = http.MultipartFile(attributeName, stream, length, filename: basename(image.path!)); + multipartRequest.files.add(multipartFile); + multipartRequest.fields.addAll({...body}); + multipartRequest.headers.addAll({'Authorization': 'Bearer ${user.token}', 'Accept': 'application/json'}); + try { + var request = await multipartRequest.send(); + var byteArray = await request.stream.toBytes(); + var stringBody = utf8.decode(byteArray); + var result = jsonDecode(stringBody); + debugPrint('stringBody $result'); + + if (result is String) return result; + return result['success']?['backendCode'.translation]; + } catch (error) { + if (error is MessageException) + throw error; + else + throw OtherException(); + } +} + +class _MultipartRequest extends http.MultipartRequest { + _MultipartRequest( + String method, + Uri url, { + required this.onProgress, + }) : super(method, url); + + final OnUploadProgressCallback onProgress; + http.ByteStream finalize() { + final byteStream = super.finalize(); + + final total = this.contentLength; + int bytes = 0; + + final t = StreamTransformer.fromHandlers(handleData: (List data, EventSink> sink) { + bytes += data.length; + onProgress(bytes, total); + sink.add(data); + }, handleDone: (sink) { + sink.close(); + }, handleError: (err, trace, sink) { + sink.close(); + }); + final stream = byteStream.transform(t); + return http.ByteStream(stream); + } +} diff --git a/lib/services/modals.dart b/lib/services/modals.dart new file mode 100644 index 0000000..1a253e3 --- /dev/null +++ b/lib/services/modals.dart @@ -0,0 +1,353 @@ +import 'package:birzha/components/localizationOverride.dart'; +import 'package:birzha/components/radio.dart'; +import 'package:birzha/constants.dart'; +import 'package:birzha/countryCodes.dart'; +import 'package:birzha/models/attributes/attribute.dart'; +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:flutter/material.dart'; +import 'package:birzha/core/adaptix/adaptix.dart'; +import 'package:flag/flag.dart'; + +Future showLanguagePicker(BuildContext context) async { + return showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 10.adaptedPx()), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + ), + height: 130.adaptedPx(), + child: ListView( + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context)), + children: [ + Align( + alignment: Alignment.centerLeft, + child: Column( + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop('tk'); + }, + child: Row( + children: [ + Flag.fromCode( + FlagsCode.TM, + height: 30.adaptedPx(), + width: 30.adaptedPx(), + // fit: BoxFit.contain, + ), + SizedBox(width: 10.adaptedPx()), + Text( + 'turkmen'.translation, + // style: TextStyle( + // fontSize: 15.adaptedPx(), color: Colors.black) + style: Theme.of(context).textTheme.headline2, + ), + ], + ), + ), + SizedBox(height: 10.adaptedPx()), + GestureDetector( + onTap: () { + Navigator.of(context).pop('ru'); + }, + child: Row( + children: [ + Flag.fromCode( + FlagsCode.RU, + height: 30.adaptedPx(), + width: 30.adaptedPx(), + // fit: BoxFit.contain, + ), + SizedBox(width: 10.adaptedPx()), + Text( + 'russian'.translation, + style: Theme.of(context).textTheme.headline2, + ), + ], + ), + ), + SizedBox(height: 10.adaptedPx()), + GestureDetector( + onTap: () { + Navigator.of(context).pop('en'); + }, + child: Row( + children: [ + Flag.fromCode( + FlagsCode.US, + height: 30.adaptedPx(), + width: 30.adaptedPx(), + // fit: BoxFit.contain, + ), + SizedBox(width: 10.adaptedPx()), + Text( + 'english'.translation, + style: Theme.of(context).textTheme.headline2, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); +} + +Future attributeSelector( + BuildContext context, + AttributeWithValueNameMixin? groupValue, + List options) { + var radios = [ + for (var method in options) + RadioItem( + name: method.value, value: method, groupValue: groupValue), + ]; + return _picker( + context: context, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.adaptedPx())), + items: radios, + height: (radios.length * 41.adaptedPx()) + + AppConstants.horizontalPadding(context) + + 32.adaptedPx(), + padding: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding(context)) + .copyWith(top: 10.adaptedPx()), + itemBuilder: (cntxt, id, dismiss) { + return item(radios[id], context, (val) { + dismiss(val); + }); + }); +} + +Future _picker({ + required List items, + required double height, + required EdgeInsets padding, + required BorderRadius borderRadius, + required BuildContext context, + required Widget Function(BuildContext, int, void Function(T)) itemBuilder, +}) { + return showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + useRootNavigator: true, + builder: (context2) { + return SizedBox( + height: height, + child: Container( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: Theme.of(context).dialogBackgroundColor, + ), + child: SingleChildScrollView( + padding: padding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + items.length, + (index) => itemBuilder(context2, index, + (T val) => Navigator.of(context2).pop(val)))))), + ); + }); +} + +Widget item( + RadioItem radio, BuildContext context, void Function(T) onChange) { + final inputStyle = TextStyle( + fontSize: 14.adaptedPx(), + ); + return GestureDetector( + onTap: () { + onChange(radio.value); + }, + child: Container( + margin: EdgeInsets.symmetric(vertical: 7.adaptedPx()), + padding: EdgeInsets.symmetric(vertical: 3.5.adaptedPx()), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: + EdgeInsets.only(right: AppConstants.horizontalPadding(context)), + child: RadioButton( + size: 17.adaptedPx(), + borderColor: Colors.grey, + checkedColor: Theme.of(context).accentColor.withOpacity(0.8), + uncheckedColor: Colors.grey.withOpacity(0.1), + borderWidth: 0.3.adaptedPx(), + animationDuration: Duration(milliseconds: 100), + onChange: (v) => onChange(v), + value: radio.value, + groupValue: radio.groupValue, + ), + ), + Expanded( + child: Container( + child: Text( + '${radio.name}', + style: inputStyle, + ), + ), + ) + ], + ), + ), + ); +} + +Future showCountryCodesSheet(BuildContext context) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + barrierColor: Colors.black.withOpacity(0.5), + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.5, //set this as you want + minChildSize: 0.4, //set this as you want + expand: false, + builder: (context, controller) => PhysicalModel( + color: Theme.of(context).cardColor, + elevation: 4.adaptedPx(), + borderRadius: + BorderRadius.vertical(top: Radius.circular(10.adaptedPx())), + child: ListView.separated( + separatorBuilder: (context, index) { + return const Divider( + height: 0, + ); + }, + padding: EdgeInsets.symmetric(vertical: 10.adaptedPx()), + controller: controller, + itemCount: countryCodes.length, + itemBuilder: (context, index) { + var title = countryCodes[index]['country_name']!; + var code = countryCodes[index]['dial_code']!; + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(10.adaptedPx()), + onTap: () { + Navigator.of(context).pop('$title $code'); + }, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 20.adaptedPx(), + horizontal: AppConstants.horizontalPadding(context), + ), + child: Row( + children: [ + Expanded( + child: Text('$title $code'), + ) + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); +} + +void showSnackBar(BuildContext ctx, + {SnackBar Function(String? content)? snackBar, + String? content, + Color? textColor, + Color? backgroundColor, + Duration? duration}) { + ScaffoldMessenger.of(ctx).hideCurrentSnackBar(); + ScaffoldMessenger.of(ctx).showSnackBar(snackBar == null + ? SnackBar( + padding: EdgeInsets.symmetric( + vertical: 5.adaptedPx(), + horizontal: 7.adaptedPx() + AppConstants.horizontalPadding(ctx)), + duration: duration ?? const Duration(seconds: 3), + backgroundColor: backgroundColor, + content: Text( + '$content', + style: TextStyle(fontSize: 15.adaptedPx(), color: textColor), + ), + ) + : snackBar(content)); +} + +Future yesOrNoDialog(BuildContext context, + {String? content, Widget? widgetContent}) async { + return (await showDialog( + context: context, + builder: (ctx) { + return LocalizationOverride( + builder: (context) => AlertDialog( + title: Text(MaterialLocalizations.of(context).alertDialogLabel), + titleTextStyle: Theme.of(context).textTheme.bodyText1?.copyWith( + fontWeight: FontWeight.w500, fontSize: 15.adaptedPx()), + titlePadding: EdgeInsets.all(18.adaptedPx()), + actions: [ + Theme( + data: Theme.of(context).copyWith( + colorScheme: Theme.of(context) + .colorScheme + .copyWith(primary: Theme.of(context).accentColor)), + child: TextButton( + onPressed: () { + Navigator.of(ctx).pop(true); + }, + child: Text( + MaterialLocalizations.of(context).continueButtonLabel, + style: TextStyle(fontSize: 11.8.adaptedPx()), + )), + ), + Theme( + data: Theme.of(context).copyWith( + colorScheme: Theme.of(context) + .colorScheme + .copyWith(primary: Theme.of(context).accentColor)), + child: TextButton( + onPressed: () { + Navigator.of(ctx).pop(false); + }, + child: Text( + MaterialLocalizations.of(context).cancelButtonLabel, + style: TextStyle(fontSize: 11.8.adaptedPx()), + )), + ), + ], + contentTextStyle: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(height: 1.5), + content: Container( + width: Resizer( + xSmall: 200, + small: 350, + medium: 430, + large: 450, + xlarge: 700, + totalWidth: MediaQuery.of(context).size.width, + ).value, + child: Row( + children: [ + if (widgetContent != null) + Expanded(child: widgetContent) + else + Expanded(child: Text(content ?? "")) + ], + ), + ), + ), + ); + }) ?? + false); +} diff --git a/lib/services/requests.dart b/lib/services/requests.dart new file mode 100644 index 0000000..02520c6 --- /dev/null +++ b/lib/services/requests.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:birzha/core/orm/orm.dart'; + +const kApiPath = 'api/v1'; + +Uri baseUrl({required String path, Map? queryParameters, String? query}) => Uri( + scheme: 'https', + host: 'tmex.gov.tm', + path: '/$path', + queryParameters: queryParameters == null ? null : {...queryParameters}, + query: query, + ); + +class FutureGetList { + late final Uri api; + late final T Function(Map)? constructor; + late final List Function(http.Response)? parser; + + List defaultParser(http.Response response) { + String output = response.body; + List list = jsonDecode(output); + return list.map((entity) => constructor!(entity)).toList(); + } + + Future> fetch([Map? headers]) async { + final response = await http.get(api, headers: {...?headers}); + if (constructor != null) { + return defaultParser(response); + } else if (parser != null) { + return parser!(response); + } else { + return []; + } + } + + FutureGetList(this.api, {this.constructor, this.parser}); +} diff --git a/lib/services/streamFetchService.dart b/lib/services/streamFetchService.dart new file mode 100644 index 0000000..4e85d8d --- /dev/null +++ b/lib/services/streamFetchService.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +mixin StreamControlledMixin on State { + Future Function() get asyncAction; + StreamSubscription? channel; + bool isLoading = false; + void start(BuildContext context) {} + void done(BuildContext context) {} + void error(BuildContext context, dynamic error) {} + void onDataRecived(BuildContext context, D data) {} + + void cancel() { + channel?.cancel(); + } + + void connect(BuildContext ctx) { + cancel(); + this.setState(() { + start(ctx); + isLoading = true; + this.channel = asyncAction().asStream().listen((event) { + this.onDataRecived(ctx, event); + }) + ..onDone(() { + done(ctx); + if (mounted) + this.setState(() { + isLoading = false; + }); + }) + ..onError((e) { + error(ctx, e); + }); + }); + } +} diff --git a/lib/services/textMetaData.dart b/lib/services/textMetaData.dart new file mode 100644 index 0000000..f9fbbb9 --- /dev/null +++ b/lib/services/textMetaData.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:birzha/services/validator.dart'; + +class TextInputMetaData { + final String key; + final String name; + final Validation validation; + final List? formatters; + final String? hint; + final String? label; + final Widget? bottomInfo; + final FutureOr Function(BuildContext)? pickerMode; + final TextInputType? type; + final bool autoFocus; + final bool password; + final Color? fillColor; + final bool filled; + final bool readOnly; + final bool showSuffix; + + TextInputMetaData({ + this.filled = false, + this.fillColor, + this.readOnly = false, + this.showSuffix = false, + required this.name, + this.password = false, + this.autoFocus = false, + this.type, + required this.validation, + required this.key, + this.formatters, + this.hint, + this.label, + this.bottomInfo, + this.pickerMode, + }); +} diff --git a/lib/services/translationServices.dart b/lib/services/translationServices.dart new file mode 100644 index 0000000..508d93e --- /dev/null +++ b/lib/services/translationServices.dart @@ -0,0 +1,24 @@ +import 'package:birzha/models/settings/settingsModel.dart'; +import 'package:birzha/models/translationModel.dart'; + +List translationsFromMap(Map data){ + var translationRaw = data['translations']; + if(translationRaw == null) + return []; + else{ + var realTranslations = (translationRaw as List).map((data) => TranslationModel(data)).toList(); + return realTranslations; + } +} + +TranslationModel? getTranslationAccordingToLocale( List translations){ + var backendCode = 'backendCode'.translation; + TranslationModel? needed; + for(var trans in translations){ + if(trans.primaryKey == backendCode){ + needed = trans; + break; + } + } + return needed; +} \ No newline at end of file diff --git a/lib/services/validator.dart b/lib/services/validator.dart new file mode 100644 index 0000000..dad4b57 --- /dev/null +++ b/lib/services/validator.dart @@ -0,0 +1,20 @@ +class Validation{ + + final List conditions; + + String? validate(String input){ + for(var condition in conditions){ + if(condition(input) != null){ + return condition(input); + } + } + return null; + } + + Validation({required this.conditions}); + + static bool phoneValidator(String input) => RegExp(r'86[1-5]\d{6}$').hasMatch('${input.trim()}'); + + static bool emailValidator(String input) => RegExp( r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch('${input.trim()}'); + +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..db93402 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,871 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + animate_do: + dependency: "direct main" + description: + name: animate_do + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + badges: + dependency: "direct main" + description: + name: badges + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + chewie: + dependency: transitive + description: + name: chewie + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" + chewie_audio: + dependency: transitive + description: + name: chewie_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" + enum_to_string: + dependency: transitive + description: + name: enum_to_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + expandable: + dependency: "direct main" + description: + name: expandable + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.1" + flag: + dependency: "direct main" + description: + name: flag + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.2" + flutter_layout_grid: + dependency: transitive + description: + name: flutter_layout_grid + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.6" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2+2" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + get: + dependency: "direct main" + description: + name: get + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.1" + google_nav_bar: + dependency: "direct main" + description: + name: google_nav_bar + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.6" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + idb_shim: + dependency: transitive + description: + name: idb_shim + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + numerus: + dependency: transitive + description: + name: numerus + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1+1" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.3" + sembast: + dependency: "direct main" + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + sembast_web: + dependency: "direct main" + description: + name: sembast_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1+1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.20" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + very_good_analysis: + dependency: transitive + description: + name: very_good_analysis + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + video_player: + dependency: transitive + description: + name: video_player + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + video_player_android: + dependency: transitive + description: + name: video_player_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + video_player_web: + dependency: transitive + description: + name: video_player_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + wakelock: + dependency: transitive + description: + name: wakelock + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+2" + wakelock_macos: + dependency: transitive + description: + name: wakelock_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + wakelock_platform_interface: + dependency: transitive + description: + name: wakelock_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + wakelock_web: + dependency: transitive + description: + name: wakelock_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + wakelock_windows: + dependency: transitive + description: + name: wakelock_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.5" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..4994b4e --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,120 @@ +name: birzha +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +version: 3.0.2+15 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter_svg: ^0.22.0 + google_nav_bar: ^5.0.5 + provider: ^6.0.2 + file_picker: ^4.0.1 + flag: ^5.0.1 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.17.0 + shared_preferences: ^2.0.6 + http: ^0.13.3 + cached_network_image: ^3.0.0 + flutter_html: ^2.2.1 + photo_view: ^0.13.0 + url_launcher: ^6.0.9 + sembast: ^3.0.4 + path_provider: ^2.0.4 + sembast_web: ^2.0.1+1 + badges: ^2.0.2 + get: ^4.6.1 + dio: ^4.0.0 + animate_do: ^2.1.0 + # flutter_screenutil: ^5.4.0+1 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + expandable: ^5.0.1 + +flutter_native_splash: + color: "#42a5f5" + image: "assets/images/Logo.png" + android_gravity: center + fullscreen: true + android: true + ios: true + +flutter_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon.jpg" + +dev_dependencies: + flutter_native_splash: ^1.2.0 + flutter_launcher_icons: ^0.9.2 + + # background_image: 'assets/' + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec +# The following section is specific to Flutter. +flutter: + generate: true + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/appBarIcon.svg + - assets/images/unauth.svg + + - assets/icons/profile_screen/cart.svg + - assets/icons/profile_screen/exit.svg + - assets/icons/profile_screen/message.svg + - assets/icons/profile_screen/popup_history.svg + - assets/icons/profile_screen/profile.svg + - assets/icons/profile_screen/phone_not_verified.svg + - assets/icons/profile_screen/phone_verified.svg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: SVGs_data + fonts: + - asset: assets/fonts/svgs.ttf + + - family: MyFlutterApp + fonts: + - asset: assets/fonts/AppBarIcons.ttf + + - family: Settings_Icons + fonts: + - asset: assets/fonts/Settings_Icons.ttf + - family: TabNavIcons + fonts: + - asset: assets/fonts/TabNavIcons.ttf + + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..ac576a6 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,14 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + var a = 0; + expect(++a, 1); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9528430 --- /dev/null +++ b/web/index.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + birzha + + + + + + + + \ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..52be736 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "birzha", + "short_name": "birzha", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/web/splash/img/dark-1x.png b/web/splash/img/dark-1x.png new file mode 100644 index 0000000..38482c2 Binary files /dev/null and b/web/splash/img/dark-1x.png differ diff --git a/web/splash/img/dark-2x.png b/web/splash/img/dark-2x.png new file mode 100644 index 0000000..ebf163d Binary files /dev/null and b/web/splash/img/dark-2x.png differ diff --git a/web/splash/img/dark-3x.png b/web/splash/img/dark-3x.png new file mode 100644 index 0000000..c3502e2 Binary files /dev/null and b/web/splash/img/dark-3x.png differ diff --git a/web/splash/img/light-1x.png b/web/splash/img/light-1x.png new file mode 100644 index 0000000..38482c2 Binary files /dev/null and b/web/splash/img/light-1x.png differ diff --git a/web/splash/img/light-2x.png b/web/splash/img/light-2x.png new file mode 100644 index 0000000..ebf163d Binary files /dev/null and b/web/splash/img/light-2x.png differ diff --git a/web/splash/img/light-3x.png b/web/splash/img/light-3x.png new file mode 100644 index 0000000..c3502e2 Binary files /dev/null and b/web/splash/img/light-3x.png differ diff --git a/web/splash/style.css b/web/splash/style.css new file mode 100644 index 0000000..d222ab6 --- /dev/null +++ b/web/splash/style.css @@ -0,0 +1,43 @@ +body, html { + margin:0; + height:100%; + background: #42a5f5; + background-image: url("img/light-background.png"); + background-size: 100% 100%; +} + +.center { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + +.contain { + display:block; + width:100%; height:100%; + object-fit: contain; +} + +.stretch { + display:block; + width:100%; height:100%; +} + +.cover { + display:block; + width:100%; height:100%; + object-fit: cover; +} + +@media (prefers-color-scheme: dark) { + body { + margin:0; + height:100%; + background: #42a5f5; + background-image: url("img/dark-background.png"); + background-size: 100% 100%; + } +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..8815dd6 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(birzha LANGUAGES CXX) + +set(BINARY_NAME "birzha") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..b2e4bd8 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..4f78848 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..88b22e5 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..de2d891 --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..2aca545 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.tpsadvertising.digital" "\0" + VALUE "FileDescription", "birzha" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "birzha" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.tpsadvertising.digital. All rights reserved." "\0" + VALUE "OriginalFilename", "birzha.exe" "\0" + VALUE "ProductName", "birzha" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b43b909 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..a8fd971 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"birzha", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..c977c4a --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..d19bdbb --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_