diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a6d170c..20f7944 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -48,6 +48,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Доступ к галерее нужен, чтобы отправлять фото и видео в чатах.
NSPhotoLibraryAddUsageDescription
Доступ к галерее нужен, чтобы сохранять полученные фото и видео.
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ ru.komet.app
+ CFBundleURLSchemes
+
+ komet
+ max
+
+
+
diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart
index 7822ab3..692f5e6 100644
--- a/lib/backend/modules/account.dart
+++ b/lib/backend/modules/account.dart
@@ -428,11 +428,20 @@ class LoginResult {
class AccountModule {
final Api _api;
final _loginStatusController = StreamController.broadcast();
+ bool _loggedIn = false;
- AccountModule(this._api);
+ AccountModule(this._api) {
+ _api.stateStream.listen((state) {
+ if (state != SessionState.online) _loggedIn = false;
+ });
+ }
Stream get loginStatusStream => _loginStatusController.stream;
+ /// `true`, только когда сервер считает сессию ONLINE — после успешного
+ /// login (opcode 19), а не просто после хэндшейка (opcode 6).
+ bool get isLoggedIn => _loggedIn;
+
Future getPrivacyConfig() async {
final accountId = await TokenStorage.getActiveAccountId();
if (accountId != null) {
@@ -999,6 +1008,7 @@ class AccountModule {
}
final result = await _processLoginResponse(dataMap, resolvedAccountId);
+ _loggedIn = true;
_loginStatusController.add(LoginStatus.success);
return result;
} catch (e) {
diff --git a/lib/backend/modules/links.dart b/lib/backend/modules/links.dart
new file mode 100644
index 0000000..4edfe9b
--- /dev/null
+++ b/lib/backend/modules/links.dart
@@ -0,0 +1,72 @@
+import 'dart:async';
+
+import '../../core/protocol/opcode_map.dart';
+import '../../core/protocol/packet.dart';
+import '../api.dart';
+
+sealed class ResolvedLink {
+ const ResolvedLink();
+}
+
+class ResolvedChat extends ResolvedLink {
+ final Map chat;
+ final Map? message;
+
+ const ResolvedChat(this.chat, this.message);
+}
+
+class ResolvedUser extends ResolvedLink {
+ final Map contact;
+
+ const ResolvedUser(this.contact);
+}
+
+class ResolvedLinkError extends ResolvedLink {
+ final String message;
+
+ const ResolvedLinkError(this.message);
+}
+
+abstract class LinkModule {
+ static Future resolve(Api api, String url) async {
+ final Packet response;
+ try {
+ response = await api.sendRequest(Opcode.linkInfo, {'link': url});
+ } on TimeoutException {
+ return const ResolvedLinkError('Превышено время ожидания');
+ } on PacketError catch (e) {
+ return ResolvedLinkError(e.message);
+ }
+
+ final payload = response.payload;
+ if (payload is! Map) return null;
+ if (!response.isOk) {
+ return ResolvedLinkError(messageFromErrorPayload(payload));
+ }
+
+ final chat = payload['chat'];
+ if (chat is Map) {
+ final message = payload['message'];
+ return ResolvedChat(chat, message is Map ? message : null);
+ }
+
+ final user = payload['user'];
+ if (user is Map && user['contact'] is Map) {
+ return ResolvedUser(user['contact'] as Map);
+ }
+
+ return null;
+ }
+
+ static Future join(Api api, String url) async {
+ try {
+ final response = await api.sendRequest(Opcode.chatJoin, {'link': url});
+ if (response.isOk) return null;
+ return messageFromErrorPayload(response.payload);
+ } on TimeoutException {
+ return 'Превышено время ожидания';
+ } on PacketError catch (e) {
+ return e.message;
+ }
+ }
+}
diff --git a/lib/core/links/deep_link_service.dart b/lib/core/links/deep_link_service.dart
new file mode 100644
index 0000000..133aecc
--- /dev/null
+++ b/lib/core/links/deep_link_service.dart
@@ -0,0 +1,91 @@
+import 'dart:async';
+
+import 'package:app_links/app_links.dart';
+
+import '../../backend/api.dart';
+import '../../frontend/widgets/max_link_handler.dart';
+import '../../main.dart';
+import 'desktop_url_scheme.dart';
+
+class DeepLinkService {
+ DeepLinkService._();
+
+ static final DeepLinkService instance = DeepLinkService._();
+
+ final AppLinks _appLinks = AppLinks();
+ StreamSubscription? _sub;
+ StreamSubscription? _stateSub;
+ String? _pending;
+ bool _ready = false;
+ bool _started = false;
+
+ Future init() async {
+ if (_started) return;
+ _started = true;
+
+ await DesktopUrlScheme.register();
+
+ _stateSub = api.stateStream.listen((state) {
+ if (state == SessionState.online) _flushPending();
+ });
+
+ _sub = _appLinks.uriLinkStream.listen(_onUri);
+ try {
+ final initial = await _appLinks.getInitialLink();
+ if (initial != null) _onUri(initial);
+ } catch (_) {}
+ }
+
+ void markReady() {
+ _ready = true;
+ _flushPending();
+ }
+
+ void _onUri(Uri uri) {
+ final url = _normalize(uri);
+ if (url == null) return;
+ _pending = url;
+ _flushPending();
+ }
+
+ void _flushPending() {
+ final pending = _pending;
+ if (pending == null || !_ready) return;
+ if (api.state != SessionState.online) return;
+ final context = KometApp.navigatorKey.currentContext;
+ if (context == null) return;
+
+ _pending = null;
+ tryHandleMaxLink(context, pending);
+ }
+
+ String? _normalize(Uri uri) {
+ final scheme = uri.scheme.toLowerCase();
+
+ if (scheme == 'https' || scheme == 'http') {
+ final host = uri.host.toLowerCase();
+ if (host == 'max.ru' || host == 'www.max.ru') return uri.toString();
+ return null;
+ }
+
+ if (scheme == 'komet' || scheme == 'max') {
+ final segments = [
+ if (uri.host.isNotEmpty && uri.host.toLowerCase() != 'max.ru') uri.host,
+ ...uri.pathSegments,
+ ].where((s) => s.isNotEmpty).toList();
+ if (segments.isEmpty) return null;
+ final query = uri.query.isNotEmpty ? '?${uri.query}' : '';
+ return 'https://max.ru/${segments.join('/')}$query';
+ }
+
+ return null;
+ }
+
+ void dispose() {
+ _sub?.cancel();
+ _sub = null;
+ _stateSub?.cancel();
+ _stateSub = null;
+ _started = false;
+ }
+}
diff --git a/lib/core/links/desktop_url_scheme.dart b/lib/core/links/desktop_url_scheme.dart
new file mode 100644
index 0000000..074c5a9
--- /dev/null
+++ b/lib/core/links/desktop_url_scheme.dart
@@ -0,0 +1,73 @@
+import 'dart:io';
+
+import '../utils/logger.dart';
+
+const List _schemes = ['komet', 'max'];
+
+abstract class DesktopUrlScheme {
+ static Future register() async {
+ try {
+ if (Platform.isWindows) {
+ await _registerWindows();
+ } else if (Platform.isLinux) {
+ await _registerLinux();
+ }
+ } catch (e) {
+ logger.w('DesktopUrlScheme: регистрация не удалась: $e');
+ }
+ }
+
+ static Future _registerWindows() async {
+ final exe = Platform.resolvedExecutable;
+ for (final scheme in _schemes) {
+ final capitalized = '${scheme[0].toUpperCase()}${scheme.substring(1)}';
+ final base = 'HKCU\\Software\\Classes\\$scheme';
+
+ await _run('reg', ['add', base, '/ve', '/d', 'URL:$capitalized', '/f']);
+ await _run('reg', ['add', base, '/v', 'URL Protocol', '/d', '', '/f']);
+ await _run('reg', [
+ 'add',
+ '$base\\shell\\open\\command',
+ '/ve',
+ '/d',
+ '"$exe" "%1"',
+ '/f',
+ ]);
+ }
+ }
+
+ static Future _registerLinux() async {
+ final home = Platform.environment['HOME'];
+ if (home == null || home.isEmpty) return;
+
+ final exe = Platform.resolvedExecutable;
+ final appsDir = Directory('$home/.local/share/applications');
+ await appsDir.create(recursive: true);
+
+ const fileName = 'komet-url-handler.desktop';
+ final mimeTypes = _schemes.map((s) => 'x-scheme-handler/$s').join(';');
+ final desktop = '[Desktop Entry]\n'
+ 'Type=Application\n'
+ 'Name=Komet\n'
+ 'Exec="$exe" %u\n'
+ 'Terminal=false\n'
+ 'NoDisplay=true\n'
+ 'MimeType=$mimeTypes;\n';
+
+ final file = File('${appsDir.path}/$fileName');
+ if (!file.existsSync() || await file.readAsString() != desktop) {
+ await file.writeAsString(desktop);
+ }
+
+ for (final scheme in _schemes) {
+ await _run('xdg-mime', ['default', fileName, 'x-scheme-handler/$scheme']);
+ }
+ await _run('update-desktop-database', [appsDir.path]);
+ }
+
+ static Future _run(String executable, List args) async {
+ try {
+ await Process.run(executable, args);
+ } catch (_) {}
+ }
+}
diff --git a/lib/core/links/max_link.dart b/lib/core/links/max_link.dart
new file mode 100644
index 0000000..5f0a679
--- /dev/null
+++ b/lib/core/links/max_link.dart
@@ -0,0 +1,58 @@
+enum MaxLinkKind { call, invite, user, content, public, auth }
+
+class MaxLink {
+ final MaxLinkKind kind;
+ final String url;
+
+ const MaxLink(this.kind, this.url);
+
+ static final RegExp _host = RegExp(
+ r'^https?://(?:www\.)?max\.ru/(.+)$',
+ caseSensitive: false,
+ );
+
+ static final RegExp _segment = RegExp(r'^[A-Za-z0-9_]+$');
+
+ static const Set _reserved = {
+ 'join',
+ 'joincall',
+ 'u',
+ 'c',
+ 'login',
+ 'ps',
+ 'tos',
+ 'privacy',
+ 'about',
+ 'help',
+ };
+
+ static bool isMaxLink(String url) => parse(url) != null;
+
+ static MaxLink? parse(String input) {
+ final url = input.trim();
+ final match = _host.firstMatch(url);
+ if (match == null) return null;
+
+ final path = match.group(1)!.split('?').first.split('#').first;
+ final segments =
+ path.split('/').where((s) => s.isNotEmpty).toList(growable: false);
+ if (segments.isEmpty) return null;
+
+ switch (segments.first.toLowerCase()) {
+ case ':auth':
+ return segments.length >= 2 ? MaxLink(MaxLinkKind.auth, url) : null;
+ case 'joincall':
+ return segments.length >= 2 ? MaxLink(MaxLinkKind.call, url) : null;
+ case 'join':
+ return segments.length >= 2 ? MaxLink(MaxLinkKind.invite, url) : null;
+ case 'u':
+ return segments.length >= 2 ? MaxLink(MaxLinkKind.user, url) : null;
+ case 'c':
+ return segments.length >= 3 ? MaxLink(MaxLinkKind.content, url) : null;
+ }
+
+ if (_reserved.contains(segments.first.toLowerCase())) return null;
+ if (!_segment.hasMatch(segments.first)) return null;
+ return MaxLink(MaxLinkKind.public, url);
+ }
+}
diff --git a/lib/core/utils/link_opener.dart b/lib/core/utils/link_opener.dart
index 9fb0cc3..089fa3f 100644
--- a/lib/core/utils/link_opener.dart
+++ b/lib/core/utils/link_opener.dart
@@ -1,11 +1,11 @@
import 'package:flutter/widgets.dart';
import 'package:url_launcher/url_launcher.dart';
-import '../../frontend/widgets/call_link_handler.dart';
import '../../frontend/widgets/custom_notification.dart';
+import '../../frontend/widgets/max_link_handler.dart';
Future openExternalUrl(BuildContext context, String url) async {
- if (await tryHandleCallLink(context, url)) return;
+ if (await tryHandleMaxLink(context, url)) return;
if (!context.mounted) return;
final uri = Uri.tryParse(url);
diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart
index b808a7c..3d0993a 100644
--- a/lib/frontend/screens/calls/calls_tab.dart
+++ b/lib/frontend/screens/calls/calls_tab.dart
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../main.dart' show api, accountModule;
-import '../../../backend/api.dart';
import '../../../backend/modules/account.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/utils/format.dart';
@@ -27,7 +26,7 @@ class _CallsTabState extends State {
@override
void initState() {
super.initState();
- if (api.state == SessionState.online) {
+ if (accountModule.isLoggedIn) {
_loadHistory();
} else {
_loginSub = accountModule.loginStatusStream.listen((status) {
diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart
index 982128d..60ef135 100644
--- a/lib/frontend/screens/profile/devices_screen.dart
+++ b/lib/frontend/screens/profile/devices_screen.dart
@@ -12,7 +12,7 @@ import '../../../backend/modules/account.dart' show SessionInfo;
import '../../widgets/custom_notification.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
-import '../../widgets/sheet_helpers.dart';
+import '../../widgets/web_qr_login.dart';
import 'web_qr_scan_screen.dart';
class DevicesScreen extends StatefulWidget {
@@ -113,71 +113,6 @@ class _DevicesScreenState extends State
}
}
- Future _confirmQrWebLoginSheet() async {
- final agreed = await showModalBottomSheet(
- context: context,
- backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
- ),
- builder: (sheetContext) {
- final cs = Theme.of(sheetContext).colorScheme;
- return SafeArea(
- child: Padding(
- padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const Center(child: SheetGrabber(margin: EdgeInsets.zero)),
- const SizedBox(height: 20),
- Text(
- 'Вход по QR',
- style: GoogleFonts.outfit(
- fontSize: 20,
- fontWeight: FontWeight.w700,
- color: cs.onSurface,
- ),
- ),
- const SizedBox(height: 12),
- Text(
- 'Вы точно хотите войти в аккаунт через веб или приложение MAX на компьютере?',
- style: TextStyle(
- fontSize: 15,
- height: 1.35,
- color: cs.onSurfaceVariant,
- ),
- ),
- const SizedBox(height: 28),
- Row(
- children: [
- Expanded(
- child: OutlinedButton(
- onPressed: () => Navigator.of(sheetContext).pop(false),
- child: Text(
- 'Отмена',
- style: TextStyle(color: cs.onSurface),
- ),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: FilledButton(
- onPressed: () => Navigator.of(sheetContext).pop(true),
- child: const Text('Войти'),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- );
- },
- );
- return agreed ?? false;
- }
-
Future _startWebQrAuth() async {
final canScan =
!kIsWeb &&
@@ -197,43 +132,8 @@ class _DevicesScreenState extends State
if (!mounted) return;
if (qr == null || qr.trim().isEmpty) return;
- final confirmed = await _confirmQrWebLoginSheet();
- if (!mounted) return;
- if (!confirmed) return;
-
- showDialog(
- context: context,
- barrierDismissible: false,
- builder: (ctx) {
- final cs = Theme.of(ctx).colorScheme;
- return PopScope(
- canPop: false,
- child: Center(
- child: Card(
- color: cs.surfaceContainerHigh,
- child: const Padding(
- padding: EdgeInsets.all(28),
- child: CircularProgressIndicator(),
- ),
- ),
- ),
- );
- },
- );
-
- try {
- await accountModule.authorizeWebQrLogin(qr.trim());
- if (mounted) {
- Navigator.of(context, rootNavigator: true).pop();
- showCustomNotification(context, 'Вход по QR подтверждён');
- _loadSessions();
- }
- } catch (e) {
- if (mounted) {
- Navigator.of(context, rootNavigator: true).pop();
- showCustomNotification(context, 'Ошибка: $e');
- }
- }
+ final success = await confirmAndAuthorizeWebQrLogin(context, qr.trim());
+ if (success && mounted) _loadSessions();
}
Future _terminateOthers() async {
diff --git a/lib/frontend/widgets/max_link_handler.dart b/lib/frontend/widgets/max_link_handler.dart
new file mode 100644
index 0000000..227861b
--- /dev/null
+++ b/lib/frontend/widgets/max_link_handler.dart
@@ -0,0 +1,132 @@
+import 'package:flutter/material.dart';
+
+import '../../backend/modules/chats.dart';
+import '../../backend/modules/links.dart';
+import '../../core/links/max_link.dart';
+import '../../core/storage/app_database.dart';
+import '../../main.dart';
+import '../screens/chats/chat_screen.dart';
+import '../screens/contacts/contact_profile_screen.dart';
+import 'call_link_handler.dart';
+import 'confirm_dialog.dart';
+import 'custom_notification.dart';
+import 'swipe_route.dart';
+import 'web_qr_login.dart';
+
+Future tryHandleMaxLink(BuildContext context, String url) async {
+ final link = MaxLink.parse(url);
+ if (link == null) return false;
+
+ if (link.kind == MaxLinkKind.call) {
+ return tryHandleCallLink(context, url);
+ }
+
+ if (link.kind == MaxLinkKind.auth) {
+ await confirmAndAuthorizeWebQrLogin(context, link.url);
+ return true;
+ }
+
+ final resolved = await LinkModule.resolve(api, link.url);
+ if (!context.mounted) return true;
+
+ switch (resolved) {
+ case null:
+ return false;
+ case ResolvedLinkError(:final message):
+ showCustomNotification(context, message);
+ return true;
+ case ResolvedUser(:final contact):
+ _openContact(context, contact);
+ return true;
+ case ResolvedChat():
+ await _openResolvedChat(context, link, resolved);
+ return true;
+ }
+}
+
+void _openContact(BuildContext context, Map contact) {
+ final id = contact['id'];
+ if (id is! int) {
+ showCustomNotification(context, 'Не удалось открыть профиль');
+ return;
+ }
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => ContactProfileScreen(
+ contactId: id,
+ initialName: _contactName(contact),
+ initialAvatarUrl: contact['baseUrl'] as String?,
+ ),
+ ),
+ );
+}
+
+Future _openResolvedChat(
+ BuildContext context,
+ MaxLink link,
+ ResolvedChat resolved,
+) async {
+ final chat = resolved.chat;
+ final id = chat['id'];
+ if (id is! int) {
+ showCustomNotification(context, 'Не удалось открыть чат');
+ return;
+ }
+
+ final title = (chat['title'] as String?)?.trim() ?? '';
+ final type = (chat['type'] as String?) ?? 'CHAT';
+ final icon = (chat['baseIconUrl'] as String?) ?? '';
+ final access = chat['access'];
+
+ final profile = await AppDatabase.loadActiveProfile();
+ final myId = profile?.id ?? 0;
+ final participants = chat['participants'];
+ final isMember = myId != 0 &&
+ participants is Map &&
+ participants.containsKey(myId.toString());
+
+ await ChatsModule.cacheServerChat(chat, myId, inList: isMember);
+ if (!context.mounted) return;
+
+ if (link.kind == MaxLinkKind.invite && access == 'PRIVATE' && !isMember) {
+ final label = title.isEmpty ? 'этот чат' : '«$title»';
+ final confirmed = await showConfirmDialog(
+ context,
+ title: 'Вступить',
+ message: 'Вступить в $label?',
+ confirmLabel: 'Вступить',
+ );
+ if (!confirmed || !context.mounted) return;
+
+ final error = await LinkModule.join(api, link.url);
+ if (error != null) {
+ if (context.mounted) showCustomNotification(context, error);
+ return;
+ }
+ if (!context.mounted) return;
+ }
+
+ pushSwipeable(
+ context,
+ (_) => ChatScreen(
+ chatId: id,
+ name: title,
+ imageUrl: icon,
+ chatType: type,
+ ),
+ );
+}
+
+String _contactName(Map contact) {
+ final names = contact['names'];
+ if (names is List && names.isNotEmpty && names.first is Map) {
+ final entry = names.first as Map;
+ final full = (entry['name'] as String?)?.trim();
+ if (full != null && full.isNotEmpty) return full;
+ final first = (entry['firstName'] as String?)?.trim() ?? '';
+ final last = (entry['lastName'] as String?)?.trim() ?? '';
+ final joined = '$first $last'.trim();
+ if (joined.isNotEmpty) return joined;
+ }
+ return 'Профиль';
+}
diff --git a/lib/frontend/widgets/web_qr_login.dart b/lib/frontend/widgets/web_qr_login.dart
new file mode 100644
index 0000000..3972632
--- /dev/null
+++ b/lib/frontend/widgets/web_qr_login.dart
@@ -0,0 +1,115 @@
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+import '../../main.dart' show accountModule;
+import 'custom_notification.dart';
+import 'sheet_helpers.dart';
+
+Future showWebQrLoginConfirmSheet(BuildContext context) async {
+ final agreed = await showModalBottomSheet(
+ context: context,
+ backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
+ ),
+ builder: (sheetContext) {
+ final cs = Theme.of(sheetContext).colorScheme;
+ return SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const Center(child: SheetGrabber(margin: EdgeInsets.zero)),
+ const SizedBox(height: 20),
+ Text(
+ 'Вход по QR',
+ style: GoogleFonts.outfit(
+ fontSize: 20,
+ fontWeight: FontWeight.w700,
+ color: cs.onSurface,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Вы точно хотите войти в аккаунт через веб или приложение MAX '
+ 'на компьютере?',
+ style: TextStyle(
+ fontSize: 15,
+ height: 1.35,
+ color: cs.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 28),
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => Navigator.of(sheetContext).pop(false),
+ child: Text(
+ 'Отмена',
+ style: TextStyle(color: cs.onSurface),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: FilledButton(
+ onPressed: () => Navigator.of(sheetContext).pop(true),
+ child: const Text('Войти'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ return agreed ?? false;
+}
+
+Future confirmAndAuthorizeWebQrLogin(
+ BuildContext context,
+ String qrLink,
+) async {
+ final confirmed = await showWebQrLoginConfirmSheet(context);
+ if (!confirmed || !context.mounted) return false;
+
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (ctx) {
+ final cs = Theme.of(ctx).colorScheme;
+ return PopScope(
+ canPop: false,
+ child: Center(
+ child: Card(
+ color: cs.surfaceContainerHigh,
+ child: const Padding(
+ padding: EdgeInsets.all(28),
+ child: CircularProgressIndicator(),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+
+ try {
+ await accountModule.authorizeWebQrLogin(qrLink.trim());
+ if (context.mounted) {
+ Navigator.of(context, rootNavigator: true).pop();
+ showCustomNotification(context, 'Вход подтверждён');
+ }
+ return true;
+ } catch (e) {
+ if (context.mounted) {
+ Navigator.of(context, rootNavigator: true).pop();
+ showCustomNotification(context, 'Не удалось подтвердить вход: $e');
+ }
+ return false;
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index ea561f7..d4c0cdf 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -44,6 +44,7 @@ import 'backend/modules/self_check.dart';
import 'backend/modules/webapp.dart';
import 'backend/modules/digital_id.dart';
import 'core/calls/call_controller.dart';
+import 'core/links/deep_link_service.dart';
import 'frontend/screens/calls/call_screen.dart';
import 'core/push/push_service.dart';
import 'core/storage/app_database.dart';
@@ -95,6 +96,7 @@ void main() async {
}
attachInfoCacheApi(api);
ChatsModule.attachGlobalPushHandlers(api);
+ unawaited(DeepLinkService.instance.init());
final packageInfoFuture = PackageInfo.fromPlatform();
final localeFuture = _loadInitialLocale();
@@ -265,6 +267,7 @@ class KometAppState extends State
_loginStatusSub = accountModule.loginStatusStream.listen((status) async {
if (status == LoginStatus.success) {
+ DeepLinkService.instance.markReady();
CallController.instance.init(api);
OutboxService.instance.init(api, messagesModule);
SelfCheckService.instance.init(api);
diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist
index 4789daa..0022fd7 100644
--- a/macos/Runner/Info.plist
+++ b/macos/Runner/Info.plist
@@ -28,5 +28,17 @@
MainMenu
NSPrincipalClass
NSApplication
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ ru.komet.app
+ CFBundleURLSchemes
+
+ komet
+ max
+
+
+
diff --git a/pubspec.lock b/pubspec.lock
index 08ac60e..8f7e772 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -17,6 +17,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.2"
+ app_links:
+ dependency: "direct main"
+ description:
+ name: app_links
+ sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.1"
+ app_links_linux:
+ dependency: transitive
+ description:
+ name: app_links_linux
+ sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
+ app_links_platform_interface:
+ dependency: transitive
+ description:
+ name: app_links_platform_interface
+ sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ app_links_web:
+ dependency: transitive
+ description:
+ name: app_links_web
+ sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
archive:
dependency: transitive
description:
@@ -597,6 +629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.3"
+ gtk:
+ dependency: transitive
+ description:
+ name: gtk
+ sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
hooks:
dependency: transitive
description:
@@ -785,10 +825,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.18.0"
+ version: "1.17.0"
mobile_scanner:
dependency: "direct main"
description:
@@ -1174,10 +1214,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
- version: "0.7.11"
+ version: "0.7.10"
timezone:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index fff9b38..f70dfa1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -63,6 +63,7 @@ dependencies:
path_provider: ^2.1.4
open_filex: ^4.5.0
url_launcher: ^6.3.1
+ app_links: ^6.3.0
video_player: ^2.9.2
firebase_core: ^4.1.1
firebase_messaging: ^16.0.2