first commit
45
.gitignore
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
49
android/app/build.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
148
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||||
23
android/app/src/main/res/drawable/ic_calc.xml
Normal 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>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
4
android/app/src/main/res/values-fa/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">صبا</string>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
4
android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Saba</string>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal 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
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
663
android/build/reports/problems/problems-report.html
Normal file
2
android/gradle.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||||
26
android/settings.gradle.kts
Normal 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
|
|
@ -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
|
||||||
24
ios/Flutter/AppFrameworkInfo.plist
Normal 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>
|
||||||
1
ios/Flutter/Debug.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
1
ios/Flutter/Release.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
620
ios/Runner.xcodeproj/project.pbxproj
Normal 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 */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal 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>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
16
ios/Runner/AppDelegate.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal 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>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal 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
|
|
@ -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>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
6
ios/Runner/SceneDelegate.swift
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal 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
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lib/models/chat_model.dart
Normal 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
773
lib/screens/compose_screen.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
931
lib/screens/group_chat_screen.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
771
lib/screens/home_screen.dart
Normal 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]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
803
lib/screens/settings_screen.dart
Normal 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;
|
||||||
|
}
|
||||||
438
lib/screens/splash_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
lib/utils/app_lock_service.dart
Normal 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
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/utils/contact_helper.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
lib/utils/crypto_helper.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1124
lib/utils/database_helper.dart
Normal file
168
lib/utils/message_processor.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
67
lib/utils/notification_helper.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/utils/phone_helper.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
467
lib/utils/protocol_helper.dart
Normal 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};
|
||||||
|
}
|
||||||
|
}
|
||||||
490
lib/utils/secure_crypto_helper.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1198
lib/utils/secure_messaging_service.dart
Normal file
306
lib/widgets/app_lock_overlay.dart
Normal 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;
|
||||||
|
}
|
||||||
439
lib/widgets/message_bubble.dart
Normal 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
|
|
@ -0,0 +1 @@
|
||||||
|
flutter/ephemeral
|
||||||
128
linux/CMakeLists.txt
Normal 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()
|
||||||
88
linux/flutter/CMakeLists.txt
Normal 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}
|
||||||
|
)
|
||||||