feat(push): FCM via MAX server — oneme flavor, dedicated FCM workflow, opcode 22, notifications + CI/lint fixes
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
name: Build Android (FCM)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'lib/**'
|
||||
- 'android/**'
|
||||
- 'pubspec.yaml'
|
||||
- '.github/workflows/build-android-fcm.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'lib/**'
|
||||
- 'android/**'
|
||||
- 'pubspec.yaml'
|
||||
- '.github/workflows/build-android-fcm.yml'
|
||||
|
||||
jobs:
|
||||
build-android-fcm:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Configure Gradle
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" >> ~/.gradle/gradle.properties
|
||||
echo "kotlin.daemon.jvmargs=-Xmx1536m" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Universal APK
|
||||
run: flutter build apk --release --flavor oneme
|
||||
|
||||
- name: Build Split APKs
|
||||
run: flutter build apk --release --split-per-abi --flavor oneme
|
||||
|
||||
- name: Upload Universal APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-universal
|
||||
path: build/app/outputs/flutter-apk/app-oneme-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload arm64-v8a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-arm64-v8a
|
||||
path: build/app/outputs/flutter-apk/app-oneme-arm64-v8a-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload armeabi-v7a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-armeabi-v7a
|
||||
path: build/app/outputs/flutter-apk/app-oneme-armeabi-v7a-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload x86_64 APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-x86_64
|
||||
path: build/app/outputs/flutter-apk/app-oneme-x86_64-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Build App Bundle
|
||||
run: flutter build appbundle --release --flavor oneme
|
||||
|
||||
- name: Upload App Bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-aab
|
||||
path: build/app/outputs/bundle/onemeRelease/app-oneme-release.aab
|
||||
retention-days: 30
|
||||
@@ -47,45 +47,45 @@ jobs:
|
||||
echo "kotlin.daemon.jvmargs=-Xmx1536m" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Universal APK
|
||||
run: flutter build apk --release
|
||||
run: flutter build apk --release --flavor komet
|
||||
|
||||
- name: Build Split APKs
|
||||
run: flutter build apk --release --split-per-abi
|
||||
run: flutter build apk --release --split-per-abi --flavor komet
|
||||
|
||||
- name: Upload Universal APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-universal
|
||||
path: build/app/outputs/flutter-apk/app-release.apk
|
||||
path: build/app/outputs/flutter-apk/app-komet-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload arm64-v8a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-arm64-v8a
|
||||
path: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
path: build/app/outputs/flutter-apk/app-komet-arm64-v8a-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload armeabi-v7a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-armeabi-v7a
|
||||
path: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
path: build/app/outputs/flutter-apk/app-komet-armeabi-v7a-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload x86_64 APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-x86_64
|
||||
path: build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
path: build/app/outputs/flutter-apk/app-komet-x86_64-release.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Build App Bundle
|
||||
run: flutter build appbundle --release
|
||||
run: flutter build appbundle --release --flavor komet
|
||||
|
||||
- name: Upload App Bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-aab
|
||||
path: build/app/outputs/bundle/release/app-release.aab
|
||||
path: build/app/outputs/bundle/kometRelease/app-komet-release.aab
|
||||
retention-days: 30
|
||||
|
||||
@@ -13,10 +13,17 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 'stable'
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
@@ -24,5 +31,11 @@ jobs:
|
||||
- name: Flutter analyze
|
||||
run: flutter analyze
|
||||
|
||||
- name: Configure Gradle
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" >> ~/.gradle/gradle.properties
|
||||
echo "kotlin.daemon.jvmargs=-Xmx1536m" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Android APK
|
||||
run: flutter build apk --release
|
||||
run: flutter build apk --release --flavor komet
|
||||
|
||||
@@ -6,17 +6,23 @@ on:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
analyze-and-build-all:
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 'stable'
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
@@ -24,8 +30,34 @@ jobs:
|
||||
- name: Flutter analyze
|
||||
run: flutter analyze
|
||||
|
||||
- name: Configure Gradle
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" >> ~/.gradle/gradle.properties
|
||||
echo "kotlin.daemon.jvmargs=-Xmx1536m" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Android APK
|
||||
run: flutter build apk --release
|
||||
run: flutter build apk --release --flavor komet
|
||||
|
||||
web-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install Linux desktop dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release
|
||||
@@ -33,13 +65,41 @@ jobs:
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Windows
|
||||
run: flutter build windows --release
|
||||
|
||||
- name: Build iOS (only macOS runners)
|
||||
if: runner.os == 'macOS'
|
||||
run: flutter build ios --release
|
||||
apple:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build macOS (only macOS runners)
|
||||
if: runner.os == 'macOS'
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.41.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build iOS
|
||||
run: flutter build ios --release --no-codesign
|
||||
|
||||
- name: Build macOS
|
||||
run: flutter build macos --release
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -20,16 +21,27 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "ru.komet.app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = maxOf(flutter.minSdkVersion, 23)
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
flavorDimensions += "distribution"
|
||||
|
||||
productFlavors {
|
||||
create("komet") {
|
||||
dimension = "distribution"
|
||||
isDefault = true
|
||||
applicationId = "ru.komet.app"
|
||||
}
|
||||
create("oneme") {
|
||||
dimension = "distribution"
|
||||
applicationId = "ru.oneme.app"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "659634599081",
|
||||
"project_id": "max-messenger-app",
|
||||
"storage_bucket": "max-messenger-app.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:659634599081:android:00000000000000000000ab",
|
||||
"android_client_info": {
|
||||
"package_name": "ru.komet.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<application
|
||||
android:label="komet"
|
||||
android:name="${applicationName}"
|
||||
@@ -33,6 +34,9 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="komet_messages" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "659634599081",
|
||||
"project_id": "max-messenger-app",
|
||||
"storage_bucket": "max-messenger-app.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:659634599081:android:9605285443b661167225b8",
|
||||
"android_client_info": {
|
||||
"package_name": "ru.oneme.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -21,6 +21,7 @@ plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("com.google.gms.google-services") version "4.4.3" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -210,6 +210,12 @@ enum AuthRequestType {
|
||||
|
||||
enum LoginStatus { idle, loading, success, error }
|
||||
|
||||
class WrongDeviceTokenException implements Exception {
|
||||
const WrongDeviceTokenException();
|
||||
@override
|
||||
String toString() => 'WrongDeviceTokenException';
|
||||
}
|
||||
|
||||
class RequestCodeResult {
|
||||
final String token;
|
||||
|
||||
@@ -429,6 +435,33 @@ class AccountModule {
|
||||
return config;
|
||||
}
|
||||
|
||||
Future<void> registerPushToken(String pushToken) async {
|
||||
_ensureOnline();
|
||||
final packet = await _api.sendRequest(Opcode.config, <dynamic, dynamic>{
|
||||
'pushToken': pushToken,
|
||||
});
|
||||
if (packet.isError) {
|
||||
final msg = messageFromErrorPayload(packet.payload).toUpperCase();
|
||||
if (msg.contains('WRONG_DEVICE_TOKEN') ||
|
||||
msg.contains('WRONG.DEVICE.TOKEN')) {
|
||||
throw const WrongDeviceTokenException();
|
||||
}
|
||||
throw PacketError(messageFromErrorPayload(packet.payload));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unregisterPushToken(String pushToken) async {
|
||||
if (_api.state != SessionState.online) return;
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return;
|
||||
final authToken = await TokenStorage.readToken(accountId);
|
||||
if (authToken == null) return;
|
||||
await _api.sendRequest(Opcode.logout, <dynamic, dynamic>{
|
||||
'token': authToken,
|
||||
'pushToken': pushToken,
|
||||
});
|
||||
}
|
||||
|
||||
Future<ProfileData> updateProfileName(String firstName, String? lastName) async {
|
||||
_ensureOnline();
|
||||
final payload = <dynamic, dynamic>{
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../backend/api.dart';
|
||||
import '../../backend/modules/account.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
const _channelId = 'komet_messages';
|
||||
const _channelName = 'Сообщения';
|
||||
const _prefsTokenKey = 'fcm_push_token';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _backgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
if (message.notification != null) return;
|
||||
final plugin = FlutterLocalNotificationsPlugin();
|
||||
await plugin.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
),
|
||||
);
|
||||
await _display(plugin, message);
|
||||
}
|
||||
|
||||
Future<void> _display(
|
||||
FlutterLocalNotificationsPlugin plugin,
|
||||
RemoteMessage message,
|
||||
) async {
|
||||
final data = message.data;
|
||||
final title = message.notification?.title ??
|
||||
data['title']?.toString() ??
|
||||
data['sender']?.toString() ??
|
||||
'MAX';
|
||||
final body = message.notification?.body ??
|
||||
data['body']?.toString() ??
|
||||
data['text']?.toString() ??
|
||||
data['message']?.toString() ??
|
||||
'Новое сообщение';
|
||||
await plugin.show(
|
||||
id: message.messageId?.hashCode ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PushService {
|
||||
PushService._();
|
||||
static final PushService instance = PushService._();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _local =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Api? _api;
|
||||
AccountModule? _account;
|
||||
String? _token;
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> init({required Api api, required AccountModule account}) async {
|
||||
if (_initialized) return;
|
||||
_api = api;
|
||||
_account = account;
|
||||
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (e) {
|
||||
logger.w('Push: Firebase init не удался: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
await _local.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
),
|
||||
);
|
||||
await _local
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
),
|
||||
);
|
||||
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await messaging.requestPermission();
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
|
||||
FirebaseMessaging.onMessage.listen((m) {
|
||||
_display(_local, m);
|
||||
});
|
||||
messaging.onTokenRefresh.listen((t) async {
|
||||
_token = t;
|
||||
await _persistToken(t);
|
||||
await _registerWithServer();
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString(_prefsTokenKey);
|
||||
try {
|
||||
_token = await messaging.getToken() ?? _token;
|
||||
if (_token != null) await _persistToken(_token!);
|
||||
logger.i('Push: FCM-токен получен (${_token?.length ?? 0} симв.)');
|
||||
} catch (e) {
|
||||
logger.w('Push: getToken не удался: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onLoginSuccess() async {
|
||||
if (!_initialized) return;
|
||||
if (_token == null) {
|
||||
try {
|
||||
_token = await FirebaseMessaging.instance.getToken();
|
||||
if (_token != null) await _persistToken(_token!);
|
||||
} catch (_) {}
|
||||
}
|
||||
await _registerWithServer();
|
||||
}
|
||||
|
||||
Future<void> unregister() async {
|
||||
if (!_initialized || _token == null) return;
|
||||
final account = _account;
|
||||
if (account != null) {
|
||||
try {
|
||||
await account.unregisterPushToken(_token!);
|
||||
} catch (e) {
|
||||
logger.w('Push: unregister не удался: $e');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await FirebaseMessaging.instance.deleteToken();
|
||||
} catch (_) {}
|
||||
_token = null;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefsTokenKey);
|
||||
}
|
||||
|
||||
Future<void> _registerWithServer() async {
|
||||
final account = _account;
|
||||
final api = _api;
|
||||
if (account == null || api == null) return;
|
||||
if (api.state != SessionState.online) return;
|
||||
final token = _token;
|
||||
if (token == null || token.isEmpty) return;
|
||||
|
||||
try {
|
||||
await account.registerPushToken(token);
|
||||
logger.i('Push: токен зарегистрирован на сервере MAX');
|
||||
} on WrongDeviceTokenException {
|
||||
logger.w('Push: WRONG_DEVICE_TOKEN, переполучаю токен');
|
||||
try {
|
||||
await FirebaseMessaging.instance.deleteToken();
|
||||
final fresh = await FirebaseMessaging.instance.getToken();
|
||||
if (fresh != null && fresh.isNotEmpty) {
|
||||
_token = fresh;
|
||||
await _persistToken(fresh);
|
||||
await account.registerPushToken(fresh);
|
||||
logger.i('Push: токен перерегистрирован');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('Push: повторная регистрация не удалась: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('Push: регистрация токена не удалась: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsTokenKey, token);
|
||||
}
|
||||
}
|
||||
@@ -1085,7 +1085,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
child: Text(
|
||||
'Кажется, тут пусто...',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.6),
|
||||
color: cs.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
@@ -1146,10 +1146,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
? ContactCache.get(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
final avatar = chat.lastMsgSenderId != null
|
||||
? ContactCache.getAvatar(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
String fullMsg = "";
|
||||
|
||||
if (name?.isNotEmpty == true && chat.id != 0) {
|
||||
@@ -2073,7 +2069,6 @@ Navigator.push(
|
||||
}
|
||||
|
||||
Widget _buildFabMenu() {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../../backend/modules/messages.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import '../../../models/attachment.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../widgets/message_bubble.dart';
|
||||
import '../../widgets/attachment_panel.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
@@ -63,6 +62,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
if (!mounted) return;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Имя сохранено');
|
||||
@@ -93,6 +93,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
final newProfile = await accountModule.removeProfilePhoto(_photoId!);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
if (!mounted) return;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Фото удалено');
|
||||
|
||||
@@ -15,8 +15,6 @@ class PasswordEntryScreen extends StatefulWidget {
|
||||
class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
|
||||
bool _isLoading = true;
|
||||
bool _is2faEnabled = false;
|
||||
String? _email;
|
||||
String? _hint;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@@ -943,7 +943,7 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
?trailing,
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Symbols.chevron_right,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/backend/modules/contacts.dart';
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../backend/modules/messages.dart';
|
||||
import '../../core/utils/haptics.dart';
|
||||
@@ -23,16 +20,18 @@ class MessageBubble extends StatelessWidget {
|
||||
static const double compactTimePadding = 8.0;
|
||||
|
||||
bool get _hasPhotoWithCaption {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final hasPhoto = message.attachments!.any((a) => a is PhotoAttachment);
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
return hasPhoto && hasCaption;
|
||||
}
|
||||
|
||||
bool get _hasMultiplePhotosNoCaption {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final photoCount = message.attachments!.whereType<PhotoAttachment>().length;
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
return photoCount >= 2 && !hasCaption;
|
||||
@@ -226,7 +225,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return 4;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
double get bottomMargin {
|
||||
@@ -253,17 +251,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case BubbleShape.groupedMiddle:
|
||||
return 1;
|
||||
}
|
||||
case MessageType.attachment:
|
||||
switch (shape) {
|
||||
case BubbleShape.singleTop:
|
||||
return 1;
|
||||
case BubbleShape.singleBottom:
|
||||
return 1;
|
||||
case BubbleShape.singleMiddle:
|
||||
return 4;
|
||||
case BubbleShape.groupedMiddle:
|
||||
return 1;
|
||||
}
|
||||
case MessageType.voice:
|
||||
switch (shape) {
|
||||
case BubbleShape.singleTop:
|
||||
@@ -278,7 +265,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return 4;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Внутренний отступ, размеры типа я хз
|
||||
@@ -311,7 +297,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 4);
|
||||
}
|
||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -491,7 +476,7 @@ class MessageBubble extends StatelessWidget {
|
||||
children: [
|
||||
Flexible(
|
||||
child: isForwarded
|
||||
? _buildForwardedInlineText(context, forwarded!, textColor)
|
||||
? _buildForwardedInlineText(context, forwarded, textColor)
|
||||
: Text(
|
||||
message.text ?? '',
|
||||
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
|
||||
@@ -590,67 +575,15 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
ForwardedMessageAttachment? _getForwardedAttachment() {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final a in message.attachments!) {
|
||||
if (a is ForwardedMessageAttachment) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildForwardedHeader(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final textColor = isMe
|
||||
? Colors.white
|
||||
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
||||
final headerColor = isMe
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: (isDark ? cs.onSurfaceVariant : const Color(0xFF8E8E93));
|
||||
|
||||
final senderName = forwarded.originalSenderName;
|
||||
final displaySender = senderName ?? forwarded.originalSenderId.toString();
|
||||
final origText = forwarded.originalText;
|
||||
final hasOrigText = origText != null && origText.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 14, color: headerColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
displaySender,
|
||||
style: TextStyle(
|
||||
color: headerColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasOrigText) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
origText,
|
||||
style: TextStyle(color: textColor, fontSize: 14),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachmentContent(BuildContext context) {
|
||||
final attachments = message.attachments;
|
||||
if (attachments == null || attachments.isEmpty) {
|
||||
@@ -843,65 +776,6 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForwardedFileContent(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
List<FileAttachment> files,
|
||||
) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final headerColor = isMe
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: cs.onSurfaceVariant;
|
||||
final displaySender =
|
||||
forwarded.originalSenderName ?? forwarded.originalSenderId.toString();
|
||||
final senderAvatar = forwarded.originalSenderAvatar;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 14, color: headerColor),
|
||||
const SizedBox(width: 4),
|
||||
if (senderAvatar != null && senderAvatar.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundImage: CachedNetworkImageProvider(senderAvatar),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
displaySender.isNotEmpty
|
||||
? displaySender[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
displaySender,
|
||||
style: TextStyle(
|
||||
color: headerColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...files.map((file) => _buildFileAttachment(context, file)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForwardedGenericContent(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
@@ -1001,7 +875,7 @@ class MessageBubble extends StatelessWidget {
|
||||
height: constrainedHeight,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => _buildPhotoPlaceholder(
|
||||
errorWidget: (_, _, _) => _buildPhotoPlaceholder(
|
||||
ctx,
|
||||
constrainedWidth,
|
||||
constrainedHeight,
|
||||
@@ -1106,7 +980,7 @@ class MessageBubble extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 100, 100),
|
||||
)
|
||||
else
|
||||
@@ -1141,7 +1015,7 @@ class MessageBubble extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 100, 100),
|
||||
)
|
||||
else
|
||||
@@ -1209,12 +1083,6 @@ class MessageBubble extends StatelessWidget {
|
||||
BuildContext ctx,
|
||||
MessageAttachment attachment,
|
||||
) {
|
||||
final cs = Theme.of(ctx).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final textColor = isMe
|
||||
? Colors.white
|
||||
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
||||
|
||||
switch (attachment.type) {
|
||||
case AttachmentType.video:
|
||||
return _buildVideoAttachment(ctx, attachment);
|
||||
@@ -1371,7 +1239,7 @@ class MessageBubble extends StatelessWidget {
|
||||
height: 150,
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 150, 150),
|
||||
)
|
||||
else
|
||||
@@ -1427,7 +1295,7 @@ class MessageBubble extends StatelessWidget {
|
||||
imageUrl: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => Icon(
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? Colors.white : cs.primary,
|
||||
size: 24,
|
||||
@@ -1573,7 +1441,7 @@ class MessageBubble extends StatelessWidget {
|
||||
imageUrl: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => Icon(
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? Colors.white : cs.primary,
|
||||
size: 24,
|
||||
@@ -1680,7 +1548,6 @@ class MessageBubble extends StatelessWidget {
|
||||
|
||||
Widget _buildMeta(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final timeColor = isMe ? Colors.white70 : cs.onSurfaceVariant;
|
||||
|
||||
return Padding(
|
||||
@@ -1700,8 +1567,6 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCompactTime(BuildContext ctx) {
|
||||
final cs = Theme.of(ctx).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final bgColor = isMe
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.black.withValues(alpha: 0.5);
|
||||
@@ -1761,12 +1626,6 @@ class MessageBubble extends StatelessWidget {
|
||||
final minute = dt.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute';
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final min = seconds ~/ 60;
|
||||
final sec = seconds % 60;
|
||||
return '$min:${sec.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
class _VoiceMessageBubble extends StatefulWidget {
|
||||
@@ -1866,6 +1725,7 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
return Icon(icon, size: 14, color: color);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = widget.cs.brightness == Brightness.dark;
|
||||
final waveInactiveColor = widget.isMe
|
||||
@@ -2042,16 +1902,6 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(Color inactive, Color active) {
|
||||
return Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: inactive,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _togglePlay() {
|
||||
setState(() {
|
||||
_isPlaying = !_isPlaying;
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:komet/l10n/app_localizations.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'backend/api.dart';
|
||||
import 'backend/modules/account.dart';
|
||||
import 'backend/modules/contacts.dart';
|
||||
import 'backend/modules/messages.dart';
|
||||
import 'core/push/push_service.dart';
|
||||
import 'core/storage/app_database.dart';
|
||||
import 'core/storage/token_storage.dart';
|
||||
import 'core/utils/haptics.dart';
|
||||
@@ -43,6 +45,12 @@ void main() async {
|
||||
await ContactsModule.primeCacheFromDb(activeAccountId);
|
||||
}
|
||||
await api.connect();
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
if (packageInfo.packageName == 'ru.oneme.app') {
|
||||
await PushService.instance.init(api: api, account: accountModule);
|
||||
}
|
||||
|
||||
final initialLocale = await _loadInitialLocale();
|
||||
|
||||
await Haptics.load();
|
||||
@@ -82,6 +90,7 @@ class KometAppState extends State<KometApp> {
|
||||
late Locale _locale;
|
||||
bool _isLoggingOut = false;
|
||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
widget.initialFpsOverlay,
|
||||
);
|
||||
@@ -105,10 +114,18 @@ class KometAppState extends State<KometApp> {
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
_loginStatusSub = accountModule.loginStatusStream.listen((status) {
|
||||
if (status == LoginStatus.success) {
|
||||
PushService.instance.onLoginSuccess();
|
||||
}
|
||||
});
|
||||
|
||||
_sessionExpiredSub = api.sessionExpiredStream.listen((SessionExpiredException e) async {
|
||||
if (_isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
|
||||
await PushService.instance.unregister();
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null) {
|
||||
await accountModule.removeAccount(accountId);
|
||||
@@ -133,6 +150,7 @@ class KometAppState extends State<KometApp> {
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionExpiredSub?.cancel();
|
||||
_loginStatusSub?.cancel();
|
||||
_profileUpdateController.close();
|
||||
fpsOverlayEnabled.dispose();
|
||||
super.dispose();
|
||||
|
||||
+124
-4
@@ -1,6 +1,22 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.71"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -105,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -169,6 +193,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.2.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.11"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -198,6 +270,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "0d9035862236fe38250fe1644d7ed3b8254e34a21b2c837c9f539fbb3bba5ef1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "21.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: e0f25e243c6c44c825bbbc6b2b2e76f7d9222362adcfe9fd780bf01923c840bd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: e7db3d5b49c2b7ecc68deba4aaaa67a348f92ee0fef34c8e4b4459dbef0d7307
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: "3a2654ba104fbb52c618ebed9def24ef270228470718c43b3a6afcd5c81bef0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -385,10 +489,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.18"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -525,6 +629,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -726,10 +838,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
version: "0.7.10"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -802,6 +914,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -58,6 +58,9 @@ dependencies:
|
||||
package_info_plus: ^9.0.1
|
||||
mobile_scanner: ^7.2.0
|
||||
cached_network_image: ^3.4.1
|
||||
firebase_core: ^4.1.1
|
||||
firebase_messaging: ^16.0.2
|
||||
flutter_local_notifications: ^21.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user