first commit

This commit is contained in:
Moj1403 2026-04-13 23:41:27 +03:30
commit 0038967f70
167 changed files with 17895 additions and 0 deletions

45
.gitignore vendored Normal file
View File

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

45
.metadata Normal file
View File

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

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# saba_secure_sms
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://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

415
analyze_results.txt Normal file
View File

@ -0,0 +1,415 @@
Analyzing saba-dart...
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/chat_screen.dart:3:8 • unnecessary_import
error • The argument type 'MessageBubble' can't be assigned to the parameter type 'Widget?'. • lib/screens/chat_screen.dart:1567:34 • argument_type_not_assignable
info • Don't invoke 'print' in production code • lib/screens/compose_screen.dart:62:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:129:7 • use_build_context_synchronously
info • Use interpolation to compose strings and values • lib/screens/compose_screen.dart:255:20 • prefer_interpolation_to_compose_strings
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:415:63 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:582:22 • deprecated_member_use
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:639:7 • curly_braces_in_flow_control_structures
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:689:11 • curly_braces_in_flow_control_structures
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:695:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:700:29 • use_build_context_synchronously
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/group_chat_screen.dart:2:8 • unnecessary_import
warning • The value of the field '_selectedSim' isn't used • lib/screens/group_chat_screen.dart:95:25 • unused_field
warning • The value of the field '_loadingSims' isn't used • lib/screens/group_chat_screen.dart:98:8 • unused_field
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:226:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:410:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:530:28 • use_build_context_synchronously
error • The element type 'MessageBubble' can't be assigned to the list type 'Widget' • lib/screens/group_chat_screen.dart:692:29 • list_element_type_not_assignable
error • Too many positional arguments: 0 expected, but 2 found • lib/screens/group_chat_screen.dart:716:21 • extra_positional_arguments_could_be_named
error • Expected to find ')' • lib/screens/group_chat_screen.dart:735:19 • expected_token
error • Expected to find ';' • lib/screens/group_chat_screen.dart:739:11 • expected_token
warning • Dead code • lib/screens/group_chat_screen.dart:739:12 • dead_code
error • Expected an identifier • lib/screens/group_chat_screen.dart:739:12 • missing_identifier
error • Expected to find ';' • lib/screens/group_chat_screen.dart:739:12 • expected_token
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:739:12 • unexpected_token
error • Expected an identifier • lib/screens/group_chat_screen.dart:740:9 • missing_identifier
error • Expected to find ';' • lib/screens/group_chat_screen.dart:740:9 • expected_token
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:740:9 • unexpected_token
error • Expected an identifier • lib/screens/group_chat_screen.dart:740:10 • missing_identifier
error • Expected to find ';' • lib/screens/group_chat_screen.dart:740:10 • expected_token
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:740:10 • unexpected_token
error • Expected an identifier • lib/screens/group_chat_screen.dart:741:7 • missing_identifier
error • Expected to find ';' • lib/screens/group_chat_screen.dart:741:7 • expected_token
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:741:7 • unexpected_token
error • Expected an identifier • lib/screens/group_chat_screen.dart:741:8 • missing_identifier
error • Expected to find ';' • lib/screens/group_chat_screen.dart:741:8 • expected_token
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:741:8 • unexpected_token
error • Expected an identifier • lib/screens/group_chat_screen.dart:742:5 • missing_identifier
error • Unexpected text ';' • lib/screens/group_chat_screen.dart:742:5 • unexpected_token
info • Unnecessary empty statement • lib/screens/group_chat_screen.dart:742:6 • empty_statements
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:819:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:824:29 • use_build_context_synchronously
error • The class '_HomeScreenState' doesn't have an unnamed constructor • lib/screens/home_screen.dart:19:38 • new_with_undefined_constructor_default
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:185:11 • use_build_context_synchronously
info • Don't invoke 'print' in production code • lib/screens/home_screen.dart:244:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:300:28 • use_build_context_synchronously
error • Expected to find ';' • lib/screens/home_screen.dart:309:39 • expected_token
warning • Dead code • lib/screens/home_screen.dart:311:15 • dead_code
error • Expected an identifier • lib/screens/home_screen.dart:311:15 • missing_identifier
error • Expected to find ')' • lib/screens/home_screen.dart:311:15 • expected_token
error • Expected a class member • lib/screens/home_screen.dart:312:14 • expected_class_member
info • An uninitialized field should have an explicit type annotation • lib/screens/home_screen.dart:313:13 • prefer_typing_uninitialized_variables
error • Expected to find ';' • lib/screens/home_screen.dart:313:13 • expected_token
error • Variables must be declared using the keywords 'const', 'final', 'var' or a type name • lib/screens/home_screen.dart:313:13 • missing_const_final_var_or_type
error • Expected a class member • lib/screens/home_screen.dart:313:18 • expected_class_member
error • Getters, setters and methods can't be declared to be 'const' • lib/screens/home_screen.dart:313:20 • const_method
info • The variable name 'Image' isn't a lowerCamelCase identifier • lib/screens/home_screen.dart:313:26 • non_constant_identifier_names
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:314:20 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:314:20 • obsolete_colon_for_default_value
info • Use 'const' with the constructor to improve performance • lib/screens/home_screen.dart:314:22 • prefer_const_constructors
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:314:22 • non_constant_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:315:21 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:315:21 • obsolete_colon_for_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:316:18 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:316:18 • obsolete_colon_for_default_value
error • A function body must be provided • lib/screens/home_screen.dart:317:14 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:317:14 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:318:11 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:318:12 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:319:9 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:319:10 • expected_class_member
info • An uninitialized field should have an explicit type annotation • lib/screens/home_screen.dart:320:9 • prefer_typing_uninitialized_variables
error • Expected to find ';' • lib/screens/home_screen.dart:320:9 • expected_token
error • Variables must be declared using the keywords 'const', 'final', 'var' or a type name • lib/screens/home_screen.dart:320:9 • missing_const_final_var_or_type
error • Expected a class member • lib/screens/home_screen.dart:320:16 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:320:18 • expected_class_member
info • The variable name 'IconButton' isn't a lowerCamelCase identifier • lib/screens/home_screen.dart:321:11 • non_constant_identifier_names
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:322:17 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:322:17 • obsolete_colon_for_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:323:22 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:323:22 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:323:24 • non_constant_default_value
error • A function body must be provided • lib/screens/home_screen.dart:327:12 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:327:12 • expected_class_member
info • The variable name 'IconButton' isn't a lowerCamelCase identifier • lib/screens/home_screen.dart:328:11 • non_constant_identifier_names
error • The name 'IconButton' is already defined • lib/screens/home_screen.dart:328:11 • duplicate_definition
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:329:17 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:329:17 • obsolete_colon_for_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:330:22 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:330:22 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:330:24 • non_constant_default_value
error • A function body must be provided • lib/screens/home_screen.dart:332:9 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:332:9 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:332:10 • expected_class_member
info • An uninitialized field should have an explicit type annotation • lib/screens/home_screen.dart:333:9 • prefer_typing_uninitialized_variables
error • Expected to find ';' • lib/screens/home_screen.dart:333:9 • expected_token
error • Variables must be declared using the keywords 'const', 'final', 'var' or a type name • lib/screens/home_screen.dart:333:9 • missing_const_final_var_or_type
error • Expected a class member • lib/screens/home_screen.dart:333:15 • expected_class_member
info • The variable name 'PreferredSize' isn't a lowerCamelCase identifier • lib/screens/home_screen.dart:333:17 • non_constant_identifier_names
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:334:24 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:334:24 • obsolete_colon_for_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:335:16 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:335:16 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:335:18 • non_constant_default_value
error • A function body must be provided • lib/screens/home_screen.dart:352:10 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:352:10 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:353:7 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:353:8 • expected_class_member
info • An uninitialized field should have an explicit type annotation • lib/screens/home_screen.dart:354:7 • prefer_typing_uninitialized_variables
error • Expected to find ';' • lib/screens/home_screen.dart:354:7 • expected_token
error • Variables must be declared using the keywords 'const', 'final', 'var' or a type name • lib/screens/home_screen.dart:354:7 • missing_const_final_var_or_type
error • Expected a class member • lib/screens/home_screen.dart:354:11 • expected_class_member
info • The variable name 'TabBarView' isn't a lowerCamelCase identifier • lib/screens/home_screen.dart:354:13 • non_constant_identifier_names
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:355:19 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:355:19 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:355:21 • non_constant_default_value
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:356:17 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:356:17 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:356:19 • non_constant_default_value
error • A function body must be provided • lib/screens/home_screen.dart:497:8 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:497:8 • expected_class_member
info • An uninitialized field should have an explicit type annotation • lib/screens/home_screen.dart:498:7 • prefer_typing_uninitialized_variables
error • Expected to find ';' • lib/screens/home_screen.dart:498:7 • expected_token
error • Variables must be declared using the keywords 'const', 'final', 'var' or a type name • lib/screens/home_screen.dart:498:7 • missing_const_final_var_or_type
error • Expected a class member • lib/screens/home_screen.dart:498:27 • expected_class_member
error • The name of a constructor must match the name of the enclosing class • lib/screens/home_screen.dart:498:29 • invalid_constructor_name
warning • A value for optional parameter 'backgroundColor' isn't ever given • lib/screens/home_screen.dart:499:9 • unused_element_parameter
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:499:24 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:499:24 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:499:26 • non_constant_default_value
warning • A value for optional parameter 'icon' isn't ever given • lib/screens/home_screen.dart:500:9 • unused_element_parameter
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:500:13 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:500:13 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:500:15 • non_constant_default_value
warning • A value for optional parameter 'label' isn't ever given • lib/screens/home_screen.dart:502:9 • unused_element_parameter
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:502:14 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:502:14 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:502:16 • non_constant_default_value
warning • A value for optional parameter 'onPressed' isn't ever given • lib/screens/home_screen.dart:505:9 • unused_element_parameter
error • Named parameters must be enclosed in curly braces ('{' and '}') • lib/screens/home_screen.dart:505:18 • named_parameter_outside_group
error • Using a colon as the separator before a default value is no longer supported • lib/screens/home_screen.dart:505:18 • obsolete_colon_for_default_value
error • The default value of an optional parameter must be constant • lib/screens/home_screen.dart:505:20 • non_constant_default_value
info • Empty constructor bodies should be written using a ';' rather than '{}' • lib/screens/home_screen.dart:510:8 • empty_constructor_bodies
error • A function body must be provided • lib/screens/home_screen.dart:510:8 • missing_function_body
error • Expected a class member • lib/screens/home_screen.dart:510:8 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:511:5 • expected_class_member
error • Expected a class member • lib/screens/home_screen.dart:511:6 • expected_class_member
error • Undefined name 'primaryColor' • lib/screens/home_screen.dart:653:54 • undefined_identifier
error • Undefined name 'primaryColor' • lib/screens/home_screen.dart:671:68 • undefined_identifier
error • Expected a method, getter, setter or operator declaration • lib/screens/home_screen.dart:684:1 • expected_executable
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:109:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:110:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:161:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:162:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:212:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:213:33 • prefer_const_literals_to_create_immutables
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/screens/settings_screen.dart:241:27 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:49 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:64 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:291:57 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/settings_screen.dart:312:30 • deprecated_member_use
error • The argument type 'CardTheme' can't be assigned to the parameter type 'CardThemeData?'. • lib/utils/app_theme.dart:81:18 • argument_type_not_assignable
info • The private field _memoryCache could be 'final' • lib/utils/contact_helper.dart:5:30 • prefer_final_fields
info • Don't invoke 'print' in production code • lib/utils/contact_helper.dart:19:7 • avoid_print
info • Don't invoke 'print' in production code • lib/utils/crypto_helper.dart:29:7 • avoid_print
error • Classes can only extend other classes • lib/widgets/message_bubble.dart:5:29 • extends_non_class
error • Undefined class 'VoidCallback' • lib/widgets/message_bubble.dart:14:9 • undefined_class
error • No associated named super constructor parameter • lib/widgets/message_bubble.dart:20:11 • super_formal_parameter_without_associated_named
error • Undefined class 'State' • lib/widgets/message_bubble.dart:35:3 • undefined_class
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:35:24 • override_on_non_overriding_member
error • Mixin can only be applied to class • lib/widgets/message_bubble.dart:38:35 • mixin_with_non_class_superclass
error • Classes can only mix in mixins and classes • lib/widgets/message_bubble.dart:39:10 • mixin_of_non_class
error • Undefined class 'AnimationController' • lib/widgets/message_bubble.dart:41:8 • undefined_class
error • Undefined class 'Animation' • lib/widgets/message_bubble.dart:42:8 • undefined_class
error • The name 'Offset' isn't a type, so it can't be used as a type argument • lib/widgets/message_bubble.dart:42:18 • non_type_as_type_argument
error • Undefined class 'Animation' • lib/widgets/message_bubble.dart:43:8 • undefined_class
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:46:8 • override_on_non_overriding_member
error • The method 'initState' isn't defined in a superclass of '_MessageBubbleState' • lib/widgets/message_bubble.dart:47:11 • undefined_super_member
error • The method 'AnimationController' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:48:28 • undefined_method
error • The method 'CurvedAnimation' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:53:22 • undefined_method
error • Undefined name 'Curves' • lib/widgets/message_bubble.dart:55:14 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:59:33 • undefined_identifier
error • The method 'Tween' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:61:23 • undefined_method
error • The name 'Offset' isn't a type, so it can't be used as a type argument • lib/widgets/message_bubble.dart:61:29 • non_type_as_type_argument
error • The method 'Offset' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:62:14 • undefined_method
error • Undefined name 'Offset' • lib/widgets/message_bubble.dart:63:12 • undefined_identifier
error • The method 'CurvedAnimation' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:64:15 • undefined_method
error • Undefined name 'Curves' • lib/widgets/message_bubble.dart:66:14 • undefined_identifier
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:73:8 • override_on_non_overriding_member
error • The method 'dispose' isn't defined in a superclass of '_MessageBubbleState' • lib/widgets/message_bubble.dart:75:11 • undefined_super_member
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:79:7 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:80:7 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:81:7 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:81:25 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:84:21 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:85:14 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:89:9 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:90:14 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:92:9 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:93:14 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:95:12 • undefined_identifier
error • Undefined class 'Widget' • lib/widgets/message_bubble.dart:99:3 • undefined_class
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:99:10 • override_on_non_overriding_member
error • Undefined class 'BuildContext' • lib/widgets/message_bubble.dart:99:16 • undefined_class
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:100:27 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:100:57 • undefined_identifier
error • The name 'Color' isn't a type, so it can't be used as a type argument • lib/widgets/message_bubble.dart:103:16 • non_type_as_type_argument
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:103:39 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:104:12 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:104:30 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:105:12 • undefined_identifier
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:106:22 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:106:47 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:107:22 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:107:47 • creation_with_non_type
error • Undefined class 'Color' • lib/widgets/message_bubble.dart:110:11 • undefined_class
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:111:17 • creation_with_non_type
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:112:12 • undefined_identifier
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:112:36 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:112:62 • creation_with_non_type
error • The method 'FadeTransition' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:114:12 • undefined_method
error • The method 'SlideTransition' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:116:14 • undefined_method
error • The method 'Row' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:118:16 • undefined_method
error • Undefined name 'TextDirection' • lib/widgets/message_bubble.dart:119:26 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:121:15 • undefined_identifier
error • Undefined name 'MainAxisAlignment' • lib/widgets/message_bubble.dart:121:29 • undefined_identifier
error • Undefined name 'MainAxisAlignment' • lib/widgets/message_bubble.dart:121:53 • undefined_identifier
error • The method 'Padding' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:123:13 • undefined_method
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:124:24 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:125:23 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:126:24 • undefined_identifier
error • The method 'CustomPaint' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:130:22 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:132:26 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:133:29 • undefined_identifier
error • The method 'LinearGradient' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:134:25 • undefined_method
error • Undefined name 'Alignment' • lib/widgets/message_bubble.dart:136:34 • undefined_identifier
error • Undefined name 'Alignment' • lib/widgets/message_bubble.dart:137:32 • undefined_identifier
error • The method 'LinearGradient' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:140:29 • undefined_method
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:142:41 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:143:41 • creation_with_non_type
error • Undefined name 'Alignment' • lib/widgets/message_bubble.dart:145:38 • undefined_identifier
error • Undefined name 'Alignment' • lib/widgets/message_bubble.dart:146:36 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:149:25 • undefined_identifier
error • The method 'Container' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:151:24 • undefined_method
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:152:34 • undefined_identifier
error • The method 'BoxConstraints' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:153:32 • undefined_method
error • Undefined name 'MediaQuery' • lib/widgets/message_bubble.dart:154:33 • undefined_identifier
error • The method 'Column' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:155:26 • undefined_method
error • Undefined name 'CrossAxisAlignment' • lib/widgets/message_bubble.dart:156:41 • undefined_identifier
error • Undefined name 'MainAxisSize' • lib/widgets/message_bubble.dart:157:35 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:159:27 • undefined_identifier
error • The method 'Padding' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:160:25 • undefined_method
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:161:42 • undefined_identifier
error • The method 'Row' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:162:34 • undefined_method
error • Undefined name 'MainAxisSize' • lib/widgets/message_bubble.dart:163:43 • undefined_identifier
error • The method 'Icon' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:165:31 • undefined_method
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:167:39 • undefined_identifier
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:168:39 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:170:40 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:171:39 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:173:43 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:174:43 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:176:37 • creation_with_non_type
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:177:31 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:180:40 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:182:44 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:184:48 • undefined_identifier
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:187:40 • undefined_method
error • Undefined name 'FontWeight' • lib/widgets/message_bubble.dart:188:47 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:189:42 • undefined_identifier
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:190:47 • creation_with_non_type
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:192:45 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:193:45 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:200:27 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:202:38 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:203:43 • undefined_identifier
error • The method 'Container' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:204:34 • undefined_method
error • The method 'BoxDecoration' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:207:41 • undefined_method
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:208:38 • undefined_identifier
error • Undefined name 'BorderRadius' • lib/widgets/message_bubble.dart:209:45 • undefined_identifier
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:214:25 • undefined_method
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:216:34 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:217:36 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:218:35 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:220:39 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:221:39 • undefined_identifier
error • Undefined name 'FontWeight' • lib/widgets/message_bubble.dart:223:52 • undefined_identifier
error • Undefined name 'FontWeight' • lib/widgets/message_bubble.dart:223:70 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:229:31 • creation_with_non_type
error • The method 'Container' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:230:25 • undefined_method
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:231:42 • undefined_identifier
error • The method 'BoxDecoration' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:233:39 • undefined_method
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:234:36 • undefined_identifier
error • Undefined name 'BorderRadius' • lib/widgets/message_bubble.dart:235:43 • undefined_identifier
error • Undefined name 'Border' • lib/widgets/message_bubble.dart:236:37 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:237:40 • undefined_identifier
error • The method 'Row' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:239:34 • undefined_method
error • The name 'Icon' isn't a class • lib/widgets/message_bubble.dart:241:37 • creation_with_non_type
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:241:42 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:242:52 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:243:37 • creation_with_non_type
error • The name 'Expanded' isn't a class • lib/widgets/message_bubble.dart:244:37 • creation_with_non_type
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:245:40 • undefined_method
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:247:42 • undefined_method
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:249:46 • undefined_identifier
error • Undefined name 'FontWeight' • lib/widgets/message_bubble.dart:250:51 • undefined_identifier
error • The method 'IconButton' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:253:31 • undefined_method
error • The name 'Icon' isn't a class • lib/widgets/message_bubble.dart:254:45 • creation_with_non_type
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:254:50 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:255:44 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:256:44 • undefined_identifier
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:257:42 • undefined_identifier
error • The name 'BoxConstraints' isn't a class • lib/widgets/message_bubble.dart:258:52 • creation_with_non_type
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:264:27 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:265:31 • creation_with_non_type
error • The method 'InkWell' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:266:25 • undefined_method
error • The method 'setState' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:267:40 • undefined_method
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:268:34 • undefined_method
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:272:36 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:274:38 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:275:37 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:276:37 • undefined_identifier
error • Undefined name 'TextDecoration' • lib/widgets/message_bubble.dart:277:43 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:282:29 • creation_with_non_type
error • The method 'Row' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:283:23 • undefined_method
error • Undefined name 'MainAxisSize' • lib/widgets/message_bubble.dart:284:39 • undefined_identifier
error • Undefined name 'MainAxisAlignment' • lib/widgets/message_bubble.dart:285:44 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:287:31 • undefined_identifier
error • The method 'Flexible' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:288:29 • undefined_method
error • The method 'Padding' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:289:38 • undefined_method
error • Undefined name 'EdgeInsets' • lib/widgets/message_bubble.dart:290:48 • undefined_identifier
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:291:40 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:292:35 • undefined_identifier
error • Undefined name 'TextAlign' • lib/widgets/message_bubble.dart:293:46 • undefined_identifier
error • Undefined name 'TextOverflow' • lib/widgets/message_bubble.dart:294:45 • undefined_identifier
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:295:42 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:297:44 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:298:43 • undefined_identifier
error • Undefined name 'Theme' • lib/widgets/message_bubble.dart:299:43 • undefined_identifier
error • The method 'Text' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:305:27 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:306:41 • undefined_identifier
error • The method 'TextStyle' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:307:36 • undefined_method
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:310:35 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:310:49 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:310:66 • undefined_identifier
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:313:31 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:314:35 • creation_with_non_type
error • Undefined class 'Widget' • lib/widgets/message_bubble.dart:330:3 • undefined_class
error • Undefined name 'widget' • lib/widgets/message_bubble.dart:331:13 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:333:22 • creation_with_non_type
error • The method 'CircularProgressIndicator' isn't defined for the type '_MessageBubbleState' • lib/widgets/message_bubble.dart:336:18 • undefined_method
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:337:20 • undefined_identifier
error • The name 'Icon' isn't a class • lib/widgets/message_bubble.dart:342:22 • creation_with_non_type
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:342:27 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:342:60 • undefined_identifier
error • The name 'Icon' isn't a class • lib/widgets/message_bubble.dart:344:22 • creation_with_non_type
error • Undefined name 'Icons' • lib/widgets/message_bubble.dart:344:27 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:344:65 • undefined_identifier
error • The name 'SizedBox' isn't a class • lib/widgets/message_bubble.dart:346:22 • creation_with_non_type
error • Classes can only extend other classes • lib/widgets/message_bubble.dart:356:29 • extends_non_class
error • Undefined class 'Color' • lib/widgets/message_bubble.dart:357:9 • undefined_class
error • Undefined class 'Gradient' • lib/widgets/message_bubble.dart:358:9 • undefined_class
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:364:8 • override_on_non_overriding_member
error • Undefined class 'Canvas' • lib/widgets/message_bubble.dart:364:14 • undefined_class
error • Undefined class 'Size' • lib/widgets/message_bubble.dart:364:29 • undefined_class
error • Undefined class 'Paint' • lib/widgets/message_bubble.dart:365:11 • undefined_class
error • The method 'Paint' isn't defined for the type 'BubblePainter' • lib/widgets/message_bubble.dart:365:25 • undefined_method
error • Undefined name 'PaintingStyle' • lib/widgets/message_bubble.dart:365:42 • undefined_identifier
error • Undefined name 'Rect' • lib/widgets/message_bubble.dart:369:11 • undefined_identifier
error • Undefined class 'Radius' • lib/widgets/message_bubble.dart:374:11 • undefined_class
error • Undefined name 'Radius' • lib/widgets/message_bubble.dart:374:33 • undefined_identifier
error • Undefined class 'Path' • lib/widgets/message_bubble.dart:375:11 • undefined_class
error • The method 'Path' isn't defined for the type 'BubblePainter' • lib/widgets/message_bubble.dart:375:23 • undefined_method
error • Undefined name 'RRect' • lib/widgets/message_bubble.dart:380:21 • undefined_identifier
error • Undefined name 'Radius' • lib/widgets/message_bubble.dart:388:22 • undefined_identifier
error • The method 'Path' isn't defined for the type 'BubblePainter' • lib/widgets/message_bubble.dart:392:22 • undefined_method
error • Undefined name 'Offset' • lib/widgets/message_bubble.dart:397:30 • undefined_identifier
error • Undefined name 'RRect' • lib/widgets/message_bubble.dart:400:21 • undefined_identifier
error • Undefined name 'Radius' • lib/widgets/message_bubble.dart:407:21 • undefined_identifier
error • The method 'Path' isn't defined for the type 'BubblePainter' • lib/widgets/message_bubble.dart:412:22 • undefined_method
error • Undefined name 'Offset' • lib/widgets/message_bubble.dart:417:30 • undefined_identifier
error • The name 'Offset' isn't a class • lib/widgets/message_bubble.dart:422:24 • creation_with_non_type
error • The name 'Color' isn't a class • lib/widgets/message_bubble.dart:423:42 • creation_with_non_type
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:423:85 • undefined_identifier
error • Undefined name 'Colors' • lib/widgets/message_bubble.dart:423:123 • undefined_identifier
warning • The method doesn't override an inherited method • lib/widgets/message_bubble.dart:431:8 • override_on_non_overriding_member
error • Undefined class 'CustomPainter' • lib/widgets/message_bubble.dart:431:22 • undefined_class
info • Can't use a relative path to import a library in 'lib' • test/background_logic_test.dart:2:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/background_logic_test.dart:15:5 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:20:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:23:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:26:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:27:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:31:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:34:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:35:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:48:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:50:3 • avoid_print
info • Can't use a relative path to import a library in 'lib' • test/protocol_test.dart:1:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/protocol_test.dart:5:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:7:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:13:3 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:79:3 • avoid_print
error • A value of type 'MessageBubble' can't be assigned to a parameter of type 'Widget?' in a const constructor • test/widget_test.dart:10:11 • const_constructor_param_type_mismatch
error • The argument type 'MessageBubble' can't be assigned to the parameter type 'Widget?'. • test/widget_test.dart:10:17 • argument_type_not_assignable
error • Invalid constant value • test/widget_test.dart:16:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:16:21 • undefined_identifier
error • A value of type 'MessageBubble' can't be assigned to a parameter of type 'Widget?' in a const constructor • test/widget_test.dart:33:11 • const_constructor_param_type_mismatch
error • The argument type 'MessageBubble' can't be assigned to the parameter type 'Widget?'. • test/widget_test.dart:33:17 • argument_type_not_assignable
error • Invalid constant value • test/widget_test.dart:37:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:37:21 • undefined_identifier
411 issues found. (ran in 4.8s)

