From 724f7d8242fe2479bad688d68f9d04f05213b12f Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Fri, 26 Dec 2025 16:38:33 +0800 Subject: [PATCH] feat: oidc support --- docs/AUTH_QUICK_START.md | 123 ++++ lib/common/conf/url_conf.dart | 11 +- lib/common/utils/url_scheme_handler.dart | 121 +++ lib/generated/proto/auth/auth.pb.dart | 689 +++++++++++++++++- lib/generated/proto/auth/auth.pbenum.dart | 2 +- lib/generated/proto/auth/auth.pbgrpc.dart | 156 +++- lib/generated/proto/auth/auth.pbjson.dart | 165 ++++- lib/generated/proto/common/common.pb.dart | 2 +- lib/generated/proto/common/common.pbenum.dart | 2 +- lib/generated/proto/common/common.pbgrpc.dart | 2 +- lib/generated/proto/common/common.pbjson.dart | 3 +- lib/generated/proto/partroom/partroom.pb.dart | 2 +- .../proto/partroom/partroom.pbenum.dart | 2 +- .../proto/partroom/partroom.pbgrpc.dart | 2 +- .../proto/partroom/partroom.pbjson.dart | 3 +- lib/provider/party_room.dart | 44 +- lib/provider/party_room.g.dart | 2 +- lib/ui/auth/auth_page.dart | 286 ++++++++ lib/ui/auth/auth_ui_model.dart | 237 ++++++ lib/ui/auth/auth_ui_model.freezed.dart | 295 ++++++++ lib/ui/auth/auth_ui_model.g.dart | 131 ++++ .../home_game_login_dialog_ui_model.g.dart | 2 +- lib/ui/index_ui.dart | 8 + lib/ui/settings/settings_ui_model.g.dart | 2 +- lib/ui/tools/tools_ui_model.g.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + linux/my_application.cc | 11 +- linux/sctoolbox.desktop | 10 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + macos/Runner/Info.plist | 11 + pubspec.lock | 44 +- pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/main.cpp | 41 ++ 37 files changed, 2386 insertions(+), 46 deletions(-) create mode 100644 docs/AUTH_QUICK_START.md create mode 100644 lib/common/utils/url_scheme_handler.dart create mode 100644 lib/ui/auth/auth_page.dart create mode 100644 lib/ui/auth/auth_ui_model.dart create mode 100644 lib/ui/auth/auth_ui_model.freezed.dart create mode 100644 lib/ui/auth/auth_ui_model.g.dart create mode 100644 linux/sctoolbox.desktop diff --git a/docs/AUTH_QUICK_START.md b/docs/AUTH_QUICK_START.md new file mode 100644 index 0000000..9420f9e --- /dev/null +++ b/docs/AUTH_QUICK_START.md @@ -0,0 +1,123 @@ +# SCToolBox OAuth 认证系统 + +## 快速开始 + +### 授权流程 + +``` +┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ +│ Web App │──1──▶│ Browser │──2──▶│ SCToolBox │──3──▶│ Server │ +└─────────┘ └──────────┘ └────────────┘ └──────────┘ + ▲ │ │ + │ ├──4──▶ 验证域名 │ + │ │ 安全性 │ + │ │ │ + │ ├──5──▶ 生成 JWT │ + │ │ 令牌 │ + │ │ │ + └─────────────────6─────────────────┘ │ + 返回令牌 │ +``` + + +### URL Scheme 格式 +``` +sctoolbox://auth/{domain}?callbackUrl={回调地址} +``` + +### 示例 +``` +sctoolbox://auth/example.com?callbackUrl=https%3A%2F%2Fexample.com%2Fauth%2Fcallback +``` + +### 回调格式 +``` +{callbackUrl}#access_token={jwt_token}&token_type=Bearer +``` + +## 功能特性 + +- ✅ 基于 JWT 的安全认证 +- ✅ 域名白名单验证 +- ✅ 跨平台支持(Windows、macOS、Linux) +- ✅ 两种授权方式(直接跳转 / 复制链接) +- ✅ 符合 OAuth 2.0 Implicit Flow 标准 + +## 实现文件 + +### 核心文件 +- `lib/ui/auth/auth_page.dart` - 授权页面 UI +- `lib/ui/auth/auth_ui_model.dart` - 授权页面状态管理 +- `lib/common/utils/url_scheme_handler.dart` - URL Scheme 处理器 + +### 平台配置 +- `macos/Runner/Info.plist` - macOS URL Scheme 配置 +- `windows/runner/main.cpp` - Windows Deep Link 处理 +- `linux/my_application.cc` - Linux Deep Link 处理 +- `linux/sctoolbox.desktop` - Linux MIME 类型注册 +- `pubspec.yaml` - MSIX 协议激活配置 + +## 使用方法 + +### 初始化 +URL Scheme handler 在 `IndexUI` 中自动初始化: + +```dart +useEffect(() { + UrlSchemeHandler().initialize(context); + return () => UrlSchemeHandler().dispose(); +}, const []); +``` + +### Web 应用集成 + +```javascript +// 发起授权 +const authUrl = `sctoolbox://auth/example.com?callbackUrl=${encodeURIComponent(callbackUrl)}`; +window.location.href = authUrl; + +// 处理回调 +const params = new URLSearchParams(window.location.hash.substring(1)); +const token = params.get('access_token'); +``` + +## 平台要求 + +- **Windows**: 需要使用 MSIX 打包版本 +- **macOS**: 需要配置 Info.plist +- **Linux**: 需要注册 .desktop 文件 + +## 安全性 + +- ✅ JWT 签名验证 +- ✅ 域名白名单检查 +- ✅ 令牌过期时间控制 +- ✅ 使用 Fragment (#) 传递令牌(更安全) + +## 详细文档 + +查看 [完整文档](./AUTH_SYSTEM.md) 了解更多信息,包括: +- 详细的授权流程 +- API 接口说明 +- Web 应用集成示例 +- 安全最佳实践 +- 常见问题解答 + +## API 端点 + +认证服务提供以下 gRPC 接口: + +- `GenerateToken` - 生成 JWT 令牌 +- `ValidateToken` - 验证令牌有效性 +- `GetPublicKey` - 获取公钥用于验证 +- `GetJWTDomainList` - 获取可信域名列表 + +## 测试 + +```bash +# macOS/Linux +open "sctoolbox://auth/test.example.com?callbackUrl=https%3A%2F%2Ftest.example.com%2Fcallback" + +# Windows +start "sctoolbox://auth/test.example.com?callbackUrl=https%3A%2F%2Ftest.example.com%2Fcallback" +``` diff --git a/lib/common/conf/url_conf.dart b/lib/common/conf/url_conf.dart index 2b74821..544a28e 100644 --- a/lib/common/conf/url_conf.dart +++ b/lib/common/conf/url_conf.dart @@ -10,8 +10,10 @@ class URLConf { static const String analyticsApiHome = "https://scbox.org"; /// PartyRoom Server - static const String partyRoomServerAddress = "ecdn.partyroom.grpc.scbox.xkeyc.cn"; - static const int partyRoomServerPort = 443; + static const String partyRoomServerAddress = "localhost"; + static const int partyRoomServerPort = 50051; + // static const String partyRoomServerAddress = "ecdn.partyroom.grpc.scbox.xkeyc.cn"; + // static const int partyRoomServerPort = 443; static bool isUrlCheckPass = false; @@ -51,10 +53,7 @@ class URLConf { gitApiHome = fasterGit; } final newsApiList = _genFinalList(await dnsLookupTxt("news.dns.scbox.org")); - final fasterNews = await rust_http.getFasterUrl( - urls: newsApiList, - pathSuffix: "/api/latest", - ); + final fasterNews = await rust_http.getFasterUrl(urls: newsApiList, pathSuffix: "/api/latest"); dPrint("DNS newsApiList ==== $newsApiList"); dPrint("newsApiList.Faster ==== $fasterNews"); if (fasterNews != null) { diff --git a/lib/common/utils/url_scheme_handler.dart b/lib/common/utils/url_scheme_handler.dart new file mode 100644 index 0000000..79d5fc1 --- /dev/null +++ b/lib/common/utils/url_scheme_handler.dart @@ -0,0 +1,121 @@ +import 'dart:async'; + +import 'package:app_links/app_links.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/ui/auth/auth_page.dart'; + +/// URL Scheme handler for deep linking +/// Handles: sctoolbox://auth?callbackUrl=https://example.com +class UrlSchemeHandler { + static final UrlSchemeHandler _instance = UrlSchemeHandler._internal(); + factory UrlSchemeHandler() => _instance; + UrlSchemeHandler._internal(); + + final _appLinks = AppLinks(); + StreamSubscription? _linkSubscription; + BuildContext? _context; + + // Debouncing variables + String? _lastHandledUri; + DateTime? _lastHandledTime; + static const _debounceDuration = Duration(seconds: 2); + + /// Initialize URL scheme handler + Future initialize(BuildContext context) async { + _context = context; + + // Handle initial link when app is launched via URL scheme + try { + final initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) { + dPrint('Initial URI: $initialUri'); + _handleUri(initialUri); + } + } catch (e) { + dPrint('Failed to get initial URI: $e'); + } + + // Handle links while app is running + _linkSubscription = _appLinks.uriLinkStream.listen( + (uri) { + dPrint('Received URI: $uri'); + _handleUri(uri); + }, + onError: (err) { + dPrint('URI link stream error: $err'); + }, + ); + } + + /// Handle incoming URI with debouncing + void _handleUri(Uri uri) { + final uriString = uri.toString(); + final now = DateTime.now(); + + // Check if this is a duplicate URI within debounce duration + if (_lastHandledUri == uriString && _lastHandledTime != null) { + final timeSinceLastHandle = now.difference(_lastHandledTime!); + if (timeSinceLastHandle < _debounceDuration) { + dPrint('Debounced duplicate URI: $uriString (${timeSinceLastHandle.inMilliseconds}ms since last)'); + return; + } + } + + // Update last handled URI and time + _lastHandledUri = uriString; + _lastHandledTime = now; + + dPrint('Handling URI: $uri'); + + // Check if it's an auth request + // Check if it's an auth request + // Expected format: sctoolbox://auth?callbackUrl=https://example.com&state=...&nonce=... + // Note: old format with domain in path (sctoolbox://auth/domain?...) is also supported but domain is ignored + if (uri.scheme == 'sctoolbox' && uri.host == 'auth') { + final callbackUrl = uri.queryParameters['callbackUrl']; + final state = uri.queryParameters['state']; + final nonce = uri.queryParameters['nonce']; + + if (callbackUrl == null || callbackUrl.isEmpty) { + dPrint('Invalid auth URI: missing callbackUrl parameter'); + return; + } + + if (state == null || state.isEmpty) { + dPrint('Invalid auth URI: missing state parameter'); + return; + } + + dPrint('Auth request - callbackUrl: $callbackUrl, state: $state'); + _showAuthDialog(callbackUrl, state, nonce); + } + } + + /// Show auth dialog + void _showAuthDialog(String callbackUrl, String state, String? nonce) { + if (_context == null || !_context!.mounted) { + dPrint('Cannot show auth dialog: context not available'); + return; + } + + showDialog( + context: _context!, + builder: (context) => AuthPage(callbackUrl: callbackUrl, stateParameter: state, nonce: nonce), + ); + } + + /// Dispose the handler + void dispose() { + _linkSubscription?.cancel(); + _linkSubscription = null; + _context = null; + _lastHandledUri = null; + _lastHandledTime = null; + } + + /// Update context (useful when switching screens) + void updateContext(BuildContext context) { + _context = context; + } +} diff --git a/lib/generated/proto/auth/auth.pb.dart b/lib/generated/proto/auth/auth.pb.dart index 2dbcda6..991f7d8 100644 --- a/lib/generated/proto/auth/auth.pb.dart +++ b/lib/generated/proto/auth/auth.pb.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; @@ -17,6 +17,258 @@ import 'package:protobuf/protobuf.dart' as $pb; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; +/// 生成 OIDC 授权码请求 +class GenerateOIDCAuthCodeRequest extends $pb.GeneratedMessage { + factory GenerateOIDCAuthCodeRequest({ + $core.String? nonce, + $core.String? redirectUri, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + if (redirectUri != null) result.redirectUri = redirectUri; + return result; + } + + GenerateOIDCAuthCodeRequest._(); + + factory GenerateOIDCAuthCodeRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GenerateOIDCAuthCodeRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GenerateOIDCAuthCodeRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'nonce') + ..aOS(2, _omitFieldNames ? '' : 'redirectUri') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GenerateOIDCAuthCodeRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GenerateOIDCAuthCodeRequest copyWith( + void Function(GenerateOIDCAuthCodeRequest) updates) => + super.copyWith( + (message) => updates(message as GenerateOIDCAuthCodeRequest)) + as GenerateOIDCAuthCodeRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GenerateOIDCAuthCodeRequest create() => + GenerateOIDCAuthCodeRequest._(); + @$core.override + GenerateOIDCAuthCodeRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GenerateOIDCAuthCodeRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GenerateOIDCAuthCodeRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get nonce => $_getSZ(0); + @$pb.TagNumber(1) + set nonce($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get redirectUri => $_getSZ(1); + @$pb.TagNumber(2) + set redirectUri($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasRedirectUri() => $_has(1); + @$pb.TagNumber(2) + void clearRedirectUri() => $_clearField(2); +} + +/// 生成 OIDC 授权码响应 +class GenerateOIDCAuthCodeResponse extends $pb.GeneratedMessage { + factory GenerateOIDCAuthCodeResponse({ + $core.String? code, + $fixnum.Int64? expiresAt, + }) { + final result = create(); + if (code != null) result.code = code; + if (expiresAt != null) result.expiresAt = expiresAt; + return result; + } + + GenerateOIDCAuthCodeResponse._(); + + factory GenerateOIDCAuthCodeResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GenerateOIDCAuthCodeResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GenerateOIDCAuthCodeResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'code') + ..aInt64(2, _omitFieldNames ? '' : 'expiresAt') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GenerateOIDCAuthCodeResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GenerateOIDCAuthCodeResponse copyWith( + void Function(GenerateOIDCAuthCodeResponse) updates) => + super.copyWith( + (message) => updates(message as GenerateOIDCAuthCodeResponse)) + as GenerateOIDCAuthCodeResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GenerateOIDCAuthCodeResponse create() => + GenerateOIDCAuthCodeResponse._(); + @$core.override + GenerateOIDCAuthCodeResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GenerateOIDCAuthCodeResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GenerateOIDCAuthCodeResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get code => $_getSZ(0); + @$pb.TagNumber(1) + set code($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasCode() => $_has(0); + @$pb.TagNumber(1) + void clearCode() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get expiresAt => $_getI64(1); + @$pb.TagNumber(2) + set expiresAt($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasExpiresAt() => $_has(1); + @$pb.TagNumber(2) + void clearExpiresAt() => $_clearField(2); +} + +/// 刷新用户资料请求 +class RefreshUserProfileRequest extends $pb.GeneratedMessage { + factory RefreshUserProfileRequest() => create(); + + RefreshUserProfileRequest._(); + + factory RefreshUserProfileRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RefreshUserProfileRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RefreshUserProfileRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RefreshUserProfileRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RefreshUserProfileRequest copyWith( + void Function(RefreshUserProfileRequest) updates) => + super.copyWith((message) => updates(message as RefreshUserProfileRequest)) + as RefreshUserProfileRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RefreshUserProfileRequest create() => RefreshUserProfileRequest._(); + @$core.override + RefreshUserProfileRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RefreshUserProfileRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RefreshUserProfileRequest? _defaultInstance; +} + +/// 刷新用户资料响应 +class RefreshUserProfileResponse extends $pb.GeneratedMessage { + factory RefreshUserProfileResponse({ + $core.bool? success, + GameUserInfo? userInfo, + }) { + final result = create(); + if (success != null) result.success = success; + if (userInfo != null) result.userInfo = userInfo; + return result; + } + + RefreshUserProfileResponse._(); + + factory RefreshUserProfileResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RefreshUserProfileResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RefreshUserProfileResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'success') + ..aOM(2, _omitFieldNames ? '' : 'userInfo', + subBuilder: GameUserInfo.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RefreshUserProfileResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RefreshUserProfileResponse copyWith( + void Function(RefreshUserProfileResponse) updates) => + super.copyWith( + (message) => updates(message as RefreshUserProfileResponse)) + as RefreshUserProfileResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RefreshUserProfileResponse create() => RefreshUserProfileResponse._(); + @$core.override + RefreshUserProfileResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RefreshUserProfileResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RefreshUserProfileResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get success => $_getBF(0); + @$pb.TagNumber(1) + set success($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasSuccess() => $_has(0); + @$pb.TagNumber(1) + void clearSuccess() => $_clearField(1); + + @$pb.TagNumber(2) + GameUserInfo get userInfo => $_getN(1); + @$pb.TagNumber(2) + set userInfo(GameUserInfo value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasUserInfo() => $_has(1); + @$pb.TagNumber(2) + void clearUserInfo() => $_clearField(2); + @$pb.TagNumber(2) + GameUserInfo ensureUserInfo() => $_ensure(1); +} + /// 服务状态请求 class StatusRequest extends $pb.GeneratedMessage { factory StatusRequest() => create(); @@ -725,6 +977,441 @@ class UnregisterResponse extends $pb.GeneratedMessage { void clearSuccess() => $_clearField(1); } +/// 验证 token 请求 +class ValidateTokenRequest extends $pb.GeneratedMessage { + factory ValidateTokenRequest({ + $core.String? token, + }) { + final result = create(); + if (token != null) result.token = token; + return result; + } + + ValidateTokenRequest._(); + + factory ValidateTokenRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ValidateTokenRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ValidateTokenRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'token') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ValidateTokenRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ValidateTokenRequest copyWith(void Function(ValidateTokenRequest) updates) => + super.copyWith((message) => updates(message as ValidateTokenRequest)) + as ValidateTokenRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ValidateTokenRequest create() => ValidateTokenRequest._(); + @$core.override + ValidateTokenRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ValidateTokenRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ValidateTokenRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get token => $_getSZ(0); + @$pb.TagNumber(1) + set token($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasToken() => $_has(0); + @$pb.TagNumber(1) + void clearToken() => $_clearField(1); +} + +/// 验证 token 响应 +class ValidateTokenResponse extends $pb.GeneratedMessage { + factory ValidateTokenResponse({ + $core.bool? valid, + $core.String? domain, + $fixnum.Int64? issuedAt, + $fixnum.Int64? expiresAt, + $core.String? errorMessage, + }) { + final result = create(); + if (valid != null) result.valid = valid; + if (domain != null) result.domain = domain; + if (issuedAt != null) result.issuedAt = issuedAt; + if (expiresAt != null) result.expiresAt = expiresAt; + if (errorMessage != null) result.errorMessage = errorMessage; + return result; + } + + ValidateTokenResponse._(); + + factory ValidateTokenResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ValidateTokenResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ValidateTokenResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'valid') + ..aOS(2, _omitFieldNames ? '' : 'domain') + ..aInt64(3, _omitFieldNames ? '' : 'issuedAt') + ..aInt64(4, _omitFieldNames ? '' : 'expiresAt') + ..aOS(5, _omitFieldNames ? '' : 'errorMessage') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ValidateTokenResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ValidateTokenResponse copyWith( + void Function(ValidateTokenResponse) updates) => + super.copyWith((message) => updates(message as ValidateTokenResponse)) + as ValidateTokenResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ValidateTokenResponse create() => ValidateTokenResponse._(); + @$core.override + ValidateTokenResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ValidateTokenResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ValidateTokenResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get valid => $_getBF(0); + @$pb.TagNumber(1) + set valid($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasValid() => $_has(0); + @$pb.TagNumber(1) + void clearValid() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get domain => $_getSZ(1); + @$pb.TagNumber(2) + set domain($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasDomain() => $_has(1); + @$pb.TagNumber(2) + void clearDomain() => $_clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get issuedAt => $_getI64(2); + @$pb.TagNumber(3) + set issuedAt($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasIssuedAt() => $_has(2); + @$pb.TagNumber(3) + void clearIssuedAt() => $_clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get expiresAt => $_getI64(3); + @$pb.TagNumber(4) + set expiresAt($fixnum.Int64 value) => $_setInt64(3, value); + @$pb.TagNumber(4) + $core.bool hasExpiresAt() => $_has(3); + @$pb.TagNumber(4) + void clearExpiresAt() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get errorMessage => $_getSZ(4); + @$pb.TagNumber(5) + set errorMessage($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasErrorMessage() => $_has(4); + @$pb.TagNumber(5) + void clearErrorMessage() => $_clearField(5); +} + +/// 获取公钥请求 +class GetPublicKeyRequest extends $pb.GeneratedMessage { + factory GetPublicKeyRequest() => create(); + + GetPublicKeyRequest._(); + + factory GetPublicKeyRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GetPublicKeyRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GetPublicKeyRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetPublicKeyRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetPublicKeyRequest copyWith(void Function(GetPublicKeyRequest) updates) => + super.copyWith((message) => updates(message as GetPublicKeyRequest)) + as GetPublicKeyRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetPublicKeyRequest create() => GetPublicKeyRequest._(); + @$core.override + GetPublicKeyRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GetPublicKeyRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GetPublicKeyRequest? _defaultInstance; +} + +/// 获取公钥响应 +class GetPublicKeyResponse extends $pb.GeneratedMessage { + factory GetPublicKeyResponse({ + $core.String? publicKeyPem, + $core.String? keyId, + $core.String? algorithm, + }) { + final result = create(); + if (publicKeyPem != null) result.publicKeyPem = publicKeyPem; + if (keyId != null) result.keyId = keyId; + if (algorithm != null) result.algorithm = algorithm; + return result; + } + + GetPublicKeyResponse._(); + + factory GetPublicKeyResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GetPublicKeyResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GetPublicKeyResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'publicKeyPem') + ..aOS(2, _omitFieldNames ? '' : 'keyId') + ..aOS(3, _omitFieldNames ? '' : 'algorithm') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetPublicKeyResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetPublicKeyResponse copyWith(void Function(GetPublicKeyResponse) updates) => + super.copyWith((message) => updates(message as GetPublicKeyResponse)) + as GetPublicKeyResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetPublicKeyResponse create() => GetPublicKeyResponse._(); + @$core.override + GetPublicKeyResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GetPublicKeyResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GetPublicKeyResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get publicKeyPem => $_getSZ(0); + @$pb.TagNumber(1) + set publicKeyPem($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasPublicKeyPem() => $_has(0); + @$pb.TagNumber(1) + void clearPublicKeyPem() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get keyId => $_getSZ(1); + @$pb.TagNumber(2) + set keyId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasKeyId() => $_has(1); + @$pb.TagNumber(2) + void clearKeyId() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get algorithm => $_getSZ(2); + @$pb.TagNumber(3) + set algorithm($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasAlgorithm() => $_has(2); + @$pb.TagNumber(3) + void clearAlgorithm() => $_clearField(3); +} + +/// JWT 域名信息 +class JWTDomainInfo extends $pb.GeneratedMessage { + factory JWTDomainInfo({ + $core.String? domain, + $core.String? name, + }) { + final result = create(); + if (domain != null) result.domain = domain; + if (name != null) result.name = name; + return result; + } + + JWTDomainInfo._(); + + factory JWTDomainInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory JWTDomainInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'JWTDomainInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'domain') + ..aOS(2, _omitFieldNames ? '' : 'name') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + JWTDomainInfo clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + JWTDomainInfo copyWith(void Function(JWTDomainInfo) updates) => + super.copyWith((message) => updates(message as JWTDomainInfo)) + as JWTDomainInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static JWTDomainInfo create() => JWTDomainInfo._(); + @$core.override + JWTDomainInfo createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static JWTDomainInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static JWTDomainInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get domain => $_getSZ(0); + @$pb.TagNumber(1) + set domain($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasDomain() => $_has(0); + @$pb.TagNumber(1) + void clearDomain() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => $_clearField(2); +} + +/// 获取 JWT 域名列表请求 +class GetJWTDomainListRequest extends $pb.GeneratedMessage { + factory GetJWTDomainListRequest() => create(); + + GetJWTDomainListRequest._(); + + factory GetJWTDomainListRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GetJWTDomainListRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GetJWTDomainListRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetJWTDomainListRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetJWTDomainListRequest copyWith( + void Function(GetJWTDomainListRequest) updates) => + super.copyWith((message) => updates(message as GetJWTDomainListRequest)) + as GetJWTDomainListRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetJWTDomainListRequest create() => GetJWTDomainListRequest._(); + @$core.override + GetJWTDomainListRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GetJWTDomainListRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GetJWTDomainListRequest? _defaultInstance; +} + +/// 获取 JWT 域名列表响应 +class GetJWTDomainListResponse extends $pb.GeneratedMessage { + factory GetJWTDomainListResponse({ + $core.Iterable? domains, + }) { + final result = create(); + if (domains != null) result.domains.addAll(domains); + return result; + } + + GetJWTDomainListResponse._(); + + factory GetJWTDomainListResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GetJWTDomainListResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GetJWTDomainListResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'auth'), + createEmptyInstance: create) + ..pPM(1, _omitFieldNames ? '' : 'domains', + subBuilder: JWTDomainInfo.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetJWTDomainListResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetJWTDomainListResponse copyWith( + void Function(GetJWTDomainListResponse) updates) => + super.copyWith((message) => updates(message as GetJWTDomainListResponse)) + as GetJWTDomainListResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetJWTDomainListResponse create() => GetJWTDomainListResponse._(); + @$core.override + GetJWTDomainListResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static GetJWTDomainListResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GetJWTDomainListResponse? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get domains => $_getList(0); +} + const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = diff --git a/lib/generated/proto/auth/auth.pbenum.dart b/lib/generated/proto/auth/auth.pbenum.dart index d79107f..6c66ae3 100644 --- a/lib/generated/proto/auth/auth.pbenum.dart +++ b/lib/generated/proto/auth/auth.pbenum.dart @@ -8,4 +8,4 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports diff --git a/lib/generated/proto/auth/auth.pbgrpc.dart b/lib/generated/proto/auth/auth.pbgrpc.dart index e366d42..dc0e775 100644 --- a/lib/generated/proto/auth/auth.pbgrpc.dart +++ b/lib/generated/proto/auth/auth.pbgrpc.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:async' as $async; import 'dart:core' as $core; @@ -73,6 +73,45 @@ class AuthServiceClient extends $grpc.Client { return $createUnaryCall(_$unregister, request, options: options); } + /// 验证 JWT token + $grpc.ResponseFuture<$0.ValidateTokenResponse> validateToken( + $0.ValidateTokenRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$validateToken, request, options: options); + } + + /// 获取公钥信息 + $grpc.ResponseFuture<$0.GetPublicKeyResponse> getPublicKey( + $0.GetPublicKeyRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$getPublicKey, request, options: options); + } + + $grpc.ResponseFuture<$0.GetJWTDomainListResponse> getJWTDomainList( + $0.GetJWTDomainListRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$getJWTDomainList, request, options: options); + } + + /// 刷新用户资料(需要认证) + $grpc.ResponseFuture<$0.RefreshUserProfileResponse> refreshUserProfile( + $0.RefreshUserProfileRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$refreshUserProfile, request, options: options); + } + + /// 生成 OIDC 授权码(供客户端 App 使用) + $grpc.ResponseFuture<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode( + $0.GenerateOIDCAuthCodeRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$generateOIDCAuthCode, request, options: options); + } + // method descriptors static final _$status = @@ -99,6 +138,31 @@ class AuthServiceClient extends $grpc.Client { '/auth.AuthService/Unregister', ($0.UnregisterRequest value) => value.writeToBuffer(), $0.UnregisterResponse.fromBuffer); + static final _$validateToken = + $grpc.ClientMethod<$0.ValidateTokenRequest, $0.ValidateTokenResponse>( + '/auth.AuthService/ValidateToken', + ($0.ValidateTokenRequest value) => value.writeToBuffer(), + $0.ValidateTokenResponse.fromBuffer); + static final _$getPublicKey = + $grpc.ClientMethod<$0.GetPublicKeyRequest, $0.GetPublicKeyResponse>( + '/auth.AuthService/GetPublicKey', + ($0.GetPublicKeyRequest value) => value.writeToBuffer(), + $0.GetPublicKeyResponse.fromBuffer); + static final _$getJWTDomainList = $grpc.ClientMethod< + $0.GetJWTDomainListRequest, $0.GetJWTDomainListResponse>( + '/auth.AuthService/GetJWTDomainList', + ($0.GetJWTDomainListRequest value) => value.writeToBuffer(), + $0.GetJWTDomainListResponse.fromBuffer); + static final _$refreshUserProfile = $grpc.ClientMethod< + $0.RefreshUserProfileRequest, $0.RefreshUserProfileResponse>( + '/auth.AuthService/RefreshUserProfile', + ($0.RefreshUserProfileRequest value) => value.writeToBuffer(), + $0.RefreshUserProfileResponse.fromBuffer); + static final _$generateOIDCAuthCode = $grpc.ClientMethod< + $0.GenerateOIDCAuthCodeRequest, $0.GenerateOIDCAuthCodeResponse>( + '/auth.AuthService/GenerateOIDCAuthCode', + ($0.GenerateOIDCAuthCodeRequest value) => value.writeToBuffer(), + $0.GenerateOIDCAuthCodeResponse.fromBuffer); } @$pb.GrpcServiceName('auth.AuthService') @@ -143,6 +207,51 @@ abstract class AuthServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $0.UnregisterRequest.fromBuffer(value), ($0.UnregisterResponse value) => value.writeToBuffer())); + $addMethod( + $grpc.ServiceMethod<$0.ValidateTokenRequest, $0.ValidateTokenResponse>( + 'ValidateToken', + validateToken_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.ValidateTokenRequest.fromBuffer(value), + ($0.ValidateTokenResponse value) => value.writeToBuffer())); + $addMethod( + $grpc.ServiceMethod<$0.GetPublicKeyRequest, $0.GetPublicKeyResponse>( + 'GetPublicKey', + getPublicKey_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.GetPublicKeyRequest.fromBuffer(value), + ($0.GetPublicKeyResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.GetJWTDomainListRequest, + $0.GetJWTDomainListResponse>( + 'GetJWTDomainList', + getJWTDomainList_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.GetJWTDomainListRequest.fromBuffer(value), + ($0.GetJWTDomainListResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.RefreshUserProfileRequest, + $0.RefreshUserProfileResponse>( + 'RefreshUserProfile', + refreshUserProfile_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.RefreshUserProfileRequest.fromBuffer(value), + ($0.RefreshUserProfileResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.GenerateOIDCAuthCodeRequest, + $0.GenerateOIDCAuthCodeResponse>( + 'GenerateOIDCAuthCode', + generateOIDCAuthCode_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.GenerateOIDCAuthCodeRequest.fromBuffer(value), + ($0.GenerateOIDCAuthCodeResponse value) => value.writeToBuffer())); } $async.Future<$0.StatusResponse> status_Pre( @@ -184,4 +293,49 @@ abstract class AuthServiceBase extends $grpc.Service { $async.Future<$0.UnregisterResponse> unregister( $grpc.ServiceCall call, $0.UnregisterRequest request); + + $async.Future<$0.ValidateTokenResponse> validateToken_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.ValidateTokenRequest> $request) async { + return validateToken($call, await $request); + } + + $async.Future<$0.ValidateTokenResponse> validateToken( + $grpc.ServiceCall call, $0.ValidateTokenRequest request); + + $async.Future<$0.GetPublicKeyResponse> getPublicKey_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.GetPublicKeyRequest> $request) async { + return getPublicKey($call, await $request); + } + + $async.Future<$0.GetPublicKeyResponse> getPublicKey( + $grpc.ServiceCall call, $0.GetPublicKeyRequest request); + + $async.Future<$0.GetJWTDomainListResponse> getJWTDomainList_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.GetJWTDomainListRequest> $request) async { + return getJWTDomainList($call, await $request); + } + + $async.Future<$0.GetJWTDomainListResponse> getJWTDomainList( + $grpc.ServiceCall call, $0.GetJWTDomainListRequest request); + + $async.Future<$0.RefreshUserProfileResponse> refreshUserProfile_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.RefreshUserProfileRequest> $request) async { + return refreshUserProfile($call, await $request); + } + + $async.Future<$0.RefreshUserProfileResponse> refreshUserProfile( + $grpc.ServiceCall call, $0.RefreshUserProfileRequest request); + + $async.Future<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.GenerateOIDCAuthCodeRequest> $request) async { + return generateOIDCAuthCode($call, await $request); + } + + $async.Future<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode( + $grpc.ServiceCall call, $0.GenerateOIDCAuthCodeRequest request); } diff --git a/lib/generated/proto/auth/auth.pbjson.dart b/lib/generated/proto/auth/auth.pbjson.dart index 076cbdd..cf03683 100644 --- a/lib/generated/proto/auth/auth.pbjson.dart +++ b/lib/generated/proto/auth/auth.pbjson.dart @@ -8,12 +8,74 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names, unused_import +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports +// ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use generateOIDCAuthCodeRequestDescriptor instead') +const GenerateOIDCAuthCodeRequest$json = { + '1': 'GenerateOIDCAuthCodeRequest', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 9, '10': 'nonce'}, + {'1': 'redirect_uri', '3': 2, '4': 1, '5': 9, '10': 'redirectUri'}, + ], +}; + +/// Descriptor for `GenerateOIDCAuthCodeRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List generateOIDCAuthCodeRequestDescriptor = + $convert.base64Decode( + 'ChtHZW5lcmF0ZU9JRENBdXRoQ29kZVJlcXVlc3QSFAoFbm9uY2UYASABKAlSBW5vbmNlEiEKDH' + 'JlZGlyZWN0X3VyaRgCIAEoCVILcmVkaXJlY3RVcmk='); + +@$core.Deprecated('Use generateOIDCAuthCodeResponseDescriptor instead') +const GenerateOIDCAuthCodeResponse$json = { + '1': 'GenerateOIDCAuthCodeResponse', + '2': [ + {'1': 'code', '3': 1, '4': 1, '5': 9, '10': 'code'}, + {'1': 'expires_at', '3': 2, '4': 1, '5': 3, '10': 'expiresAt'}, + ], +}; + +/// Descriptor for `GenerateOIDCAuthCodeResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List generateOIDCAuthCodeResponseDescriptor = + $convert.base64Decode( + 'ChxHZW5lcmF0ZU9JRENBdXRoQ29kZVJlc3BvbnNlEhIKBGNvZGUYASABKAlSBGNvZGUSHQoKZX' + 'hwaXJlc19hdBgCIAEoA1IJZXhwaXJlc0F0'); + +@$core.Deprecated('Use refreshUserProfileRequestDescriptor instead') +const RefreshUserProfileRequest$json = { + '1': 'RefreshUserProfileRequest', +}; + +/// Descriptor for `RefreshUserProfileRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List refreshUserProfileRequestDescriptor = + $convert.base64Decode('ChlSZWZyZXNoVXNlclByb2ZpbGVSZXF1ZXN0'); + +@$core.Deprecated('Use refreshUserProfileResponseDescriptor instead') +const RefreshUserProfileResponse$json = { + '1': 'RefreshUserProfileResponse', + '2': [ + {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, + { + '1': 'user_info', + '3': 2, + '4': 1, + '5': 11, + '6': '.auth.GameUserInfo', + '10': 'userInfo' + }, + ], +}; + +/// Descriptor for `RefreshUserProfileResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List refreshUserProfileResponseDescriptor = + $convert.base64Decode( + 'ChpSZWZyZXNoVXNlclByb2ZpbGVSZXNwb25zZRIYCgdzdWNjZXNzGAEgASgIUgdzdWNjZXNzEi' + '8KCXVzZXJfaW5mbxgCIAEoCzISLmF1dGguR2FtZVVzZXJJbmZvUgh1c2VySW5mbw=='); + @$core.Deprecated('Use statusRequestDescriptor instead') const StatusRequest$json = { '1': 'StatusRequest', @@ -186,3 +248,104 @@ const UnregisterResponse$json = { final $typed_data.Uint8List unregisterResponseDescriptor = $convert.base64Decode( 'ChJVbnJlZ2lzdGVyUmVzcG9uc2USGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2Vzcw=='); + +@$core.Deprecated('Use validateTokenRequestDescriptor instead') +const ValidateTokenRequest$json = { + '1': 'ValidateTokenRequest', + '2': [ + {'1': 'token', '3': 1, '4': 1, '5': 9, '10': 'token'}, + ], +}; + +/// Descriptor for `ValidateTokenRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List validateTokenRequestDescriptor = + $convert.base64Decode( + 'ChRWYWxpZGF0ZVRva2VuUmVxdWVzdBIUCgV0b2tlbhgBIAEoCVIFdG9rZW4='); + +@$core.Deprecated('Use validateTokenResponseDescriptor instead') +const ValidateTokenResponse$json = { + '1': 'ValidateTokenResponse', + '2': [ + {'1': 'valid', '3': 1, '4': 1, '5': 8, '10': 'valid'}, + {'1': 'domain', '3': 2, '4': 1, '5': 9, '10': 'domain'}, + {'1': 'issued_at', '3': 3, '4': 1, '5': 3, '10': 'issuedAt'}, + {'1': 'expires_at', '3': 4, '4': 1, '5': 3, '10': 'expiresAt'}, + {'1': 'error_message', '3': 5, '4': 1, '5': 9, '10': 'errorMessage'}, + ], +}; + +/// Descriptor for `ValidateTokenResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List validateTokenResponseDescriptor = $convert.base64Decode( + 'ChVWYWxpZGF0ZVRva2VuUmVzcG9uc2USFAoFdmFsaWQYASABKAhSBXZhbGlkEhYKBmRvbWFpbh' + 'gCIAEoCVIGZG9tYWluEhsKCWlzc3VlZF9hdBgDIAEoA1IIaXNzdWVkQXQSHQoKZXhwaXJlc19h' + 'dBgEIAEoA1IJZXhwaXJlc0F0EiMKDWVycm9yX21lc3NhZ2UYBSABKAlSDGVycm9yTWVzc2FnZQ' + '=='); + +@$core.Deprecated('Use getPublicKeyRequestDescriptor instead') +const GetPublicKeyRequest$json = { + '1': 'GetPublicKeyRequest', +}; + +/// Descriptor for `GetPublicKeyRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getPublicKeyRequestDescriptor = + $convert.base64Decode('ChNHZXRQdWJsaWNLZXlSZXF1ZXN0'); + +@$core.Deprecated('Use getPublicKeyResponseDescriptor instead') +const GetPublicKeyResponse$json = { + '1': 'GetPublicKeyResponse', + '2': [ + {'1': 'public_key_pem', '3': 1, '4': 1, '5': 9, '10': 'publicKeyPem'}, + {'1': 'key_id', '3': 2, '4': 1, '5': 9, '10': 'keyId'}, + {'1': 'algorithm', '3': 3, '4': 1, '5': 9, '10': 'algorithm'}, + ], +}; + +/// Descriptor for `GetPublicKeyResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getPublicKeyResponseDescriptor = $convert.base64Decode( + 'ChRHZXRQdWJsaWNLZXlSZXNwb25zZRIkCg5wdWJsaWNfa2V5X3BlbRgBIAEoCVIMcHVibGljS2' + 'V5UGVtEhUKBmtleV9pZBgCIAEoCVIFa2V5SWQSHAoJYWxnb3JpdGhtGAMgASgJUglhbGdvcml0' + 'aG0='); + +@$core.Deprecated('Use jWTDomainInfoDescriptor instead') +const JWTDomainInfo$json = { + '1': 'JWTDomainInfo', + '2': [ + {'1': 'domain', '3': 1, '4': 1, '5': 9, '10': 'domain'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + ], +}; + +/// Descriptor for `JWTDomainInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List jWTDomainInfoDescriptor = $convert.base64Decode( + 'Cg1KV1REb21haW5JbmZvEhYKBmRvbWFpbhgBIAEoCVIGZG9tYWluEhIKBG5hbWUYAiABKAlSBG' + '5hbWU='); + +@$core.Deprecated('Use getJWTDomainListRequestDescriptor instead') +const GetJWTDomainListRequest$json = { + '1': 'GetJWTDomainListRequest', +}; + +/// Descriptor for `GetJWTDomainListRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getJWTDomainListRequestDescriptor = + $convert.base64Decode('ChdHZXRKV1REb21haW5MaXN0UmVxdWVzdA=='); + +@$core.Deprecated('Use getJWTDomainListResponseDescriptor instead') +const GetJWTDomainListResponse$json = { + '1': 'GetJWTDomainListResponse', + '2': [ + { + '1': 'domains', + '3': 1, + '4': 3, + '5': 11, + '6': '.auth.JWTDomainInfo', + '10': 'domains' + }, + ], +}; + +/// Descriptor for `GetJWTDomainListResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getJWTDomainListResponseDescriptor = + $convert.base64Decode( + 'ChhHZXRKV1REb21haW5MaXN0UmVzcG9uc2USLQoHZG9tYWlucxgBIAMoCzITLmF1dGguSldURG' + '9tYWluSW5mb1IHZG9tYWlucw=='); diff --git a/lib/generated/proto/common/common.pb.dart b/lib/generated/proto/common/common.pb.dart index ac359d1..fb01863 100644 --- a/lib/generated/proto/common/common.pb.dart +++ b/lib/generated/proto/common/common.pb.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; diff --git a/lib/generated/proto/common/common.pbenum.dart b/lib/generated/proto/common/common.pbenum.dart index 429a18e..dcbd7c8 100644 --- a/lib/generated/proto/common/common.pbenum.dart +++ b/lib/generated/proto/common/common.pbenum.dart @@ -8,4 +8,4 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports diff --git a/lib/generated/proto/common/common.pbgrpc.dart b/lib/generated/proto/common/common.pbgrpc.dart index 88aba82..ad453fa 100644 --- a/lib/generated/proto/common/common.pbgrpc.dart +++ b/lib/generated/proto/common/common.pbgrpc.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:async' as $async; import 'dart:core' as $core; diff --git a/lib/generated/proto/common/common.pbjson.dart b/lib/generated/proto/common/common.pbjson.dart index 75a7c16..80c7982 100644 --- a/lib/generated/proto/common/common.pbjson.dart +++ b/lib/generated/proto/common/common.pbjson.dart @@ -8,7 +8,8 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names, unused_import +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports +// ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; diff --git a/lib/generated/proto/partroom/partroom.pb.dart b/lib/generated/proto/partroom/partroom.pb.dart index aa5820d..3aa21ea 100644 --- a/lib/generated/proto/partroom/partroom.pb.dart +++ b/lib/generated/proto/partroom/partroom.pb.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; diff --git a/lib/generated/proto/partroom/partroom.pbenum.dart b/lib/generated/proto/partroom/partroom.pbenum.dart index f919bb5..1652d36 100644 --- a/lib/generated/proto/partroom/partroom.pbenum.dart +++ b/lib/generated/proto/partroom/partroom.pbenum.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; diff --git a/lib/generated/proto/partroom/partroom.pbgrpc.dart b/lib/generated/proto/partroom/partroom.pbgrpc.dart index a00f136..a626bb5 100644 --- a/lib/generated/proto/partroom/partroom.pbgrpc.dart +++ b/lib/generated/proto/partroom/partroom.pbgrpc.dart @@ -8,7 +8,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:async' as $async; import 'dart:core' as $core; diff --git a/lib/generated/proto/partroom/partroom.pbjson.dart b/lib/generated/proto/partroom/partroom.pbjson.dart index acf8c45..4d2ab0b 100644 --- a/lib/generated/proto/partroom/partroom.pbjson.dart +++ b/lib/generated/proto/partroom/partroom.pbjson.dart @@ -8,7 +8,8 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names, unused_import +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports +// ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; diff --git a/lib/provider/party_room.dart b/lib/provider/party_room.dart index db863c2..4aad8f0 100644 --- a/lib/provider/party_room.dart +++ b/lib/provider/party_room.dart @@ -127,11 +127,17 @@ class PartyRoom extends _$PartyRoom { final serverAddress = URLConf.partyRoomServerAddress; final serverPort = URLConf.partyRoomServerPort; + var credentials = ChannelCredentials.secure(); + + if (serverAddress == '127.0.0.1' || serverAddress == 'localhost') { + credentials = ChannelCredentials.insecure(); + } final channel = ClientChannel( serverAddress, port: serverPort, options: ChannelOptions( + credentials: credentials, keepAlive: ClientKeepAliveOptions( pingInterval: Duration(seconds: 30), timeout: Duration(seconds: 10), @@ -179,7 +185,7 @@ class PartyRoom extends _$PartyRoom { } /// 获取认证 CallOptions - CallOptions _getAuthCallOptions() { + CallOptions getAuthCallOptions() { return CallOptions(metadata: {'uuid': state.auth.uuid, 'secret-key': state.auth.secretKey}); } @@ -191,7 +197,7 @@ class PartyRoom extends _$PartyRoom { final client = state.client.authClient; if (client == null) throw Exception('Not connected to server'); - final response = await client.login(auth.LoginRequest(), options: _getAuthCallOptions()); + final response = await client.login(auth.LoginRequest(), options: getAuthCallOptions()); state = state.copyWith( auth: state.auth.copyWith( @@ -255,7 +261,7 @@ class PartyRoom extends _$PartyRoom { final client = state.client.authClient; if (client == null) throw Exception('Not connected to server'); - await client.unregister(auth.UnregisterRequest(), options: _getAuthCallOptions()); + await client.unregister(auth.UnregisterRequest(), options: getAuthCallOptions()); // 清除本地认证信息 await _confBox?.delete(_secretKeyKey); @@ -349,7 +355,7 @@ class PartyRoom extends _$PartyRoom { password_5: password ?? '', socialLinks: socialLinks ?? [], ), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); state = state.copyWith(room: state.room.copyWith(roomUuid: response.roomUuid, isInRoom: true, isOwner: true)); @@ -376,7 +382,7 @@ class PartyRoom extends _$PartyRoom { await client.joinRoom( partroom.JoinRoomRequest(roomUuid: roomUuid, password: password ?? ''), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); state = state.copyWith(room: state.room.copyWith(roomUuid: roomUuid, isInRoom: true, isOwner: false)); @@ -404,7 +410,7 @@ class PartyRoom extends _$PartyRoom { final roomUuid = state.room.roomUuid; if (roomUuid == null) return; - await client.leaveRoom(partroom.LeaveRoomRequest(roomUuid: roomUuid), options: _getAuthCallOptions()); + await client.leaveRoom(partroom.LeaveRoomRequest(roomUuid: roomUuid), options: getAuthCallOptions()); await _stopHeartbeat(); await _stopEventStream(); @@ -427,7 +433,7 @@ class PartyRoom extends _$PartyRoom { final roomUuid = state.room.roomUuid; if (roomUuid == null) return; - await client.dismissRoom(partroom.DismissRoomRequest(roomUuid: roomUuid), options: _getAuthCallOptions()); + await client.dismissRoom(partroom.DismissRoomRequest(roomUuid: roomUuid), options: getAuthCallOptions()); await _stopHeartbeat(); await _stopEventStream(); @@ -449,7 +455,7 @@ class PartyRoom extends _$PartyRoom { final response = await client.getRoomInfo( partroom.GetRoomInfoRequest(roomUuid: roomUuid), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); // 检查是否为房主 @@ -477,7 +483,7 @@ class PartyRoom extends _$PartyRoom { final response = await client.getRoomMembers( partroom.GetRoomMembersRequest(roomUuid: roomUuid, page: page, pageSize: pageSize), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); state = state.copyWith(room: state.room.copyWith(members: response.members)); @@ -495,7 +501,7 @@ class PartyRoom extends _$PartyRoom { final client = state.client.roomClient; if (client == null) return; - final response = await client.getMyRoom(partroom.GetMyRoomRequest(), options: _getAuthCallOptions()); + final response = await client.getMyRoom(partroom.GetMyRoomRequest(), options: getAuthCallOptions()); if (response.hasRoom() && response.room.roomUuid.isNotEmpty) { final isOwner = response.room.ownerGameId == state.auth.userInfo?.gameUserId; @@ -553,7 +559,7 @@ class PartyRoom extends _$PartyRoom { request.socialLinks.addAll(state.room.currentRoom!.socialLinks); } - await client.updateRoom(request, options: _getAuthCallOptions()); + await client.updateRoom(request, options: getAuthCallOptions()); // 刷新房间信息 await getRoomInfo(roomUuid); @@ -576,7 +582,7 @@ class PartyRoom extends _$PartyRoom { await client.kickMember( partroom.KickMemberRequest(roomUuid: roomUuid, targetGameUserId: targetGameUserId), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); dPrint('[PartyRoom] Member kicked: $targetGameUserId'); @@ -608,7 +614,7 @@ class PartyRoom extends _$PartyRoom { playTime: Int64(playTime ?? 0), ), ), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); dPrint('[PartyRoom] Status updated'); @@ -638,7 +644,7 @@ class PartyRoom extends _$PartyRoom { request.params.addAll(params); } - await client.sendSignal(request, options: _getAuthCallOptions()); + await client.sendSignal(request, options: getAuthCallOptions()); dPrint('[PartyRoom] Signal sent: $signalId'); } catch (e) { @@ -658,7 +664,7 @@ class PartyRoom extends _$PartyRoom { await client.transferOwnership( partroom.TransferOwnershipRequest(roomUuid: roomUuid, targetGameUserId: targetGameUserId), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); // 更新房主状态 @@ -685,7 +691,7 @@ class PartyRoom extends _$PartyRoom { final response = await client.getKickedMembers( partroom.GetKickedMembersRequest(roomUuid: roomUuid, page: page, pageSize: pageSize), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); return response; @@ -706,7 +712,7 @@ class PartyRoom extends _$PartyRoom { await client.removeKickedMember( partroom.RemoveKickedMemberRequest(roomUuid: roomUuid, gameUserId: gameUserId), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); dPrint('[PartyRoom] Kicked member removed: $gameUserId'); @@ -732,7 +738,7 @@ class PartyRoom extends _$PartyRoom { final client = state.client.roomClient; if (client == null) return; - await client.heartbeat(partroom.HeartbeatRequest(roomUuid: roomUuid), options: _getAuthCallOptions()); + await client.heartbeat(partroom.HeartbeatRequest(roomUuid: roomUuid), options: getAuthCallOptions()); dPrint('[PartyRoom] Heartbeat sent'); } catch (e) { @@ -760,7 +766,7 @@ class PartyRoom extends _$PartyRoom { final stream = client.listenRoomEvents( partroom.ListenRoomEventsRequest(roomUuid: roomUuid), - options: _getAuthCallOptions(), + options: getAuthCallOptions(), ); _eventStreamSubscription = stream.listen( diff --git a/lib/provider/party_room.g.dart b/lib/provider/party_room.g.dart index 5b5b124..d121c18 100644 --- a/lib/provider/party_room.g.dart +++ b/lib/provider/party_room.g.dart @@ -44,7 +44,7 @@ final class PartyRoomProvider } } -String _$partyRoomHash() => r'2ce3ac365bec3af8f7e1d350b53262c8e4e2872d'; +String _$partyRoomHash() => r'3247b6ea5482a8938f118c2d0d6f9ecf2e55fba7'; /// PartyRoom Provider diff --git a/lib/ui/auth/auth_page.dart b/lib/ui/auth/auth_page.dart new file mode 100644 index 0000000..1e476aa --- /dev/null +++ b/lib/ui/auth/auth_page.dart @@ -0,0 +1,286 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/provider/party_room.dart'; +import 'package:starcitizen_doctor/ui/auth/auth_ui_model.dart'; +import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class AuthPage extends HookConsumerWidget { + final String? callbackUrl; + final String? stateParameter; + final String? nonce; + + const AuthPage({super.key, this.callbackUrl, this.stateParameter, this.nonce}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = authUIModelProvider(callbackUrl: callbackUrl, stateParameter: stateParameter, nonce: nonce); + final model = ref.watch(provider); + final modelNotifier = ref.read(provider.notifier); + + final partyRoomState = ref.watch(partyRoomProvider); + final userName = partyRoomState.auth.userInfo?.handleName ?? '未知用户'; + final userEmail = partyRoomState.auth.userInfo?.gameUserId ?? ''; // Using gameUserId as email-like identifier + final avatarUrl = partyRoomState.auth.userInfo?.avatarUrl; + final fullAvatarUrl = PartyRoomUtils.getAvatarUrl(avatarUrl); + + useEffect(() { + Future.microtask(() => modelNotifier.initialize()); + return null; + }, const []); + + return ContentDialog( + constraints: const BoxConstraints(maxWidth: 450, maxHeight: 600), + // Remove standard title to customize layout + title: const SizedBox.shrink(), + content: _buildBody(context, model, modelNotifier, userName, userEmail, fullAvatarUrl), + actions: [ + if (model.error == null && model.isLoggedIn) ...[ + // Cancel button + Button(onPressed: () => Navigator.of(context).pop(), child: const Text('拒绝')), + // Allow button (Primary) + FilledButton( + onPressed: model.isLoading ? null : () => _handleAuthorize(context, ref, false), + child: const Text('允许'), + ), + ] else ...[ + Button(onPressed: () => Navigator.of(context).pop(), child: const Text('关闭')), + ], + ], + ); + } + + Widget _buildBody( + BuildContext context, + AuthUIState state, + AuthUIModel model, + String userName, + String userEmail, + String? avatarUrl, + ) { + if (state.isLoading) { + return const SizedBox(height: 300, child: Center(child: ProgressRing())); + } + + if (state.isWaitingForConnection) { + return SizedBox( + height: 300, + child: Center( + child: Column(mainAxisSize: MainAxisSize.min, children: [const ProgressRing(), const SizedBox(height: 24)]), + ), + ); + } + + if (state.error != null) { + return SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FluentIcons.error_badge, size: 48, color: Colors.red), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + state.error!, + style: TextStyle(color: Colors.red, fontSize: 14), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24), + FilledButton(onPressed: () => model.initialize(), child: const Text('重试')), + ], + ), + ), + ); + } + + if (!state.isLoggedIn) { + return SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FluentIcons.warning, size: 48, color: Colors.orange), + const SizedBox(height: 16), + const Text('您需要先登录才能授权', style: TextStyle(fontSize: 16)), + const SizedBox(height: 24), + FilledButton(onPressed: () => Navigator.of(context).pop(), child: const Text('前往登录')), + ], + ), + ), + ); + } + + final displayDomain = state.domainName ?? state.domain ?? '未知应用'; + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + // Title + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle(fontSize: 20, color: Colors.white.withValues(alpha: 0.95), fontFamily: 'Segoe UI'), + children: [ + TextSpan( + text: displayDomain, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan(text: ' 申请访问您的账户'), + ], + ), + ), + + if (!state.isDomainTrusted) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FluentIcons.warning, size: 12, color: Colors.orange), + const SizedBox(width: 6), + Text('未验证的应用', style: TextStyle(fontSize: 12, color: Colors.orange)), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // 2. User Account Info + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + borderRadius: BorderRadius.circular(24), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClipOval( + child: SizedBox( + width: 24, + height: 24, + child: avatarUrl != null + ? CacheNetImage(url: avatarUrl, fit: BoxFit.cover) + : const Icon(FluentIcons.contact, size: 24), + ), + ), + const SizedBox(width: 12), + Text( + userEmail.isNotEmpty ? userEmail : userName, + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.8), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + + const SizedBox(height: 32), + + // 3. Permission Scope + Align( + alignment: Alignment.centerLeft, + child: Text('此操作将允许 $displayDomain:', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + ), + const SizedBox(height: 16), + + _buildPermissionItem(FluentIcons.contact_info, '访问您的公开资料', '包括用户名、头像'), + + const SizedBox(height: 48), + ], + ), + ); + } + + Widget _buildPermissionItem(IconData icon, String title, String subtitle) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: Colors.blue), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 2), + Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.6))), + ], + ), + ), + ], + ); + } + + Future _handleAuthorize(BuildContext context, WidgetRef ref, bool copyOnly) async { + final provider = authUIModelProvider(callbackUrl: callbackUrl, stateParameter: stateParameter, nonce: nonce); + final modelNotifier = ref.read(provider.notifier); + final model = ref.read(provider); + + // First, generate the code if not already generated + if (model.code == null) { + final success = await modelNotifier.generateCodeOnConfirm(); + if (!success) { + if (context.mounted) { + final currentState = ref.read(provider); + await showToast(context, currentState.error ?? '生成授权码失败'); + } + return; + } + } + + final authUrl = modelNotifier.getAuthorizationUrl(); + if (authUrl == null) { + if (context.mounted) { + await showToast(context, '生成授权链接失败'); + } + return; + } + + if (copyOnly) { + await modelNotifier.copyAuthorizationUrl(); + if (context.mounted) { + await showToast(context, '授权链接已复制到剪贴板'); + if (context.mounted) { + Navigator.of(context).pop(); + } + } + } else { + try { + final launched = await launchUrlString(authUrl); + if (!launched) { + if (context.mounted) { + await showToast(context, '打开浏览器失败,请复制链接手动访问'); + } + return; + } + if (context.mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + if (context.mounted) { + await showToast(context, '打开浏览器失败: $e'); + } + } + } + } +} diff --git a/lib/ui/auth/auth_ui_model.dart b/lib/ui/auth/auth_ui_model.dart new file mode 100644 index 0000000..a3b16f1 --- /dev/null +++ b/lib/ui/auth/auth_ui_model.dart @@ -0,0 +1,237 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/generated/proto/auth/auth.pb.dart'; +import 'package:starcitizen_doctor/provider/party_room.dart'; +import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart'; + +part 'auth_ui_model.freezed.dart'; +part 'auth_ui_model.g.dart'; + +@freezed +sealed class AuthUIState with _$AuthUIState { + const factory AuthUIState({ + @Default(false) bool isLoading, + @Default(false) bool isLoggedIn, + @Default(false) bool isWaitingForConnection, + String? domain, + String? callbackUrl, + String? stateParameter, + String? nonce, + String? code, + String? error, + @Default(false) bool isDomainTrusted, + String? domainName, + }) = _AuthUIState; +} + +@riverpod +class AuthUIModel extends _$AuthUIModel { + @override + AuthUIState build({String? callbackUrl, String? stateParameter, String? nonce}) { + // Listen to party room connection and auth state changes + ref.listen(partyRoomProvider, (previous, next) { + // If we're waiting for connection and now connected, re-initialize + if (state.isWaitingForConnection && next.client.isConnected && next.client.authClient != null) { + dPrint('[AuthUI] Connection established, re-initializing...'); + Future.microtask(() => initialize()); + } + + // If not logged in before and now logged in, re-initialize + if (!state.isLoggedIn && previous?.auth.isLoggedIn == false && next.auth.isLoggedIn) { + dPrint('[AuthUI] User logged in, re-initializing...'); + Future.microtask(() => initialize()); + } + }); + + // Listen to party room UI model for login status changes + ref.listen(partyRoomUIModelProvider, (previous, next) { + // If was logging in and now finished (success or fail), re-check logic + if (previous?.isLoggingIn == true && !next.isLoggingIn) { + dPrint('[AuthUI] Login process finished, re-initializing...'); + Future.microtask(() => initialize()); + } + }); + + return AuthUIState(callbackUrl: callbackUrl, stateParameter: stateParameter, nonce: nonce); + } + + Future initialize() async { + state = state.copyWith(isLoading: true, error: null, isWaitingForConnection: false); + + try { + // Check if domain and callbackUrl are provided + + if (state.callbackUrl == null || state.callbackUrl!.isEmpty) { + state = state.copyWith(isLoading: false, error: '缺少回调地址参数'); + return; + } + + if (state.stateParameter == null || state.stateParameter!.isEmpty) { + state = state.copyWith(isLoading: false, error: '缺少 state 参数'); + return; + } + + // Extract domain from callbackUrl + String? domain; + try { + final uri = Uri.parse(state.callbackUrl!); + if (uri.host.isNotEmpty) { + domain = uri.host; + } + } catch (e) { + dPrint('Failed to parse callbackUrl: $e'); + } + + if (domain == null || domain.isEmpty) { + state = state.copyWith(isLoading: false, error: '无法从回调地址解析域名'); + return; + } + + // Update state with extracted domain + state = state.copyWith(domain: domain); + + // Get party room providers + final partyRoom = ref.read(partyRoomProvider); + final partyRoomUI = ref.read(partyRoomUIModelProvider); + + // Check if connected to server + if (!partyRoom.client.isConnected || partyRoom.client.authClient == null) { + dPrint('[AuthUI] Server not connected, waiting for connection...'); + state = state.copyWith(isLoading: false, isWaitingForConnection: true); + return; + } + + // Check if user is logged in + if (!partyRoom.auth.isLoggedIn) { + // If still logging in process (auto-login after connect), keep waiting + if (partyRoomUI.isLoggingIn) { + dPrint('[AuthUI] Auto-login in progress, waiting...'); + state = state.copyWith(isLoading: false, isWaitingForConnection: true); + return; + } + + state = state.copyWith(isLoading: false, isLoggedIn: false); + return; + } + + // Check domain trust status + final domainListResponse = await _getDomainList(); + bool isDomainTrusted = false; + String? domainName; + + if (domainListResponse != null) { + final domainInfo = domainListResponse.domains + .cast< + JWTDomainInfo? + >() // Cast to nullable to use firstWhere with orElse returning null if needed, though JWTDomainInfo is not nullable in proto usually + .firstWhere((d) => d?.domain.toLowerCase() == state.domain!.toLowerCase(), orElse: () => null); + + if (domainInfo != null && domainInfo.domain.isNotEmpty) { + isDomainTrusted = true; + domainName = domainInfo.name; + } + } + + // Don't generate token yet - wait for user confirmation + state = state.copyWith( + isLoading: false, + isLoggedIn: true, + isDomainTrusted: isDomainTrusted, + domainName: domainName, + ); + } catch (e) { + dPrint('Auth initialization error: $e'); + state = state.copyWith(isLoading: false, error: '初始化失败: $e'); + } + } + + Future generateCodeOnConfirm() async { + // Only generate code if user is logged in and no previous error + if (!state.isLoggedIn) { + return false; + } + + state = state.copyWith(isLoading: true); + + try { + // Generate OIDC Auth Code + final code = await _generateOIDCAuthCode(); + + if (code == null) { + state = state.copyWith(isLoading: false, error: '生成授权码失败'); + return false; + } + + state = state.copyWith(isLoading: false, code: code); + return true; + } catch (e) { + dPrint('Generate code on confirm error: $e'); + state = state.copyWith(isLoading: false, error: '生成授权码失败: $e'); + return false; + } + } + + Future _getDomainList() async { + try { + final partyRoom = ref.read(partyRoomProvider); + final partyRoomNotifier = ref.read(partyRoomProvider.notifier); + final client = partyRoom.client.authClient; + if (client == null) return null; + + final response = await client.getJWTDomainList( + GetJWTDomainListRequest(), + options: partyRoomNotifier.getAuthCallOptions(), + ); + return response; + } catch (e) { + dPrint('Get domain list error: $e'); + return null; + } + } + + Future _generateOIDCAuthCode() async { + try { + final partyRoom = ref.read(partyRoomProvider); + final partyRoomNotifier = ref.read(partyRoomProvider.notifier); + final client = partyRoom.client.authClient; + if (client == null || state.callbackUrl == null) return null; + + final request = GenerateOIDCAuthCodeRequest(redirectUri: state.callbackUrl!, nonce: state.nonce ?? ''); + + final response = await client.generateOIDCAuthCode(request, options: partyRoomNotifier.getAuthCallOptions()); + return response.code; + } catch (e) { + dPrint('Generate OIDC Auth Code error: $e'); + return null; + } + } + + String? getAuthorizationUrl() { + if (state.code == null || state.callbackUrl == null || state.stateParameter == null) { + return null; + } + + // Build authorization URL + // Using query parameters (?) to allow both server-side and client-side processing + final uri = Uri.parse(state.callbackUrl!); + + // Merge existing query parameters with new ones + final newQueryParameters = Map.from(uri.queryParameters); + newQueryParameters['code'] = state.code!; + newQueryParameters['state'] = state.stateParameter!; + + final authUri = uri.replace(queryParameters: newQueryParameters); + return authUri.toString(); + } + + Future copyAuthorizationUrl() async { + final url = getAuthorizationUrl(); + if (url != null) { + await Clipboard.setData(ClipboardData(text: url)); + } + } +} diff --git a/lib/ui/auth/auth_ui_model.freezed.dart b/lib/ui/auth/auth_ui_model.freezed.dart new file mode 100644 index 0000000..e70c12e --- /dev/null +++ b/lib/ui/auth/auth_ui_model.freezed.dart @@ -0,0 +1,295 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_ui_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$AuthUIState { + + bool get isLoading; bool get isLoggedIn; bool get isWaitingForConnection; String? get domain; String? get callbackUrl; String? get stateParameter; String? get nonce; String? get code; String? get error; bool get isDomainTrusted; String? get domainName; +/// Create a copy of AuthUIState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AuthUIStateCopyWith get copyWith => _$AuthUIStateCopyWithImpl(this as AuthUIState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthUIState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoggedIn, isLoggedIn) || other.isLoggedIn == isLoggedIn)&&(identical(other.isWaitingForConnection, isWaitingForConnection) || other.isWaitingForConnection == isWaitingForConnection)&&(identical(other.domain, domain) || other.domain == domain)&&(identical(other.callbackUrl, callbackUrl) || other.callbackUrl == callbackUrl)&&(identical(other.stateParameter, stateParameter) || other.stateParameter == stateParameter)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.code, code) || other.code == code)&&(identical(other.error, error) || other.error == error)&&(identical(other.isDomainTrusted, isDomainTrusted) || other.isDomainTrusted == isDomainTrusted)&&(identical(other.domainName, domainName) || other.domainName == domainName)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isLoading,isLoggedIn,isWaitingForConnection,domain,callbackUrl,stateParameter,nonce,code,error,isDomainTrusted,domainName); + +@override +String toString() { + return 'AuthUIState(isLoading: $isLoading, isLoggedIn: $isLoggedIn, isWaitingForConnection: $isWaitingForConnection, domain: $domain, callbackUrl: $callbackUrl, stateParameter: $stateParameter, nonce: $nonce, code: $code, error: $error, isDomainTrusted: $isDomainTrusted, domainName: $domainName)'; +} + + +} + +/// @nodoc +abstract mixin class $AuthUIStateCopyWith<$Res> { + factory $AuthUIStateCopyWith(AuthUIState value, $Res Function(AuthUIState) _then) = _$AuthUIStateCopyWithImpl; +@useResult +$Res call({ + bool isLoading, bool isLoggedIn, bool isWaitingForConnection, String? domain, String? callbackUrl, String? stateParameter, String? nonce, String? code, String? error, bool isDomainTrusted, String? domainName +}); + + + + +} +/// @nodoc +class _$AuthUIStateCopyWithImpl<$Res> + implements $AuthUIStateCopyWith<$Res> { + _$AuthUIStateCopyWithImpl(this._self, this._then); + + final AuthUIState _self; + final $Res Function(AuthUIState) _then; + +/// Create a copy of AuthUIState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isLoggedIn = null,Object? isWaitingForConnection = null,Object? domain = freezed,Object? callbackUrl = freezed,Object? stateParameter = freezed,Object? nonce = freezed,Object? code = freezed,Object? error = freezed,Object? isDomainTrusted = null,Object? domainName = freezed,}) { + return _then(_self.copyWith( +isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable +as bool,isLoggedIn: null == isLoggedIn ? _self.isLoggedIn : isLoggedIn // ignore: cast_nullable_to_non_nullable +as bool,isWaitingForConnection: null == isWaitingForConnection ? _self.isWaitingForConnection : isWaitingForConnection // ignore: cast_nullable_to_non_nullable +as bool,domain: freezed == domain ? _self.domain : domain // ignore: cast_nullable_to_non_nullable +as String?,callbackUrl: freezed == callbackUrl ? _self.callbackUrl : callbackUrl // ignore: cast_nullable_to_non_nullable +as String?,stateParameter: freezed == stateParameter ? _self.stateParameter : stateParameter // ignore: cast_nullable_to_non_nullable +as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable +as String?,code: freezed == code ? _self.code : code // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as String?,isDomainTrusted: null == isDomainTrusted ? _self.isDomainTrusted : isDomainTrusted // ignore: cast_nullable_to_non_nullable +as bool,domainName: freezed == domainName ? _self.domainName : domainName // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AuthUIState]. +extension AuthUIStatePatterns on AuthUIState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AuthUIState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AuthUIState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AuthUIState value) $default,){ +final _that = this; +switch (_that) { +case _AuthUIState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AuthUIState value)? $default,){ +final _that = this; +switch (_that) { +case _AuthUIState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isLoading, bool isLoggedIn, bool isWaitingForConnection, String? domain, String? callbackUrl, String? stateParameter, String? nonce, String? code, String? error, bool isDomainTrusted, String? domainName)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AuthUIState() when $default != null: +return $default(_that.isLoading,_that.isLoggedIn,_that.isWaitingForConnection,_that.domain,_that.callbackUrl,_that.stateParameter,_that.nonce,_that.code,_that.error,_that.isDomainTrusted,_that.domainName);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool isLoading, bool isLoggedIn, bool isWaitingForConnection, String? domain, String? callbackUrl, String? stateParameter, String? nonce, String? code, String? error, bool isDomainTrusted, String? domainName) $default,) {final _that = this; +switch (_that) { +case _AuthUIState(): +return $default(_that.isLoading,_that.isLoggedIn,_that.isWaitingForConnection,_that.domain,_that.callbackUrl,_that.stateParameter,_that.nonce,_that.code,_that.error,_that.isDomainTrusted,_that.domainName);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isLoading, bool isLoggedIn, bool isWaitingForConnection, String? domain, String? callbackUrl, String? stateParameter, String? nonce, String? code, String? error, bool isDomainTrusted, String? domainName)? $default,) {final _that = this; +switch (_that) { +case _AuthUIState() when $default != null: +return $default(_that.isLoading,_that.isLoggedIn,_that.isWaitingForConnection,_that.domain,_that.callbackUrl,_that.stateParameter,_that.nonce,_that.code,_that.error,_that.isDomainTrusted,_that.domainName);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AuthUIState implements AuthUIState { + const _AuthUIState({this.isLoading = false, this.isLoggedIn = false, this.isWaitingForConnection = false, this.domain, this.callbackUrl, this.stateParameter, this.nonce, this.code, this.error, this.isDomainTrusted = false, this.domainName}); + + +@override@JsonKey() final bool isLoading; +@override@JsonKey() final bool isLoggedIn; +@override@JsonKey() final bool isWaitingForConnection; +@override final String? domain; +@override final String? callbackUrl; +@override final String? stateParameter; +@override final String? nonce; +@override final String? code; +@override final String? error; +@override@JsonKey() final bool isDomainTrusted; +@override final String? domainName; + +/// Create a copy of AuthUIState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AuthUIStateCopyWith<_AuthUIState> get copyWith => __$AuthUIStateCopyWithImpl<_AuthUIState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AuthUIState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoggedIn, isLoggedIn) || other.isLoggedIn == isLoggedIn)&&(identical(other.isWaitingForConnection, isWaitingForConnection) || other.isWaitingForConnection == isWaitingForConnection)&&(identical(other.domain, domain) || other.domain == domain)&&(identical(other.callbackUrl, callbackUrl) || other.callbackUrl == callbackUrl)&&(identical(other.stateParameter, stateParameter) || other.stateParameter == stateParameter)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.code, code) || other.code == code)&&(identical(other.error, error) || other.error == error)&&(identical(other.isDomainTrusted, isDomainTrusted) || other.isDomainTrusted == isDomainTrusted)&&(identical(other.domainName, domainName) || other.domainName == domainName)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isLoading,isLoggedIn,isWaitingForConnection,domain,callbackUrl,stateParameter,nonce,code,error,isDomainTrusted,domainName); + +@override +String toString() { + return 'AuthUIState(isLoading: $isLoading, isLoggedIn: $isLoggedIn, isWaitingForConnection: $isWaitingForConnection, domain: $domain, callbackUrl: $callbackUrl, stateParameter: $stateParameter, nonce: $nonce, code: $code, error: $error, isDomainTrusted: $isDomainTrusted, domainName: $domainName)'; +} + + +} + +/// @nodoc +abstract mixin class _$AuthUIStateCopyWith<$Res> implements $AuthUIStateCopyWith<$Res> { + factory _$AuthUIStateCopyWith(_AuthUIState value, $Res Function(_AuthUIState) _then) = __$AuthUIStateCopyWithImpl; +@override @useResult +$Res call({ + bool isLoading, bool isLoggedIn, bool isWaitingForConnection, String? domain, String? callbackUrl, String? stateParameter, String? nonce, String? code, String? error, bool isDomainTrusted, String? domainName +}); + + + + +} +/// @nodoc +class __$AuthUIStateCopyWithImpl<$Res> + implements _$AuthUIStateCopyWith<$Res> { + __$AuthUIStateCopyWithImpl(this._self, this._then); + + final _AuthUIState _self; + final $Res Function(_AuthUIState) _then; + +/// Create a copy of AuthUIState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isLoggedIn = null,Object? isWaitingForConnection = null,Object? domain = freezed,Object? callbackUrl = freezed,Object? stateParameter = freezed,Object? nonce = freezed,Object? code = freezed,Object? error = freezed,Object? isDomainTrusted = null,Object? domainName = freezed,}) { + return _then(_AuthUIState( +isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable +as bool,isLoggedIn: null == isLoggedIn ? _self.isLoggedIn : isLoggedIn // ignore: cast_nullable_to_non_nullable +as bool,isWaitingForConnection: null == isWaitingForConnection ? _self.isWaitingForConnection : isWaitingForConnection // ignore: cast_nullable_to_non_nullable +as bool,domain: freezed == domain ? _self.domain : domain // ignore: cast_nullable_to_non_nullable +as String?,callbackUrl: freezed == callbackUrl ? _self.callbackUrl : callbackUrl // ignore: cast_nullable_to_non_nullable +as String?,stateParameter: freezed == stateParameter ? _self.stateParameter : stateParameter // ignore: cast_nullable_to_non_nullable +as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable +as String?,code: freezed == code ? _self.code : code // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as String?,isDomainTrusted: null == isDomainTrusted ? _self.isDomainTrusted : isDomainTrusted // ignore: cast_nullable_to_non_nullable +as bool,domainName: freezed == domainName ? _self.domainName : domainName // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/ui/auth/auth_ui_model.g.dart b/lib/ui/auth/auth_ui_model.g.dart new file mode 100644 index 0000000..1ad190f --- /dev/null +++ b/lib/ui/auth/auth_ui_model.g.dart @@ -0,0 +1,131 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(AuthUIModel) +const authUIModelProvider = AuthUIModelFamily._(); + +final class AuthUIModelProvider + extends $NotifierProvider { + const AuthUIModelProvider._({ + required AuthUIModelFamily super.from, + required ({String? callbackUrl, String? stateParameter, String? nonce}) + super.argument, + }) : super( + retry: null, + name: r'authUIModelProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$authUIModelHash(); + + @override + String toString() { + return r'authUIModelProvider' + '' + '$argument'; + } + + @$internal + @override + AuthUIModel create() => AuthUIModel(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AuthUIState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is AuthUIModelProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$authUIModelHash() => r'cef2bc3fecb2c52e507fa24bc352b4553d918a38'; + +final class AuthUIModelFamily extends $Family + with + $ClassFamilyOverride< + AuthUIModel, + AuthUIState, + AuthUIState, + AuthUIState, + ({String? callbackUrl, String? stateParameter, String? nonce}) + > { + const AuthUIModelFamily._() + : super( + retry: null, + name: r'authUIModelProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + AuthUIModelProvider call({ + String? callbackUrl, + String? stateParameter, + String? nonce, + }) => AuthUIModelProvider._( + argument: ( + callbackUrl: callbackUrl, + stateParameter: stateParameter, + nonce: nonce, + ), + from: this, + ); + + @override + String toString() => r'authUIModelProvider'; +} + +abstract class _$AuthUIModel extends $Notifier { + late final _$args = + ref.$arg + as ({String? callbackUrl, String? stateParameter, String? nonce}); + String? get callbackUrl => _$args.callbackUrl; + String? get stateParameter => _$args.stateParameter; + String? get nonce => _$args.nonce; + + AuthUIState build({ + String? callbackUrl, + String? stateParameter, + String? nonce, + }); + @$mustCallSuper + @override + void runBuild() { + final created = build( + callbackUrl: _$args.callbackUrl, + stateParameter: _$args.stateParameter, + nonce: _$args.nonce, + ); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + AuthUIState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart index 71c979f..e8b3b43 100644 --- a/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart +++ b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart @@ -42,7 +42,7 @@ final class HomeGameLoginUIModelProvider } String _$homeGameLoginUIModelHash() => - r'd81831f54c6b1e98ea8a1e94b5e6049fe552996f'; + r'ca905904d20a6b1956fee04dcb501f0d1c19f86b'; abstract class _$HomeGameLoginUIModel extends $Notifier { HomeGameLoginState build(); diff --git a/lib/ui/index_ui.dart b/lib/ui/index_ui.dart index e7d7f5b..14f99e7 100644 --- a/lib/ui/index_ui.dart +++ b/lib/ui/index_ui.dart @@ -18,6 +18,7 @@ import 'party_room/party_room_ui_model.dart'; import 'settings/settings_ui.dart'; import 'tools/tools_ui.dart'; import 'index_ui_widgets/user_avatar_widget.dart'; +import 'package:starcitizen_doctor/common/utils/url_scheme_handler.dart'; class IndexUI extends HookConsumerWidget { const IndexUI({super.key}); @@ -32,6 +33,13 @@ class IndexUI extends HookConsumerWidget { ref.watch(partyRoomUIModelProvider.select((value) => null)); final curIndex = useState(0); + + // Initialize URL scheme handler + useEffect(() { + UrlSchemeHandler().initialize(context); + return () => UrlSchemeHandler().dispose(); + }, const []); + return NavigationView( appBar: NavigationAppBar( automaticallyImplyLeading: false, diff --git a/lib/ui/settings/settings_ui_model.g.dart b/lib/ui/settings/settings_ui_model.g.dart index a95376b..d7c23ae 100644 --- a/lib/ui/settings/settings_ui_model.g.dart +++ b/lib/ui/settings/settings_ui_model.g.dart @@ -41,7 +41,7 @@ final class SettingsUIModelProvider } } -String _$settingsUIModelHash() => r'd34b1a2fac69d10f560d9a2e1a7431dd5a7954ca'; +String _$settingsUIModelHash() => r'c448be241a29891e90d8f17bef9e7f61d66c05be'; abstract class _$SettingsUIModel extends $Notifier { SettingsUIState build(); diff --git a/lib/ui/tools/tools_ui_model.g.dart b/lib/ui/tools/tools_ui_model.g.dart index bac99b0..596e238 100644 --- a/lib/ui/tools/tools_ui_model.g.dart +++ b/lib/ui/tools/tools_ui_model.g.dart @@ -41,7 +41,7 @@ final class ToolsUIModelProvider } } -String _$toolsUIModelHash() => r'd50710acb3cf2e858128541dbfe8389ea9db4452'; +String _$toolsUIModelHash() => r'2b0c851c677a03a88c02f6f9d146624d505e974f'; abstract class _$ToolsUIModel extends $Notifier { ToolsUIState build(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2826f60..2d1ad07 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin"); flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7cb4963..5dd98aa 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_multi_window flutter_acrylic + gtk screen_retriever_linux url_launcher_linux window_manager diff --git a/linux/my_application.cc b/linux/my_application.cc index 10d77af..798f336 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -20,6 +20,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -85,7 +92,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GApplication::startup. @@ -126,6 +133,6 @@ static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/linux/sctoolbox.desktop b/linux/sctoolbox.desktop new file mode 100644 index 0000000..d207245 --- /dev/null +++ b/linux/sctoolbox.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=StarCitizen ToolBox +Comment=StarCitizen ToolBox Application +Exec=app %u +Icon=app +Terminal=false +Categories=Utility; +MimeType=x-scheme-handler/sctoolbox; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1fa8adc..9924959 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import desktop_multi_window import device_info_plus import file_picker @@ -15,6 +16,7 @@ import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 63e0462..931f8fa 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - app_links (6.4.1): + - FlutterMacOS - desktop_multi_window (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -21,6 +23,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) @@ -33,6 +36,8 @@ DEPENDENCIES: - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos desktop_multi_window: :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos device_info_plus: @@ -55,6 +60,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 90a8d29..840d269 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -31,6 +31,17 @@ $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu + CFBundleURLTypes + + + CFBundleURLName + com.xkeyc.sctoolbox + CFBundleURLSchemes + + sctoolbox + + + NSPrincipalClass NSApplication diff --git a/pubspec.lock b/pubspec.lock index efa94e9..30cc6b7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + 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: "direct main" description: @@ -613,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hexcolor: dependency: "direct main" description: @@ -1618,5 +1658,5 @@ packages: source: hosted version: "2.2.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml index 8277f73..01c93df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,8 +71,9 @@ dependencies: xml: ^6.6.1 # gRPC and protobuf - grpc: ^5.0.0 + grpc: ^5.1.0 protobuf: ^6.0.0 + app_links: ^7.0.0 dependency_overrides: http: ^1.6.0 intl: ^0.20.2 @@ -118,5 +119,6 @@ msix_config: languages: zh-cn windows_build_args: --dart-define="MSE=true" store: true + protocol_activation: sctoolbox flutter_intl: enabled: true \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b09f122..de87a95 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DesktopMultiWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); FlutterAcrylicPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 547b0a1..3705478 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links desktop_multi_window flutter_acrylic screen_retriever_windows diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 3f7e1c5..1510dd7 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -1,12 +1,53 @@ #include #include #include +#include "app_links/app_links_plugin_c_api.h" #include "flutter_window.h" #include "utils.h" +bool SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END (Optional) Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Check if another instance is already running + if (SendAppLinkToInstance(L"SCToolBox")) { + return EXIT_SUCCESS; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {