Initial Commit

This commit is contained in:
ivan2282
2025-11-15 20:06:40 +03:00
commit 205d11df0d
233 changed files with 52572 additions and 0 deletions

45
.gitignore vendored Normal file
View File

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

45
.metadata Normal file
View File

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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 needle10
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
README.md Normal file
View File

@@ -0,0 +1,18 @@
<center><img src="assets/icon/komet_512.png" width="100"></center>
<center><h1>Komet Messenger</h1>
<h3>An Open Source alternative unnoficial client for MAX</h3></center>
<center><a href="Telegram Group">Telegram Group</a></center>
## How to install?
### Download file from releases tab or our telegram group and install it
## How to build?
### This is app built on flutter, use flutter guide
## How to countibute?
### Create a fork, do everything
### And create pull requeste
### Make sure your commits looks like:
<code>fix: something went worng when user...</code>
<code>add: search by id</code>
<code>edit: refactored something</code>
<code>Other actions should marked as "other:" and discribes what you did</code>

28
analysis_options.yaml Normal file
View File

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

14
android/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,87 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.gwid.app.gwid"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
}
}
defaultConfig {
applicationId = "com.gwid.app.gwid"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
val keyPropertiesFile = rootProject.file("key.properties")
val keyProperties = Properties()
signingConfigs {
create("release") {
val envStoreFile = System.getenv("RELEASE_STORE_FILE")
val envStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
val envKeyAlias = System.getenv("RELEASE_KEY_ALIAS")
val envKeyPassword = System.getenv("RELEASE_KEY_PASSWORD")
if (envStoreFile != null && envStorePassword != null &&
envKeyAlias != null && envKeyPassword != null) {
storeFile = file(envStoreFile)
storePassword = envStorePassword
keyAlias = envKeyAlias
keyPassword = envKeyPassword
} else if (keyPropertiesFile.exists()) {
keyProperties.load(FileInputStream(keyPropertiesFile))
storeFile = file(keyProperties["storeFile"] as String? ?: "")
storePassword = keyProperties["storePassword"] as String? ?: ""
keyAlias = keyProperties["keyAlias"] as String? ?: ""
keyPassword = keyProperties["keyPassword"] as String? ?: ""
}
}
}
buildTypes {
getByName("release") {
// Only use release signing if keys are available
if (file(keyPropertiesFile).exists() ||
System.getenv("RELEASE_STORE_FILE") != null) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}

11
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,11 @@
# Flutter-специфичные правила (у вас уже должны быть)
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.** { *; }
# Игнорировать предупреждения о недостающих классах Play Core
-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
-dontwarn com.google.android.play.core.splitinstall.**
-dontwarn com.google.android.play.core.tasks.**

View File

@@ -0,0 +1,11 @@
<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"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

View File

@@ -0,0 +1,59 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:label="Komet"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
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"
/>
<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.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="max.ru" />
<data android:pathPrefix="/join/" />
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.gwid.app.gwid
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

View File

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

View File

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

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

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

BIN
assets/icon/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
assets/icon/icon1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
assets/icon/komet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
assets/icon/komet_512.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
assets/icon/komet_512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/images/komet_512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/images/meteor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
assets/images/spermum.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

34
ios/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,26 @@
<?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>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

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

View File

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

View File

@@ -0,0 +1,616 @@
// !$*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 */; };
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>"; };
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 */,
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 */,
);
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.gwid.app.gwid;
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.gwid.app.gwid.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.gwid.app.gwid.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.gwid.app.gwid.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.gwid.app.gwid;
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.gwid.app.gwid;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

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

@@ -0,0 +1,49 @@
<?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>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gwid</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>gwid</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>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>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

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

2860
lib/api_service.dart Normal file

File diff suppressed because it is too large Load Diff

774
lib/api_service_simple.dart Normal file
View File

@@ -0,0 +1,774 @@
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'connection/connection_manager_simple.dart';
import 'connection/connection_logger.dart';
import 'connection/connection_state.dart';
import 'connection/health_monitor.dart';
import 'models/message.dart';
import 'models/contact.dart';
class ApiServiceSimple {
ApiServiceSimple._privateConstructor();
static final ApiServiceSimple instance =
ApiServiceSimple._privateConstructor();
final ConnectionManagerSimple _connectionManager = ConnectionManagerSimple();
final ConnectionLogger _logger = ConnectionLogger();
String? _authToken;
bool _isInitialized = false;
final Map<int, List<Message>> _messageCache = {};
final Map<int, Contact> _contactCache = {};
Map<String, dynamic>? _lastChatsPayload;
DateTime? _lastChatsAt;
final Duration _chatsCacheTtl = const Duration(seconds: 5);
bool _chatsFetchedInThisSession = false;
final Map<String, dynamic> _presenceData = {};
final StreamController<Contact> _contactUpdatesController =
StreamController<Contact>.broadcast();
final StreamController<Map<String, dynamic>> _messageController =
StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get messages => _messageController.stream;
Stream<Contact> get contactUpdates => _contactUpdatesController.stream;
Stream<ConnectionInfo> get connectionState => _connectionManager.stateStream;
Stream<LogEntry> get logs => _connectionManager.logStream;
Stream<HealthMetrics> get healthMetrics =>
_connectionManager.healthMetricsStream;
ConnectionInfo get currentState => _connectionManager.currentState;
bool get isOnline => _connectionManager.isConnected;
bool get canSendMessages => _connectionManager.canSendMessages;
Future<void> initialize() async {
if (_isInitialized) {
_logger.logConnection('ApiServiceSimple уже инициализирован');
return;
}
_logger.logConnection('Инициализация ApiServiceSimple');
try {
await _connectionManager.initialize();
_setupMessageHandlers();
_isInitialized = true;
_logger.logConnection('ApiServiceSimple успешно инициализирован');
} catch (e) {
_logger.logError('Ошибка инициализации ApiServiceSimple', error: e);
rethrow;
}
}
void _setupMessageHandlers() {
_connectionManager.messageStream.listen((message) {
_handleIncomingMessage(message);
});
}
void _handleIncomingMessage(Map<String, dynamic> message) {
try {
_logger.logMessage('IN', message);
if (message['opcode'] == 128 && message['payload'] != null) {
_handleContactUpdate(message['payload']);
}
if (message['opcode'] == 129 && message['payload'] != null) {
_handlePresenceUpdate(message['payload']);
}
_messageController.add(message);
} catch (e) {
_logger.logError(
'Ошибка обработки входящего сообщения',
data: {'message': message, 'error': e.toString()},
);
}
}
void _handleContactUpdate(Map<String, dynamic> payload) {
try {
final contact = Contact.fromJson(payload);
_contactCache[contact.id] = contact;
_contactUpdatesController.add(contact);
_logger.logConnection(
'Контакт обновлен',
data: {'contact_id': contact.id, 'contact_name': contact.name},
);
} catch (e) {
_logger.logError(
'Ошибка обработки обновления контакта',
data: {'payload': payload, 'error': e.toString()},
);
}
}
void _handlePresenceUpdate(Map<String, dynamic> payload) {
try {
_presenceData.addAll(payload);
_logger.logConnection(
'Presence данные обновлены',
data: {'keys': payload.keys.toList()},
);
} catch (e) {
_logger.logError(
'Ошибка обработки presence данных',
data: {'payload': payload, 'error': e.toString()},
);
}
}
Future<void> connect() async {
_logger.logConnection('Запрос подключения к серверу');
try {
await _connectionManager.connect(authToken: _authToken);
_logger.logConnection('Подключение к серверу успешно');
} catch (e) {
_logger.logError('Ошибка подключения к серверу', error: e);
rethrow;
}
}
Future<void> reconnect() async {
_logger.logConnection('Запрос переподключения');
try {
await _connectionManager.connect(authToken: _authToken);
_logger.logConnection('Переподключение успешно');
} catch (e) {
_logger.logError('Ошибка переподключения', error: e);
rethrow;
}
}
Future<void> disconnect() async {
_logger.logConnection('Отключение от сервера');
try {
await _connectionManager.disconnect();
_logger.logConnection('Отключение от сервера успешно');
} catch (e) {
_logger.logError('Ошибка отключения', error: e);
}
}
int _sendMessage(int opcode, Map<String, dynamic> payload) {
if (!canSendMessages) {
_logger.logConnection(
'Сообщение не отправлено - соединение не готово',
data: {'opcode': opcode, 'payload': payload},
);
return -1;
}
try {
final seq = _connectionManager.sendMessage(opcode, payload);
_logger.logConnection(
'Сообщение отправлено',
data: {'opcode': opcode, 'seq': seq, 'payload': payload},
);
return seq;
} catch (e) {
_logger.logError(
'Ошибка отправки сообщения',
data: {'opcode': opcode, 'payload': payload, 'error': e.toString()},
);
return -1;
}
}
Future<void> sendHandshake() async {
_logger.logConnection('Отправка handshake');
final payload = {
"userAgent": {
"deviceType": "WEB",
"locale": "ru",
"deviceLocale": "ru",
"osVersion": "Windows",
"deviceName": "Chrome",
"headerUserAgent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"appVersion": "25.9.15",
"screen": "1920x1080 1.0x",
"timezone": "Europe/Moscow",
},
"deviceId": _generateDeviceId(),
};
_sendMessage(6, payload);
}
void requestOtp(String phoneNumber) {
_logger.logConnection('Запрос OTP', data: {'phone': phoneNumber});
final payload = {
"phone": phoneNumber,
"type": "START_AUTH",
"language": "ru",
};
_sendMessage(17, payload);
}
void verifyCode(String token, String code) {
_logger.logConnection(
'Проверка кода',
data: {'token': token, 'code': code},
);
final payload = {
"token": token,
"verifyCode": code,
"authTokenType": "CHECK_CODE",
};
_sendMessage(18, payload);
}
Future<Map<String, dynamic>> authenticateWithToken(String token) async {
_logger.logConnection('Аутентификация с токеном');
_authToken = token;
await saveToken(token);
final payload = {"interactive": true, "token": token, "chatsCount": 100};
final seq = _sendMessage(19, payload);
try {
final response = await messages
.firstWhere((msg) => msg['seq'] == seq)
.timeout(const Duration(seconds: 30));
_logger.logConnection(
'Аутентификация успешна',
data: {'seq': seq, 'response_cmd': response['cmd']},
);
return response['payload'] ?? {};
} catch (e) {
_logger.logError(
'Ошибка аутентификации',
data: {'token': token, 'error': e.toString()},
);
rethrow;
}
}
Future<Map<String, dynamic>> getChatsAndContacts({bool force = false}) async {
_logger.logConnection('Запрос чатов и контактов', data: {'force': force});
if (!force && _lastChatsPayload != null && _lastChatsAt != null) {
if (DateTime.now().difference(_lastChatsAt!) < _chatsCacheTtl) {
_logger.logConnection('Возвращаем данные из кэша');
return _lastChatsPayload!;
}
}
try {
final payload = {"chatsCount": 100};
final seq = _sendMessage(48, payload);
final response = await messages
.firstWhere((msg) => msg['seq'] == seq)
.timeout(const Duration(seconds: 30));
final List<dynamic> chatListJson = response['payload']?['chats'] ?? [];
if (chatListJson.isEmpty) {
final result = {'chats': [], 'contacts': [], 'profile': null};
_lastChatsPayload = result;
_lastChatsAt = DateTime.now();
return result;
}
final contactIds = <int>{};
for (var chatJson in chatListJson) {
final participants =
chatJson['participants'] as Map<String, dynamic>? ?? {};
contactIds.addAll(participants.keys.map((id) => int.parse(id)));
}
final contactSeq = _sendMessage(32, {"contactIds": contactIds.toList()});
final contactResponse = await messages
.firstWhere((msg) => msg['seq'] == contactSeq)
.timeout(const Duration(seconds: 30));
final List<dynamic> contactListJson =
contactResponse['payload']?['contacts'] ?? [];
final result = {
'chats': chatListJson,
'contacts': contactListJson,
'profile': null,
'presence': null,
};
_lastChatsPayload = result;
_lastChatsAt = DateTime.now();
_chatsFetchedInThisSession = true;
final contacts = contactListJson
.map((json) => Contact.fromJson(json))
.toList();
updateContactCache(contacts);
_logger.logConnection(
'Чаты и контакты получены',
data: {
'chats_count': chatListJson.length,
'contacts_count': contactListJson.length,
},
);
return result;
} catch (e) {
_logger.logError('Ошибка получения чатов и контактов', error: e);
rethrow;
}
}
Future<List<Message>> getMessageHistory(
int chatId, {
bool force = false,
}) async {
_logger.logConnection(
'Запрос истории сообщений',
data: {'chat_id': chatId, 'force': force},
);
if (!force && _messageCache.containsKey(chatId)) {
_logger.logConnection('История сообщений загружена из кэша');
return _messageCache[chatId]!;
}
try {
final payload = {
"chatId": chatId,
"from": DateTime.now()
.add(const Duration(days: 1))
.millisecondsSinceEpoch,
"forward": 0,
"backward": 1000,
"getMessages": true,
};
final seq = _sendMessage(49, payload);
final response = await messages
.firstWhere((msg) => msg['seq'] == seq)
.timeout(const Duration(seconds: 30));
if (response['cmd'] == 3) {
final error = response['payload'];
_logger.logError(
'Ошибка получения истории сообщений',
data: {'chat_id': chatId, 'error': error},
);
throw Exception('Ошибка получения истории: ${error['message']}');
}
final List<dynamic> messagesJson = response['payload']?['messages'] ?? [];
final messagesList =
messagesJson.map((json) => Message.fromJson(json)).toList()
..sort((a, b) => a.time.compareTo(b.time));
_messageCache[chatId] = messagesList;
_logger.logConnection(
'История сообщений получена',
data: {'chat_id': chatId, 'messages_count': messagesList.length},
);
return messagesList;
} catch (e) {
_logger.logError(
'Ошибка получения истории сообщений',
data: {'chat_id': chatId, 'error': e.toString()},
);
return [];
}
}
void sendMessage(int chatId, String text, {String? replyToMessageId}) {
_logger.logConnection(
'Отправка сообщения',
data: {
'chat_id': chatId,
'text_length': text.length,
'reply_to': replyToMessageId,
},
);
final int clientMessageId = DateTime.now().millisecondsSinceEpoch;
final payload = {
"chatId": chatId,
"message": {
"text": text,
"cid": clientMessageId,
"elements": [],
"attaches": [],
if (replyToMessageId != null)
"link": {"type": "REPLY", "messageId": replyToMessageId},
},
"notify": true,
};
clearChatsCache();
_sendMessage(64, payload);
}
Future<void> sendPhotoMessage(
int chatId, {
String? localPath,
String? caption,
int? cidOverride,
int? senderId,
}) async {
_logger.logConnection(
'Отправка фото',
data: {'chat_id': chatId, 'local_path': localPath, 'caption': caption},
);
try {
XFile? image;
if (localPath != null) {
image = XFile(localPath);
} else {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
if (image == null) return;
}
final seq80 = _sendMessage(80, {"count": 1});
final resp80 = await messages
.firstWhere((m) => m['seq'] == seq80)
.timeout(const Duration(seconds: 30));
final String uploadUrl = resp80['payload']['url'];
var request = http.MultipartRequest('POST', Uri.parse(uploadUrl));
request.files.add(await http.MultipartFile.fromPath('file', image.path));
var streamed = await request.send();
var httpResp = await http.Response.fromStream(streamed);
if (httpResp.statusCode != 200) {
throw Exception(
'Ошибка загрузки фото: ${httpResp.statusCode} ${httpResp.body}',
);
}
final uploadJson = jsonDecode(httpResp.body) as Map<String, dynamic>;
final Map photos = uploadJson['photos'] as Map;
if (photos.isEmpty) throw Exception('Не получен токен фото');
final String photoToken = (photos.values.first as Map)['token'];
final int cid = cidOverride ?? DateTime.now().millisecondsSinceEpoch;
final payload = {
"chatId": chatId,
"message": {
"text": caption?.trim() ?? "",
"cid": cid,
"elements": [],
"attaches": [
{"_type": "PHOTO", "photoToken": photoToken},
],
},
"notify": true,
};
clearChatsCache();
_sendMessage(64, payload);
_logger.logConnection(
'Фото отправлено',
data: {'chat_id': chatId, 'photo_token': photoToken},
);
} catch (e) {
_logger.logError(
'Ошибка отправки фото',
data: {'chat_id': chatId, 'error': e.toString()},
);
}
}
Future<void> blockContact(int contactId) async {
_logger.logConnection(
'Блокировка контакта',
data: {'contact_id': contactId},
);
_sendMessage(34, {'contactId': contactId, 'action': 'BLOCK'});
}
Future<void> unblockContact(int contactId) async {
_logger.logConnection(
'Разблокировка контакта',
data: {'contact_id': contactId},
);
_sendMessage(34, {'contactId': contactId, 'action': 'UNBLOCK'});
}
void getBlockedContacts() {
_logger.logConnection('Запрос заблокированных контактов');
_sendMessage(36, {'status': 'BLOCKED', 'count': 100, 'from': 0});
}
void createGroup(String name, List<int> participantIds) {
_logger.logConnection(
'Создание группы',
data: {'name': name, 'participants': participantIds},
);
final payload = {"name": name, "participantIds": participantIds};
_sendMessage(48, payload);
}
void addGroupMember(
int chatId,
List<int> userIds, {
bool showHistory = true,
}) {
_logger.logConnection(
'Добавление участника в группу',
data: {'chat_id': chatId, 'user_ids': userIds},
);
final payload = {
"chatId": chatId,
"userIds": userIds,
"showHistory": showHistory,
"operation": "add",
};
_sendMessage(77, payload);
}
void removeGroupMember(
int chatId,
List<int> userIds, {
int cleanMsgPeriod = 0,
}) {
_logger.logConnection(
'Удаление участника из группы',
data: {'chat_id': chatId, 'user_ids': userIds},
);
final payload = {
"chatId": chatId,
"userIds": userIds,
"operation": "remove",
"cleanMsgPeriod": cleanMsgPeriod,
};
_sendMessage(77, payload);
}
void leaveGroup(int chatId) {
_logger.logConnection('Выход из группы', data: {'chat_id': chatId});
_sendMessage(58, {"chatId": chatId});
}
void sendReaction(int chatId, String messageId, String emoji) {
_logger.logConnection(
'Отправка реакции',
data: {'chat_id': chatId, 'message_id': messageId, 'emoji': emoji},
);
final payload = {
"chatId": chatId,
"messageId": messageId,
"reaction": {"reactionType": "EMOJI", "id": emoji},
};
_sendMessage(178, payload);
}
void removeReaction(int chatId, String messageId) {
_logger.logConnection(
'Удаление реакции',
data: {'chat_id': chatId, 'message_id': messageId},
);
final payload = {"chatId": chatId, "messageId": messageId};
_sendMessage(179, payload);
}
void sendTyping(int chatId, {String type = "TEXT"}) {
final payload = {"chatId": chatId, "type": type};
_sendMessage(65, payload);
}
DateTime? getLastSeen(int userId) {
final userPresence = _presenceData[userId.toString()];
if (userPresence != null && userPresence['seen'] != null) {
final seenTimestamp = userPresence['seen'] as int;
return DateTime.fromMillisecondsSinceEpoch(seenTimestamp * 1000);
}
return null;
}
void updateContactCache(List<Contact> contacts) {
_contactCache.clear();
for (final contact in contacts) {
_contactCache[contact.id] = contact;
}
_logger.logConnection(
'Кэш контактов обновлен',
data: {'contacts_count': contacts.length},
);
}
Contact? getCachedContact(int contactId) {
return _contactCache[contactId];
}
void clearChatsCache() {
_lastChatsPayload = null;
_lastChatsAt = null;
_chatsFetchedInThisSession = false;
_logger.logConnection('Кэш чатов очищен');
}
void clearMessageCache(int chatId) {
_messageCache.remove(chatId);
_logger.logConnection('Кэш сообщений очищен', data: {'chat_id': chatId});
}
void clearAllCaches() {
_messageCache.clear();
_contactCache.clear();
clearChatsCache();
_logger.logConnection('Все кэши очищены');
}
Future<void> saveToken(String token) async {
_authToken = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('authToken', token);
_logger.logConnection('Токен сохранен');
}
Future<bool> hasToken() async {
final prefs = await SharedPreferences.getInstance();
_authToken = prefs.getString('authToken');
return _authToken != null;
}
Future<void> logout() async {
_logger.logConnection('Выход из системы');
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('authToken');
_authToken = null;
clearAllCaches();
await disconnect();
_logger.logConnection('Выход из системы выполнен');
} catch (e) {
_logger.logError('Ошибка при выходе из системы', error: e);
}
}
String _generateDeviceId() {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final random = (timestamp % 1000000).toString().padLeft(6, '0');
return "$timestamp$random";
}
Map<String, dynamic> getStatistics() {
return {
'api_service': {
'is_initialized': _isInitialized,
'has_auth_token': _authToken != null,
'message_cache_size': _messageCache.length,
'contact_cache_size': _contactCache.length,
'chats_fetched_in_session': _chatsFetchedInThisSession,
},
'connection': _connectionManager.getStatistics(),
};
}
void dispose() {
_logger.logConnection('Освобождение ресурсов ApiServiceSimple');
_connectionManager.dispose();
_messageController.close();
_contactUpdatesController.close();
}
}