80
analyze_results_v2.txt Normal file
View File

@ -0,0 +1,80 @@
Analyzing saba-dart...
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/chat_screen.dart:3:8 • unnecessary_import
info • Don't invoke 'print' in production code • lib/screens/compose_screen.dart:62:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:129:7 • use_build_context_synchronously
info • Use interpolation to compose strings and values • lib/screens/compose_screen.dart:255:20 • prefer_interpolation_to_compose_strings
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:415:63 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:582:22 • deprecated_member_use
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:639:7 • curly_braces_in_flow_control_structures
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:689:11 • curly_braces_in_flow_control_structures
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:695:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:700:29 • use_build_context_synchronously
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/group_chat_screen.dart:2:8 • unnecessary_import
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:226:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:410:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:530:28 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:897:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:902:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:185:11 • use_build_context_synchronously
info • Don't invoke 'print' in production code • lib/screens/home_screen.dart:244:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:300:28 • use_build_context_synchronously
error • The named parameter 'bottom' isn't defined • lib/screens/home_screen.dart:352:9 • undefined_named_parameter
error • Expected to find ';' • lib/screens/home_screen.dart:372:7 • expected_token
warning • Dead code • lib/screens/home_screen.dart:372:8 • dead_code
error • Expected an identifier • lib/screens/home_screen.dart:372:8 • missing_identifier
error • Unexpected text ';' • lib/screens/home_screen.dart:372:8 • unexpected_token
warning • The label 'body' isn't used • lib/screens/home_screen.dart:373:7 • unused_label
error • Expected to find ';' • lib/screens/home_screen.dart:516:7 • expected_token
error • Expected an identifier • lib/screens/home_screen.dart:516:8 • missing_identifier
error • Unexpected text ';' • lib/screens/home_screen.dart:516:8 • unexpected_token
warning • The label 'floatingActionButton' isn't used • lib/screens/home_screen.dart:517:7 • unused_label
error • Expected to find ';' • lib/screens/home_screen.dart:529:7 • expected_token
error • Expected an identifier • lib/screens/home_screen.dart:529:8 • missing_identifier
error • Expected to find ';' • lib/screens/home_screen.dart:529:8 • expected_token
error • Unexpected text ';' • lib/screens/home_screen.dart:529:8 • unexpected_token
error • Expected an identifier • lib/screens/home_screen.dart:530:5 • missing_identifier
error • Unexpected text ';' • lib/screens/home_screen.dart:530:5 • unexpected_token
info • Unnecessary empty statement • lib/screens/home_screen.dart:530:6 • empty_statements
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:109:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:110:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:161:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:162:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:212:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:213:33 • prefer_const_literals_to_create_immutables
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/screens/settings_screen.dart:241:27 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:49 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:64 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:291:57 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/settings_screen.dart:312:30 • deprecated_member_use
error • The argument type 'CardTheme' can't be assigned to the parameter type 'CardThemeData?'. • lib/utils/app_theme.dart:81:18 • argument_type_not_assignable
info • The private field _memoryCache could be 'final' • lib/utils/contact_helper.dart:5:30 • prefer_final_fields
info • Don't invoke 'print' in production code • lib/utils/contact_helper.dart:19:7 • avoid_print
info • Don't invoke 'print' in production code • lib/utils/crypto_helper.dart:29:7 • avoid_print
warning • Unused import: '../utils/app_theme.dart' • lib/widgets/message_bubble.dart:4:8 • unused_import
info • Use 'const' with the constructor to improve performance • lib/widgets/message_bubble.dart:141:29 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/widgets/message_bubble.dart:142:39 • prefer_const_literals_to_create_immutables
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/widgets/message_bubble.dart:235:49 • deprecated_member_use
info • Use 'const' for final variables initialized to a constant value • lib/widgets/message_bubble.dart:375:5 • prefer_const_declarations
info • Can't use a relative path to import a library in 'lib' • test/background_logic_test.dart:2:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/background_logic_test.dart:15:5 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:20:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:23:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:26:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:27:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:31:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:34:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:35:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:48:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:50:3 • avoid_print
info • Can't use a relative path to import a library in 'lib' • test/protocol_test.dart:1:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/protocol_test.dart:5:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:7:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:13:3 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:79:3 • avoid_print
error • Invalid constant value • test/widget_test.dart:16:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:16:21 • undefined_identifier
error • Invalid constant value • test/widget_test.dart:37:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:37:21 • undefined_identifier
76 issues found. (ran in 6.4s)

62
analyze_results_v3.txt Normal file
View File

@ -0,0 +1,62 @@
Analyzing saba-dart...
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/chat_screen.dart:3:8 • unnecessary_import
info • Don't invoke 'print' in production code • lib/screens/compose_screen.dart:62:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:129:7 • use_build_context_synchronously
info • Use interpolation to compose strings and values • lib/screens/compose_screen.dart:255:20 • prefer_interpolation_to_compose_strings
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:415:63 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/compose_screen.dart:582:22 • deprecated_member_use
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:639:7 • curly_braces_in_flow_control_structures
info • Statements in an if should be enclosed in a block • lib/screens/compose_screen.dart:689:11 • curly_braces_in_flow_control_structures
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:695:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/compose_screen.dart:700:29 • use_build_context_synchronously
info • The import of 'dart:ui' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/group_chat_screen.dart:2:8 • unnecessary_import
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:226:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/group_chat_screen.dart:410:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:530:28 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:897:15 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/group_chat_screen.dart:902:29 • use_build_context_synchronously
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:185:11 • use_build_context_synchronously
info • Don't invoke 'print' in production code • lib/screens/home_screen.dart:244:7 • avoid_print
info • Don't use 'BuildContext's across async gaps • lib/screens/home_screen.dart:300:28 • use_build_context_synchronously
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:109:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:110:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:161:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:162:33 • prefer_const_literals_to_create_immutables
info • Use 'const' with the constructor to improve performance • lib/screens/settings_screen.dart:212:21 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings_screen.dart:213:33 • prefer_const_literals_to_create_immutables
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/screens/settings_screen.dart:241:27 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:49 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:286:64 • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings_screen.dart:291:57 • deprecated_member_use
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/screens/settings_screen.dart:312:30 • deprecated_member_use
info • The private field _memoryCache could be 'final' • lib/utils/contact_helper.dart:5:30 • prefer_final_fields
info • Don't invoke 'print' in production code • lib/utils/contact_helper.dart:19:7 • avoid_print
info • Don't invoke 'print' in production code • lib/utils/crypto_helper.dart:29:7 • avoid_print
warning • Unused import: '../utils/app_theme.dart' • lib/widgets/message_bubble.dart:4:8 • unused_import
info • Use 'const' with the constructor to improve performance • lib/widgets/message_bubble.dart:141:29 • prefer_const_constructors
info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/widgets/message_bubble.dart:142:39 • prefer_const_literals_to_create_immutables
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • lib/widgets/message_bubble.dart:235:49 • deprecated_member_use
info • Use 'const' for final variables initialized to a constant value • lib/widgets/message_bubble.dart:375:5 • prefer_const_declarations
info • Can't use a relative path to import a library in 'lib' • test/background_logic_test.dart:2:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/background_logic_test.dart:15:5 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:20:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:23:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:26:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:27:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:31:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:34:3 • avoid_print
info • Use 'const' for final variables initialized to a constant value • test/background_logic_test.dart:35:3 • prefer_const_declarations
info • Don't invoke 'print' in production code • test/background_logic_test.dart:48:3 • avoid_print
info • Don't invoke 'print' in production code • test/background_logic_test.dart:50:3 • avoid_print
info • Can't use a relative path to import a library in 'lib' • test/protocol_test.dart:1:8 • avoid_relative_lib_imports
info • Don't invoke 'print' in production code • test/protocol_test.dart:5:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:7:5 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:13:3 • avoid_print
info • Don't invoke 'print' in production code • test/protocol_test.dart:79:3 • avoid_print
error • Invalid constant value • test/widget_test.dart:16:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:16:21 • undefined_identifier
error • Invalid constant value • test/widget_test.dart:37:21 • invalid_constant
error • Undefined name 'MessageStatus' • test/widget_test.dart:37:21 • undefined_identifier
58 issues found. (ran in 6.1s)

14
android/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,148 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
<activity-alias
android:name=".MainActivitySaba"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityCalculator"
android:targetActivity=".MainActivity"
android:label="ماشین حساب"
android:icon="@drawable/ic_calc"
android:roundIcon="@drawable/ic_calc"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</activity-alias>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<receiver
android:name="com.shounakmulay.telephony.sms.IncomingSmsReceiver"
android:permission="android.permission.BROADCAST_SMS"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".SmsDeliverReceiver"
android:permission="android.permission.BROADCAST_SMS"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_DELIVER" />
</intent-filter>
</receiver>
<!-- Required for Default SMS App: Receiver for WAP_PUSH (MMS) -->
<receiver
android:name=".MmsReceiver"
android:permission="android.permission.BROADCAST_WAP_PUSH"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<!-- Required for Default SMS App: Service for quick responses -->
<service
android:name=".HeadlessSmsSendService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</service>
<service
android:name="com.shounakmulay.telephony.sms.IncomingSmsService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,140 @@
package com.example.saba_secure_sms
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Telephony
import android.widget.Toast
import android.provider.Settings
import android.content.ComponentName
import android.content.pm.PackageManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.saba_secure_sms/sms_role"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"isDefaultSmsApp" -> {
result.success(isDefaultSmsApp())
}
"requestDefaultSmsApp" -> {
val status = requestDefaultSmsApp()
result.success(status)
}
"openDefaultAppsSettings" -> {
openDefaultAppsSettings()
result.success(true)
}
"setStealthMode" -> {
val enabled = call.argument<Boolean>("enabled") ?: false
setStealthMode(enabled)
result.success(true)
}
"getStealthMode" -> {
result.success(getStealthMode())
}
else -> {
result.notImplemented()
}
}
}
}
private fun isDefaultSmsApp(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val packageName = packageName
val defaultSmsPackage = Telephony.Sms.getDefaultSmsPackage(this)
packageName == defaultSmsPackage
} else {
false
}
}
private fun requestDefaultSmsApp(): String {
if (isDefaultSmsApp()) return "already_default"
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (roleManager.isRoleAvailable(RoleManager.ROLE_SMS)) {
if (!roleManager.isRoleHeld(RoleManager.ROLE_SMS)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)
startActivityForResult(intent, 1001)
"requesting_role"
} else {
"role_already_held"
}
} else {
"role_not_available"
}
} else {
val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, packageName)
startActivity(intent)
"requesting_intent"
}
} catch (e: Exception) {
Toast.makeText(this, "Error requesting SMS role: ${e.message}", Toast.LENGTH_LONG).show()
"error: ${e.message}"
}
}
private fun openDefaultAppsSettings() {
try {
val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
startActivity(intent)
} catch (e: Exception) {
try {
val intent = Intent(Settings.ACTION_SETTINGS)
startActivity(intent)
} catch (e2: Exception) {
Toast.makeText(this, "عدم دسترسی به تنظیمات سیستم", Toast.LENGTH_SHORT).show()
}
}
}
private fun setStealthMode(enabled: Boolean) {
val packageManager = packageManager
val sabaComponent = ComponentName(this, "com.example.saba_secure_sms.MainActivitySaba")
val calcComponent = ComponentName(this, "com.example.saba_secure_sms.MainActivityCalculator")
if (enabled) {
// Enable Calculator, Disable Saba
packageManager.setComponentEnabledSetting(calcComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
packageManager.setComponentEnabledSetting(sabaComponent, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0) // Setting 0 (no flag) will kill the app to force refresh launcher
} else {
// Enable Saba, Disable Calculator
packageManager.setComponentEnabledSetting(sabaComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
packageManager.setComponentEnabledSetting(calcComponent, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0)
}
}
private fun getStealthMode(): Boolean {
val calcComponent = ComponentName(this, "com.example.saba_secure_sms.MainActivityCalculator")
val status = packageManager.getComponentEnabledSetting(calcComponent)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
}
// Required components for Default SMS App eligibility
class SmsDeliverReceiver : android.content.BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// Handled by another_telephony plugin via AndroidManifest entry pointing to its receiver
}
}
class MmsReceiver : android.content.BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// MMS support not fully implemented but required for role eligibility
}
}
class HeadlessSmsSendService : android.app.Service() {
override fun onBind(intent: Intent?): android.os.IBinder? = null
}

View File

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

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#EEEEEE"
android:pathData="M14,14h80v80h-80z" />
<!-- Screen -->
<path
android:fillColor="#333333"
android:pathData="M24,24h60v20h-60z" />
<!-- Buttons -->
<path
android:fillColor="#444444"
android:pathData="M24,50h15v10h-15z M46.5,50h15v10h-15z M69,50h15v10h-15z" />
<path
android:fillColor="#444444"
android:pathData="M24,65h15v10h-15z M46.5,65h15v10h-15z M69,65h15v10h-15z" />
<path
android:fillColor="#444444"
android:pathData="M24,80h15v10h-15z M46.5,80h15v10h-15z M69,80h15v10h-15z" />
</vector>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">صبا</string>
</resources>

View File

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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Saba</string>
</resources>

View File

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

View File

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

43
android/build.gradle.kts Normal file
View File

@ -0,0 +1,43 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
subprojects {
fun applyNamespace() {
if (project.hasProperty("android")) {
val extension = project.extensions.findByName("android")
if (extension is com.android.build.gradle.BaseExtension) {
if (extension.namespace == null) {
extension.namespace = project.group.toString()
}
}
}
}
if (project.state.executed) {
applyNamespace()
} else {
afterEvaluate { applyNamespace() }
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
ios/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

70
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Saba Secure Sms</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>saba_secure_sms</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

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

View File

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

View File

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

59
lib/main.dart Normal file
View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_windowmanager_plus/flutter_windowmanager_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'screens/splash_screen.dart';
import 'utils/app_lock_service.dart';
import 'utils/app_theme.dart';
import 'widgets/app_lock_overlay.dart';
// Global notifier for the primary theme color
final ValueNotifier<Color> themeNotifier =
ValueNotifier<Color>(const Color(0xFF3F51B5));
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterWindowManagerPlus.addFlags(FlutterWindowManagerPlus.FLAG_SECURE);
// Load saved theme color
final prefs = await SharedPreferences.getInstance();
final colorValue = prefs.getInt('primary_color_value');
if (colorValue != null) {
themeNotifier.value = Color(colorValue);
}
await AppLockService.instance.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Color>(
valueListenable: themeNotifier,
builder: (context, primaryColor, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'saba',
theme: AppTheme.neoDarkTheme(primaryColor),
builder: (context, child) => AppLockOverlay(
child: child ?? const SizedBox.shrink(),
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('fa', 'IR'),
],
locale: const Locale('fa', 'IR'),
home: const SplashScreen(),
);
},
);
}
}

View File

@ -0,0 +1,41 @@
enum MessageStatus { sending, sent, received, failed, ignored }
class ChatModel {
int? id;
String? localId;
String body;
String? rawBody;
String? rawViewBody;
String? encryptedPayload;
String? statusLabel;
String? packetId;
String? packetMode;
String? rawPacketId;
int date;
bool isMe;
MessageStatus status;
bool isSecure;
bool canRetryDecryption;
bool isRead;
bool isPendingMultipart;
ChatModel({
this.id,
this.localId,
required this.body,
this.rawBody,
this.rawViewBody,
this.encryptedPayload,
this.statusLabel,
this.packetId,
this.packetMode,
this.rawPacketId,
required this.date,
required this.isMe,
required this.status,
this.isSecure = false,
this.canRetryDecryption = false,
this.isPendingMultipart = false,
this.isRead = true,
});
}

2232
lib/screens/chat_screen.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,773 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:another_telephony/telephony.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../utils/secure_messaging_service.dart';
import '../utils/database_helper.dart';
import '../utils/contact_helper.dart';
import '../utils/secure_crypto_helper.dart';
import '../utils/app_theme.dart';
import 'chat_screen.dart';
class ComposeScreen extends StatefulWidget {
const ComposeScreen({super.key});
@override
State<ComposeScreen> createState() => _ComposeScreenState();
}
class _ComposeScreenState extends State<ComposeScreen> {
final Telephony telephony = Telephony.instance;
static const platform = MethodChannel('com.example.saba/sim_cards');
static const asymmetricModeLabel = 'رمزنگاری غیر متقارن (طولانی‌تر و امن‌تر)';
final _phoneController = TextEditingController();
final _msgController = TextEditingController();
final _keyController = TextEditingController();
final _groupNameController = TextEditingController();
final List<Map<String, String>> _selectedContacts = [];
// --- کش داخلی (لیست سبک مخاطبین) ---
// به جای Map، از کلاس Contact استفاده میکنیم که فقط ID و Name دارد
List<Contact> _contactsCache = [];
List<Map<String, dynamic>> _simCards = [];
Map<String, dynamic>? _selectedSim;
bool _loadingSims = true;
bool isSending = false;
String _selectedSecurityLevel = 'normal';
Color get primaryColor => Theme.of(context).primaryColor;
final Color backgroundColor = const Color(0xFFF5F7FA);
@override
void initState() {
super.initState();
_fetchSimCards();
// لود کردن لیست سبک در شروع
_loadLightContacts();
}
// دریافت لیست سبک (نام و آیدی) - بسیار سریع
Future<void> _loadLightContacts() async {
try {
final contacts = await ContactHelper.getContactsLight();
if (mounted) {
setState(() {
_contactsCache = contacts;
});
}
} catch (e) {
print("Error loading light contacts: $e");
}
}
// دکمه آپدیت: هم لیست جستجو را آپدیت میکند، هم دیتابیس را
Future<void> _forceSyncContacts({bool silent = false}) async {
if (!silent) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
// 1. آپدیت لیست جستجو (سریع)
await _loadLightContacts();
// 2. آپدیت دیتابیس برای نمایش نام در صفحه اصلی (سنگین)
await ContactHelper.syncWithDevice();
if (!silent && mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("✅ مخاطبین بروزرسانی شدند")));
}
}
Future<void> _fetchSimCards() async {
if (!await Permission.phone.request().isGranted) {
setState(() => _loadingSims = false);
return;
}
try {
final List<dynamic> result = await platform.invokeMethod('getSimCards');
List<Map<String, dynamic>> cleanList = result.map((e) {
final Map<Object?, Object?> rawMap = e as Map<Object?, Object?>;
return rawMap.map((key, value) => MapEntry(key.toString(), value));
}).toList();
setState(() {
_simCards = cleanList;
if (_simCards.isNotEmpty) _selectedSim = _simCards[0];
_loadingSims = false;
});
} catch (e) {
setState(() => _loadingSims = false);
}
}
void _openContactSearch() async {
if (!await FlutterContacts.requestPermission(readonly: true)) return;
// اگر لیست خالی بود، سعی کن دوباره بگیری
if (_contactsCache.isEmpty) {
await _loadLightContacts();
}
if (_contactsCache.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("مخاطبی یافت نشد")));
}
return;
}
// باز کردن سرچ با لیست سبک
final result = await showSearch<Map<String, String>?>(
context: context,
delegate: ContactsSearchDelegate(_contactsCache),
);
if (result != null) {
setState(() {
// نرمالسازی شماره قبل از افزودن
final normalizedPhone = ContactHelper.normalizePhone(result['phone']!);
final exists = _selectedContacts.any((c) =>
ContactHelper.normalizePhone(c['phone']!) == normalizedPhone);
if (!exists) {
result['phone'] = normalizedPhone;
_selectedContacts.add(result);
}
if (_selectedContacts.length == 1) {
_phoneController.text = _selectedContacts[0]['phone']!;
} else {
_phoneController.clear();
}
});
}
}
void _removeContact(int index) {
setState(() {
_selectedContacts.removeAt(index);
if (_selectedContacts.length == 1) {
_phoneController.text = _selectedContacts[0]['phone']!;
} else if (_selectedContacts.isEmpty) {
_phoneController.clear();
}
});
}
String _securityLabel(String level) {
switch (level) {
case 'symmetric':
return 'رمزنگاری متقارن';
case 'asymmetric':
return asymmetricModeLabel;
default:
return 'ارسال عادی';
}
}
Widget _buildSecurityModeCard() {
const options = <String>['normal', 'symmetric', 'asymmetric'];
return AppTheme.glassWrapper(
radius: 18,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'سطح امنیت',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: options.map((level) {
return ChoiceChip(
label: Text(_securityLabel(level)),
selected: _selectedSecurityLevel == level,
onSelected: (_) {
setState(() {
_selectedSecurityLevel = level;
});
},
selectedColor: primaryColor,
backgroundColor: Colors.white.withValues(alpha: 0.05),
labelStyle: TextStyle(
color: _selectedSecurityLevel == level
? Colors.white
: Colors.white70,
fontWeight: FontWeight.w600,
),
);
}).toList(),
),
const SizedBox(height: 12),
Text(
_selectedSecurityLevel == 'asymmetric'
? 'در حالت $asymmetricModeLabel برنامه در صورت نیاز ابتدا تبادل کلید را از طریق پیامک انجام می‌دهد و سپس پیام اصلی را می‌فرستد.'
: _selectedSecurityLevel == 'symmetric'
? 'در این حالت باید کلید مشترک یکسانی در دو طرف وارد شده باشد.'
: 'در این حالت پیام بدون رمزنگاری اضافه ارسال می‌شود.',
style: const TextStyle(
color: Colors.white60,
fontSize: 12,
height: 1.5,
),
),
],
),
),
);
}
Future<void> _sendMessage() async {
bool isGroupMode = _selectedContacts.length > 1;
if (isGroupMode) {
if (_groupNameController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("لطفاً نام گروه را وارد کنید")));
return;
}
if (_msgController.text.isEmpty) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("متن پیام خالی است")));
return;
}
int groupId = await DatabaseHelper.instance
.createGroup(_groupNameController.text, _selectedContacts);
String finalMsg = _msgController.text;
if (_keyController.text.isNotEmpty) {
final crypto = SecureCryptoHelper();
finalMsg = await crypto.encryptSymmetric(_msgController.text, _keyController.text);
finalMsg = "@G:SYM|" + finalMsg; // Prefix for Group Symmetric
}
setState(() => isSending = true);
int? subId =
_selectedSim != null ? _selectedSim!['subscriptionId'] as int : null;
for (var member in _selectedContacts) {
try {
String phone = ContactHelper.normalizePhone(member['phone']!);
if (subId != null) {
await telephony.sendSms(
to: phone, message: finalMsg, subscriptionId: subId);
} else {
await telephony.sendSms(to: phone, message: finalMsg);
}
} catch (_) {}
}
await DatabaseHelper.instance.saveGroupMessage(
groupId, finalMsg, DateTime.now().millisecondsSinceEpoch);
setState(() => isSending = false);
if (mounted) Navigator.pop(context, true);
return;
}
String rawPhone = _phoneController.text;
if (rawPhone.isEmpty && _selectedContacts.isNotEmpty) {
rawPhone = _selectedContacts[0]['phone']!;
}
if (rawPhone.isEmpty || _msgController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("شماره و متن الزامی است")));
return;
}
final normalizedPhone = ContactHelper.normalizePhone(rawPhone);
final messageText = _msgController.text.trim();
final symmetricKey = _keyController.text.trim();
if (_selectedSecurityLevel == 'symmetric' && symmetricKey.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('برای رمزنگاری متقارن، کلید لازم است')),
);
return;
}
setState(() => isSending = true);
try {
final result = await SecureMessagingService.instance.sendMessage(
normalizedPhone,
messageText,
securityLevel: _selectedSecurityLevel,
symmetricKey: _selectedSecurityLevel == 'symmetric' ? symmetricKey : null,
);
if (!mounted) return;
final sentText = result['sentText'] == true;
final notice = result['notice'] as String? ?? '';
final snackText = sentText
? (notice == 'sent_fragmented' ? 'پیام چندبخشی ارسال شد' : 'پیام ارسال شد')
: notice;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(snackText)));
setState(() => isSending = false);
if (sentText) {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(address: normalizedPhone),
),
result: true,
);
}
});
}
return;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا: $e')),
);
setState(() => isSending = false);
return;
}
}
@override
Widget build(BuildContext context) {
bool isGroupMode = _selectedContacts.length > 1;
return Scaffold(
backgroundColor: AppTheme.darkBg,
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight + 8),
child: AppTheme.glassWrapper(
radius: 0,
sigma: 18,
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Text(isGroupMode ? "ساخت گروه جدید" : "پیام جدید",
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
foregroundColor: Colors.white,
),
),
),
body: Stack(
children: [
Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())),
SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, MediaQuery.of(context).padding.top + kToolbarHeight + 20, 20, 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!_loadingSims && _simCards.length > 1)
Container(
margin: const EdgeInsets.only(bottom: 20),
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _simCards.length,
itemBuilder: (context, index) {
final sim = _simCards[index];
final isSelected = _selectedSim == sim;
return Padding(
padding: const EdgeInsets.only(right: 10),
child: ChoiceChip(
label: Text("${sim['carrierName']} (Slot ${sim['slotIndex'] + 1})"),
selected: isSelected,
onSelected: (selected) => setState(() => _selectedSim = sim),
selectedColor: primaryColor,
labelStyle: TextStyle(
color: isSelected ? Colors.white : Colors.white70,
fontSize: 13),
backgroundColor: Colors.white.withValues(alpha: 0.05),
),
);
},
),
),
// --- بخش گیرندگان ---
AppTheme.glassWrapper(
radius: 20,
sigma: 10,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
if (_selectedContacts.isNotEmpty)
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: _selectedContacts.asMap().entries.map((entry) {
return Chip(
avatar: CircleAvatar(
backgroundColor: primaryColor.withValues(alpha: 0.8),
child: Text(
entry.value['name']!.isNotEmpty ? entry.value['name']![0] : "?",
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
label: Text(entry.value['name']!, style: const TextStyle(color: Colors.white, fontSize: 13)),
deleteIcon: const Icon(Icons.close, size: 16, color: Colors.white70),
onDeleted: () => _removeContact(entry.key),
backgroundColor: Colors.white.withValues(alpha: 0.1),
side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
);
}).toList(),
),
),
if (isGroupMode)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: _groupNameController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "نام گروه",
labelStyle: const TextStyle(color: Colors.white60),
prefixIcon: const Icon(Icons.groups_rounded, color: Colors.white70),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
Row(
children: [
Expanded(
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
enabled: !isGroupMode && _selectedContacts.isEmpty,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "شماره گیرنده",
labelStyle: const TextStyle(color: Colors.white60),
prefixIcon: const Icon(Icons.phone_iphone_rounded, color: Colors.white70),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
const SizedBox(width: 8),
_buildSmallButton(Icons.sync_rounded, Colors.orangeAccent, () => _forceSyncContacts(silent: false)),
const SizedBox(width: 8),
_buildSmallButton(Icons.person_add_rounded, primaryColor, _openContactSearch),
],
),
],
),
),
),
const SizedBox(height: 18),
// --- بخش تنظیمات امنیتی ---
if (!isGroupMode) ...[
_buildSecurityModeCard(),
const SizedBox(height: 18),
],
if (isGroupMode || _selectedSecurityLevel == 'symmetric')
AppTheme.glassWrapper(
radius: 18,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
),
child: TextField(
controller: _keyController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: isGroupMode ? 'کلید گروه (اختیاری)' : 'کلید رمزنگاری متقارن',
hintStyle: const TextStyle(color: Colors.white30),
icon: const Icon(Icons.vpn_key_rounded, color: Colors.orangeAccent),
border: InputBorder.none,
helperText: isGroupMode
? 'پیام به‌صورت متقارن برای همه اعضا رمز می‌شود.'
: 'کلید باید در هر دو سمت یکسان باشد.',
helperStyle: const TextStyle(color: Colors.white38, fontSize: 11),
),
),
),
),
if (_selectedSecurityLevel == 'asymmetric' && !isGroupMode)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.indigo.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.indigo.withValues(alpha: 0.2)),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.shield_rounded, color: Colors.indigoAccent, size: 20),
SizedBox(width: 12),
Expanded(
child: Text(
'در حالت رمزنگاری غیر متقارن، تبادل کلید خودکار انجام می‌شود. پیام‌های طولانی به‌صورت چندبخشی ارسال می‌شوند.',
style: TextStyle(color: Colors.white70, fontSize: 12, height: 1.5),
),
),
],
),
),
const SizedBox(height: 18),
// --- بخش متن پیام ---
AppTheme.glassWrapper(
radius: 20,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
),
child: TextField(
controller: _msgController,
maxLength: 600,
maxLines: 6,
minLines: 4,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: "متن پیام خود را بنویسید...",
hintStyle: TextStyle(color: Colors.white30),
border: InputBorder.none,
counterStyle: TextStyle(color: Colors.white38),
helperText: "استفاده از پروتکل چندبخشی در صورت نیاز.",
helperStyle: TextStyle(color: Colors.white38, fontSize: 11),
),
),
),
),
const SizedBox(height: 32),
// --- دکمه ارسال ---
Container(
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
gradient: LinearGradient(
colors: [primaryColor, primaryColor.withValues(alpha: 0.8)],
),
boxShadow: [
BoxShadow(
color: primaryColor.withValues(alpha: 0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: ElevatedButton.icon(
icon: isSending
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send_rounded, size: 20),
label: Text(isSending ? "در حال ارسال..." : "ارسال پیام",
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
onPressed: isSending ? null : _sendMessage,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
),
),
),
],
),
),
],
),
);
}
Widget _buildSmallButton(IconData icon, Color color, VoidCallback onTap) {
return Container(
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: IconButton(
icon: Icon(icon, color: color, size: 22),
onPressed: onTap,
splashRadius: 24,
),
);
}
}
// --- Delegate جدید: ورودی List<Contact> ---
class ContactsSearchDelegate extends SearchDelegate<Map<String, String>?> {
final List<Contact> contacts; // لیست سبک
ContactsSearchDelegate(this.contacts);
@override
String? get searchFieldLabel => 'جستجو نام مخاطب...';
@override
List<Widget>? buildActions(BuildContext context) {
return [
if (query.isNotEmpty)
IconButton(icon: const Icon(Icons.clear), onPressed: () => query = ''),
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, null),
);
}
@override
Widget buildResults(BuildContext context) => _buildList(context);
@override
Widget buildSuggestions(BuildContext context) => _buildList(context);
Widget _buildList(BuildContext context) {
if (contacts.isEmpty) return const Center(child: Text("مخاطبی یافت نشد."));
final filtered = query.isEmpty
? contacts
: contacts.where((c) {
final name = c.displayName.toLowerCase();
final q = query.toLowerCase();
return name.contains(q);
}).toList();
// بررسی اینکه آیا کوئری شبیه شماره تلفن است
final bool isQueryNumeric = query.length >= 3 && RegExp(r'^[0-9+\-*#]+$').hasMatch(query);
if (filtered.isEmpty && !isQueryNumeric)
return const Center(child: Text("نتیجه‌ای یافت نشد."));
return ListView.separated(
itemCount: filtered.length + (isQueryNumeric ? 1 : 0),
separatorBuilder: (ctx, i) => const Divider(height: 1),
itemBuilder: (context, index) {
if (isQueryNumeric && index == 0) {
return ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.green,
child: Icon(Icons.phone, color: Colors.white),
),
title: Text("افزودن شماره دستی: $query",
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green)),
subtitle: const Text("استفاده از این شماره"),
onTap: () => close(context, {'name': query, 'phone': query}),
);
}
final contact = filtered[isQueryNumeric ? index - 1 : index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.indigo.shade100,
child: Text(
contact.displayName.isNotEmpty ? contact.displayName[0] : "?",
style: TextStyle(color: Colors.indigo.shade900)),
),
title: Text(contact.displayName,
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle:
const Text("برای انتخاب کلیک کنید"), // شماره هنوز معلوم نیست
onTap: () => _onContactSelect(context, contact.id),
);
},
);
}
// --- دریافت شماره فقط زمان کلیک ---
Future<void> _onContactSelect(BuildContext context, String contactId) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()));
try {
// اینجا شماره را از گوشی میگیریم
final fullContact = await ContactHelper.getFullContact(contactId);
if (context.mounted) Navigator.pop(context);
if (fullContact == null || fullContact.phones.isEmpty) {
if (context.mounted)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("این مخاطب شماره تلفن ندارد")));
return;
}
if (fullContact.phones.length == 1) {
close(context, {
'name': fullContact.displayName,
'phone': fullContact.phones.first.number
});
} else {
_showPhoneSelection(context, fullContact);
}
} catch (e) {
if (context.mounted) Navigator.pop(context);
}
}
void _showPhoneSelection(BuildContext context, Contact contact) {
showDialog(
context: context,
builder: (_) => SimpleDialog(
title: Text("انتخاب شماره ${contact.displayName}"),
children: contact.phones
.map((p) => SimpleDialogOption(
onPressed: () {
Navigator.pop(context);
close(context,
{'name': contact.displayName, 'phone': p.number});
},
child: Text(p.number),
))
.toList(),
),
);
}
}

View File