1131
lib/api_service_v2.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,431 @@
import 'package:flutter/material.dart';
import 'package:gwid/services/cache_service.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/services/chat_cache_service.dart';
class CacheManagementScreen extends StatefulWidget {
const CacheManagementScreen({super.key});
@override
State<CacheManagementScreen> createState() => _CacheManagementScreenState();
}
class _CacheManagementScreenState extends State<CacheManagementScreen> {
Map<String, dynamic> _cacheStats = {};
Map<String, dynamic> _avatarCacheStats = {};
Map<String, dynamic> _chatCacheStats = {};
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCacheStats();
}
Future<void> _loadCacheStats() async {
setState(() {
_isLoading = true;
});
try {
final cacheService = CacheService();
final avatarService = AvatarCacheService();
final chatService = ChatCacheService();
await cacheService.initialize();
await chatService.initialize();
await avatarService.initialize();
final cacheStats = await cacheService.getCacheStats();
final avatarStats = await avatarService.getAvatarCacheStats();
final chatStats = await chatService.getChatCacheStats();
if (!mounted) return;
setState(() {
_cacheStats = cacheStats;
_avatarCacheStats = avatarStats;
_chatCacheStats = chatStats;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка загрузки статистики кэша: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
Future<void> _clearAllCache() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Очистить весь кэш?'),
content: const Text(
'Это действие удалит все кэшированные данные, включая чаты, сообщения и аватарки. '
'Приложение будет работать медленнее до повторной загрузки данных.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
child: const Text('Очистить'),
),
],
),
);
if (confirmed == true) {
try {
final cacheService = CacheService();
final avatarService = AvatarCacheService();
final chatService = ChatCacheService();
await cacheService.initialize();
await chatService.initialize();
await avatarService.initialize();
await cacheService.clear();
await avatarService.clearAvatarCache();
await chatService.clearAllChatCache();
await _loadCacheStats();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Весь кэш очищен'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка очистки кэша: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
}
}
Future<void> _clearAvatarCache() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Очистить кэш аватарок?'),
content: const Text(
'Это действие удалит все кэшированные аватарки. '
'Они будут загружены заново при следующем просмотре.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Очистить'),
),
],
),
);
if (confirmed == true) {
try {
final avatarService = AvatarCacheService();
await avatarService.initialize();
await avatarService.clearAvatarCache();
await _loadCacheStats();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Кэш аватарок очищен'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка очистки кэша аватарок: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
}
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
title,
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildCacheSection(String title, Map<String, dynamic>? data) {
if (data == null || data.isEmpty) {
return const SizedBox.shrink();
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
...data.entries.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key),
Text(
entry.value.toString(),
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text("Управление кэшем"),
actions: [
IconButton(
onPressed: _loadCacheStats,
icon: const Icon(Icons.refresh),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
"Общая статистика",
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 12),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
_buildStatCard(
"Чаты в кэше",
_chatCacheStats['cachedChats']?.toString() ?? "0",
Icons.chat,
colors.primary,
),
_buildStatCard(
"Контакты в кэше",
_chatCacheStats['cachedContacts']?.toString() ?? "0",
Icons.contacts,
colors.secondary,
),
_buildStatCard(
"Аватарки в памяти",
_avatarCacheStats['memoryImages']?.toString() ?? "0",
Icons.person,
colors.tertiary,
),
_buildStatCard(
"Размер кэша",
"${_avatarCacheStats['diskSizeMB'] ?? "0"} МБ",
Icons.storage,
colors.error,
),
],
),
const SizedBox(height: 24),
Text(
"Детальная статистика",
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 12),
_buildCacheSection("Кэш чатов", _chatCacheStats['cacheStats']),
const SizedBox(height: 12),
_buildCacheSection("Кэш аватарок", _avatarCacheStats),
const SizedBox(height: 12),
_buildCacheSection("Общий кэш", _cacheStats),
const SizedBox(height: 12),
_buildCacheSection("Кэш в памяти", {
'Записей в памяти': _cacheStats['memoryEntries'] ?? 0,
'Максимум записей': _cacheStats['maxMemorySize'] ?? 0,
}),
const SizedBox(height: 32),
Text(
"Управление кэшем",
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 12),
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.delete_sweep),
title: const Text("Очистить кэш аватарок"),
subtitle: const Text(
"Удалить все кэшированные аватарки",
),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: _clearAvatarCache,
),
const Divider(height: 1),
ListTile(
leading: Icon(
Icons.delete_forever,
color: colors.error,
),
title: Text(
"Очистить весь кэш",
style: TextStyle(color: colors.error),
),
subtitle: const Text("Удалить все кэшированные данные"),
trailing: Icon(
Icons.chevron_right_rounded,
color: colors.error,
),
onTap: _clearAllCache,
),
],
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: colors.primary),
const SizedBox(width: 8),
Text(
"О кэшировании",
style: TextStyle(
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
],
),
const SizedBox(height: 12),
const Text(
"Кэширование ускоряет работу приложения, сохраняя часто используемые данные локально. "
"Чаты кэшируются на 1 час, контакты на 6 часов, сообщения на 2 часа, аватарки на 7 дней.",
),
const SizedBox(height: 8),
const Text(
"Очистка кэша может замедлить работу приложения до повторной загрузки данных.",
style: TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,342 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/models/channel.dart';
import 'package:gwid/search_channels_screen.dart';
class ChannelsListScreen extends StatefulWidget {
const ChannelsListScreen({super.key});
@override
State<ChannelsListScreen> createState() => _ChannelsListScreenState();
}
class _ChannelsListScreenState extends State<ChannelsListScreen> {
final TextEditingController _searchController = TextEditingController();
StreamSubscription? _apiSubscription;
bool _isLoading = false;
List<Channel> _channels = [];
String? _errorMessage;
@override
void initState() {
super.initState();
_listenToApiMessages();
_loadPopularChannels();
}
@override
void dispose() {
_searchController.dispose();
_apiSubscription?.cancel();
super.dispose();
}
void _listenToApiMessages() {
_apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return;
if (message['type'] == 'channels_found') {
setState(() {
_isLoading = false;
_errorMessage = null;
});
final payload = message['payload'];
final channelsData = payload['contacts'] as List<dynamic>?;
if (channelsData != null) {
_channels = channelsData
.map((channelJson) => Channel.fromJson(channelJson))
.toList();
}
}
if (message['type'] == 'channels_not_found') {
setState(() {
_isLoading = false;
_channels.clear();
});
final payload = message['payload'];
String errorMessage = 'Каналы не найдены';
if (payload != null) {
if (payload['localizedMessage'] != null) {
errorMessage = payload['localizedMessage'];
} else if (payload['message'] != null) {
errorMessage = payload['message'];
}
}
setState(() {
_errorMessage = errorMessage;
});
}
});
}
void _loadPopularChannels() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
await ApiService.instance.searchChannels('каналы');
} catch (e) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка загрузки каналов: ${e.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
}
}
void _searchChannels() async {
final searchQuery = _searchController.text.trim();
if (searchQuery.isEmpty) {
_loadPopularChannels();
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
await ApiService.instance.searchChannels(searchQuery);
} catch (e) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка поиска каналов: ${e.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
}
}
void _viewChannel(Channel channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChannelDetailsScreen(channel: channel),
),
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Каналы'),
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SearchChannelsScreen(),
),
);
},
),
],
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Поиск каналов...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_loadPopularChannels();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onSubmitted: (_) => _searchChannels(),
onChanged: (value) {
setState(() {});
},
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _channels.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broadcast_on_personal,
size: 64,
color: colors.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
_errorMessage ?? 'Каналы не найдены',
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 16,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadPopularChannels,
child: const Text('Обновить'),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _channels.length,
itemBuilder: (context, index) {
final channel = _channels[index];
return _buildChannelCard(channel);
},
),
),
],
),
);
}
Widget _buildChannelCard(Channel channel) {
final colors = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: CircleAvatar(
radius: 24,
backgroundImage: channel.photoBaseUrl != null
? NetworkImage(channel.photoBaseUrl!)
: null,
child: channel.photoBaseUrl == null
? Text(
channel.name.isNotEmpty ? channel.name[0].toUpperCase() : '?',
style: TextStyle(
color: colors.onSurface,
fontWeight: FontWeight.w600,
),
)
: null,
),
title: Text(
channel.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (channel.description?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Text(
channel.description!,
style: TextStyle(color: colors.onSurfaceVariant, fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 4,
children: [
if (channel.options.contains('BOT'))
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: colors.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Бот',
style: TextStyle(
color: colors.onPrimaryContainer,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
if (channel.options.contains('HAS_WEBAPP'))
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: colors.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Веб-приложение',
style: TextStyle(
color: colors.onSecondaryContainer,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: colors.onSurfaceVariant,
),
onTap: () => _viewChannel(channel),
),
);
}
}

4302
lib/chat_screen.dart Normal file

File diff suppressed because it is too large Load Diff

3324
lib/chats_screen.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
enum LogLevel { debug, info, warning, error, critical }
class ConnectionLogger {
static final ConnectionLogger _instance = ConnectionLogger._internal();
factory ConnectionLogger() => _instance;
ConnectionLogger._internal();
final List<LogEntry> _logs = [];
final StreamController<LogEntry> _logController =
StreamController<LogEntry>.broadcast();
Stream<LogEntry> get logStream => _logController.stream;
List<LogEntry> get logs => List.unmodifiable(_logs);
static const int maxLogs = 1000;
LogLevel _currentLevel = LogLevel.debug;
void setLogLevel(LogLevel level) {
_currentLevel = level;
}
void log(
String message, {
LogLevel level = LogLevel.info,
String? category,
Map<String, dynamic>? data,
Object? error,
StackTrace? stackTrace,
}) {
if (level.index < _currentLevel.index) return;
final entry = LogEntry(
timestamp: DateTime.now(),
level: level,
message: message,
category: category ?? 'CONNECTION',
data: data,
error: error,
stackTrace: stackTrace,
);
_logs.add(entry);
if (_logs.length > maxLogs) {
_logs.removeRange(0, _logs.length - maxLogs);
}
_logController.add(entry);
if (kDebugMode) {
final emoji = _getEmojiForLevel(level);
final timestamp = entry.timestamp.toIso8601String().substring(11, 23);
final categoryStr = category != null ? '[$category]' : '';
final dataStr = data != null ? ' | Data: ${jsonEncode(data)}' : '';
final errorStr = error != null ? ' | Error: $error' : '';
print('$emoji [$timestamp] $categoryStr $message$dataStr$errorStr');
}
}
void logConnection(
String message, {
Map<String, dynamic>? data,
Object? error,
}) {
log(
message,
level: LogLevel.info,
category: 'CONNECTION',
data: data,
error: error,
);
}
void logError(
String message, {
Map<String, dynamic>? data,
Object? error,
StackTrace? stackTrace,
}) {
log(
message,
level: LogLevel.error,
category: 'ERROR',
data: data,
error: error,
stackTrace: stackTrace,
);
}
void logMessage(
String direction,
dynamic message, {
Map<String, dynamic>? metadata,
}) {
final data = <String, dynamic>{
'direction': direction,
'message': message,
if (metadata != null) ...metadata,
};
log(
'WebSocket $direction',
level: LogLevel.debug,
category: 'WEBSOCKET',
data: data,
);
}
void logReconnect(int attempt, String reason, {Duration? delay}) {
final data = <String, dynamic>{
'attempt': attempt,
'reason': reason,
if (delay != null) 'delay_seconds': delay.inSeconds,
};
log(
'Переподключение: $reason (попытка $attempt)',
level: LogLevel.warning,
category: 'RECONNECT',
data: data,
);
}
void logPerformance(
String operation,
Duration duration, {
Map<String, dynamic>? metadata,
}) {
final data = <String, dynamic>{
'operation': operation,
'duration_ms': duration.inMilliseconds,
if (metadata != null) ...metadata,
};
log(
'Performance: $operation за ${duration.inMilliseconds}ms',
level: LogLevel.debug,
category: 'PERFORMANCE',
data: data,
);
}
void logState(String from, String to, {Map<String, dynamic>? metadata}) {
final data = <String, dynamic>{
'from': from,
'to': to,
if (metadata != null) ...metadata,
};
log(
'Состояние: $from$to',
level: LogLevel.info,
category: 'STATE',
data: data,
);
}
List<LogEntry> getLogsByCategory(String category) {
return _logs.where((log) => log.category == category).toList();
}
List<LogEntry> getLogsByLevel(LogLevel level) {
return _logs.where((log) => log.level == level).toList();
}
Map<String, int> getLogStats() {
final stats = <String, int>{};
for (final log in _logs) {
stats[log.category] = (stats[log.category] ?? 0) + 1;
}
return stats;
}
void clearLogs() {
_logs.clear();
log('Логи очищены', level: LogLevel.info, category: 'LOGGER');
}
String exportLogs() {
final logsJson = _logs.map((log) => log.toJson()).toList();
return jsonEncode(logsJson);
}
void importLogs(String jsonString) {
try {
final List<dynamic> logsList = jsonDecode(jsonString);
_logs.clear();
for (final logJson in logsList) {
_logs.add(LogEntry.fromJson(logJson));
}
log(
'Импортировано ${_logs.length} логов',
level: LogLevel.info,
category: 'LOGGER',
);
} catch (e) {
logError('Ошибка импорта логов', error: e);
}
}
String _getEmojiForLevel(LogLevel level) {
switch (level) {
case LogLevel.debug:
return '🔍';
case LogLevel.info:
return '';
case LogLevel.warning:
return '⚠️';
case LogLevel.error:
return '';
case LogLevel.critical:
return '🚨';
}
}
void dispose() {
_logController.close();
}
}
class LogEntry {
final DateTime timestamp;
final LogLevel level;
final String message;
final String category;
final Map<String, dynamic>? data;
final Object? error;
final StackTrace? stackTrace;
LogEntry({
required this.timestamp,
required this.level,
required this.message,
required this.category,
this.data,
this.error,
this.stackTrace,
});
Map<String, dynamic> toJson() {
return {
'timestamp': timestamp.toIso8601String(),
'level': level.name,
'message': message,
'category': category,
'data': data,
'error': error?.toString(),
'stackTrace': stackTrace?.toString(),
};
}
factory LogEntry.fromJson(Map<String, dynamic> json) {
return LogEntry(
timestamp: DateTime.parse(json['timestamp']),
level: LogLevel.values.firstWhere((l) => l.name == json['level']),
message: json['message'],
category: json['category'],
data: json['data'] != null
? Map<String, dynamic>.from(json['data'])
: null,
error: json['error'],
stackTrace: json['stackTrace'] != null
? StackTrace.fromString(json['stackTrace'])
: null,
);
}
@override
String toString() {
final emoji = _getEmojiForLevel(level);
final timestamp = this.timestamp.toIso8601String().substring(11, 23);
final dataStr = data != null ? ' | Data: ${jsonEncode(data)}' : '';
final errorStr = error != null ? ' | Error: $error' : '';
return '$emoji [$timestamp] [$category] $message$dataStr$errorStr';
}
String _getEmojiForLevel(LogLevel level) {
switch (level) {
case LogLevel.debug:
return '🔍';
case LogLevel.info:
return '';
case LogLevel.warning:
return '⚠️';
case LogLevel.error:
return '';
case LogLevel.critical:
return '🚨';
}
}
}

View File

@@ -0,0 +1,652 @@
import 'dart:async';
import 'dart:convert';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'connection_logger.dart';
import 'connection_state.dart';
import 'retry_strategy.dart';
import 'health_monitor.dart';
class ConnectionManager {
static final ConnectionManager _instance = ConnectionManager._internal();
factory ConnectionManager() => _instance;
ConnectionManager._internal();
final ConnectionLogger _logger = ConnectionLogger();
final ConnectionStateManager _stateManager = ConnectionStateManager();
final RetryManager _retryManager = RetryManager();
final HealthMonitor _healthMonitor = HealthMonitor();
IOWebSocketChannel? _channel;
StreamSubscription? _messageSubscription;
final List<String> _serverUrls = [
'wss://ws-api.oneme.ru:443/websocket',
'wss://ws-api.oneme.ru/websocket',
'wss://ws-api.oneme.ru:8443/websocket',
'ws://ws-api.oneme.ru:80/websocket',
'ws://ws-api.oneme.ru/websocket',
'ws://ws-api.oneme.ru:8080/websocket',
];
int _currentUrlIndex = 0;
String? _currentServerUrl;
bool _isConnecting = false;
bool _isDisposed = false;
int _sequenceNumber = 0;
String? _authToken;
final List<Map<String, dynamic>> _messageQueue = [];
Timer? _pingTimer;
Timer? _reconnectTimer;
final StreamController<Map<String, dynamic>> _messageController =
StreamController<Map<String, dynamic>>.broadcast();
final StreamController<String> _connectionStatusController =
StreamController<String>.broadcast();
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
Stream<String> get connectionStatusStream =>
_connectionStatusController.stream;
Stream<ConnectionInfo> get stateStream => _stateManager.stateStream;
Stream<LogEntry> get logStream => _logger.logStream;
Stream<HealthMetrics> get healthMetricsStream => _healthMonitor.metricsStream;
ConnectionInfo get currentState => _stateManager.currentInfo;
bool get isConnected => currentState.isActive;
bool get canSendMessages => currentState.canSendMessages;
Future<void> initialize() async {
if (_isDisposed) {
_logger.logError('Попытка инициализации после dispose');
return;
}
_logger.logConnection('Инициализация ConnectionManager');
_stateManager.setState(
ConnectionState.disconnected,
message: 'Инициализация',
);
}
Future<void> _fullReconnect() async {
_logger.logConnection('Начинаем полное переподключение');
_cleanup();
_stopMonitoring(); // Убедимся, что все таймеры точно остановлены
_currentUrlIndex = 0;
_sequenceNumber = 0;
_messageQueue.clear();
_stateManager.setState(
ConnectionState.disconnected,
message: 'Подготовка к переподключению',
);
await Future.delayed(const Duration(milliseconds: 250));
await connect(authToken: _authToken);
}
Future<void> connect({String? authToken}) async {
if (_isDisposed) {
_logger.logError('Попытка подключения после dispose');
return;
}
if (_isConnecting) {
_logger.logConnection('Подключение уже в процессе');
return;
}
_authToken = authToken;
_isConnecting = true;
_logger.logConnection(
'Начало подключения',
data: {
'auth_token_present': authToken != null,
'server_count': _serverUrls.length,
},
);
_stateManager.setState(
ConnectionState.connecting,
message: 'Подключение к серверу',
attemptNumber: 1,
);
try {
await _connectWithFallback();
} catch (e) {
_logger.logError('Ошибка подключения', error: e);
_stateManager.setState(
ConnectionState.error,
message: 'Ошибка подключения: ${e.toString()}',
);
rethrow;
} finally {
_isConnecting = false;
}
}
Future<void> _connectWithFallback() async {
final sessionId = 'connect_${DateTime.now().millisecondsSinceEpoch}';
final session = _retryManager.startSession(sessionId, ErrorType.network);
while (_currentUrlIndex < _serverUrls.length) {
final url = _serverUrls[_currentUrlIndex];
_currentServerUrl = url;
_logger.logConnection(
'Попытка подключения',
data: {
'url': url,
'attempt': _currentUrlIndex + 1,
'total_servers': _serverUrls.length,
},
);
try {
await _connectToUrl(url);
_logger.logConnection(
'Успешное подключение',
data: {'url': url, 'server_index': _currentUrlIndex},
);
_stateManager.setState(
ConnectionState.connected,
message: 'Подключен к серверу',
serverUrl: url,
);
_healthMonitor.startMonitoring(serverUrl: url);
_retryManager.endSession(sessionId);
return;
} catch (e) {
final errorInfo = ErrorInfo(
type: _getErrorType(e),
message: e.toString(),
timestamp: DateTime.now(),
);
session.addAttempt(errorInfo.type, message: e.toString());
_logger.logError(
'Ошибка подключения к серверу',
data: {
'url': url,
'error': e.toString(),
'error_type': errorInfo.type.name,
},
);
_currentUrlIndex++;
if (_currentUrlIndex < _serverUrls.length) {
final delay = Duration(milliseconds: 500);
_logger.logConnection(
'Переход к следующему серверу через ${delay.inMilliseconds}ms',
);
await Future.delayed(delay);
}
}
}
_logger.logError(
'Все серверы недоступны',
data: {'total_servers': _serverUrls.length},
);
_stateManager.setState(
ConnectionState.error,
message: 'Все серверы недоступны',
);
throw Exception('Не удалось подключиться ни к одному серверу');
}
Future<void> _connectToUrl(String url) async {
final uri = Uri.parse(url);
_logger.logConnection(
'Подключение к URL',
data: {'host': uri.host, 'port': uri.port, 'scheme': uri.scheme},
);
final headers = <String, String>{
'Origin': 'https://web.max.ru',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Sec-WebSocket-Extensions': 'permessage-deflate',
};
_channel = IOWebSocketChannel.connect(uri, headers: headers);
await _channel!.ready;
_logger.logConnection('WebSocket канал готов');
_setupMessageListener();
await _sendHandshake();
_startPingTimer();
}
void _setupMessageListener() {
_messageSubscription?.cancel();
_messageSubscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
cancelOnError: true,
);
_logger.logConnection('Слушатель сообщений настроен');
}
void _handleMessage(dynamic message) {
if (message == null || (message is String && message.trim().isEmpty)) {
return;
}
try {
_logger.logMessage('IN', message);
final decodedMessage = message is String ? jsonDecode(message) : message;
if (decodedMessage is Map && decodedMessage['opcode'] == 1) {
_healthMonitor.onPongReceived();
}
if (decodedMessage is Map &&
decodedMessage['opcode'] == 6 &&
decodedMessage['cmd'] == 1) {
_handleHandshakeSuccess(Map<String, dynamic>.from(decodedMessage));
return;
}
if (decodedMessage is Map && decodedMessage['cmd'] == 3) {
_handleServerError(Map<String, dynamic>.from(decodedMessage));
return;
}
if (decodedMessage is Map &&
decodedMessage['opcode'] == 97 &&
decodedMessage['cmd'] == 1) {
_handleSessionTermination();
return;
}
_messageController.add(decodedMessage);
} catch (e) {
_logger.logError(
'Ошибка обработки сообщения',
data: {'message': message.toString(), 'error': e.toString()},
);
}
}
void _handleHandshakeSuccess(Map<String, dynamic> message) {
_logger.logConnection(
'Handshake успешен',
data: {'payload': message['payload']},
);
_stateManager.setState(
ConnectionState.ready,
message: 'Сессия готова к работе',
);
_processMessageQueue();
}
void _handleServerError(Map<String, dynamic> message) {
final error = message['payload'];
_logger.logError('Ошибка сервера', data: {'error': error});
if (error != null) {
if (error['error'] == 'proto.state') {
_logger.logConnection('Ошибка состояния сессии, переподключаемся');
_scheduleReconnect('Ошибка состояния сессии');
} else if (error['error'] == 'login.token') {
_logger.logConnection('Недействительный токен');
_handleInvalidToken();
}
}
}
void _handleSessionTermination() {
_logger.logConnection('Сессия завершена сервером');
_stateManager.setState(
ConnectionState.disconnected,
message: 'Сессия завершена сервером',
);
_clearAuthData();
}
void _handleInvalidToken() {
_logger.logConnection('Обработка недействительного токена');
_clearAuthData();
_stateManager.setState(
ConnectionState.disconnected,
message: 'Требуется повторная авторизация',
);
}
void _clearAuthData() {
_authToken = null;
_logger.logConnection('Данные аутентификации очищены');
}
void _handleError(dynamic error) {
_logger.logError('Ошибка WebSocket', error: error);
_healthMonitor.onError(error.toString());
_scheduleReconnect('Ошибка WebSocket: $error');
}
void _handleDisconnection() {
_logger.logConnection('WebSocket соединение закрыто');
_healthMonitor.onReconnect();
_scheduleReconnect('Соединение закрыто');
}
void _scheduleReconnect(String reason) {
if (_isDisposed) return;
_reconnectTimer?.cancel();
final sessionId = 'reconnect_${DateTime.now().millisecondsSinceEpoch}';
final session = _retryManager.startSession(sessionId, ErrorType.network);
if (!session.canRetry()) {
_logger.logError(
'Превышено максимальное количество попыток переподключения',
);
_stateManager.setState(
ConnectionState.error,
message: 'Не удалось переподключиться',
);
return;
}
final delay = session.getNextDelay();
_logger.logReconnect(session.attemptCount + 1, reason, delay: delay);
_stateManager.setState(
ConnectionState.reconnecting,
message: 'Переподключение через ${delay.inSeconds}с',
reconnectDelay: delay,
);
_reconnectTimer = Timer(delay, () async {
try {
await _fullReconnect();
} catch (e) {
_logger.logError('Ошибка во время полного переподключения', error: e);
_scheduleReconnect('Ошибка при попытке полного переподключения');
}
});
}
Future<void> _sendHandshake() async {
_logger.logConnection('Отправка handshake');
final payload = {
"userAgent": {
"deviceType": "WEB",
"locale": "ru",
"deviceLocale": "ru",
"osVersion": "Windows",
"deviceName": "Chrome",
"headerUserAgent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"appVersion": "25.9.15",
"screen": "1920x1080 1.0x",
"timezone": "Europe/Moscow",
},
"deviceId": _generateDeviceId(),
};
_sendMessage(6, payload);
}
int _sendMessage(int opcode, Map<String, dynamic> payload) {
if (_channel == null) {
_logger.logError('WebSocket не подключен');
return -1;
}
final message = {
"ver": 11,
"cmd": 0,
"seq": _sequenceNumber,
"opcode": opcode,
"payload": payload,
};
final encodedMessage = jsonEncode(message);
_logger.logMessage('OUT', encodedMessage);
_channel!.sink.add(encodedMessage);
return _sequenceNumber++;
}
int sendMessage(int opcode, Map<String, dynamic> payload) {
if (!canSendMessages) {
_logger.logConnection(
'Сообщение добавлено в очередь',
data: {'opcode': opcode, 'reason': 'Соединение не готово'},
);
_messageQueue.add({'opcode': opcode, 'payload': payload});
return -1;
}
return _sendMessage(opcode, payload);
}
void _processMessageQueue() {
if (_messageQueue.isEmpty) return;
_logger.logConnection(
'Обработка очереди сообщений',
data: {'count': _messageQueue.length},
);
for (final message in _messageQueue) {
_sendMessage(message['opcode'], message['payload']);
}
_messageQueue.clear();
}
void _startPingTimer() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(const Duration(seconds: 25), (_) {
if (canSendMessages) {
_logger.logConnection('Отправка ping');
_sendMessage(1, {"interactive": true});
}
});
}
String _generateDeviceId() {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final random = (timestamp % 1000000).toString().padLeft(6, '0');
return "$timestamp$random";
}
ErrorType _getErrorType(dynamic error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('timeout') ||
errorString.contains('connection') ||
errorString.contains('network')) {
return ErrorType.network;
}
if (errorString.contains('unauthorized') ||
errorString.contains('forbidden')) {
return ErrorType.authentication;
}
if (errorString.contains('server') || errorString.contains('internal')) {
return ErrorType.server;
}
return ErrorType.unknown;
}
Future<void> disconnect() async {
_logger.logConnection('Отключение');
_stateManager.setState(
ConnectionState.disconnected,
message: 'Отключение по запросу',
);
_stopMonitoring();
_cleanup();
}
Future<void> forceReconnect() async {
if (_isDisposed) {
_logger.logError('Попытка переподключения после dispose');
return;
}
_logger.logConnection('Принудительное переподключение');
_reconnectTimer?.cancel();
_pingTimer?.cancel();
_cleanup();
_currentUrlIndex = 0;
_sequenceNumber = 0;
_messageQueue.clear();
_stateManager.setState(
ConnectionState.disconnected,
message: 'Подготовка к переподключению',
);
await Future.delayed(const Duration(milliseconds: 500));
await connect(authToken: _authToken);
}
void _stopMonitoring() {
_pingTimer?.cancel();
_reconnectTimer?.cancel();
_messageSubscription?.cancel();
_healthMonitor.stopMonitoring();
}
void _cleanup() {
_channel?.sink.close(status.goingAway);
_channel = null;
_messageQueue.clear();
_currentUrlIndex = 0;
_sequenceNumber = 0;
}
Map<String, dynamic> getStatistics() {
return {
'connection_state': currentState.state.name,
'health_metrics': _healthMonitor.getStatistics(),
'retry_statistics': _retryManager.getStatistics(),
'log_statistics': _logger.getLogStats(),
'message_queue_size': _messageQueue.length,
'current_server': _currentServerUrl,
'server_index': _currentUrlIndex,
};
}
void dispose() {
if (_isDisposed) return;
_logger.logConnection('Освобождение ресурсов ConnectionManager');
_isDisposed = true;
_stopMonitoring();
_cleanup();
_messageController.close();
_connectionStatusController.close();
_stateManager.dispose();
_logger.dispose();
_healthMonitor.dispose();
}
}

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