@ -0,0 +1,931 @@
import 'dart:async';
import 'dart:ui';
import 'package:another_telephony/telephony.dart';
import '../utils/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
import '../utils/contact_helper.dart';
import '../utils/database_helper.dart';
import '../utils/protocol_helper.dart';
import '../utils/secure_crypto_helper.dart';
import '../utils/secure_messaging_service.dart';
import '../models/chat_model.dart';
import '../widgets/message_bubble.dart';
class MeshBackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..maskFilter = const MaskFilter.blur(BlurStyle.normal, 80);
// Warm accent for groups
paint.color = const Color(0xFFFFF3E0).withValues(alpha: 0.35);
canvas.drawCircle(Offset(size.width * 0.8, size.height * 0.1), 160, paint);
paint.color = const Color(0xFFF3E5F5).withValues(alpha: 0.35);
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.8), 200, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class GroupMessageModel {
int? id;
String body;
String? rawBody;
int date;
bool isMe;
MessageStatus status;
String? senderPhone;
String? senderName;
bool isSecure;
bool canRetryDecryption;
String? packetId;
String? packetMode;
bool isPendingMultipart;
GroupMessageModel({
this.id,
required this.body,
this.rawBody,
required this.date,
required this.isMe,
required this.status,
this.senderPhone,
this.senderName,
this.isSecure = false,
this.canRetryDecryption = false,
this.packetId,
this.packetMode,
this.isPendingMultipart = false,
});
}
class GroupChatScreen extends StatefulWidget {
final int groupId;
final String groupName;
const GroupChatScreen({
super.key,
required this.groupId,
required this.groupName,
});
@override
State<GroupChatScreen> createState() => _GroupChatScreenState();
}
class _GroupChatScreenState extends State<GroupChatScreen> {
final Telephony telephony = Telephony.instance;
static const platform = MethodChannel('com.example.saba/sim_cards');
final TextEditingController _msgController = TextEditingController();
final TextEditingController _keyController = TextEditingController();
final ScrollController _scrollController = ScrollController();
Color get primaryColor => Theme.of(context).primaryColor;
final Color backgroundColor = const Color(0xFFF5F7FA);
List<GroupMessageModel> messages = [];
List<Map<String, dynamic>> members = [];
List<Map<String, dynamic>> _simCards = [];
Map<String, dynamic>? _selectedSim;
StreamSubscription? _msgSub;
bool _loadingSims = true;
String currentGroupName = '';
@override
void initState() {
super.initState();
currentGroupName = widget.groupName;
SecureMessagingService.instance.currentChatPhone = 'group_${widget.groupId}';
DatabaseHelper.instance.markGroupAsRead(widget.groupId);
_fetchSimCards();
_loadGroupKeyAndData();
_scrollController.addListener(_onScroll);
_msgSub = SecureMessagingService.instance.messageStream.listen((data) {
if (data['phone'] == 'group_${widget.groupId}') {
if (!mounted) return;
final body = data['body'] as String? ?? '';
final packetId = data['packetId'] as String?;
final isMultipartComplete = data['isMultipartComplete'] as bool? ?? false;
final isMeEvent = data['isMe'] as bool? ?? false;
if (body == 'REFRESH') {
_loadData();
return;
}
if (packetId != null) {
setState(() {
final idx = messages.indexWhere((m) => m.packetId == packetId);
if (idx != -1) {
messages[idx].body = body;
messages[idx].isPendingMultipart = !isMultipartComplete;
if (isMultipartComplete) {
messages[idx].status = isMeEvent ? MessageStatus.sent : MessageStatus.received;
}
} else {
// If it's a new fragmented message or just completed but not in list
_loadData();
}
});
} else {
_loadData();
}
}
});
_keyController.addListener(() {
if (mounted) _onKeyChanged();
});
}
void _onScroll() {
if (!_scrollController.hasClients) return;
// Group chat might need pagination in the future,
// for now we just monitor the state to prevent jumps.
}
@override
void dispose() {
if (SecureMessagingService.instance.currentChatPhone == 'group_${widget.groupId}') {
SecureMessagingService.instance.currentChatPhone = null;
}
_msgSub?.cancel();
_msgController.dispose();
_keyController.dispose();
_scrollController.dispose();
super.dispose();
}
Future<void> _loadGroupKeyAndData() async {
final groups = await DatabaseHelper.instance.getGroups();
final group = groups.cast<Map<String, dynamic>?>().firstWhere(
(g) => g?['id'] == widget.groupId,
orElse: () => null,
);
if (group != null && (group['group_key'] as String?)?.isNotEmpty == true) {
_keyController.text = group['group_key'] as String;
}
await _loadData();
}
void _onKeyChanged() {
DatabaseHelper.instance.updateGroupKey(widget.groupId, _keyController.text);
_loadData();
}
bool _isSecurePlaceholderBody(String body) {
return body.startsWith('پیام امن گروهی') ||
body.startsWith('بازگشایی پیام امن گروهی') ||
body.contains(' ::PAYLOAD::');
}
String? _extractPayload(String body) {
const separator = ' ::PAYLOAD::';
final splitIndex = body.lastIndexOf(separator);
if (splitIndex == -1) return null;
final payload = body.substring(splitIndex + separator.length).trim();
return payload.isEmpty ? null : payload;
}
Future<void> _handleRetryDecryption(GroupMessageModel msg) async {
String? payload = _extractPayload(msg.body);
if (payload == null && msg.rawBody != null) {
payload = ProtocolHelper.parseMessage(msg.rawBody!)['payload'] as String?;
}
if (payload == null) return;
final keyController = TextEditingController(text: _keyController.text.trim());
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text('بازگشایی دستی پیام گروه'),
content: TextField(
controller: keyController,
decoration: const InputDecoration(
hintText: 'کلید گروه',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')),
ElevatedButton(
onPressed: () async {
if (keyController.text.trim().isEmpty) return;
final decrypted = await SecureMessagingService.instance.decryptWithKey(
payload!,
keyController.text.trim(),
);
if (!mounted) return;
Navigator.pop(ctx);
if (decrypted != null) {
_keyController.text = keyController.text.trim();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('پیام گروهی بازگشایی شد.')),
);
_loadData();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('کلید گروه نادرست است.')),
);
}
},
child: const Text('بازگشایی'),
),
],
),
);
}
Future<Map<String, dynamic>> _resolveGroupMessageState(String rawBody,
{bool isMe = false, bool dbIsSecure = false}) async {
final parsed = ProtocolHelper.parseMessage(rawBody);
final type = parsed['type'] as String;
final key = _keyController.text.trim();
String body = rawBody;
bool isSecure = dbIsSecure || type != 'plain' || rawBody.contains(' ::PAYLOAD::');
bool canRetryDecryption = false;
if (type == 'sym' && parsed['isGroup'] == true) {
final payload = parsed['payload'] as String?;
if (payload != null && key.isNotEmpty) {
final decrypted = await SecureMessagingService.instance.decryptWithKey(payload, key);
if (decrypted != null) {
body = decrypted;
} else {
final label = isMe ? 'ارسال شده' : 'دریافت شد';
body = 'بازگشایی پیام امن گروهی $label ناموفق بود. کلید گروه را بررسی کنید. ::PAYLOAD::$payload';
canRetryDecryption = true;
}
} else if (payload != null) {
final label = isMe ? 'ارسال شده' : 'دریافت شد';
body = 'پیام امن گروهی $label. برای مشاهده، کلید گروه را وارد کنید. ::PAYLOAD::$payload';
canRetryDecryption = true;
}
} else if (type == 'sfra' && parsed['isGroup'] == true) {
body = 'در حال دریافت قطعات... (${parsed['partNo']}/${parsed['totalParts']})';
isSecure = true;
} else if (rawBody.contains(' ::PAYLOAD::')) {
canRetryDecryption = true;
}
return {
'body': body,
'isSecure': isSecure,
'canRetryDecryption': canRetryDecryption,
'packetId': parsed['packetId'],
'packetMode': type == 'plain' ? null : (type == 'afrag' ? 'AE' : 'SYM'),
'isPendingMultipart': type == 'sfra' || type == 'afrag',
};
}
Future<void> _fetchSimCards() async {
if (!await Permission.phone.request().isGranted) {
if (mounted) setState(() => _loadingSims = false);
return;
}
try {
final List<dynamic> result = await platform.invokeMethod('getSimCards');
final cleanList = result.map((e) {
final rawMap = e as Map<Object?, Object?>;
return rawMap.map((key, value) => MapEntry(key.toString(), value));
}).toList();
if (!mounted) return;
setState(() {
_simCards = cleanList;
if (_simCards.isNotEmpty) _selectedSim = _simCards.first;
_loadingSims = false;
});
} catch (_) {
if (mounted) setState(() => _loadingSims = false);
}
}
bool _isLoading = false;
bool _needsReload = false;
Future<void> _loadData() async {
if (_isLoading) {
_needsReload = true;
return;
}
_isLoading = true;
_needsReload = false;
try {
final dbMessages = await DatabaseHelper.instance.getGroupMessages(widget.groupId);
final dbMembers = await DatabaseHelper.instance.getGroupMembers(widget.groupId);
final uiMessages = <GroupMessageModel>[];
for (final m in dbMessages) {
final raw = m['body'] as String;
final senderPhone = m['sender_phone'] as String?;
final senderName = senderPhone != null ? ContactHelper.getName(senderPhone) : 'من';
final dbIsSecure = (m['is_secure'] as int? ?? 0) == 1;
final resolved = await _resolveGroupMessageState(raw, isMe: senderPhone == null, dbIsSecure: dbIsSecure);
uiMessages.add(GroupMessageModel(
id: m['id'] as int?,
body: resolved['body'] as String,
rawBody: raw,
date: m['date'] as int,
isMe: senderPhone == null,
status: MessageStatus.received,
senderPhone: senderPhone,
senderName: senderName,
isSecure: resolved['isSecure'] as bool,
canRetryDecryption:
resolved['canRetryDecryption'] as bool || _isSecurePlaceholderBody(resolved['body'] as String),
packetId: resolved['packetId'] as String?,
packetMode: resolved['packetMode'] as String?,
isPendingMultipart: resolved['isPendingMultipart'] as bool? ?? false,
));
}
if (mounted) {
setState(() {
messages = uiMessages;
members = dbMembers;
});
}
} finally {
_isLoading = false;
if (_needsReload) _loadData();
}
}
void _deleteMessage(int messageId) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text('حذف پیام'),
content: const Text('آیا مطمئن هستید؟'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
onPressed: () async {
Navigator.pop(ctx);
await DatabaseHelper.instance.deleteGroupMessage(messageId);
_loadData();
},
child: const Text('حذف'),
),
],
),
);
}
void _editGroupName() {
final nameController = TextEditingController(text: currentGroupName);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text('تغییر نام گروه'),
content: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'نام جدید',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: primaryColor, foregroundColor: Colors.white),
onPressed: () async {
if (nameController.text.trim().isEmpty) return;
await DatabaseHelper.instance.updateGroupName(widget.groupId, nameController.text.trim());
if (!mounted) return;
setState(() => currentGroupName = nameController.text.trim());
Navigator.pop(ctx);
},
child: const Text('ذخیره'),
),
],
),
);
}
void _showGroupInfo() {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setStateDialog) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('مدیریت اعضا', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
IconButton(
icon: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.person_add, color: primaryColor, size: 20),
),
onPressed: () {
Navigator.pop(context);
_addNewMember();
},
),
],
),
content: SizedBox(
width: double.maxFinite,
child: members.isEmpty
? const Center(child: Text('هیچ عضوی وجود ندارد', style: TextStyle(color: Colors.grey)))
: ListView.separated(
shrinkWrap: true,
itemCount: members.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (_, index) {
final member = members[index];
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
backgroundColor: Colors.orange.shade100,
child: Text(
(member['name'] as String)[0],
style: TextStyle(color: Colors.orange.shade900),
),
),
title: Text(
member['name'] as String,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(member['phone'] as String),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () async {
await DatabaseHelper.instance.removeGroupMember(
widget.groupId,
member['phone'] as String,
);
final newMembers = await DatabaseHelper.instance.getGroupMembers(widget.groupId);
if (!mounted) return;
setState(() => members = newMembers);
setStateDialog(() {});
},
),
);
},
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('بستن')),
],
);
},
);
},
);
}
Future<void> _addNewMember() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
final contacts = await ContactHelper.getContactsLight();
if (mounted) Navigator.pop(context);
if (contacts.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('مخاطبی یافت نشد')));
}
return;
}
if (!mounted) return;
final result = await showSearch<Map<String, String>?>(
context: context,
delegate: ContactsSearchDelegate(contacts),
);
if (result == null) return;
final added = await DatabaseHelper.instance.addMemberToGroup(
widget.groupId,
result['name']!,
result['phone']!,
);
if (!mounted) return;
if (added) {
await _loadData();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('عضو جدید اضافه شد')));
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('این عضو قبلاً در گروه وجود دارد')),
);
}
}
Future<void> _sendGroupMessage() async {
final text = _msgController.text.trim();
final key = _keyController.text.trim();
if (text.isEmpty) return;
String? rawBody;
if (key.isNotEmpty) {
final crypto = SecureCryptoHelper();
rawBody = '@G:SYM|${await crypto.encryptSymmetric(text, key)}';
}
final tempMsg = GroupMessageModel(
body: text,
rawBody: rawBody,
date: DateTime.now().millisecondsSinceEpoch,
isMe: true,
status: MessageStatus.sending,
senderName: 'من',
isSecure: key.isNotEmpty,
);
setState(() {
messages.insert(0, tempMsg);
_msgController.clear();
});
try {
final groupMemberPhones = members.map((m) => m['phone'] as String).toList();
await SecureMessagingService.instance.sendGroupSecureMessage(
widget.groupId,
groupMemberPhones,
text,
key,
);
if (!mounted) return;
setState(() => tempMsg.status = MessageStatus.sent);
Future.delayed(const Duration(milliseconds: 1200), _loadData);
} catch (_) {
if (!mounted) return;
setState(() => tempMsg.status = MessageStatus.failed);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.darkBg,
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight + 8),
child: AppTheme.glassWrapper(
radius: 0,
sigma: 18,
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: InkWell(
onTap: _editGroupName,
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Hero(
tag: 'avatar_${currentGroupName}_group',
child: Container(
width: 38,
height: 38,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Colors.orange, Colors.orangeAccent],
),
),
child: const Center(
child: Icon(Icons.groups, color: Colors.white, size: 22),
),
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(currentGroupName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
Text('${members.length} عضو', style: const TextStyle(fontSize: 11, color: Colors.white70)),
],
),
const SizedBox(width: 6),
const Icon(Icons.edit_outlined, size: 14, color: Colors.white70),
],
),
),
actions: [
IconButton(
icon: const Icon(Icons.group_outlined, color: Colors.white),
tooltip: 'مدیریت اعضا',
onPressed: _showGroupInfo,
),
],
),
),
),
body: Stack(
children: [
Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())),
Column(
children: [
SizedBox(height: MediaQuery.of(context).padding.top + kToolbarHeight + 8),
Expanded(
child: messages.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.mark_chat_unread_outlined, size: 60, color: Colors.grey[300]),
const SizedBox(height: 10),
Text('پیامی ارسال نشده است', style: TextStyle(color: Colors.grey[500])),
],
),
)
: ListView.builder(
key: const PageStorageKey('group_chat_list'),
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return GestureDetector(
onLongPress: () {
if (msg.id != null) {
_deleteMessage(msg.id!);
}
},
child: Column(
crossAxisAlignment: msg.isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
if (!msg.isMe && msg.senderName != null)
Padding(
padding: const EdgeInsets.only(left: 12, bottom: 4),
child: Text(
msg.senderName!,
style: const TextStyle(
fontSize: 10,
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
),
MessageBubble(
body: msg.body,
rawBody: msg.rawBody,
date: msg.date,
isMe: msg.isMe,
status: msg.status,
isSecure: msg.isSecure,
canRetryDecryption: msg.canRetryDecryption,
onRetryDecryption: () => _handleRetryDecryption(msg),
packetMode: msg.packetMode,
isPendingMultipart: msg.isPendingMultipart,
),
],
),
);
},
),
),
AppTheme.glassWrapper(
radius: 0,
sigma: 15,
child: Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
border: Border(
top: BorderSide(color: Colors.white.withValues(alpha: 0.08), width: 0.5),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!_loadingSims && _simCards.length > 1)
SizedOverflowBox(
size: const Size.fromHeight(35),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _simCards.length,
itemBuilder: (context, index) {
final sim = _simCards[index];
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(sim['displayName']?.toString() ?? 'SIM ${index + 1}'),
selected: _selectedSim == sim,
onSelected: (_) => setState(() => _selectedSim = sim),
),
);
},
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _keyController,
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: InputDecoration(
hintText: 'کلید امنیتی گروه (اختیاری)',
hintStyle: const TextStyle(color: Colors.white30),
prefixIcon: const Icon(Icons.vpn_key_rounded, size: 18, color: Colors.orangeAccent),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _msgController,
minLines: 1,
maxLines: 4,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'پیام گروهی...',
hintStyle: const TextStyle(color: Colors.white30),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
),
),
),
const SizedBox(width: 12),
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Colors.indigo, Colors.indigoAccent],
),
boxShadow: [
BoxShadow(
color: Colors.indigo.withValues(alpha: 0.3),
blurRadius: 12,
spreadRadius: 1,
),
],
),
child: IconButton(
icon: const Icon(Icons.send_rounded, color: Colors.white, size: 22),
onPressed: _sendGroupMessage,
),
),
],
),
],
),
),
),
],
),
],
),
);
}
}
class ContactsSearchDelegate extends SearchDelegate<Map<String, String>?> {
final List<Contact> contacts;
ContactsSearchDelegate(this.contacts);
@override
String? get searchFieldLabel => 'جست‌وجو نام مخاطب...';
@override
List<Widget>? buildActions(BuildContext context) {
return [
if (query.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => query = '',
),
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, null),
);
}
@override
Widget buildResults(BuildContext context) => _buildList(context);
@override
Widget buildSuggestions(BuildContext context) => _buildList(context);
Widget _buildList(BuildContext context) {
final filtered = query.isEmpty
? contacts
: contacts.where((c) => c.displayName.toLowerCase().contains(query.toLowerCase())).toList();
if (filtered.isEmpty) {
return const Center(child: Text('نتیجه‌ای یافت نشد.'));
}
return ListView.builder(
itemCount: filtered.length,
itemBuilder: (context, index) {
final contact = filtered[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.indigo.shade100,
child: Text(
contact.displayName.isNotEmpty ? contact.displayName[0] : '?',
style: TextStyle(color: Colors.indigo.shade900),
),
),
title: Text(contact.displayName),
onTap: () => _onContactSelect(context, contact.id),
);
},
);
}
Future<void> _onContactSelect(BuildContext context, String contactId) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
try {
final fullContact = await ContactHelper.getFullContact(contactId);
if (context.mounted) Navigator.pop(context);
if (fullContact == null || fullContact.phones.isEmpty) return;
if (fullContact.phones.length == 1) {
close(context, {
'name': fullContact.displayName,
'phone': fullContact.phones.first.number,
});
} else {
_showPhoneSelection(context, fullContact);
}
} catch (_) {
if (context.mounted) Navigator.pop(context);
}
}
void _showPhoneSelection(BuildContext context, Contact contact) {
showDialog(
context: context,
builder: (_) => SimpleDialog(
title: Text(contact.displayName),
children: contact.phones
.map(
(p) => SimpleDialogOption(
onPressed: () {
Navigator.pop(context);
close(context, {
'name': contact.displayName,
'phone': p.number,
});
},
child: Text(p.number),
),
)
.toList(),
),
);
}
}

View File

@ -0,0 +1,771 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:another_telephony/telephony.dart';
import '../utils/contact_helper.dart';
import '../utils/database_helper.dart';
import 'chat_screen.dart';
import 'group_chat_screen.dart';
import 'compose_screen.dart';
import 'settings_screen.dart';
import '../utils/secure_messaging_service.dart';
import '../utils/protocol_helper.dart';
import '../utils/notification_helper.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../utils/app_theme.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Map<String, dynamic>> conversations = [];
List<Map<String, dynamic>> groups = [];
List<Contact> contacts = [];
final TextEditingController _contactSearchController =
TextEditingController();
bool isLoading = true;
bool isLoadingContacts = false;
final Telephony telephony = Telephony.instance;
Map<String, int> unreadCounts = {};
Map<int, int> groupUnreadCounts = {};
// رنگهای تم حرفهای
Color get primaryColor => Theme.of(context).primaryColor;
Color get secondaryColor => Theme.of(context).colorScheme.secondary;
final Color backgroundColor = AppTheme.darkBg;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (mounted) setState(() {});
if (_tabController.index == 2 && contacts.isEmpty) {
_loadContacts();
}
});
// Delay heavy initialization until transition finishes (800ms)
// This makes the logo transition buttery smooth (60fps)
Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) initApp();
});
_initMessageStreamListener();
}
StreamSubscription? _messageSubscription;
StreamSubscription? _notificationSubscription;
void _initMessageStreamListener() {
_messageSubscription =
SecureMessagingService.instance.messageStream.listen((data) {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) loadData();
});
});
_notificationSubscription =
NotificationHelper.instance.notificationStream.listen((payload) {
if (!mounted) return;
if (payload.startsWith('group_')) {
final groupId = int.tryParse(payload.replaceFirst('group_', ''));
if (groupId != null) {
// Find group name
String name = "گروه";
for (var g in groups) {
if (g['id'] == groupId) {
name = g['name'];
break;
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
GroupChatScreen(groupId: groupId, groupName: name),
),
).then((_) => loadData());
}
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(address: payload),
),
).then((_) => loadData());
}
});
}
@override
void dispose() {
_tabController.dispose();
_contactSearchController.dispose();
_messageSubscription?.cancel();
_notificationSubscription?.cancel();
super.dispose();
}
void initApp() {
loadMessages();
// همگامسازی نام مخاطبین در پسزمینه
Future.delayed(const Duration(seconds: 1), () async {
// ابتدا از کش محلی لود میکنیم برای سرعت
await ContactHelper.loadFromLocalCache();
if (mounted) setState(() {});
// سپس با دستگاه سینک میکنیم برای آپدیت نامهای جدید
await ContactHelper.syncWithDevice();
if (mounted) setState(() {});
});
}
void loadData() async {
setState(() => isLoading = true);
await loadMessages();
final g = await DatabaseHelper.instance.getGroups();
final counts = <int, int>{};
for (final group in g) {
counts[group['id']] =
await DatabaseHelper.instance.getGroupUnreadCount(group['id']);
}
if (_tabController.index == 2 || contacts.isNotEmpty) {
await _loadContacts();
}
if (mounted) {
setState(() {
groups = g;
groupUnreadCounts = counts;
isLoading = false;
});
}
}
Future<void> _loadContacts() async {
if (isLoadingContacts) return;
setState(() => isLoadingContacts = true);
try {
final c = await ContactHelper.getContactsLight();
if (mounted) {
setState(() {
contacts = c;
isLoadingContacts = false;
});
}
} catch (e) {
if (mounted) setState(() => isLoadingContacts = false);
}
}
Future<void> _startChatFromContact(Contact contact) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
try {
final full = await ContactHelper.getFullContact(contact.id);
if (mounted) Navigator.pop(context);
if (full == null || full.phones.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("این مخاطب شماره تلفن ندارد")),
);
}
return;
}
String phone = full.phones.first.number;
if (full.phones.length > 1) {
if (!mounted) return;
final selected = await showDialog<String>(
context: context,
builder: (ctx) => SimpleDialog(
title: Text("انتخاب شماره ${full.displayName}"),
children: full.phones
.map((p) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, p.number),
child: Text(p.number),
))
.toList(),
),
);
if (selected == null) return;
phone = selected;
}
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(address: phone),
),
).then((_) => loadData());
}
} catch (e) {
if (mounted) Navigator.pop(context);
}
}
Future<void> loadMessages() async {
try {
// Fetch from local cache for instant updates
final localRows = await DatabaseHelper.instance.getConversations();
final installDate =
await SecureMessagingService.instance.getInstallDate();
// Filter by install date
List<Map<String, dynamic>> filtered = localRows.where((m) {
if (installDate == null) return true;
final date = m['date'] as int? ?? 0;
return date >= installDate;
}).toList();
// Fetch unread counts
final counts = <String, int>{};
for (final msg in filtered) {
final addr = msg['address'] as String?;
if (addr != null) {
counts[addr] = await DatabaseHelper.instance.getUnreadCount(addr);
}
}
if (mounted) {
setState(() {
conversations = filtered;
unreadCounts = counts;
isLoading = false;
});
}
} catch (e) {
debugPrint("[SABA] Error in local loadMessages: $e");
if (mounted) setState(() => isLoading = false);
}
}
void _deleteGroup(int groupId) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text("حذف گروه"),
content: const Text("آیا از حذف این گروه مطمئنید؟"),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx), child: const Text("لغو")),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, foregroundColor: Colors.white),
onPressed: () async {
Navigator.pop(ctx);
await DatabaseHelper.instance.deleteGroup(groupId);
if (!mounted) return;
loadData();
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("گروه حذف شد")));
},
child: const Text("حذف"),
),
],
),
);
}
void _deleteIndividualChat(String address) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text("حذف گفتگو"),
content: const Text(
"به دلیل محدودیت‌های اندروید، حذف پیامک‌های سیستمی فقط توسط برنامه پیش‌فرض پیامک امکان‌پذیر است."),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx), child: const Text("باشه")),
],
),
);
}
Future<void> _addContactToDevice(String phone) async {
if (!await FlutterContacts.requestPermission()) return;
try {
final contact = Contact(phones: [Phone(phone)]);
await FlutterContacts.openExternalInsert(contact);
if (!mounted) return;
loadData(); // همگامسازی مجدد بعد از افزودن
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("خطا در افزودن مخاطب: $e")),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(85),
child: AppTheme.glassWrapper(
radius: 0,
sigma: 18,
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Padding(
padding: const EdgeInsets.only(top: 15),
child: Hero(
tag: 'app_logo',
child: GestureDetector(
onTap: () {
if (mounted) setState(() {});
},
child: const Image(
image: AssetImage('صبا بالا.png'),
height: 42,
fit: BoxFit.contain,
),
),
),
),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined,
color: Colors.white, size: 22),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SettingsScreen()),
).then((_) => loadData()),
),
IconButton(
icon: const Icon(Icons.refresh_rounded,
color: Colors.white, size: 22),
onPressed: loadData,
),
const SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Container(
color: Colors.transparent, // Let glass show through
child: TabBar(
controller: _tabController,
labelColor: primaryColor,
unselectedLabelColor: Colors.grey,
indicatorColor: primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
dividerColor: Colors.transparent,
tabs: const [
Tab(text: "گفتگوها"),
Tab(text: "گروه‌ها"),
Tab(text: "مخاطبین"),
],
),
),
),
),
),
),
body: TabBarView(
controller: _tabController,
children: [
// --- لیست گفتگوها ---
isLoading
? Center(child: CircularProgressIndicator(color: primaryColor))
: conversations.isEmpty
? _buildEmptyState("هیچ گفتگویی وجود ندارد")
: ListView.builder(
padding: const EdgeInsets.only(top: 10, bottom: 80),
itemCount: conversations.length,
itemBuilder: (context, index) {
final msg = conversations[index];
final addr = msg['address'] as String? ?? "";
final displayName = ContactHelper.getName(addr);
final body = msg['body'] as String? ?? "";
final parsed = ProtocolHelper.parseMessage(body);
final isSecure = parsed['type'] != 'plain';
final displayText =
isSecure ? "🔒 پیام امن (رمزگذاری شده)" : body;
return _buildChatCard(
index: index,
title: displayName,
subtitle: displayText,
isEncrypted: isSecure,
unreadCount: unreadCounts[addr] ?? 0,
avatarText:
displayName.isNotEmpty ? displayName[0] : "?",
colorSeed: index,
showAddButton: ContactHelper.isRawNumber(displayName),
onAddContact: () => _addContactToDevice(addr),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ChatScreen(address: addr)))
.then((_) => loadData()),
onLongPress: () => _deleteIndividualChat(addr),
);
},
),
// --- لیست گروهها ---
groups.isEmpty
? _buildEmptyState("گروهی ساخته نشده است")
: ListView.builder(
padding: const EdgeInsets.only(top: 10, bottom: 80),
itemCount: groups.length,
itemBuilder: (context, index) {
final group = groups[index];
return _buildChatCard(
index: index,
title: group['name'],
subtitle: "پیام گروهی",
isEncrypted: false,
isGroup: true,
unreadCount: groupUnreadCounts[group['id']] ?? 0,
avatarText: "#",
colorSeed: index + 5,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GroupChatScreen(
groupId: group['id'],
groupName: group['name']))),
onLongPress: () => _deleteGroup(group['id']),
);
},
),
// --- لیست مخاطبین ---
Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: AppTheme.glassWrapper(
radius: 12,
sigma: 5,
child: TextField(
controller: _contactSearchController,
onChanged: (val) => setState(() {}),
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: "جستجو در مخاطبین...",
hintStyle: const TextStyle(color: Colors.white60),
prefixIcon:
const Icon(Icons.search, color: Colors.white70),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
),
),
Expanded(
child: isLoadingContacts && contacts.isEmpty
? Center(
child: CircularProgressIndicator(color: primaryColor))
: contacts.isEmpty
? _buildEmptyState("مخاطبی یافت نشد")
: Builder(builder: (context) {
final query = _contactSearchController.text
.trim()
.toLowerCase();
final filtered = query.isEmpty
? contacts
: contacts.where((c) {
final name = c.displayName.toLowerCase();
return name.contains(query);
}).toList();
if (filtered.isEmpty) {
return _buildEmptyState("نتیجه‌ای یافت نشد");
}
return ListView.builder(
padding: const EdgeInsets.only(bottom: 80),
itemCount: filtered.length,
itemBuilder: (context, index) {
final contact = filtered[index];
final displayName = contact.displayName;
return _buildChatCard(
index: index,
title: displayName,
subtitle: "شروع گفتگوی جدید",
isEncrypted: false,
avatarText: displayName.isNotEmpty
? displayName[0]
: "?",
colorSeed: index,
onTap: () => _startChatFromContact(contact),
onLongPress: () {},
);
},
);
}),
),
],
),
],
),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: primaryColor,
icon: Icon(_tabController.index == 0 ? Icons.edit : Icons.group_add,
color: Colors.white),
label: Text(_tabController.index == 0 ? "پیام جدید" : "گروه جدید",
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
onPressed: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const ComposeScreen()))
.then((res) {
if (res == true) loadData();
}),
),
);
}
Widget _buildEmptyState(String text) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_bubble_outline, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(text, style: TextStyle(color: Colors.grey[600], fontSize: 18)),
],
),
);
}
Widget _buildChatCard({
required int index,
required String title,
required String subtitle,
required bool isEncrypted,
required String avatarText,
required int colorSeed,
int unreadCount = 0,
bool isGroup = false,
bool showAddButton = false,
VoidCallback? onAddContact,
required VoidCallback onTap,
required VoidCallback onLongPress,
}) {
final List<Color> avatarColors = [
Colors.blueAccent,
Colors.teal,
Colors.deepPurple,
Colors.indigo,
Colors.orangeAccent,
Colors.pinkAccent,
Colors.cyan
];
final avatarBg =
isGroup ? Colors.orange : avatarColors[colorSeed % avatarColors.length];
// Slide and Fade animation for that premium staggered feel
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 450),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutQuart,
// Delay based on index for the staggered effect
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(30 * (1 - value), 0),
child: child,
),
);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.darkCard,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Colors.white.withValues(alpha: 0.05), width: 0.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.4),
spreadRadius: 1,
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(18),
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: onTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.all(14.0),
child: Row(
children: [
Hero(
tag: 'avatar_${title}_${isGroup ? 'group' : 'ind'}',
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
avatarBg,
avatarBg.withValues(alpha: 0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: avatarBg.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: isGroup
? const Icon(Icons.groups,
color: Colors.white, size: 28)
: Text(
avatarText,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold),
),
),
),
if (unreadCount > 0)
Positioned(
top: -4,
left: -4,
child: Container(
constraints: const BoxConstraints(
minWidth: 22,
minHeight: 22,
),
padding:
const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: const Color(0xFFFF5A5F),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.darkCard,
width: 2,
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFF5A5F)
.withValues(alpha: 0.35),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Center(
child: Text(
unreadCount > 99
? '99+'
: unreadCount.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w800,
),
textAlign: TextAlign.center,
),
),
),
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: unreadCount > 0
? FontWeight.w900
: FontWeight.bold,
fontSize: 17,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textDirection: TextDirection.ltr,
textAlign: TextAlign.right,
),
const SizedBox(height: 4),
Row(
children: [
if (isEncrypted)
Padding(
padding: const EdgeInsets.only(left: 4),
child: Icon(Icons.lock,
size: 14, color: primaryColor),
),
Expanded(
child: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unreadCount > 0
? Colors.white.withValues(alpha: 0.9)
: Colors.white.withValues(alpha: 0.6),
fontSize: 13,
fontWeight: unreadCount > 0
? FontWeight.w600
: FontWeight.w400,
),
),
),
],
),
],
),
),
if (showAddButton)
IconButton(
icon:
Icon(Icons.person_add_outlined, color: primaryColor),
onPressed: onAddContact,
tooltip: "افزودن به مخاطبین",
),
Icon(Icons.chevron_right, color: Colors.grey[300]),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,803 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../main.dart';
import '../utils/app_lock_service.dart';
import '../utils/contact_helper.dart';
import '../utils/secure_messaging_service.dart';
import '../utils/app_theme.dart';
import 'splash_screen.dart';
enum _AppLockDialogMode { enable, change, disable }
String? _validateAppLockPasscode(String value) {
final normalized = value.trim();
if (normalized.isEmpty) {
return 'رمز را وارد کنید.';
}
if (!RegExp(r'^\d{4,8}$').hasMatch(normalized)) {
return 'رمز باید ۴ تا ۸ رقم باشد.';
}
return null;
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _isResetting = false;
static const _platform =
MethodChannel('com.example.saba_secure_sms/sms_role');
bool _isCurrentlyDefault = false;
bool _isAppLockEnabled = false;
bool _isAppLockLoading = true;
@override
void initState() {
super.initState();
_checkDefaultStatus();
_loadAppLockStatus();
}
Future<void> _checkDefaultStatus() async {
try {
final bool isDefault = await _platform.invokeMethod('isDefaultSmsApp');
if (mounted) setState(() => _isCurrentlyDefault = isDefault);
} catch (_) {}
}
Future<void> _changeDefaultApp() async {
try {
if (_isCurrentlyDefault) {
// If already default, open settings to allow unsetting
await _platform.invokeMethod('openDefaultAppsSettings');
} else {
// If not default, request it
await _platform.invokeMethod('requestDefaultSmsApp');
}
// Re-check after returning
_checkDefaultStatus();
} catch (_) {}
}
Future<void> _confirmReset() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('ریست کامل برنامه'),
content: const Text(
'با این کار تمام کلیدها، گروه‌ها، پیام‌های ذخیره‌شده، کش بازگشایی و تنظیمات امنیتی حذف می‌شوند و برنامه مثل نصب تازه می‌شود. ادامه می‌دهید؟',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('لغو'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text('ریست کامل'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _isResetting = true);
await SecureMessagingService.instance.resetForFreshInstall();
await AppLockService.instance.clear();
ContactHelper.clearCache();
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const SplashScreen()),
(route) => false,
);
}
Future<void> _loadAppLockStatus() async {
await AppLockService.instance.init();
if (!mounted) return;
setState(() {
_isAppLockEnabled = AppLockService.instance.isEnabled;
_isAppLockLoading = false;
});
}
Future<void> _showEnableAppLockDialog() async {
final enabled = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.enable),
);
if (enabled == true) {
await _loadAppLockStatus();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه با موفقیت فعال شد.'),
),
);
}
}
Future<void> _showChangeAppLockDialog() async {
final changed = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.change),
);
if (changed == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه با موفقیت تغییر کرد.'),
),
);
}
}
Future<void> _showDisableAppLockDialog() async {
final disabled = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.disable),
);
if (disabled == true) {
await _loadAppLockStatus();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه غیرفعال شد.'),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.darkBg,
appBar: AppBar(
title: const Text('تنظیمات'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())),
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSettingSection(
icon: Icons.sms_rounded,
iconColor: Colors.blueAccent,
title: 'برنامه پیش‌فرض پیامک',
content: _isCurrentlyDefault
? 'صبا در حال حاضر برنامه پیش‌فرض پیامک گوشی شماست.'
: 'صبا برنامه پیش‌فرض نیست. برای استفاده از تمام امکانات امنیتی، صبا را به عنوان گزینه پیش‌فرض انتخاب کنید.',
action: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _changeDefaultApp,
style: OutlinedButton.styleFrom(
foregroundColor: _isCurrentlyDefault
? Colors.white70
: Colors.blueAccent,
side: BorderSide(
color: _isCurrentlyDefault
? Colors.white24
: Colors.blueAccent),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
icon: Icon(_isCurrentlyDefault
? Icons.settings_applications
: Icons.check_circle_outline),
label: Text(_isCurrentlyDefault
? 'تغییر در تنظیمات سیستمی'
: 'انتخاب به عنوان پیش‌فرض'),
),
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.palette_rounded,
iconColor: Colors.purpleAccent,
title: 'رنگ‌بندی برنامه',
content:
'رنگ مورد علاقه‌ی خود را برای محیط برنامه انتخاب کنید:',
action: SizedBox(
height: 55,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_buildColorItem(const Color(0xFF7000FF), 'بنفش'),
_buildColorItem(const Color(0xFF00D2FF), 'آبی'),
_buildColorItem(const Color(0xFF00FFD1), 'فیروزه‌ای'),
_buildColorItem(const Color(0xFF4CAF50), 'سبز'),
_buildColorItem(const Color(0xFFFF9800), 'نارنجی'),
_buildColorItem(const Color(0xFFE91E63), 'صورتی'),
_buildColorItem(const Color(0xFFF44336), 'قرمز'),
],
),
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.lock_outline_rounded,
iconColor: Colors.amberAccent,
title: 'قفل برنامه',
content: _isAppLockEnabled
? 'رمز برنامه فعال است. از این به بعد، هر بار ورود یا بازگشت به اپ نیاز به وارد کردن رمز خواهد داشت.'
: 'اگر این گزینه را فعال کنید، بدون وارد کردن رمز هیچ‌کس نمی‌تواند وارد برنامه شود.',
action: _isAppLockLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _isAppLockEnabled
? Colors.greenAccent
.withValues(alpha: 0.25)
: Colors.white12,
),
),
child: Row(
children: [
Icon(
_isAppLockEnabled
? Icons.verified_user_rounded
: Icons.lock_open_rounded,
color: _isAppLockEnabled
? Colors.greenAccent
: Colors.white60,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_isAppLockEnabled
? 'رمز برنامه فعال است'
: 'رمز برنامه غیرفعال است',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isAppLockEnabled
? _showChangeAppLockDialog
: _showEnableAppLockDialog,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: Icon(
_isAppLockEnabled
? Icons.password_rounded
: Icons.lock_rounded,
),
label: Text(
_isAppLockEnabled
? 'تغییر رمز برنامه'
: 'فعال‌سازی رمز برنامه',
),
),
),
if (_isAppLockEnabled) ...[
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _showDisableAppLockDialog,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(
color: Colors.redAccent,
),
padding: const EdgeInsets.symmetric(
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.lock_reset_rounded),
label:
const Text('غیرفعال‌سازی رمز برنامه'),
),
),
],
],
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.security_rounded,
iconColor: Colors.cyanAccent,
title: 'حریم خصوصی و استتار',
content:
'با فعال‌سازی این گزینه، آیکون و نام برنامه در لیست برنامه‌های گوشی به "ماشین حساب" تغییر می‌کند تا امنیت شما حفظ شود.',
action: FutureBuilder<bool>(
future: _platform
.invokeMethod<bool>('getStealthMode')
.then((v) => v ?? false),
builder: (context, snapshot) {
final isStealth = snapshot.data ?? false;
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
),
child: SwitchListTile(
title: const Text('حالت استتار (ماشین حساب)',
style: TextStyle(
fontSize: 14, color: Colors.white)),
subtitle: Text(
isStealth
? 'فعال (برنامه مخفی است)'
: 'غیرفعال',
style: const TextStyle(
fontSize: 12, color: Colors.white60)),
value: isStealth,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
secondary: const Icon(Icons.calculate_outlined,
color: Colors.white70),
activeThumbColor: Colors.cyanAccent,
onChanged: (bool value) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.darkCard,
title: Text(
value
? 'فعال‌سازی حالت مخفی'
: 'خروج از حالت مخفی',
style:
const TextStyle(color: Colors.white)),
content: Text(
value
? 'با تایید این گزینه، برنامه بسته شده و آیکون آن به ماشین حساب تغییر می‌کند.'
: 'با تایید این گزینه، آیکون اصلی صبا باز می‌گردد.',
style: const TextStyle(
color: Colors.white70)),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(ctx, false),
child: const Text('لغو')),
ElevatedButton(
onPressed: () =>
Navigator.pop(ctx, true),
child: const Text('تایید')),
],
),
);
if (confirmed == true) {
await _platform.invokeMethod(
'setStealthMode', {'enabled': value});
}
},
),
);
},
),
),
const SizedBox(height: 32),
Center(
child: TextButton.icon(
onPressed: _isResetting ? null : _confirmReset,
style: TextButton.styleFrom(
foregroundColor:
Colors.redAccent.withValues(alpha: 0.8)),
icon: const Icon(Icons.dangerous_outlined),
label: const Text('ریست کامل و حذف تمام داده‌ها',
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 40),
],
),
),
),
],
),
);
}
Widget _buildSettingSection({
required IconData icon,
required Color iconColor,
required String title,
required String content,
required Widget action,
}) {
return AppTheme.glassWrapper(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white),
),
],
),
const SizedBox(height: 12),
Text(
content,
style: const TextStyle(
height: 1.5, color: Colors.white70, fontSize: 13),
),
const SizedBox(height: 20),
action,
],
),
),
);
}
Widget _buildColorItem(Color color, String label) {
final bool isSelected = themeNotifier.value.toARGB32() == color.toARGB32();
return GestureDetector(
onTap: () async {
themeNotifier.value = color;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('primary_color_value', color.toARGB32());
setState(() {}); // Refresh local UI for selection check
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.4),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 20)
: null,
),
),
);
}
}
class _AppLockDialog extends StatefulWidget {
const _AppLockDialog({required this.mode});
final _AppLockDialogMode mode;
@override
State<_AppLockDialog> createState() => _AppLockDialogState();
}
class _AppLockDialogState extends State<_AppLockDialog> {
final TextEditingController _currentController = TextEditingController();
final TextEditingController _nextController = TextEditingController();
final TextEditingController _confirmController = TextEditingController();
bool _isSubmitting = false;
String? _errorText;
bool get _isEnableMode => widget.mode == _AppLockDialogMode.enable;
bool get _isChangeMode => widget.mode == _AppLockDialogMode.change;
bool get _isDisableMode => widget.mode == _AppLockDialogMode.disable;
@override
void dispose() {
_currentController.dispose();
_nextController.dispose();
_confirmController.dispose();
super.dispose();
}
Future<void> _close([bool result = false]) async {
FocusManager.instance.primaryFocus?.unfocus();
await Future<void>.delayed(const Duration(milliseconds: 40));
if (!mounted) return;
Navigator.of(context).pop(result);
}
Future<void> _submit() async {
if (_isSubmitting) return;
if (_isEnableMode) {
final nextError = _validateAppLockPasscode(_nextController.text);
if (nextError != null) {
setState(() => _errorText = nextError);
return;
}
if (_nextController.text.trim() != _confirmController.text.trim()) {
setState(() => _errorText = 'تکرار رمز با رمز اصلی یکسان نیست.');
return;
}
}
if (_isChangeMode) {
final currentError = _validateAppLockPasscode(_currentController.text);
final nextError = _validateAppLockPasscode(_nextController.text);
if (currentError != null) {
setState(() => _errorText = 'رمز فعلی معتبر نیست.');
return;
}
if (nextError != null) {
setState(() => _errorText = nextError);
return;
}
if (_nextController.text.trim() != _confirmController.text.trim()) {
setState(() => _errorText = 'تکرار رمز جدید با رمز جدید یکسان نیست.');
return;
}
}
if (_isDisableMode) {
final currentError = _validateAppLockPasscode(_currentController.text);
if (currentError != null) {
setState(() => _errorText = 'رمز فعلی را درست وارد کنید.');
return;
}
}
setState(() {
_isSubmitting = true;
_errorText = null;
});
if (_isEnableMode) {
await AppLockService.instance.setPasscode(_nextController.text.trim());
await _close(true);
return;
}
if (_isChangeMode) {
final updated = await AppLockService.instance.changePasscode(
currentPasscode: _currentController.text.trim(),
newPasscode: _nextController.text.trim(),
);
if (!mounted) return;
if (!updated) {
setState(() {
_isSubmitting = false;
_errorText = 'رمز فعلی درست نیست.';
});
return;
}
await _close(true);
return;
}
final removed = await AppLockService.instance
.disablePasscode(_currentController.text.trim());
if (!mounted) return;
if (!removed) {
setState(() {
_isSubmitting = false;
_errorText = 'رمز فعلی درست نیست.';
});
return;
}
await _close(true);
}
String get _title {
switch (widget.mode) {
case _AppLockDialogMode.enable:
return 'فعال‌سازی رمز برنامه';
case _AppLockDialogMode.change:
return 'تغییر رمز برنامه';
case _AppLockDialogMode.disable:
return 'غیرفعال‌سازی رمز برنامه';
}
}
String get _primaryActionLabel {
switch (widget.mode) {
case _AppLockDialogMode.enable:
return _isSubmitting ? 'در حال ذخیره...' : 'فعال‌سازی';
case _AppLockDialogMode.change:
return _isSubmitting ? 'در حال ذخیره...' : 'ذخیره تغییرات';
case _AppLockDialogMode.disable:
return _isSubmitting ? 'در حال بررسی...' : 'حذف رمز';
}
}
Widget _buildPasscodeField({
required TextEditingController controller,
required String label,
ValueChanged<String>? onSubmitted,
}) {
return TextField(
controller: controller,
keyboardType: TextInputType.number,
obscureText: true,
obscuringCharacter: '',
textInputAction: TextInputAction.done,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
onSubmitted: onSubmitted,
decoration: InputDecoration(
labelText: label,
hintText: '۴ تا ۸ رقم',
filled: true,
fillColor: Colors.white.withValues(alpha: 0.06),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
),
),
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppTheme.darkCard,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
_title,
style: const TextStyle(color: Colors.white),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isEnableMode)
const Text(
'یک رمز ۴ تا ۸ رقمی انتخاب کنید. از این به بعد، برای ورود یا بازگشت به برنامه باید همین رمز وارد شود.',
style: TextStyle(color: Colors.white70, height: 1.6),
),
if (_isDisableMode)
const Text(
'برای حذف قفل برنامه، رمز فعلی را وارد کنید.',
style: TextStyle(color: Colors.white70, height: 1.6),
),
if (_isEnableMode || _isDisableMode) const SizedBox(height: 16),
if (_isChangeMode) ...[
_buildPasscodeField(
controller: _currentController,
label: 'رمز فعلی',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _nextController,
label: 'رمز جدید',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _confirmController,
label: 'تکرار رمز جدید',
onSubmitted: (_) => _submit(),
),
],
if (_isEnableMode) ...[
_buildPasscodeField(
controller: _nextController,
label: 'رمز جدید',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _confirmController,
label: 'تکرار رمز',
onSubmitted: (_) => _submit(),
),
],
if (_isDisableMode)
_buildPasscodeField(
controller: _currentController,
label: 'رمز فعلی',
onSubmitted: (_) => _submit(),
),
if (_errorText != null) ...[
const SizedBox(height: 12),
Text(
_errorText!,
style: const TextStyle(color: Colors.redAccent),
),
],
],
),
actions: [
TextButton(
onPressed: _isSubmitting ? null : _close,
child: const Text('لغو'),
),
ElevatedButton(
style: _isDisableMode
? ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
)
: null,
onPressed: _isSubmitting ? null : _submit,
child: Text(_primaryActionLabel),
),
],
);
}
}
class MeshBackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint paint1 = Paint()
..shader = RadialGradient(
colors: [
const Color(0xFF7000FF).withValues(alpha: 0.12),
const Color(0xFF7000FF).withValues(alpha: 0.0),
],
).createShader(Rect.fromCircle(
center: Offset(size.width * 0.2, size.height * 0.15), radius: 300));
canvas.drawCircle(
Offset(size.width * 0.2, size.height * 0.15), 300, paint1);
final Paint paint2 = Paint()
..shader = RadialGradient(
colors: [
const Color(0xFF00D2FF).withValues(alpha: 0.1),
const Color(0xFF00D2FF).withValues(alpha: 0.0),
],
).createShader(Rect.fromCircle(
center: Offset(size.width * 0.8, size.height * 0.8), radius: 400));
canvas.drawCircle(Offset(size.width * 0.8, size.height * 0.8), 400, paint2);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,438 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'home_screen.dart';
import '../utils/secure_messaging_service.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
static const _platform =
MethodChannel('com.example.saba_secure_sms/sms_role');
String _statusText = "در حال بررسی مجوزها...";
String? _startupError;
late AnimationController _animationController;
late AnimationController _loaderController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_scaleAnimation = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn)),
);
_loaderController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1800),
)..repeat();
_animationController.forward();
_checkPermissions();
}
@override
void dispose() {
_animationController.dispose();
_loaderController.dispose();
super.dispose();
}
Future<void> _checkPermissions() async {
// کمی تاخیر برای اینکه انیمیشن دیده شود
await Future.delayed(const Duration(seconds: 2));
// ... rest of permission logic remains the same ...
Map<Permission, PermissionStatus> statuses = await [
Permission.sms,
Permission.phone,
Permission.contacts,
Permission.notification,
].request();
if (statuses[Permission.sms]!.isGranted &&
statuses[Permission.phone]!.isGranted &&
statuses[Permission.contacts]!.isGranted) {
await _checkAndRequestDefaultSms();
if (mounted) {
setState(() {
_statusText = "آماده‌سازی امن صبا...";
_startupError = null;
});
}
await _initializeSecureLayer();
} else {
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text("مجوزهای مورد نیاز"),
content: const Text(
"صبا برای پیام‌رسانی امن به دسترسی پیامک و مخاطبین نیاز دارد."),
actions: [
TextButton(
onPressed: () => openAppSettings(),
child: const Text("تنظیمات"),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_checkPermissions();
},
child: const Text("تلاش مجدد"),
),
],
),
);
}
}
}
Future<void> _initializeSecureLayer() async {
try {
await SecureMessagingService.instance.init();
// تاخیر کوتاه برای نمایش حالت 'آماده'
await Future.delayed(const Duration(milliseconds: 800));
_goToHome();
} catch (e) {
if (!mounted) return;
setState(() {
_startupError = e.toString();
_statusText = "خطا در راه‌اندازی لایه امنیتی";
});
}
}
Future<void> _checkAndRequestDefaultSms() async {
try {
final bool isDefault = await _platform.invokeMethod('isDefaultSmsApp');
if (!isDefault) {
await _platform.invokeMethod('requestDefaultSmsApp');
await Future.delayed(const Duration(seconds: 1));
}
} catch (_) {}
}
void _goToHome() {
if (mounted) {
Navigator.pushReplacement(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 800),
pageBuilder: (context, animation, secondaryAnimation) =>
const HomeScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
}
}
Widget _buildSecurityLoader() {
return AnimatedBuilder(
animation: _loaderController,
builder: (context, child) {
final t = _loaderController.value;
final scanProgress = Curves.easeInOut.transform(t);
final pulses = [
(t + 0.08) % 1.0,
(t + 0.58) % 1.0,
];
return SizedBox(
width: 180,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 120,
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
for (final pulse in pulses)
Transform.scale(
scale: 0.78 + (pulse * 0.72),
child: Opacity(
opacity: (1 - pulse) * 0.22,
child: Container(
width: 88,
height: 88,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 1.2,
),
),
),
),
),
Container(
width: 92,
height: 92,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withValues(alpha: 0.18),
Colors.white.withValues(alpha: 0.06),
],
),
border: Border.all(
color: Colors.white.withValues(alpha: 0.22),
width: 1,
),
boxShadow: [
BoxShadow(
color:
const Color(0xFF8EC5FF).withValues(alpha: 0.18),
blurRadius: 20,
spreadRadius: 1,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 10 + (scanProgress * 50),
child: Container(
width: 78,
height: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.white.withValues(alpha: 0.0),
const Color(0xFF7FD8FF)
.withValues(alpha: 0.34),
Colors.white.withValues(alpha: 0.0),
Colors.transparent,
],
),
),
),
),
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.1),
),
),
const Icon(
Icons.verified_user_rounded,
color: Colors.white,
size: 34,
),
],
),
),
),
Positioned(
bottom: 2,
child: Row(
children: List.generate(4, (index) {
final wave = 0.5 +
0.5 *
math.sin(
((t + (index * 0.16)) * 2 * math.pi),
);
final height = 10 + (wave * 18);
return Container(
width: 9,
height: height,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Color(0xFF7FD8FF),
],
),
),
);
}),
),
),
],
),
),
const SizedBox(height: 16),
Container(
width: 150,
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99),
color: Colors.white.withValues(alpha: 0.12),
),
child: Stack(
children: [
AnimatedAlign(
duration: const Duration(milliseconds: 120),
alignment: Alignment(-1 + (scanProgress * 2), 0),
child: Container(
width: 52,
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99),
gradient: const LinearGradient(
colors: [
Color(0xFF7FD8FF),
Color(0xFFFFFFFF),
],
),
),
),
),
],
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Theme.of(context).primaryColor, const Color(0xFF1A237E)],
),
),
child: Stack(
children: [
// Background subtle patterns could go here
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Hero(
tag: 'app_logo',
child: SizedBox(
width: 200,
height: 200,
child: Image.asset('صبا بالا.png'),
),
),
),
);
},
),
const SizedBox(height: 40),
const Text(
"صبا",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
const Text(
"پیام‌رسان امن و پیشرفته",
style: TextStyle(
fontSize: 16,
color: Colors.white70,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 60),
if (_startupError == null) _buildSecurityLoader(),
if (_startupError != null)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue.shade900,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
onPressed: () {
setState(() {
_startupError = null;
_statusText = "در حال اتصال...";
});
_checkPermissions();
},
child: const Text("تلاش مجدد"),
),
const SizedBox(height: 20),
Text(
_statusText,
style: const TextStyle(color: Colors.white60, fontSize: 13),
),
],
),
),
const Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: Text(
"SABA SECURE MESSENGER",
style: TextStyle(
color: Colors.white24,
fontSize: 12,
letterSpacing: 2,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,158 @@
import 'dart:convert';
import 'dart:math';
import 'package:cryptography/cryptography.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AppLockService extends ChangeNotifier {
AppLockService._();
static final AppLockService instance = AppLockService._();
static const _enabledKey = 'app_lock_enabled_v1';
static const _saltKey = 'app_lock_salt_v1';
static const _hashKey = 'app_lock_hash_v1';
static const _pbkdf2Iterations = 120000;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
final Pbkdf2 _kdf = Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: _pbkdf2Iterations,
bits: 256,
);
bool _initialized = false;
bool _enabled = false;
bool _locked = false;
bool get initialized => _initialized;
bool get isEnabled => _enabled;
bool get isLocked => _enabled && _locked;
Future<void> init() async {
if (_initialized) return;
await _reloadFromStorage(lockIfEnabled: true);
}
Future<void> refresh() async {
await _reloadFromStorage(lockIfEnabled: false);
}
Future<void> setPasscode(String passcode) async {
final normalized = _normalizePasscode(passcode);
final salt = _generateSalt();
final hash = await _derivePasscodeHash(normalized, salt);
await _storage.write(key: _saltKey, value: salt);
await _storage.write(key: _hashKey, value: hash);
await _storage.write(key: _enabledKey, value: '1');
_enabled = true;
_locked = false;
_initialized = true;
notifyListeners();
}
Future<bool> verifyPasscode(String passcode) async {
final isValid = await _matchesStoredPasscode(passcode);
if (!isValid) return false;
_enabled = true;
_locked = false;
_initialized = true;
notifyListeners();
return true;
}
Future<bool> changePasscode({
required String currentPasscode,
required String newPasscode,
}) async {
final isCurrentValid = await _matchesStoredPasscode(currentPasscode);
if (!isCurrentValid) return false;
await setPasscode(newPasscode);
return true;
}
Future<bool> disablePasscode(String currentPasscode) async {
final isCurrentValid = await _matchesStoredPasscode(currentPasscode);
if (!isCurrentValid) return false;
await clear();
return true;
}
Future<void> clear() async {
await _storage.delete(key: _enabledKey);
await _storage.delete(key: _saltKey);
await _storage.delete(key: _hashKey);
_enabled = false;
_locked = false;
_initialized = true;
notifyListeners();
}
void lock() {
if (!_enabled || _locked) return;
_locked = true;
notifyListeners();
}
Future<void> _reloadFromStorage({required bool lockIfEnabled}) async {
try {
final enabledValue = await _storage.read(key: _enabledKey);
_enabled = enabledValue == '1';
_locked = _enabled && lockIfEnabled ? true : (_enabled && _locked);
} catch (e) {
debugPrint('[APP_LOCK] Failed to load state: $e');
_enabled = false;
_locked = false;
}
if (!_enabled) {
_locked = false;
}
_initialized = true;
notifyListeners();
}
Future<bool> _matchesStoredPasscode(String passcode) async {
try {
final normalized = _normalizePasscode(passcode);
final salt = await _storage.read(key: _saltKey);
final storedHash = await _storage.read(key: _hashKey);
final enabledValue = await _storage.read(key: _enabledKey);
if (enabledValue != '1' || salt == null || storedHash == null) {
return false;
}
final computedHash = await _derivePasscodeHash(normalized, salt);
return computedHash == storedHash;
} catch (e) {
debugPrint('[APP_LOCK] Failed to verify passcode: $e');
return false;
}
}
Future<String> _derivePasscodeHash(String passcode, String salt) async {
final secretKey = await _kdf.deriveKey(
secretKey: SecretKey(utf8.encode(passcode)),
nonce: utf8.encode(salt),
);
final bytes = await secretKey.extractBytes();
return base64UrlEncode(bytes);
}
String _generateSalt() {
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
return base64UrlEncode(bytes);
}
String _normalizePasscode(String input) => input.trim();
}

91
lib/utils/app_theme.dart Normal file
View File

@ -0,0 +1,91 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
// --- Neo-Dark Color Palette ---
static const Color darkBg = Color(0xFF050505);
static const Color darkCard = Color(0xFF121212);
static const Color accentPurple = Color(0xFF7000FF);
static const Color accentBlue = Color(0xFF00D2FF);
static const Color accentCyan = Color(0xFF00FFD1);
static const Color secureGradientStart = Color(0xFF6A11CB);
static const Color secureGradientEnd = Color(0xFF2575FC);
static const Color glassWhite = Color(0x1AFFFFFF);
static const Color glassBorder = Color(0x33FFFFFF);
// --- Glassmorphism Styles ---
static BoxDecoration glassDecoration({double radius = 16}) {
return BoxDecoration(
color: glassWhite,
borderRadius: BorderRadius.circular(radius),
border: Border.all(color: glassBorder, width: 0.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
spreadRadius: -2,
),
],
);
}
static Widget glassWrapper({
required Widget child,
double radius = 16,
double sigma = 10,
}) {
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: Container(
decoration: glassDecoration(radius: radius),
child: child,
),
),
);
}
// --- Theme Data ---
static ThemeData neoDarkTheme(Color seedColor) {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: darkBg,
primaryColor: seedColor,
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
surface: darkCard,
onSurface: Colors.white,
),
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme).copyWith(
displayLarge: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
color: Colors.white,
),
titleMedium: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: Colors.white70,
),
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
cardTheme: CardThemeData(
color: darkCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: glassBorder, width: 0.5),
),
),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'database_helper.dart';
import 'phone_helper.dart';
class ContactHelper {
static final Map<String, String> _memoryCache = {};
static void clearCache() {
_memoryCache.clear();
}
// بارگذاری نامها از دیتابیس برای نمایش در لیست چتها (Home Screen)
static Future<void> loadFromLocalCache() async {
try {
final cachedData = await DatabaseHelper.instance.getCachedContacts();
if (cachedData.isNotEmpty) {
_memoryCache.addAll(cachedData);
}
} catch (e) {
debugPrint("Cache load error: $e");
}
}
// --- متد ۱: دریافت لیست سبک (فقط نام و ID) - بسیار سریع ---
// این متد برای لیست جستجو استفاده میشود
static Future<List<Contact>> getContactsLight() async {
if (!await FlutterContacts.requestPermission(readonly: true)) return [];
// withProperties: false یعنی فقط ID و Name را بده (بدون عکس، بدون شماره)
// این کار حجم انتقال داده را تا ۹۰٪ کاهش میدهد و "Slow Binder" را رفع میکند
return await FlutterContacts.getContacts(
withProperties: false, withPhoto: false, sorted: true);
}
// --- متد ۲: دریافت اطلاعات کامل یک نفر (وقتی رویش کلیک شد) ---
static Future<Contact?> getFullContact(String contactId) async {
return await FlutterContacts.getContact(contactId);
}
// --- متد ۳: همگامسازی پسزمینه (برای دیتابیس) ---
// این متد سنگین است و باید فقط برای مپ کردن نامها در دیتابیس استفاده شود
static Future<bool> syncWithDevice() async {
if (!await FlutterContacts.requestPermission(readonly: true)) return false;
try {
// دریافت مخاطبین با شماره (سنگین)
List<Contact> contacts = await FlutterContacts.getContacts(
withProperties: true,
withPhoto: false,
);
Map<String, String> newMap = {};
for (var contact in contacts) {
if (contact.phones.isNotEmpty) {
for (var phone in contact.phones) {
String cleanPhone = normalizePhone(phone.number);
if (cleanPhone.isNotEmpty) {
newMap[cleanPhone] = contact.displayName;
}
}
}
}
_memoryCache.addAll(newMap);
await DatabaseHelper.instance.cacheContacts(newMap);
return true;
} catch (e) {
return false;
}
}
static String getName(String phone) {
String cleanPhone = normalizePhone(phone);
return _memoryCache[cleanPhone] ?? phone;
}
static String normalizePhone(String phone) {
return PhoneHelper.normalizePhone(phone);
}
static bool isRawNumber(String input) {
if (input.isEmpty) return false;
// اگر فقط شامل اعداد، علامت مثبت یا خط فاصله باشد، احتمالا شماره خام است
return RegExp(r'^[0-9+\-\s]+$').hasMatch(input);
}
}

View File

@ -0,0 +1,60 @@
import 'package:encrypt/encrypt.dart' as enc;
import 'package:crypto/crypto.dart';
import 'dart:convert';
import 'dart:typed_data';
class CryptoHelper {
// این پیشوند باعث میشود بفهمیم پیام رمزدار است
static const String prefix = "ENC:";
// تبدیل رمز ساده کاربر (مثلا 1234) به کلید امن ۳۲ بایتی
static enc.Key _generateKey(String password) {
var bytes = utf8.encode(password);
var digest = sha256.convert(bytes);
return enc.Key(Uint8List.fromList(digest.bytes));
}
// متد رمزنگاری
static String encrypt(String plainText, String password) {
try {
final key = _generateKey(password);
final iv = enc.IV.fromLength(16); // تولید IV تصادفی
final encrypter = enc.Encrypter(enc.AES(key));
final encrypted = encrypter.encrypt(plainText, iv: iv);
// فرمت خروجی: ENC:IV_BASE64:MESSAGE_BASE64
return "$prefix${iv.base64}:${encrypted.base64}";
} catch (e) {
print("Error creating encryption: $e");
return plainText;
}
}
// متد رمزگشایی
static String decrypt(String encryptedText, String password) {
// اگر پیام پیشوند ما را نداشت، یعنی پیام معمولی است
if (!encryptedText.startsWith(prefix)) return encryptedText;
try {
// جدا کردن بخشها (حذف ENC و جدا کردن IV از متن)
final parts = encryptedText.substring(4).split(':');
if (parts.length != 2) return "فرمت پیام نامعتبر است";
final iv = enc.IV.fromBase64(parts[0]);
final cipherText = parts[1];
final key = _generateKey(password);
final encrypter = enc.Encrypter(enc.AES(key));
return encrypter.decrypt64(cipherText, iv: iv);
} catch (e) {
return "⛔ رمز اشتباه است یا پیام مخدوش شده.";
}
}
// متد کمکی برای تشخیص اینکه آیا پیام رمزدار است یا نه
static bool isEncrypted(String text) {
return text.startsWith(prefix);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
import '../models/chat_model.dart';
import 'protocol_helper.dart';
class MessageProcessor {
/// Processes raw SMS data into ChatModel objects in a background-friendly way.
/// This is intended to be called via compute() or Isolate.run().
static List<ChatModel> processSmsList(
List<Map<String, dynamic>> rawSmsList, String targetAddress) {
// 1. Sort by date
final allSms = List<Map<String, dynamic>>.from(rawSmsList)
..sort((a, b) {
final dateComp =
(a['date'] as int? ?? 0).compareTo(b['date'] as int? ?? 0);
if (dateComp != 0) return dateComp;
final smsIdComp =
(a['sms_id'] as int? ?? 0).compareTo(b['sms_id'] as int? ?? 0);
if (smsIdComp != 0) return smsIdComp;
return (a['id'] as int? ?? 0).compareTo(b['id'] as int? ?? 0);
});
final convertedMessages = <ChatModel>[];
final processedPacketIds = <String>{};
final packetLeaders = <String, int>{}; // packetKey -> newest SMS ID
final packetFragments = <String, List<Map<String, dynamic>>>{};
final packetTotals = <String, int>{};
final allDecryptedBodies = <String>{};
// PASS 1: Identify all packets and their status
for (final sms in allSms) {
final bodyText = (sms['body'] as String).trim();
final parsed = ProtocolHelper.parseMessage(bodyText);
final type = parsed['type'] as String;
final bool dbIsSecure = (sms['is_secure'] as int? ?? 0) == 1;
final packetId = parsed['packetId'] as String?;
final packetMode = (type == 'asym' || type == 'afrag') ? 'AE' : 'SYM';
final packetKey = (packetId == null || packetId.isEmpty)
? null
: '$packetMode::$packetId';
if (type == 'sym' || type == 'asym') {
if (packetKey != null) {
processedPacketIds.add(packetKey);
}
}
if (dbIsSecure && type == 'plain') {
allDecryptedBodies.add(bodyText);
}
if (type == 'key_init' || type == 'key_reply' || type == 'norm') continue;
if ((type == 'sfra' || type == 'afrag') && packetKey != null) {
packetLeaders[packetKey] = sms['sms_id'] as int? ?? 0;
packetFragments.putIfAbsent(packetKey, () => []).add(sms);
packetTotals[packetKey] = parsed['totalParts'] as int? ?? 1;
}
}
// PASS 2: Convert to ChatModel with linked deduplication
for (final sms in allSms) {
final isMe = (sms['is_me'] as int? ?? 0) == 1;
final smsId = sms['sms_id'] as int?;
final cacheRowId = sms['id'] as int?;
final rawBody = (sms['body'] as String);
final bool dbIsSecure = (sms['is_secure'] as int? ?? 0) == 1;
final parsed = ProtocolHelper.parseMessage(rawBody);
final type = parsed['type'] as String;
// Deduplication Rule: If this is a plain message but its content is a known decrypted version of a secure message, skip.
if (type == 'plain' &&
!dbIsSecure &&
allDecryptedBodies.contains(rawBody.trim())) {
continue;
}
if (type == 'key_init' || type == 'key_reply' || type == 'norm') continue;
final String? dbPacketId = sms['packet_id'] as String?;
final String? dbPacketMode = sms['packet_mode'] as String?;
final packetId = dbPacketId ?? parsed['packetId'] as String?;
final bool isAsymmetric = type == 'afrag' || type == 'asym';
final String packetMode = dbPacketMode ?? (isAsymmetric ? 'AE' : 'SYM');
final packetKey = (packetId == null || packetId.isEmpty)
? null
: '$packetMode::$packetId';
final isFragment = type == 'sfra' || type == 'afrag';
// Deduplication: If we already have a decrypted/complete version of this packet, skip the fragments.
if (packetKey != null &&
processedPacketIds.contains(packetKey) &&
!dbIsSecure &&
(isFragment || type == 'sym' || type == 'asym')) {
continue;
}
String bodyText = rawBody;
String? encryptedPayload;
bool isSecureMessage = dbIsSecure || type != 'plain';
bool isAssembled = false;
if (isFragment && packetKey != null) {
if (packetLeaders[packetKey] != sms['sms_id']) continue;
final fragments = packetFragments[packetKey] ?? [];
final totalPartsCount = packetTotals[packetKey] ?? 1;
final receivedPartsCount = fragments.length;
if (receivedPartsCount == totalPartsCount) {
fragments.sort((a, b) {
final pA =
ProtocolHelper.parseMessage(a['body'] as String)['partNo']
as int? ??
0;
final pB =
ProtocolHelper.parseMessage(b['body'] as String)['partNo']
as int? ??
0;
return pA.compareTo(pB);
});
final payload = fragments
.map((f) =>
ProtocolHelper.parseMessage(f['body'] as String)['chunk']
as String? ??
'')
.join();
final isGroup = parsed['isGroup'] == true;
bodyText = (type == 'sfra')
? (isGroup
? '${ProtocolHelper.gPrefix}${ProtocolHelper.typeSym}|$payload'
: ProtocolHelper.buildSymmetricMsg(payload))
: ProtocolHelper.buildAsymmetricMsg(payload);
encryptedPayload = payload;
isAssembled = true;
processedPacketIds.add(packetKey);
} else {
bodyText =
'در حال ${isMe ? "ارسال" : "دریافت"} قطعات... ($receivedPartsCount/$totalPartsCount)';
}
} else if (type != 'plain') {
if (type == 'sym' || type == 'asym') {
encryptedPayload = parsed['payload'] as String?;
if (packetKey != null) processedPacketIds.add(packetKey);
}
}
convertedMessages.add(ChatModel(
id: smsId,
localId: cacheRowId != null ? 'cache::$cacheRowId' : null,
body: bodyText,
rawBody: rawBody,
encryptedPayload: encryptedPayload,
packetId: packetId,
packetMode: (packetId != null) ? packetMode : null,
date: sms['date'] as int? ?? 0,
isMe: isMe,
status: isMe ? MessageStatus.sent : MessageStatus.received,
isSecure: isSecureMessage,
isPendingMultipart: isFragment && !isAssembled,
statusLabel:
(isFragment && !isAssembled) ? 'در حال دریافت قطعات...' : null,
));
}
return convertedMessages.reversed.toList();
}
}

View File

@ -0,0 +1,67 @@
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationHelper {
static final NotificationHelper instance = NotificationHelper._init();
final StreamController<String> notificationStreamController = StreamController<String>.broadcast();
Stream<String> get notificationStream => notificationStreamController.stream;
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
NotificationHelper._init();
Future<void> init() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
);
await _notificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (details) {
if (details.payload != null) {
// This will be handled by the UI listening to a stream or similar
// or we can use a global navigator key if available.
// For now, we'll use a static notificationStream to let listeners know.
notificationStreamController.add(details.payload!);
}
},
);
_isInitialized = true;
}
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
if (!_isInitialized) await init();
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'sms_channel',
'SMS Notifications',
channelDescription: 'Notifications for incoming secure messages',
importance: Importance.max,
priority: Priority.high,
showWhen: true,
);
const NotificationDetails platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
);
await _notificationsPlugin.show(
id,
title,
body,
platformChannelSpecifics,
payload: payload,
);
}
}

View File

@ -0,0 +1,36 @@
class PhoneHelper {
static String normalizePhone(String phone) {
final raw = phone.trim();
if (raw.isEmpty) return '';
final digits = raw.replaceAll(RegExp(r'\D'), '');
if (digits.isEmpty) return raw;
if (raw.startsWith('+')) {
return '+$digits';
}
if (raw.startsWith('00') && digits.length > 2) {
return '+${digits.substring(2)}';
}
// Iranian local formats
if (digits.length == 11 && digits.startsWith('0')) {
return '+98${digits.substring(1)}';
}
if (digits.length == 10) {
return '+98$digits';
}
if (digits.startsWith('98') && digits.length >= 12 && digits.length <= 15) {
return '+$digits';
}
// Preserve full international numbers instead of collapsing them to
// the last 10 digits, which can merge different identities together.
if (digits.length >= 11 && digits.length <= 15) {
return '+$digits';
}
return raw;
}
}

View File

@ -0,0 +1,467 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
class ProtocolHelper {
static const String prefix = "@S:";
static const String gPrefix = "@G:";
static const String typeNack = "NACK";
static const String typeSym = "SYM";
static const String typeSfra = "SFRA";
static const String typeNorm = "NORM";
static const String typeKeyInit = "KI";
static const String typeKeyReply = "KR";
static const String typeAsym = "AE";
static const String typeAsymFrag = "AF";
static const int maxSmsChars = 136;
static const int fragmentHeaderBytes = 6;
static const int fragmentChunkBytes = 90;
static String encodeTransportPayload(List<int> bytes) {
return base64UrlEncode(bytes).replaceAll('=', '');
}
static List<int> decodeTransportPayload(String value) {
// Parity with Python _repair_base64 and SecureCryptoHelper.b64uDecode
// 1. Map common mangles including space-to-plus
String clean = value.replaceAll(' ', '+');
// 2. Map mangled separators to standard Base64 characters
clean =
clean.replaceAll('|', '/').replaceAll(';', '/').replaceAll('!', '/');
// 3. Keep ONLY valid Base64/Base64URL characters
clean = clean.replaceAll(RegExp(r'[^A-Za-z0-9+/=\-_]'), '');
// 4. Standardize alphabet (Base64URL -> Standard Base64 for the decoder)
clean = clean.replaceAll('-', '+').replaceAll('_', '/');
// 5. Recalculate padding
clean = clean.split('=')[0];
final missingPadding = (4 - (clean.length % 4)) % 4;
return base64.decode(clean + ('=' * missingPadding));
}
static String _b64Encode(List<int> bytes) => encodeTransportPayload(bytes);
static List<int> _b64Decode(String value) => decodeTransportPayload(value);
static List<String> _transportCandidates(String value) {
final normalized = value
.replaceAll('¡', '@')
.replaceAll('¡', '@')
.replaceAll('ö', '|')
.replaceAll('ö', '|')
.replaceAll('§', '|')
.replaceAll('§', '|')
.replaceAll(RegExp(r'\s+'), '');
final candidates = <String>{normalized};
final separatorPattern = RegExp(r'[|;!]');
final matches = separatorPattern.allMatches(normalized).toList();
if (matches.isEmpty) return candidates.toList();
final replacementOptions = ['', '-', '_'];
final maxRepairs = matches.length > 4 ? 4 : matches.length;
void build(int index, String current, int offset) {
if (index >= maxRepairs) {
candidates.add(current);
return;
}
final match = matches[index];
final start = match.start + offset;
final end = match.end + offset;
for (final replacement in replacementOptions) {
final next =
current.substring(0, start) + replacement + current.substring(end);
final nextOffset =
offset + replacement.length - (match.end - match.start);
build(index + 1, next, nextOffset);
}
}
build(0, normalized, 0);
return candidates.toList();
}
static Map<String, dynamic>? _parseAsymmetricFragmentPayload(
String encoded, {
required bool isGroup,
}) {
for (final candidate in _transportCandidates(encoded)) {
try {
final raw = _b64Decode(candidate);
if (raw.length < 6) continue;
final packetId = raw
.sublist(0, 4)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
return {
"type": "afrag",
"isGroup": isGroup,
"packetId": packetId,
"totalParts": raw[4],
"partNo": raw[5],
"chunk": _b64Encode(raw.sublist(6))
};
} catch (_) {}
}
return null;
}
static String buildSymmetricMsg(String encryptedPayload) {
return "$prefix$typeSym|$encryptedPayload";
}
static String buildAsymmetricMsg(String b64Payload) {
return "$prefix$typeAsym|$b64Payload";
}
static String buildSymmetricFrag(
String packetId, int partNo, int totalParts, String chunk) {
return "$prefix$typeSfra|$packetId|$partNo|$totalParts|$chunk";
}
static String buildNormalMode() {
return "$prefix$typeNorm|";
}
static String buildKeyInit(String publicKeyPayload) {
return "$prefix$typeKeyInit|$publicKeyPayload";
}
static String buildKeyReply(String publicKeyPayload) {
return "$prefix$typeKeyReply|$publicKeyPayload";
}
static List<String> buildAsymmetricFrames(List<int> payloadBytes,
{String? packetId}) {
final single = "$prefix$typeAsym|${_b64Encode(payloadBytes)}";
if (single.length <= maxSmsChars) {
return [single];
}
return buildAsymmetricFragments(payloadBytes, packetId: packetId);
}
static List<String> buildAsymmetricFragments(List<int> payloadBytes,
{String? packetId}) {
final random = Random();
final packetRaw = packetId != null
? Uint8List.fromList(List<int>.generate(
packetId.length ~/ 2,
(index) => int.parse(packetId.substring(index * 2, index * 2 + 2),
radix: 16),
))
: Uint8List.fromList(List<int>.generate(4, (_) => random.nextInt(256)));
if (packetRaw.length != 4) {
throw StateError('Asymmetric packet ids must contain exactly 4 bytes.');
}
final total = (payloadBytes.length / fragmentChunkBytes).ceil();
if (total > 255) {
throw StateError('Payload is too large for SMS fragmentation.');
}
final frames = <String>[];
for (var i = 0; i < total; i++) {
final start = i * fragmentChunkBytes;
final end = min(start + fragmentChunkBytes, payloadBytes.length);
final chunk = payloadBytes.sublist(start, end);
final raw = Uint8List.fromList([
...packetRaw,
total,
i + 1,
...chunk,
]);
frames.add("$prefix$typeAsymFrag|${_b64Encode(raw)}");
}
return frames;
}
static Map<String, dynamic> parseMessage(String rawText) {
if (rawText.isEmpty) return {"type": "plain", "body": ""};
// Step 1: Preliminary cleaning (Mojibake Fix & Control Code Removal)
// Remove all non-printable control characters immediately
String cleaned = rawText.replaceAll(RegExp(r'[\x00-\x1F\x7F-\x9F]'), '');
// Parity with Python: DO NOT trim(). Only remove newlines and nulls at the end.
// This preserves trailing spaces that might be mangled '+' characters.
String body = cleaned.replaceAll(RegExp(r'[\x00\r\n]+$'), '');
// Mojibake Fix
body = body
.replaceAll('¡', '@')
.replaceAll('¡', '@')
.replaceAll('ö', '|')
.replaceAll('ö', '|')
.replaceAll('§', '|')
.replaceAll('§', '|')
.replaceAll('¿', '?')
.replaceAll('¿', '?');
// Fast-path for legacy symmetric fragments. This avoids falling through to
// generic SYM/b1 parsing when the SMS transport slightly damages the
// prefix but the SFRA structure is still intact.
final directSfra = RegExp(
r'SFRA\s*[|;!ö§]\s*([^|;!ö§\s]+)\s*[|;!ö§]\s*(\d+)\s*[|;!ö§]\s*(\d+)\s*[|;!ö§](.*)$',
caseSensitive: false,
).firstMatch(body);
if (directSfra != null) {
final lowerBodyForGroup = body.toUpperCase();
final rawChunk = directSfra.group(4) ?? '';
return {
"type": "sfra",
"isGroup": lowerBodyForGroup.contains('@G:') ||
lowerBodyForGroup.contains('G|'),
"packetId": (directSfra.group(1) ?? '').trim(),
"partNo": int.tryParse((directSfra.group(2) ?? '').trim()) ?? 1,
"totalParts": int.tryParse((directSfra.group(3) ?? '').trim()) ?? 1,
"chunk": rawChunk.replaceAll(RegExp(r'\s+'), ''),
};
}
final directAf = RegExp(
r'AF\s*[|;!ö§]\s*(.+)$',
caseSensitive: false,
).firstMatch(body);
if (directAf != null) {
final lowerBodyForGroup = body.toUpperCase();
final parsedAf = _parseAsymmetricFragmentPayload(
directAf.group(1) ?? '',
isGroup: lowerBodyForGroup.contains('@G:') ||
lowerBodyForGroup.contains('G|'),
);
if (parsedAf != null) {
return parsedAf;
}
}
// Step 2: Protocol Search
final protocolTypes = [
typeSfra,
typeSym,
typeAsym,
typeAsymFrag,
typeNorm,
typeKeyInit,
typeKeyReply,
typeNack
];
// Structural delimiters (Mainly | ; ! but also ö and § as fallbacks)
final sepRegex = RegExp(r'[|;!ö§]');
int startIdx = -1;
String? detectedType;
bool isGroup = false;
// A. Standard Header Search (@S: or @G:) - Case Insensitive
final lowerBody = body.toLowerCase();
final sIdx = lowerBody.indexOf(prefix.toLowerCase());
final gIdx = lowerBody.indexOf(gPrefix.toLowerCase());
if (sIdx != -1 || gIdx != -1) {
isGroup = gIdx != -1 && (sIdx == -1 || gIdx < sIdx);
startIdx = isGroup ? gIdx : sIdx;
final actualPrefixLength = isGroup ? gPrefix.length : prefix.length;
final contentAfterPrefix =
body.substring(startIdx + actualPrefixLength).trim();
final lowerContent = contentAfterPrefix.toLowerCase();
for (final t in protocolTypes) {
if (lowerContent.startsWith(t.toLowerCase())) {
detectedType = t;
break;
}
}
}
debugPrint(
'[PROTOCOL] Search Header: detected=$detectedType, startIdx=$startIdx, isGroup=$isGroup');
// B. Fuzzy Fallback (Marker + Separator)
if (detectedType == null) {
final fuzzyProtocolTypes = [typeSfra, typeSym, typeAsymFrag];
for (final t in fuzzyProtocolTypes) {
// Broaden the separator character set for fuzzy matching
final fuzzyRegex =
RegExp('${t.toLowerCase()}\\s*[|;!ö§:]', caseSensitive: false);
final match = fuzzyRegex.firstMatch(body);
if (match != null) {
detectedType = t;
startIdx = max(0, match.start - 3);
final lookback = body.substring(0, match.start).toUpperCase();
isGroup = lookback.contains('@G:') || lookback.contains('G|');
break;
}
}
debugPrint(
'[PROTOCOL] Search Fuzzy: detected=$detectedType, startIdx=$startIdx, isGroup=$isGroup');
}
// C. Raw Variant Fallback (b1: or h1:)
if (detectedType == null) {
final normalizedBody = body.trimLeft();
final lower = normalizedBody.toLowerCase();
if (lower.startsWith('b1:') || lower.startsWith('h1:')) {
return {
"type": "sym",
"isGroup": false,
"payload": normalizedBody.trim()
};
}
return {"type": "plain", "body": rawText};
}
// Step 3: Extract Payload from First Structural Separator
// Find the first occurrence of detectedType after startIdx
final typeStartPos =
body.toUpperCase().indexOf(detectedType.toUpperCase(), startIdx);
if (typeStartPos == -1) return {"type": "plain", "body": rawText};
final sepMatch = sepRegex.firstMatch(body.substring(typeStartPos));
if (sepMatch == null) {
if (detectedType == typeKeyInit ||
detectedType == typeKeyReply ||
detectedType == typeSym) {
return {
"type": detectedType.toLowerCase(),
"isGroup": isGroup,
"payload": body.substring(typeStartPos + detectedType.length).trim()
};
}
return {"type": "plain", "body": rawText};
}
final firstSepIdxInSubstring = sepMatch.start;
final firstSepIdxInBody = typeStartPos + firstSepIdxInSubstring;
// Crucial difference: Keep the absolute raw content without trimming to prevent truncation of trailing Base64 spaces (mangled '+')
final absoluteRawContent = body.substring(firstSepIdxInBody + 1);
final rawContent = absoluteRawContent.trim();
final sepChar = sepMatch.group(0)!;
final parts = [detectedType, ...rawContent.split(sepChar)];
debugPrint(
'[PROTOCOL] Parsed: type=$detectedType, isGroup=$isGroup, sepChar=$sepChar, partsCount=${parts.length}, rawLen=${body.length}');
if (detectedType == typeSfra) {
debugPrint('[PROTOCOL] Fragment Debug: parts=$parts');
}
// Type Handlers
if (detectedType == typeNorm) return {"type": "norm", "isGroup": isGroup};
if (detectedType == typeSym) {
// Support both: SYM|payload AND SYM|packetId|payload
if (parts.length >= 3) {
return {
"type": "sym",
"isGroup": isGroup,
"packetId": parts[1].trim(),
"payload": parts.sublist(2).join(sepChar)
};
}
return {"type": "sym", "isGroup": isGroup, "payload": absoluteRawContent};
}
if (detectedType == typeSfra && parts.length >= 4) {
final packetId = parts[1].trim();
final partNo = int.tryParse(parts[2].trim()) ?? 1;
final totalParts = int.tryParse(parts[3].trim()) ?? 1;
// Accurate Chunk Extraction: skip exactly 3 delimiters in absoluteRawContent
int dCount = 0;
int chunkStartPos = 0;
for (int i = 0; i < absoluteRawContent.length; i++) {
if (absoluteRawContent[i] == sepChar) {
dCount++;
if (dCount == 3) {
chunkStartPos = i + 1;
break;
}
}
}
// Preserve suspicious separator noise inside chunks so the crypto layer
// can try repair candidates instead of silently dropping real data.
final String rawChunk = (chunkStartPos > 0)
? absoluteRawContent.substring(chunkStartPos)
: (parts.length > 4 ? parts.sublist(4).join(sepChar) : "");
final String chunk = rawChunk.replaceAll(RegExp(r'\s+'), '');
return {
"type": "sfra",
"isGroup": isGroup,
"packetId": packetId,
"partNo": partNo,
"totalParts": totalParts,
"chunk": chunk
};
}
if (detectedType == typeNack && parts.length >= 3) {
return {
"type": "nack",
"isGroup": isGroup,
"packetId": parts[1],
"missingPart": int.tryParse(parts[2]) ?? 1
};
}
if (detectedType == typeKeyInit) {
return {
"type": "key_init",
"isGroup": isGroup,
"payload": absoluteRawContent
};
}
if (detectedType == typeKeyReply) {
return {
"type": "key_reply",
"isGroup": isGroup,
"payload": absoluteRawContent
};
}
if (detectedType == typeAsym) {
if (parts.length >= 3) {
return {
"type": "asym",
"isGroup": isGroup,
"packetId": parts[1].trim(),
"payload": parts.sublist(2).join(sepChar)
};
}
return {
"type": "asym",
"isGroup": isGroup,
"payload": absoluteRawContent
};
}
if (detectedType == typeSym) {
final payload = parts.length > 1 ? parts.sublist(1).join(sepChar) : "";
return {
"type": "sym",
"isGroup": isGroup,
"payload": payload.replaceAll(RegExp(r'\s+'), '')
};
}
if (detectedType == typeAsymFrag) {
final parsedAf = _parseAsymmetricFragmentPayload(
absoluteRawContent,
isGroup: isGroup,
);
if (parsedAf != null) {
return parsedAf;
}
}
return {"type": "plain", "body": rawText};
}
}

View File

@ -0,0 +1,490 @@
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart' as dart_crypto;
import 'package:cryptography/cryptography.dart';
import 'package:flutter/foundation.dart';
import 'package:pointycastle/export.dart' as pc;
class EccIdentityMaterial {
final String privateKey;
final String publicKey;
final String fingerprint;
const EccIdentityMaterial({
required this.privateKey,
required this.publicKey,
required this.fingerprint,
});
}
class SecureCryptoHelper {
final _aesGcm = AesGcm.with256bits();
final _hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
final _random = Random.secure();
final pc.ECDomainParameters _ecDomain = pc.ECDomainParameters('secp256r1');
static const _variantMap = {
"ك": "ک",
"ي": "ی",
"ى": "ی",
"ة": "ه",
"ە": "ه",
"٠": "0",
"١": "1",
"٢": "2",
"٣": "3",
"٤": "4",
"٥": "5",
"٦": "6",
"٧": "7",
"٨": "8",
"٩": "9",
"۰": "0",
"۱": "1",
"۲": "2",
"۳": "3",
"۴": "4",
"۵": "5",
"۶": "6",
"۷": "7",
"۸": "8",
"۹": "9",
};
String _visualSafe(String text) {
String res = text.trim();
// 1. Remove invisible/zero-width characters (matches Python's _LEGACY_INVISIBLE_CHARS)
res = res.replaceAll(RegExp(r'[\u200c\u200d\u200e\u200f\ufeff]'), '');
// 2. Normalize common decomposed (NFD) Persian characters to composite (NFC) legacy fallback
res = res.replaceAll('\u0627\u0653', '\u0622'); // Alif + Madda -> آ
res = res.replaceAll('\u0627\u0654', '\u0623'); // Alif + Hamza -> أ
res = res.replaceAll('\u0627\u0655', '\u0625'); // Alif + Lower Hamza -> إ
res = res.replaceAll('\u0648\u0654', '\u0624'); // Waw + Hamza -> ؤ
res = res.replaceAll('\u064a\u0654', '\u0626'); // Yeh + Hamza (Arabic) -> ئ
res =
res.replaceAll('\u06cc\u0654', '\u0626'); // Yeh + Hamza (Persian) -> ئ
res = res.replaceAll(
'\u0627\u0644\u0644\u0647', 'الله'); // Allah ligature (Standard)
// 3. Map Arabic variants and digits to Persian/Western standards (matches Python's dict)
_variantMap.forEach((key, value) {
res = res.replaceAll(key, value);
});
// 4. Aggressive cleanup of invisible characters (Matches Python's _LEGACY_INVISIBLE_CHARS)
res = res.replaceAll(RegExp(r'[\u200c\u200d\u200e\u200f\ufeff]'), '');
// NOTE: We DO NOT remove Harakats (diacritics) here because Python's NFC normalization
// preserves them. We only map letter/digit variants.
return res;
}
String b64uEncode(List<int> bytes) {
return base64UrlEncode(bytes).replaceAll('=', '');
}
List<int> b64uDecode(String value) {
// 1. Strip transport prefixes if present
String input = value.trim();
if (input.startsWith('b1:')) {
input = input.substring(3);
}
// 2. Log illegal characters for diagnostics
final illegalChars = input.replaceAll(RegExp(r'[A-Za-z0-9+/=\s\-_]'), '');
if (illegalChars.isNotEmpty) {
debugPrint('[CRYPTO] Found illegal chars in Base64: "$illegalChars"');
}
// 3. Aggressive cleaning - ALLOW | ; ! as they might be mangled separators/chars
String clean = input.replaceAll(RegExp(r'[^A-Za-z0-9+/=\s\-_|;!]'), '');
// 4. Map standard mangles (Parity with Python's _repair_base64)
clean = clean.replaceAll(' ', '+');
clean =
clean.replaceAll('|', '/').replaceAll(';', '/').replaceAll('!', '/');
// 4. Parity with Python: translate URL-safe to Standard before decoding
clean = clean.replaceAll('-', '+').replaceAll('_', '/');
// 5. Handle padding: discard all '=' to recalculate from scratch based on data bits
clean = clean.split('=')[0];
// 6. Critical: Final cleanup from non-alphabet symbols after mapping/split
clean = clean.replaceAll(RegExp(r'[^A-Za-z0-9+/]'), '');
// 7. Base64 length validation logic
if (clean.length % 4 == 1) {
debugPrint(
'[CRYPTO] Base64 Critical Failure: len ${clean.length} % 4 == 1. Prefix: ${clean.substring(0, min(10, clean.length))}');
return [];
}
final missingPadding = (4 - (clean.length % 4)) % 4;
final padded = clean + ('=' * missingPadding);
try {
return base64.decode(padded);
} catch (e) {
debugPrint('[CRYPTO] Base64 Final Decode Error: $e');
return [];
}
}
String hexEncode(List<int> bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
List<int> hexDecode(String value) {
String input = value.trim();
if (input.startsWith('h1:')) {
input = input.substring(3);
}
final clean = input.replaceAll(RegExp(r'[^0-9A-Fa-f]'), '');
final evenLength = clean.length - (clean.length % 2);
final normalized = clean.substring(0, evenLength);
return List<int>.generate(
normalized.length ~/ 2,
(i) => int.parse(normalized.substring(i * 2, i * 2 + 2), radix: 16),
);
}
List<int> decodeTransportPayload(String value) {
if (value.startsWith('h1:')) {
return hexDecode(value.substring(3));
}
if (value.startsWith('b1:')) {
return b64uDecode(value.substring(3));
}
return b64uDecode(value);
}
Future<String> encryptSymmetric(String message, String password) async {
final normKeyText = _visualSafe(password);
final digest = dart_crypto.sha256.convert(utf8.encode(normKeyText));
final keyBytes = Uint8List.fromList(digest.bytes);
final nonce = _aesGcm.newNonce();
final secretBox = await _aesGcm.encrypt(
utf8.encode(message),
secretKey: SecretKey(keyBytes),
nonce: nonce,
);
final combined = Uint8List.fromList([
...secretBox.nonce,
...secretBox.cipherText,
...secretBox.mac.bytes,
]);
return 'b1:${b64uEncode(combined)}';
}
Future<String?> decryptSymmetric(String payload, String password) async {
try {
final raw = decodeTransportPayload(payload);
final keyVariants = [
{'label': 'visual_safe', 'text': _visualSafe(password)},
{'label': 'raw', 'text': password.trim()},
];
if (raw.length < 28) {
debugPrint('[CRYPTO] Symmetric decryption failed: payload too short.');
return null;
}
final nonce = raw.sublist(0, 12);
final ciphertextWithTag = raw.sublist(12);
final ciphertext =
ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16);
final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16);
for (final variant in keyVariants) {
final label = variant['label']!;
final keyText = variant['text']!;
final digest = dart_crypto.sha256.convert(utf8.encode(keyText));
final keyBytes = Uint8List.fromList(digest.bytes);
debugPrint('[CRYPTO] Trying symmetric decryption variant "$label".');
try {
final secretBox = SecretBox(
ciphertext,
nonce: nonce,
mac: Mac(mac),
);
final decryptedBytes = await _aesGcm.decrypt(
secretBox,
secretKey: SecretKey(keyBytes),
);
debugPrint(
'[CRYPTO] Symmetric decryption succeeded via variant "$label".');
try {
return utf8.decode(decryptedBytes);
} catch (e) {
debugPrint('[CRYPTO] UTF-8 decode failed after decryption.');
return '[UTF-8 Decode Error]';
}
} catch (e) {
debugPrint('[CRYPTO] Symmetric decryption variant "$label" failed.');
}
}
debugPrint('[CRYPTO] All symmetric decryption variants failed.');
return null;
} catch (e) {
debugPrint('[CRYPTO] Symmetric decryption error: $e');
return null;
}
}
String fingerprintFromPublicBytes(List<int> publicBytes) {
final digest =
dart_crypto.sha256.convert(publicBytes).toString().toUpperCase();
final parts = <String>[];
for (var i = 0; i < digest.length; i += 4) {
parts.add(digest.substring(i, i + 4));
}
return parts.join(' ');
}
List<int> _normalizeP256Coordinate(List<int> value) {
if (value.length == 32) return List<int>.from(value);
if (value.length == 33 && value.first == 0) {
return value.sublist(1);
}
if (value.length < 32) {
return List<int>.filled(32 - value.length, 0) + value;
}
return value.sublist(value.length - 32);
}
pc.SecureRandom _newSecureRandom() {
final seed = Uint8List.fromList(
List<int>.generate(32, (_) => _random.nextInt(256)),
);
return pc.FortunaRandom()..seed(pc.KeyParameter(seed));
}
BigInt _bytesToBigInt(List<int> bytes) {
var result = BigInt.zero;
for (final byte in bytes) {
result = (result << 8) | BigInt.from(byte);
}
return result;
}
List<int> _bigIntToBytes(BigInt value) {
if (value == BigInt.zero) {
return [0];
}
final result = <int>[];
var current = value;
while (current > BigInt.zero) {
result.insert(0, (current & BigInt.from(0xff)).toInt());
current = current >> 8;
}
return result;
}
String publicKeyTransport(String value) {
final publicBytes = decodePublicKey(value);
return 'b1:${b64uEncode(publicBytes)}';
}
Future<EccIdentityMaterial> generateIdentity() async {
final params = pc.ParametersWithRandom<pc.ECKeyGeneratorParameters>(
pc.ECKeyGeneratorParameters(_ecDomain),
_newSecureRandom(),
);
final generator = pc.KeyGenerator('EC')..init(params);
final keyPair = generator.generateKeyPair();
final privateKey = keyPair.privateKey as pc.ECPrivateKey;
final publicKey = keyPair.publicKey as pc.ECPublicKey;
final privateBytes = Uint8List.fromList(
_normalizeP256Coordinate(_bigIntToBytes(privateKey.d!)),
);
final publicBytes = Uint8List.fromList([
..._normalizeP256Coordinate(
_bigIntToBytes(publicKey.Q!.x!.toBigInteger()!),
),
..._normalizeP256Coordinate(
_bigIntToBytes(publicKey.Q!.y!.toBigInteger()!),
),
]);
return EccIdentityMaterial(
privateKey: b64uEncode(privateBytes),
publicKey: b64uEncode(publicBytes),
fingerprint: fingerprintFromPublicBytes(publicBytes),
);
}
List<int> decodePublicKey(String value) {
try {
final String input = value.startsWith('h1:') || value.startsWith('b1:')
? value.substring(3)
: value;
final raw = b64uDecode(input);
if (raw.isNotEmpty) {
if (raw.length == 65 && raw.first == 4) {
return raw.sublist(1);
}
if (raw.length == 64) {
return raw;
}
}
throw StateError(
'P-256 public key length must be 64 bytes (got ${raw.length}).');
} catch (e) {
debugPrint('Public key decode error for value "$value": $e');
rethrow;
}
}
Future<List<int>> deriveSharedKey({
required String privateKey,
required String localPublicKey,
required String publicKey,
}) async {
final privateBytes = b64uDecode(privateKey);
final publicBytes = decodePublicKey(publicKey);
if (privateBytes.length != 32) {
throw StateError('P-256 private key must contain 32 bytes.');
}
final peerX = publicBytes.sublist(0, 32);
final peerY = publicBytes.sublist(32, 64);
final localPublicBytes = decodePublicKey(localPublicKey);
if (localPublicBytes.length != 64) {
throw StateError('Local P-256 public key must contain 64 bytes.');
}
final agreement = pc.ECDHBasicAgreement()
..init(
pc.ECPrivateKey(
_bytesToBigInt(privateBytes),
_ecDomain,
),
);
final sharedPoint = pc.ECPublicKey(
_ecDomain.curve.createPoint(
_bytesToBigInt(peerX),
_bytesToBigInt(peerY),
),
_ecDomain,
);
final sharedValue = agreement.calculateAgreement(sharedPoint);
final sharedBytes = Uint8List.fromList(
_normalizeP256Coordinate(_bigIntToBytes(sharedValue)),
);
final derived = await _hkdf.deriveKey(
secretKey: SecretKey(sharedBytes),
nonce: utf8.encode('SABA:ECDH:P256:HKDF:v1'),
info: utf8.encode('SABA:ECDH:P256:AES-256-GCM'),
);
final finalKey = await derived.extractBytes();
return finalKey;
}
Future<String> encryptWithSharedKey(
String message, List<int> sharedKey) async {
final nonce = _aesGcm.newNonce();
final secretBox = await _aesGcm.encrypt(
utf8.encode(message),
secretKey: SecretKey(sharedKey),
nonce: nonce,
);
final combined = [
...secretBox.nonce,
...secretBox.cipherText,
...secretBox.mac.bytes,
];
return b64uEncode(combined);
}
Future<String?> decryptWithSharedKey(
String payload, List<int> sharedKey) async {
try {
final raw = b64uDecode(payload);
if (raw.length < 28) return null;
final nonce = raw.sublist(0, 12);
final ciphertextWithTag = raw.sublist(12);
final ciphertext =
ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16);
final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16);
try {
final secretBox = SecretBox(
ciphertext,
nonce: nonce,
mac: Mac(mac),
);
final plain =
await _aesGcm.decrypt(secretBox, secretKey: SecretKey(sharedKey));
return utf8.decode(plain);
} catch (e) {
debugPrint('[CRYPTO] Asymmetric decryption failed: $e');
return null;
}
} catch (e) {
debugPrint('Asymmetric payload decryption error: $e');
return null;
}
}
List<int> randomBytes(int length) {
return List<int>.generate(length, (_) => _random.nextInt(256));
}
Future<String> encryptStorageText(String value, List<int> keyBytes) async {
final nonce = _aesGcm.newNonce();
final secretBox = await _aesGcm.encrypt(
utf8.encode(value),
secretKey: SecretKey(keyBytes),
nonce: nonce,
);
return 'enc1:${b64uEncode([
...secretBox.nonce,
...secretBox.cipherText,
...secretBox.mac.bytes,
])}';
}
Future<String?> decryptStorageText(String? value, List<int> keyBytes) async {
if (value == null || value.isEmpty) return value;
if (!value.startsWith('enc1:')) return value;
try {
final raw = b64uDecode(value.substring(5));
if (raw.length < 28) {
return null;
}
final nonce = raw.sublist(0, 12);
final ciphertextWithTag = raw.sublist(12);
final ciphertext =
ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16);
final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16);
final secretBox = SecretBox(
ciphertext,
nonce: nonce,
mac: Mac(mac),
);
final plain = await _aesGcm.decrypt(
secretBox,
secretKey: SecretKey(keyBytes),
);
return utf8.decode(plain);
} catch (e) {
debugPrint('Storage payload decryption error: $e');
return null;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/app_lock_service.dart';
import '../utils/app_theme.dart';
class AppLockOverlay extends StatefulWidget {
const AppLockOverlay({
super.key,
required this.child,
});
final Widget child;
@override
State<AppLockOverlay> createState() => _AppLockOverlayState();
}
class _AppLockOverlayState extends State<AppLockOverlay>
with WidgetsBindingObserver {
bool _shouldRelockOnResume = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final service = AppLockService.instance;
if (!service.isEnabled) return;
if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.paused ||
state == AppLifecycleState.hidden) {
_shouldRelockOnResume = true;
return;
}
if (state == AppLifecycleState.resumed && _shouldRelockOnResume) {
_shouldRelockOnResume = false;
service.lock();
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: AppLockService.instance,
builder: (context, _) {
final service = AppLockService.instance;
return Stack(
fit: StackFit.expand,
children: [
widget.child,
if (service.initialized && service.isLocked)
const Positioned.fill(child: _AppLockBarrier()),
],
);
},
);
}
}
class _AppLockBarrier extends StatelessWidget {
const _AppLockBarrier();
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: Material(
color: AppTheme.darkBg.withValues(alpha: 0.96),
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: CustomPaint(
painter: _LockBackgroundPainter(),
),
),
const SafeArea(
child: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: _AppLockCard(),
),
),
),
],
),
),
);
}
}
class _AppLockCard extends StatefulWidget {
const _AppLockCard();
@override
State<_AppLockCard> createState() => _AppLockCardState();
}
class _AppLockCardState extends State<_AppLockCard> {
final TextEditingController _controller = TextEditingController();
bool _isVerifying = false;
String? _errorText;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _unlock() async {
if (_isVerifying) return;
FocusScope.of(context).unfocus();
final passcode = _controller.text.trim();
if (passcode.isEmpty) {
setState(() => _errorText = 'رمز برنامه را وارد کنید.');
return;
}
setState(() {
_isVerifying = true;
_errorText = null;
});
final isValid = await AppLockService.instance.verifyPasscode(passcode);
if (!mounted) return;
setState(() {
_isVerifying = false;
_errorText = isValid ? null : 'رمز واردشده درست نیست.';
});
if (isValid) {
_controller.clear();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: AppTheme.glassWrapper(
radius: 28,
sigma: 14,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 76,
height: 76,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
theme.primaryColor.withValues(alpha: 0.95),
AppTheme.accentCyan.withValues(alpha: 0.95),
],
),
boxShadow: [
BoxShadow(
color: theme.primaryColor.withValues(alpha: 0.28),
blurRadius: 20,
spreadRadius: 1,
),
],
),
child: const Icon(
Icons.lock_rounded,
size: 38,
color: Colors.white,
),
),
const SizedBox(height: 18),
const Text(
'قفل برنامه فعال است',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
const Text(
'برای ورود به صبا، رمز برنامه را وارد کنید.',
style: TextStyle(
fontSize: 14,
height: 1.6,
color: Colors.white70,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: _controller,
autofocus: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
obscureText: true,
obscuringCharacter: '',
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
onSubmitted: (_) => _unlock(),
decoration: InputDecoration(
labelText: 'رمز برنامه',
hintText: '۴ تا ۸ رقم',
errorText: _errorText,
filled: true,
fillColor: Colors.white.withValues(alpha: 0.06),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
const SizedBox(height: 18),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isVerifying ? null : _unlock,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
icon: _isVerifying
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.lock_open_rounded),
label:
Text(_isVerifying ? 'در حال بررسی...' : 'ورود به برنامه'),
),
),
],
),
),
),
);
}
}
class _LockBackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final purplePaint = Paint()
..shader = RadialGradient(
colors: [
AppTheme.accentPurple.withValues(alpha: 0.22),
AppTheme.accentPurple.withValues(alpha: 0),
],
).createShader(
Rect.fromCircle(
center: Offset(size.width * 0.18, size.height * 0.2),
radius: size.width * 0.55,
),
);
final cyanPaint = Paint()
..shader = RadialGradient(
colors: [
AppTheme.accentCyan.withValues(alpha: 0.14),
AppTheme.accentCyan.withValues(alpha: 0),
],
).createShader(
Rect.fromCircle(
center: Offset(size.width * 0.82, size.height * 0.78),
radius: size.width * 0.6,
),
);
canvas.drawCircle(
Offset(size.width * 0.18, size.height * 0.2),
size.width * 0.55,
purplePaint,
);
canvas.drawCircle(
Offset(size.width * 0.82, size.height * 0.78),
size.width * 0.6,
cyanPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,439 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../models/chat_model.dart';
class MessageBubble extends StatefulWidget {
final String body;
final String? rawBody;
final String? statusLabel;
final int date;
final bool isMe;
final MessageStatus status;
final bool isSecure;
final bool canRetryDecryption;
final VoidCallback? onRetryDecryption;
final String? packetMode;
final bool isPendingMultipart;
const MessageBubble({
super.key,
required this.body,
this.rawBody,
this.statusLabel,
required this.date,
required this.isMe,
required this.status,
this.isSecure = false,
this.canRetryDecryption = false,
this.onRetryDecryption,
this.packetMode,
this.isPendingMultipart = false,
});
@override
State<MessageBubble> createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble>
with SingleTickerProviderStateMixin {
bool _showRaw = false;
late AnimationController _animationController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeIn,
);
// Directional Slide: Received from left, Sent from right
final double startOffsetX = widget.isMe ? 0.3 : -0.3;
_slideAnimation = Tween<Offset>(
begin: Offset(startOffsetX, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut, // Added premium bounce effect
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool get _hasRawView =>
widget.rawBody != null &&
widget.rawBody!.isNotEmpty &&
widget.rawBody != widget.body;
String get _displayBody {
if (_showRaw && widget.rawBody != null) {
return widget.rawBody!;
}
// Handle the payload separator
if (widget.body.contains(' ::PAYLOAD::')) {
return widget.body.split(' ::PAYLOAD::')[0];
}
if (widget.body.contains(' | ')) {
return widget.body.split(' | ')[0];
}
return widget.body;
}
@override
Widget build(BuildContext context) {
final bool isLocked = widget.canRetryDecryption && !widget.isMe;
// Premium Gradient logic for Sent messages
final List<Color> sentGradients = widget.status == MessageStatus.failed
? [Colors.redAccent, Colors.red.shade900]
: (widget.isSecure
? [const Color(0xFF6A11CB), const Color(0xFF2575FC), const Color(0xFF00D2FF)] // Secure: Violet to Cyan
: [const Color(0xFF7000FF), const Color(0xFF5C6BC0)]); // Normal: Deep Purple to Indigo
// Received background colors
final Color receivedBg = isLocked
? const Color(0xFF0A192F) // Deep Blue Glass for locked
: (widget.isSecure ? const Color(0xFF0D0D15) : const Color(0xFF0F0F0F));
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.7, curve: Curves.easeOutBack),
),
child: Row(
textDirection: TextDirection.ltr,
mainAxisAlignment:
widget.isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(
left: widget.isMe ? 50 : 8,
right: widget.isMe ? 8 : 50,
top: 6,
bottom: 6,
),
child: CustomPaint(
painter: BubblePainter(
color: widget.isMe ? null : receivedBg,
gradient: widget.isMe
? LinearGradient(
colors: sentGradients,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: sentGradients.length == 3 ? [0.0, 0.5, 1.0] : null,
)
: (!widget.isMe && widget.isSecure && !isLocked
? const LinearGradient(
colors: [Color(0xFF1A1A2E), Color(0xFF16213E)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null),
isMe: widget.isMe,
isSecure: widget.isSecure,
),
child: Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.78),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.isSecure)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isLocked
? Icons.security_rounded
: Icons.verified_user_rounded,
size: 14,
color: widget.isMe
? Colors.white70
: (isLocked
? const Color(0xFF00E5FF)
: const Color(0xFF00D2FF)),
),
const SizedBox(width: 6),
Expanded(
child: Text(
isLocked
? "پیام رمزگذاری شده (نیاز به کلید)"
: (widget.isPendingMultipart
? "در حال دریافت قطعات پیام..."
: (widget.packetMode == 'SYM'
? "رمزنگاری متقارن (AES-256)"
: (widget.packetMode == 'AE'
? "رمزنگاری نامتقارن (ECC)"
: "پیام امن تایید شده"))),
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.isMe
? Colors.white.withValues(alpha: 0.8)
: (isLocked
? const Color(0xFF00E5FF)
: const Color(0xFF00D2FF)),
fontSize: 10,
letterSpacing: 0.2,
),
),
),
],
),
),
if (widget.isPendingMultipart)
Shimmer.fromColors(
baseColor: Colors.white.withValues(alpha: 0.1),
highlightColor: Colors.white.withValues(alpha: 0.3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(height: 14, width: double.infinity, decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 6),
Container(height: 14, width: 150, decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(4))),
],
),
)
else
Directionality(
textDirection: TextDirection.rtl,
child: Text(
_displayBody,
style: TextStyle(
color: widget.isMe
? Colors.white
: Colors.white.withValues(alpha: 0.95),
fontSize: 15.5,
fontWeight: isLocked ? FontWeight.w700 : FontWeight.w400,
height: 1.5,
fontFamily: _showRaw ? "monospace" : null,
),
),
),
if (isLocked) ...[
const SizedBox(height: 12),
InkWell(
onTap: widget.onRetryDecryption,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF00E5FF).withValues(alpha: 0.3), width: 1),
),
child: const Row(
children: [
Icon(Icons.vpn_key_rounded, size: 18, color: Color(0xFF00E5FF)),
SizedBox(width: 10),
Expanded(
child: Text(
"برای بازگشایی ضربه بزنید",
style: TextStyle(fontSize: 12, color: Color(0xFF00E5FF), fontWeight: FontWeight.bold),
),
),
Icon(Icons.chevron_right_rounded, color: Color(0xFF00E5FF)),
],
),
),
),
],
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.isSecure && _hasRawView)
Padding(
padding: const EdgeInsets.only(right: 8),
child: InkWell(
onTap: () => setState(() => _showRaw = !_showRaw),
child: Text(
_showRaw ? "متن اصلی" : "دیتای خام",
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
color: widget.isMe ? Colors.white70 : const Color(0xFF00D2FF),
),
),
),
),
if (widget.statusLabel != null)
Flexible(
child: Text(
widget.statusLabel!,
style: TextStyle(fontSize: 9, color: widget.isMe ? Colors.white60 : Colors.white38),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
Text(
_formatTime(widget.date),
style: TextStyle(fontSize: 10, color: widget.isMe ? Colors.white60 : Colors.white38),
),
if (widget.isMe) ...[
const SizedBox(width: 4),
_buildStatusIcon(),
]
],
),
],
),
),
),
),
],
),
),
),
);
}
Widget _buildStatusIcon() {
switch (widget.status) {
case MessageStatus.sending:
return const SizedBox(
width: 8,
height: 8,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 1,
),
);
case MessageStatus.sent:
return const Icon(Icons.done_all, size: 12, color: Colors.white70);
case MessageStatus.failed:
return const Icon(Icons.error_outline, size: 12, color: Colors.white);
default:
return const SizedBox();
}
}
String _formatTime(int millis) {
final d = DateTime.fromMillisecondsSinceEpoch(millis);
return "${d.hour}:${d.minute.toString().padLeft(2, '0')}";
}
}
class BubblePainter extends CustomPainter {
final Color? color;
final Gradient? gradient;
final bool isMe;
final bool isSecure;
BubblePainter({
this.color,
this.gradient,
required this.isMe,
this.isSecure = false,
});
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..style = PaintingStyle.fill;
const double tailWidth = 10.0;
const double tailHeight = 12.0;
if (gradient != null) {
paint.shader = gradient!.createShader(Rect.fromLTWH(0, 0, size.width, size.height));
} else if (color != null) {
paint.color = color!;
}
const double R = 22.0; // Standard radius
final Path path = Path();
if (isMe) {
// Sent Message: Tail on bottom-right
path.moveTo(R, 0);
path.lineTo(size.width - R, 0);
path.quadraticBezierTo(size.width, 0, size.width, R);
path.lineTo(size.width, size.height - tailHeight - R);
path.quadraticBezierTo(size.width, size.height - tailHeight, size.width - tailWidth, size.height - tailHeight);
path.lineTo(size.width, size.height); // Pointy tail tip
path.lineTo(size.width - tailWidth - R, size.height - tailHeight);
path.lineTo(R, size.height - tailHeight);
path.quadraticBezierTo(0, size.height - tailHeight, 0, size.height - tailHeight - R);
path.lineTo(0, R);
path.quadraticBezierTo(0, 0, R, 0);
path.close();
} else {
// Received Message: Tail on bottom-left
path.moveTo(tailWidth + R, 0);
path.lineTo(size.width - R, 0);
path.quadraticBezierTo(size.width, 0, size.width, R);
path.lineTo(size.width, size.height - tailHeight - R);
path.quadraticBezierTo(size.width, size.height - tailHeight, size.width - R, size.height - tailHeight);
path.lineTo(tailWidth + R, size.height - tailHeight);
path.lineTo(0, size.height); // Pointy tail tip
path.lineTo(tailWidth, size.height - tailHeight);
path.lineTo(tailWidth, R);
path.quadraticBezierTo(tailWidth, 0, tailWidth + R, 0);
path.close();
}
// --- Enhanced Glowing Shadows ---
if (isSecure) {
final Color shadowColor = isMe ? const Color(0xFF7000FF) : const Color(0xFF00D2FF);
// Layered glow effect
for (int i = 1; i <= 3; i++) {
canvas.drawShadow(
path.shift(Offset(0, 1.0 * i)),
shadowColor.withValues(alpha: 0.15 / i),
4.0 * i,
false,
);
}
} else {
canvas.drawShadow(
path.shift(const Offset(0, 3)),
Colors.black.withValues(alpha: 0.4),
6.0,
false,
);
}
canvas.drawPath(path, paint);
// --- Subtle Inner Border for Glass Effect ---
if (!isMe) {
final Paint borderPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.5
..color = Colors.white.withValues(alpha: 0.15);
canvas.drawPath(path, borderPaint);
}
}
@override
bool shouldRepaint(covariant BubblePainter oldDelegate) =>
oldDelegate.color != color ||
oldDelegate.gradient != gradient ||
oldDelegate.isMe != isMe ||
oldDelegate.isSecure != isSecure;
}
// Removed incorrect ColorExt

1
linux/.gitignore vendored Normal file
View File

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

128
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "saba_secure_sms")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.saba_secure_sms")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

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