From 337bf34bc0a6d89a38e8a4d28391f332a1a0d6e2 Mon Sep 17 00:00:00 2001 From: Plague Fox <7805176+PlugFox@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:50:55 +0400 Subject: [PATCH] Feature/subscriptions (#7) * Update README.md * chore: Update .github/workflows/checkout.yml to fix Codecov upload issue * chore: Update .github/workflows/checkout.yml to fix Codecov upload issue * chore: Fix Codecov upload issue in .github/workflows/checkout.yml * Add token * Refactor imports to use 'channel_event.dart' instead of 'channel_push.dart' * chore: Refactor ChatRepositorySpinifyImpl to use lazy loading for event streams * Refactor imports to use 'channel_event.dart' instead of 'channel_push.dart' * Refactor imports to use 'channel_event.dart' instead of 'channel_push.dart' * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Refactor SpinifySubscriptionMixin to use specific implementation classes for client and server subscriptions * Reformat * Refactor SpinifyChannelEvents extension to use a more descriptive parameter name * Refactor SpinifyChannelEvents extension parameter name for clarity * Refactor SpinifyServerSubscription test to use fakeAsync * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor task groups in .vscode/tasks.json for better organization * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to improve handling of server subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to use Future instead of FutureOr in callback types * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle missing subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to add support for Protobuf transport * Refactor SpinifyImpl to improve handling of server subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to improve handling of server subscriptions in SpinifySubscriptionMixin * Refactor SpinifyImpl to update JSON codec support in SpinifySubscriptionMixin * Refactor SpinifyImpl to add support for Protobuf transport * Refactor SpinifyImpl to add support for Protobuf transport * Refactor SpinifyImpl to add support for Protobuf transport * Refactor SpinifyImpl to handle nullable data in subscription state * Refactor SpinifyImpl to update JSON codec support in SpinifySubscriptionMixin * Refactor SpinifyImpl to update JSON codec support in SpinifySubscriptionMixin * Refactor SpinifyImpl to update JSON codec support in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle disconnect advice from the server * Refactor SpinifyImpl to handle disconnect advice from the server * Refactor SpinifyImpl to handle disconnect advice from the server * Refactor SpinifyImpl to handle disconnect advice from the server * Refactor SpinifyImpl to improve handling of WebSocket close codes and reasons * Refactor SpinifyImpl to remove redundant code in SpinifySubscriptionMixin * Refactor SpinifyImpl to handle WebSocket close codes and reasons more effectively * Refactor SpinifyImpl to remove redundant code in SpinifySubscriptionMixin * Refactor SpinifyImpl to add dart:build_runner:watch task * Refactor SpinifyImpl to add WebSocket close code and reason handling * Refactor SpinifyImpl to add optional subscription on creation * Refactor SpinifyImpl to handle WebSocket close codes and reasons more effectively * Refactor SpinifyImpl to handle WebSocket close codes and reasons more effectively * Refactor SpinifyImpl to add SpinifyAnnotation for manual user methods * Refactor SpinifyImpl to improve handling of WebSocket close codes and reasons * Refactor SpinifyImpl to remove redundant code in SpinifySubscriptionMixin * Refactor SpinifyMetrics to remove commented out code for ping * Merge branch 'feature/subscriptions' of https://github.com/PlugFox/spinify into feature/subscriptions * Refactor SpinifyImpl to handle WebSocket close codes and reasons more effectively * Refactor SpinifyImpl to tear down reconnect timer on subscription implementation * Refactor SpinifyImpl to add publications to client _eventController * Refactor SpinifyImpl to remove commented out code and add publication handling * Refactor SpinifyImpl to remove commented out code, handle publications, and improve WebSocket close code handling * Refactor SpinifySubscriptionImpl to update error message for closed Spinify client * Refactor SpinifyImpl to update publication handling and offset in client subscription * Refactor SpinifyImpl to export SpinifyMetrics and SpinifyMetrics$Channel * Refactor SpinifyImpl to update subscription state handling and fix setState calls * Refactor SpinifyImpl to import Metric model and handle publications * Refactor SpinifyImpl to update SpinifyMetrics class references * Refactor SpinifyImpl to implement rotating log buffer * Refactor SpinifySubscriptionState to update data parameter in subscribed factory constructor * Refactor SpinifyImpl to update log level handling and add isError property * Refactor SpinifyClientSubscriptionImpl to improve unsubscribe logic and error handling * Refactor SpinifyClientSubscriptionImpl to update refresh subscription timer logic * Refactor SpinifyImpl to update SpinifyMetrics class references * Refactor SpinifyImpl to update SpinifyMetrics class references * Refactor SpinifyImpl to improve unsubscribe logic and error handling * Refactor SpinifyImpl to implement rotating log buffer * Refactor SpinifyImpl to remove unused code and update dependencies * Refactor SpinifyImpl to update dependencies and improve WebSocket handling * Refactor SpinifyImpl to update dependencies and improve WebSocket handling * Refactor SpinifyImpl to update dependencies and improve WebSocket handling --- .github/workflows/checkout.yml | 11 +- .vscode/launch.json | 42 +- .vscode/tasks.json | 271 +- Makefile | 2 +- README.md | 39 +- build.yaml | 1 + example/.fvm/fvm_config.json | 4 - example/.gitignore | 79 - example/.metadata | 45 - example/README.md | 24 - example/analysis_options.yaml | 100 - example/android/.gitignore | 13 - example/android/app/build.gradle | 69 - .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 33 - .../spinify/spinifyapp/MainActivity.kt | 6 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - example/android/build.gradle | 31 - example/android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.properties | 5 - example/android/settings.gradle | 20 - example/build.yaml | 11 - example/config/development.json | 6 - example/ios/.gitignore | 34 - example/ios/Flutter/AppFrameworkInfo.plist | 26 - example/ios/Flutter/Debug.xcconfig | 2 - example/ios/Flutter/Release.xcconfig | 2 - example/ios/Podfile | 44 - example/ios/Runner.xcodeproj/project.pbxproj | 614 --- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 98 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - example/ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 122 - .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - example/ios/Runner/Base.lproj/Main.storyboard | 26 - example/ios/Runner/Info.plist | 51 - example/ios/Runner/Runner-Bridging-Header.h | 1 - example/ios/RunnerTests/RunnerTests.swift | 12 - example/lib/main.dart | 33 - example/lib/src/common/constant/config.dart | 79 - .../src/common/constant/pubspec.yaml.g.dart | 527 --- .../lib/src/common/controller/controller.dart | 77 - .../controller/controller_observer.dart | 25 - .../droppable_controller_concurrency.dart | 40 - .../sequential_controller_concurrency.dart | 124 - .../src/common/controller/state_consumer.dart | 129 - .../common/controller/state_controller.dart | 35 - .../generated/intl/messages_all.dart | 63 - .../generated/intl/messages_en.dart | 154 - .../common/localization/generated/l10n.dart | 981 ----- .../lib/src/common/localization/intl_en.arb | 98 - .../src/common/localization/localization.dart | 237 -- example/lib/src/common/util/color_util.dart | 29 - example/lib/src/common/util/date_util.dart | 27 - example/lib/src/common/util/error_util.dart | 106 - example/lib/src/common/util/log_buffer.dart | 47 - example/lib/src/common/util/logger_util.dart | 12 - .../common/util/platform/error_util_js.dart | 17 - .../common/util/platform/error_util_vm.dart | 47 - example/lib/src/common/util/screen_util.dart | 256 -- example/lib/src/common/util/timeouts.dart | 19 - example/lib/src/common/widget/app.dart | 52 - .../widget/radial_progress_indicator.dart | 106 - .../lib/src/common/widget/window_scope.dart | 242 -- .../controller/authentication_controller.dart | 79 - .../controller/authentication_state.dart | 128 - .../data/authentication_repository.dart | 86 - .../authentication/model/sign_in_data.dart | 82 - .../feature/authentication/model/user.dart | 175 - .../widget/authentication_scope.dart | 150 - .../widget/authentication_screen.dart | 19 - .../authentication/widget/sign_in_form.dart | 406 -- .../chat_connection_controller.dart | 26 - .../controller/chat_connection_state.dart | 111 - .../controller/chat_messages_controller.dart | 63 - .../chat/controller/chat_messages_state.dart | 193 - .../feature/chat/data/chat_repository.dart | 110 - .../lib/src/feature/chat/model/message.dart | 212 - .../src/feature/chat/widget/chat_room.dart | 240 -- .../src/feature/chat/widget/chat_screen.dart | 45 - .../initialization/initialization.dart | 103 - .../initialize_dependencies.dart | 118 - .../platform/initialization_js.dart | 21 - .../platform/initialization_vm.dart | 44 - .../dependencies/model/app_metadata.dart | 92 - .../dependencies/model/dependencies.dart | 14 - .../widget/dependencies_scope.dart | 83 - .../widget/initialization_splash_screen.dart | 62 - example/linux/.gitignore | 1 - example/linux/CMakeLists.txt | 139 - example/linux/flutter/CMakeLists.txt | 88 - .../flutter/generated_plugin_registrant.cc | 19 - .../flutter/generated_plugin_registrant.h | 15 - example/linux/flutter/generated_plugins.cmake | 25 - example/linux/main.cc | 6 - example/linux/my_application.cc | 108 - example/linux/my_application.h | 18 - example/macos/.gitignore | 7 - example/macos/Flutter/Flutter-Debug.xcconfig | 2 - .../macos/Flutter/Flutter-Release.xcconfig | 2 - .../Flutter/GeneratedPluginRegistrant.swift | 14 - example/macos/Podfile | 43 - .../macos/Runner.xcodeproj/project.pbxproj | 695 ---- .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 98 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - example/macos/Runner/AppDelegate.swift | 10 - .../AppIcon.appiconset/Contents.json | 68 - .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes example/macos/Runner/Base.lproj/MainMenu.xib | 343 -- example/macos/Runner/Configs/AppInfo.xcconfig | 14 - example/macos/Runner/Configs/Debug.xcconfig | 2 - example/macos/Runner/Configs/Release.xcconfig | 2 - .../macos/Runner/Configs/Warnings.xcconfig | 13 - .../macos/Runner/DebugProfile.entitlements | 12 - example/macos/Runner/Info.plist | 32 - example/macos/Runner/MainFlutterWindow.swift | 21 - example/macos/Runner/Release.entitlements | 8 - example/macos/RunnerTests/RunnerTests.swift | 12 - example/main.dart | 3 + example/pubspec.yaml | 113 - example/test/widget_test.dart | 1 - example/web/favicon.png | Bin 917 -> 0 bytes example/web/icons/Icon-192.png | Bin 5292 -> 0 bytes example/web/icons/Icon-512.png | Bin 8252 -> 0 bytes example/web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes example/web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes example/web/index.html | 59 - example/web/manifest.json | 35 - example/windows/.gitignore | 17 - example/windows/CMakeLists.txt | 102 - example/windows/flutter/CMakeLists.txt | 104 - .../flutter/generated_plugin_registrant.cc | 17 - .../flutter/generated_plugin_registrant.h | 15 - .../windows/flutter/generated_plugins.cmake | 25 - example/windows/runner/CMakeLists.txt | 40 - example/windows/runner/Runner.rc | 121 - example/windows/runner/flutter_window.cpp | 66 - example/windows/runner/flutter_window.h | 33 - example/windows/runner/main.cpp | 43 - example/windows/runner/resource.h | 16 - example/windows/runner/resources/app_icon.ico | Bin 33772 -> 0 bytes example/windows/runner/runner.exe.manifest | 20 - example/windows/runner/utils.cpp | 65 - example/windows/runner/utils.h | 19 - example/windows/runner/win32_window.cpp | 280 -- example/windows/runner/win32_window.h | 102 - lib/spinify.dart | 11 +- lib/src.old/client/config.dart | 114 - lib/src.old/client/disconnect_code.dart | 38 - lib/src.old/client/observer.dart | 41 - lib/src.old/client/spinify.dart | 822 ---- lib/src.old/client/spinify_interface.dart | 146 - lib/src.old/client/state.dart | 486 --- lib/src.old/client/states_stream.dart | 36 - lib/src.old/model/channel_presence.dart | 78 - lib/src.old/model/channel_push.dart | 26 - lib/src.old/model/client_info.dart | 53 - lib/src.old/model/connect.dart | 59 - lib/src.old/model/disconnect.dart | 32 - lib/src.old/model/event.dart | 28 - lib/src.old/model/exception.dart | 137 - lib/src.old/model/history.dart | 25 - lib/src.old/model/jwt.dart | 409 -- lib/src.old/model/message.dart | 24 - lib/src.old/model/metrics.dart | 148 - lib/src.old/model/presence.dart | 24 - lib/src.old/model/presence_stats.dart | 27 - lib/src.old/model/publication.dart | 41 - lib/src.old/model/pubspec.yaml.g.dart | 497 --- lib/src.old/model/pushes_stream.dart | 54 - lib/src.old/model/refresh.dart | 28 - lib/src.old/model/refresh_result.dart | 40 - lib/src.old/model/stream_position.dart | 5 - lib/src.old/model/subscribe.dart | 37 - lib/src.old/model/unsubscribe.dart | 28 - .../client_subscription_impl.dart | 582 --- .../client_subscription_manager.dart | 147 - .../server_subscription_impl.dart | 450 --- .../server_subscription_manager.dart | 133 - .../subscription/subcibed_on_channel.dart | 57 - lib/src.old/subscription/subscription.dart | 157 - .../subscription/subscription_config.dart | 102 - .../subscription/subscription_state.dart | 317 -- .../subscription_states_stream.dart | 38 - .../subscription/unsubscribe_code.dart | 19 - lib/src.old/transport/protobuf/client.pb.dart | 3505 ----------------- .../transport/protobuf/client.pbenum.dart | 10 - .../transport/protobuf/client.pbjson.dart | 1116 ------ .../transport/protobuf/client.pbserver.dart | 13 - lib/src.old/transport/protobuf/client.proto | 273 -- .../transport/transport_interface.dart | 98 - .../transport/transport_protobuf_codec.dart | 63 - .../transport/ws_protobuf_transport.dart | 998 ----- lib/src.old/util/event_queue.dart | 95 - lib/src.old/util/logger.dart | 52 - lib/src.old/util/notifier.dart | 34 - lib/src.old/util/speed_meter.dart | 32 - lib/src/model/annotations.dart | 28 + .../{channel_push.dart => channel_event.dart} | 319 +- lib/src/model/channel_events.dart | 55 + lib/src/model/command.dart | 6 +- lib/src/model/config.dart | 90 +- lib/src/model/history.dart | 2 +- lib/src/model/metric.dart | 245 +- lib/src/model/pubspec.yaml.g.dart | 14 +- lib/src/model/pushes_stream.dart | 51 - lib/src/model/reply.dart | 16 +- lib/src/model/state.dart | 95 +- lib/src/model/subscription_config.dart | 9 +- lib/src/model/subscription_state.dart | 113 +- lib/src/model/subscription_states.dart | 35 + lib/src/model/subscription_states_stream.dart | 39 - lib/src/protobuf/protobuf_codec.dart | 54 +- lib/src/spinify_impl.dart | 579 ++- lib/src/{model => }/spinify_interface.dart | 51 +- lib/src/subscription_impl.dart | 650 +++ ...ption.dart => subscription_interface.dart} | 49 +- lib/src/transport_fake.dart | 53 +- lib/src/transport_ws_pb_js.dart | 395 +- lib/src/transport_ws_pb_vm.dart | 107 +- lib/src/util/backoff.dart | 3 + pubspec.yaml | 13 +- test/smoke/smoke_test.dart | 185 +- test/unit/server_subscription_test.dart | 83 + test/unit/spinify_test.dart | 47 +- test/unit_test.dart | 4 +- tool/echo/echo.go | 25 +- 269 files changed, 3234 insertions(+), 23371 deletions(-) delete mode 100644 example/.fvm/fvm_config.json delete mode 100644 example/.gitignore delete mode 100644 example/.metadata delete mode 100644 example/README.md delete mode 100644 example/analysis_options.yaml delete mode 100644 example/android/.gitignore delete mode 100644 example/android/app/build.gradle delete mode 100644 example/android/app/src/debug/AndroidManifest.xml delete mode 100644 example/android/app/src/main/AndroidManifest.xml delete mode 100644 example/android/app/src/main/kotlin/dev/plugfox/spinify/spinifyapp/MainActivity.kt delete mode 100644 example/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 example/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/values-night/styles.xml delete mode 100644 example/android/app/src/main/res/values/styles.xml delete mode 100644 example/android/app/src/profile/AndroidManifest.xml delete mode 100644 example/android/build.gradle delete mode 100644 example/android/gradle.properties delete mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 example/android/settings.gradle delete mode 100644 example/build.yaml delete mode 100644 example/config/development.json delete mode 100644 example/ios/.gitignore delete mode 100644 example/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 example/ios/Flutter/Debug.xcconfig delete mode 100644 example/ios/Flutter/Release.xcconfig delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 example/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 example/ios/Runner/AppDelegate.swift delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 example/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 example/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 example/ios/Runner/Info.plist delete mode 100644 example/ios/Runner/Runner-Bridging-Header.h delete mode 100644 example/ios/RunnerTests/RunnerTests.swift delete mode 100644 example/lib/main.dart delete mode 100644 example/lib/src/common/constant/config.dart delete mode 100644 example/lib/src/common/constant/pubspec.yaml.g.dart delete mode 100644 example/lib/src/common/controller/controller.dart delete mode 100644 example/lib/src/common/controller/controller_observer.dart delete mode 100644 example/lib/src/common/controller/droppable_controller_concurrency.dart delete mode 100644 example/lib/src/common/controller/sequential_controller_concurrency.dart delete mode 100644 example/lib/src/common/controller/state_consumer.dart delete mode 100644 example/lib/src/common/controller/state_controller.dart delete mode 100644 example/lib/src/common/localization/generated/intl/messages_all.dart delete mode 100644 example/lib/src/common/localization/generated/intl/messages_en.dart delete mode 100644 example/lib/src/common/localization/generated/l10n.dart delete mode 100644 example/lib/src/common/localization/intl_en.arb delete mode 100644 example/lib/src/common/localization/localization.dart delete mode 100644 example/lib/src/common/util/color_util.dart delete mode 100644 example/lib/src/common/util/date_util.dart delete mode 100644 example/lib/src/common/util/error_util.dart delete mode 100644 example/lib/src/common/util/log_buffer.dart delete mode 100644 example/lib/src/common/util/logger_util.dart delete mode 100644 example/lib/src/common/util/platform/error_util_js.dart delete mode 100644 example/lib/src/common/util/platform/error_util_vm.dart delete mode 100644 example/lib/src/common/util/screen_util.dart delete mode 100644 example/lib/src/common/util/timeouts.dart delete mode 100644 example/lib/src/common/widget/app.dart delete mode 100644 example/lib/src/common/widget/radial_progress_indicator.dart delete mode 100644 example/lib/src/common/widget/window_scope.dart delete mode 100644 example/lib/src/feature/authentication/controller/authentication_controller.dart delete mode 100644 example/lib/src/feature/authentication/controller/authentication_state.dart delete mode 100644 example/lib/src/feature/authentication/data/authentication_repository.dart delete mode 100644 example/lib/src/feature/authentication/model/sign_in_data.dart delete mode 100644 example/lib/src/feature/authentication/model/user.dart delete mode 100644 example/lib/src/feature/authentication/widget/authentication_scope.dart delete mode 100644 example/lib/src/feature/authentication/widget/authentication_screen.dart delete mode 100644 example/lib/src/feature/authentication/widget/sign_in_form.dart delete mode 100644 example/lib/src/feature/chat/controller/chat_connection_controller.dart delete mode 100644 example/lib/src/feature/chat/controller/chat_connection_state.dart delete mode 100644 example/lib/src/feature/chat/controller/chat_messages_controller.dart delete mode 100644 example/lib/src/feature/chat/controller/chat_messages_state.dart delete mode 100644 example/lib/src/feature/chat/data/chat_repository.dart delete mode 100644 example/lib/src/feature/chat/model/message.dart delete mode 100644 example/lib/src/feature/chat/widget/chat_room.dart delete mode 100644 example/lib/src/feature/chat/widget/chat_screen.dart delete mode 100644 example/lib/src/feature/dependencies/initialization/initialization.dart delete mode 100644 example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart delete mode 100644 example/lib/src/feature/dependencies/initialization/platform/initialization_js.dart delete mode 100644 example/lib/src/feature/dependencies/initialization/platform/initialization_vm.dart delete mode 100644 example/lib/src/feature/dependencies/model/app_metadata.dart delete mode 100644 example/lib/src/feature/dependencies/model/dependencies.dart delete mode 100644 example/lib/src/feature/dependencies/widget/dependencies_scope.dart delete mode 100644 example/lib/src/feature/dependencies/widget/initialization_splash_screen.dart delete mode 100644 example/linux/.gitignore delete mode 100644 example/linux/CMakeLists.txt delete mode 100644 example/linux/flutter/CMakeLists.txt delete mode 100644 example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 example/linux/flutter/generated_plugin_registrant.h delete mode 100644 example/linux/flutter/generated_plugins.cmake delete mode 100644 example/linux/main.cc delete mode 100644 example/linux/my_application.cc delete mode 100644 example/linux/my_application.h delete mode 100644 example/macos/.gitignore delete mode 100644 example/macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 example/macos/Flutter/Flutter-Release.xcconfig delete mode 100644 example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 example/macos/Podfile delete mode 100644 example/macos/Runner.xcodeproj/project.pbxproj delete mode 100644 example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 example/macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 example/macos/Runner/AppDelegate.swift delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 example/macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 example/macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 example/macos/Runner/Configs/Debug.xcconfig delete mode 100644 example/macos/Runner/Configs/Release.xcconfig delete mode 100644 example/macos/Runner/Configs/Warnings.xcconfig delete mode 100644 example/macos/Runner/DebugProfile.entitlements delete mode 100644 example/macos/Runner/Info.plist delete mode 100644 example/macos/Runner/MainFlutterWindow.swift delete mode 100644 example/macos/Runner/Release.entitlements delete mode 100644 example/macos/RunnerTests/RunnerTests.swift create mode 100644 example/main.dart delete mode 100644 example/pubspec.yaml delete mode 100644 example/test/widget_test.dart delete mode 100644 example/web/favicon.png delete mode 100644 example/web/icons/Icon-192.png delete mode 100644 example/web/icons/Icon-512.png delete mode 100644 example/web/icons/Icon-maskable-192.png delete mode 100644 example/web/icons/Icon-maskable-512.png delete mode 100644 example/web/index.html delete mode 100644 example/web/manifest.json delete mode 100644 example/windows/.gitignore delete mode 100644 example/windows/CMakeLists.txt delete mode 100644 example/windows/flutter/CMakeLists.txt delete mode 100644 example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 example/windows/flutter/generated_plugin_registrant.h delete mode 100644 example/windows/flutter/generated_plugins.cmake delete mode 100644 example/windows/runner/CMakeLists.txt delete mode 100644 example/windows/runner/Runner.rc delete mode 100644 example/windows/runner/flutter_window.cpp delete mode 100644 example/windows/runner/flutter_window.h delete mode 100644 example/windows/runner/main.cpp delete mode 100644 example/windows/runner/resource.h delete mode 100644 example/windows/runner/resources/app_icon.ico delete mode 100644 example/windows/runner/runner.exe.manifest delete mode 100644 example/windows/runner/utils.cpp delete mode 100644 example/windows/runner/utils.h delete mode 100644 example/windows/runner/win32_window.cpp delete mode 100644 example/windows/runner/win32_window.h delete mode 100644 lib/src.old/client/config.dart delete mode 100644 lib/src.old/client/disconnect_code.dart delete mode 100644 lib/src.old/client/observer.dart delete mode 100644 lib/src.old/client/spinify.dart delete mode 100644 lib/src.old/client/spinify_interface.dart delete mode 100644 lib/src.old/client/state.dart delete mode 100644 lib/src.old/client/states_stream.dart delete mode 100644 lib/src.old/model/channel_presence.dart delete mode 100644 lib/src.old/model/channel_push.dart delete mode 100644 lib/src.old/model/client_info.dart delete mode 100644 lib/src.old/model/connect.dart delete mode 100644 lib/src.old/model/disconnect.dart delete mode 100644 lib/src.old/model/event.dart delete mode 100644 lib/src.old/model/exception.dart delete mode 100644 lib/src.old/model/history.dart delete mode 100644 lib/src.old/model/jwt.dart delete mode 100644 lib/src.old/model/message.dart delete mode 100644 lib/src.old/model/metrics.dart delete mode 100644 lib/src.old/model/presence.dart delete mode 100644 lib/src.old/model/presence_stats.dart delete mode 100644 lib/src.old/model/publication.dart delete mode 100644 lib/src.old/model/pubspec.yaml.g.dart delete mode 100644 lib/src.old/model/pushes_stream.dart delete mode 100644 lib/src.old/model/refresh.dart delete mode 100644 lib/src.old/model/refresh_result.dart delete mode 100644 lib/src.old/model/stream_position.dart delete mode 100644 lib/src.old/model/subscribe.dart delete mode 100644 lib/src.old/model/unsubscribe.dart delete mode 100644 lib/src.old/subscription/client_subscription_impl.dart delete mode 100644 lib/src.old/subscription/client_subscription_manager.dart delete mode 100644 lib/src.old/subscription/server_subscription_impl.dart delete mode 100644 lib/src.old/subscription/server_subscription_manager.dart delete mode 100644 lib/src.old/subscription/subcibed_on_channel.dart delete mode 100644 lib/src.old/subscription/subscription.dart delete mode 100644 lib/src.old/subscription/subscription_config.dart delete mode 100644 lib/src.old/subscription/subscription_state.dart delete mode 100644 lib/src.old/subscription/subscription_states_stream.dart delete mode 100644 lib/src.old/subscription/unsubscribe_code.dart delete mode 100644 lib/src.old/transport/protobuf/client.pb.dart delete mode 100644 lib/src.old/transport/protobuf/client.pbenum.dart delete mode 100644 lib/src.old/transport/protobuf/client.pbjson.dart delete mode 100644 lib/src.old/transport/protobuf/client.pbserver.dart delete mode 100644 lib/src.old/transport/protobuf/client.proto delete mode 100644 lib/src.old/transport/transport_interface.dart delete mode 100644 lib/src.old/transport/transport_protobuf_codec.dart delete mode 100644 lib/src.old/transport/ws_protobuf_transport.dart delete mode 100644 lib/src.old/util/event_queue.dart delete mode 100644 lib/src.old/util/logger.dart delete mode 100644 lib/src.old/util/notifier.dart delete mode 100644 lib/src.old/util/speed_meter.dart create mode 100644 lib/src/model/annotations.dart rename lib/src/model/{channel_push.dart => channel_event.dart} (51%) create mode 100644 lib/src/model/channel_events.dart delete mode 100644 lib/src/model/pushes_stream.dart create mode 100644 lib/src/model/subscription_states.dart delete mode 100644 lib/src/model/subscription_states_stream.dart rename lib/src/{model => }/spinify_interface.dart (76%) create mode 100644 lib/src/subscription_impl.dart rename lib/src/{model/subscription.dart => subscription_interface.dart} (80%) create mode 100644 test/unit/server_subscription_test.dart diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index f4c2317..79105b4 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -53,12 +53,13 @@ jobs: key: ${{ runner.os }}-spinify-${{ env.cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - name: 🗄️ Export pub cache directory - run: export PUB_CACHE=$PWD/.pub_cache/ + run: | + export PUB_CACHE=$PWD/.pub_cache/ + export PATH="$PATH":"$HOME/.pub-cache/bin" - name: 👷 Install Dependencies timeout-minutes: 1 - run: | - dart pub get --no-example + run: dart pub get --no-example - name: 🔎 Check format timeout-minutes: 1 @@ -81,5 +82,5 @@ jobs: - name: 📥 Upload coverage to Codecov timeout-minutes: 1 uses: codecov/codecov-action@v3 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos diff --git a/.vscode/launch.json b/.vscode/launch.json index cce9597..f4c3b17 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,7 @@ { "version": "0.2.0", "configurations": [ - { + /* { "name": "[Flutter] Example (Local)", "request": "launch", "type": "dart", @@ -17,8 +17,8 @@ "args": [ "--dart-define-from-file=config/local.json" ] - }, - { + }, */ + /* { "name": "[Flutter] Example (Development)", "request": "launch", "type": "dart", @@ -34,11 +34,11 @@ "args": [ "--dart-define-from-file=config/development.json" ] - }, + }, */ // https://pub.dev/packages/test // dart test test/unit_test.dart --color --platform=vm { - "name": "[Dart] Unit test (VM)", + "name": "[Dart] Unit test (vm)", "request": "launch", "type": "dart", "program": "test/unit_test.dart", @@ -115,7 +115,7 @@ }, // dart test test/smoke_test.dart --color --platform=vm { - "name": "[Dart] Smoke Test (VM)", + "name": "[Dart] Smoke Test (vm)", "request": "launch", "type": "dart", "program": "test/smoke_test.dart", @@ -136,8 +136,34 @@ "--concurrency=12" ], "args": [], - "preLaunchTask": "echo-server:start", - "postDebugTask": "echo-server:stop" + "preLaunchTask": "echo:start", + "postDebugTask": "echo:stop" + }, + { + "name": "[Dart] Smoke Test (dart2js)", + "request": "launch", + "type": "dart", + "program": "test/smoke_test.dart", + "env": { + "ENVIRONMENT": "test" + }, + "console": "debugConsole", + "runTestsOnDevice": false, + "templateFor": "test", + "toolArgs": [ + "--color", + "--debug", + "--coverage=coverage", + "--reporter=expanded", + "--platform=chrome", + "--compiler=dart2js", + "--file-reporter=json:coverage/tests.json", + "--timeout=30s", + "--concurrency=12" + ], + "args": [], + /* "preLaunchTask": "echo:start", + "postDebugTask": "echo:stop" */ }, // dart run server/bin/server.dart { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 60ae787..075d269 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,34 +2,70 @@ "version": "2.0.0", "tasks": [ { - "label": "dart:dependencies", + "label": "dart:pub:get", + "detail": "Get dependencies for the project", + "icon": { + "color": "terminal.ansiGreen", + "id": "cloud-download" + }, + "dependsOn": [], "type": "shell", - "command": [ - "dart pub get" - ], + "command": "dart pub get", + "args": [], "group": { "kind": "none", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } }, { "label": "dart:get-protoc-plugin", "detail": "Get protoc plugin", + "icon": { + "color": "terminal.ansiGreen", + "id": "cloud-download" + }, "type": "shell", - "command": [ - "dart pub global activate protoc_plugin" - ], + "command": "dart pub global activate protoc_plugin", "dependsOn": [], + "args": [], "group": { "kind": "none", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } }, { "label": "dart:generate-protobuf", "detail": "Generate protobuf files", + "icon": { + "color": "terminal.ansiGreen", + "id": "code" + }, "type": "shell", "command": [ "protoc", @@ -39,45 +75,155 @@ "dependsOn": [ "dart:get-protoc-plugin" ], + "args": [], "group": { "kind": "none", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } }, { - "label": "dart:codegenerate", + "label": "dart:build_runner:all", "detail": "Generate code for the project", + "icon": { + "color": "terminal.ansiGreen", + "id": "code" + }, "type": "shell", "command": [ - "dart run build_runner build --delete-conflicting-outputs" + "dart run build_runner build --delete-conflicting-outputs", + "&& dart format --fix -l 80 lib test tool example" ], "dependsOn": [ "dart:dependencies", "dart:generate-protobuf" ], + "args": [], "group": { "kind": "none", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } + }, + { + "label": "dart:build_runner:dir", + "detail": "Generate code for the directory", + "type": "shell", + "icon": { + "color": "terminal.ansiGreen", + "id": "code" + }, + "command": [ + "dart run build_runner build --build-filter '${fileDirname}/*.dart'", + "&& dart format --fix -l 80 '${fileDirname}'" + ], + "group": { + "kind": "none", + "isDefault": true + }, + "problemMatcher": [], + "dependsOn": [ + "dart:pub:get" + ], + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } + }, + { + "label": "dart:build_runner:watch", + "detail": "Watch for changes in the project", + "type": "shell", + "icon": { + "color": "terminal.ansiGreen", + "id": "code" + }, + "command": "dart run build_runner watch --build-filter \"${input:directory}/**/*.dart\"", + "group": { + "kind": "none", + "isDefault": true + }, + "problemMatcher": [], + "dependsOn": [ + "dart:pub:get" + ], + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } }, { "label": "dart:format", "detail": "Format all files in the project", + "icon": { + "color": "terminal.ansiGreen", + "id": "lightbulb-autofix" + }, "type": "shell", "command": [ "dart format --fix -l 80 lib test tool example" ], + "dependsOn": [], + "args": [], "group": { "kind": "none", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "presentation": { + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "dart" + } }, { "label": "centrifugo:start", "detail": "Start centrifugo server", + "icon": { + "color": "terminal.ansiBlue", + "id": "server" + }, "type": "shell", "windows": { "command": "docker", @@ -133,38 +279,65 @@ "--log_level=debug" ] }, + "dependsOn": [], + "args": [], "group": { "kind": "none", "isDefault": true }, "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, "presentation": { - "reveal": "always", - "panel": "shared" + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "centrifugo" } }, { "label": "centrifugo:stop", "detail": "Stop centrifugo server", + "icon": { + "color": "terminal.ansiRed", + "id": "server" + }, "type": "shell", "command": "docker", "args": [ "stop", "centrifugo" ], + "dependsOn": [], "group": { "kind": "none", "isDefault": true }, "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, "presentation": { - "reveal": "always", - "panel": "shared" + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "centrifugo" } }, { "label": "centrifugo:gentoken", "detail": "Generate new user token for centrifugo server", + "icon": { + "color": "terminal.ansiCyan", + "id": "key" + }, "type": "shell", "command": "docker", "args": [ @@ -179,21 +352,34 @@ "--user=dart", "--ttl=604800" ], + "dependsOn": [], "group": { "kind": "none", "isDefault": true }, "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, "presentation": { - "reveal": "always", - "panel": "shared" + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "centrifugo" } }, { - "label": "echo-server:start", + "label": "echo:start", + "detail": "Start echo server", + "icon": { + "color": "terminal.ansiBlue", + "id": "server" + }, "type": "shell", "command": "dart", - "detail": "Start echo server", "isBackground": false, "options": { "cwd": "${workspaceFolder}/tool", @@ -209,18 +395,27 @@ }, "problemMatcher": [], "presentation": { - "reveal": "never", - "panel": "shared" + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "centrifugo" } }, { - "label": "echo-server:stop", + "label": "echo:stop", + "detail": "Stop echo server", + "icon": { + "color": "terminal.ansiRed", + "id": "server" + }, "type": "shell", "command": "dart", - "detail": "Stop echo server", "options": { "cwd": "${workspaceFolder}/tool", }, + "dependsOn": [], "args": [ "run", "echo_down.dart" @@ -230,14 +425,23 @@ "isDefault": true }, "problemMatcher": [], + "isBackground": false, "presentation": { - "reveal": "never", - "panel": "shared" + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false, + "group": "echo" } }, { - "label": "go:run:echo", - "detail": "Start echo server with Go", + "label": "echo:run", + "detail": "Run echo server with Go", + "icon": { + "color": "terminal.ansiGreen", + "id": "server" + }, "options": { "cwd": "${workspaceFolder}/tool/echo", "env": {} @@ -256,8 +460,11 @@ "problemMatcher": [], "presentation": { "reveal": "always", - "panel": "dedicated" + "panel": "dedicated", + "focus": true, + "clear": true, + "group": "echo" } - } + }, ] } \ No newline at end of file diff --git a/Makefile b/Makefile index 6aa7a9e..1404286 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ generate: get @dart pub global activate protoc_plugin @protoc --proto_path=lib/src/protobuf --dart_out=lib/src/protobuf lib/src/protobuf/client.proto @dart run build_runner build --delete-conflicting-outputs - @dart format -l 80 lib/src/model/pubspec.yaml.g.dart lib/src/protobuf/ + @dart format -l 80 lib/src/model/pubspec.yaml.g.dart lib/src/protobuf/ test/ gen: generate diff --git a/README.md b/README.md index e448ed0..0500753 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Linter](https://img.shields.io/badge/style-linter-40c4ff.svg)](https://pub.dev/packages/linter) [![GitHub stars](https://img.shields.io/github/stars/plugfox/spinify?style=social)](https://github.com/plugfox/spinify/) -Websocket client for [Centrifugo server](https://github.com/centrifugal/centrifugo) and [Centrifuge library](https://github.com/centrifugal/centrifuge) based on [ws library](https://pub.dev/packages/ws). +Websocket client for [Centrifugo server](https://github.com/centrifugal/centrifugo) and [Centrifuge library](https://github.com/centrifugal/centrifuge). ## Installation @@ -20,12 +20,11 @@ dependencies: ## Features and Roadmap -Connection related features - - ✅ Connect to a server -- ✅ Setting client options +- ✅ Setting client configuration - ✅ Automatic reconnect with backoff algorithm - ✅ Client state changes +- ✅ Protobuf transport - ✅ Command-reply - ✅ Command timeouts - ✅ Async pushes @@ -36,30 +35,34 @@ Connection related features - ✅ Presence stats - ✅ History information - ✅ Send custom RPC commands -- ❌ Handle disconnect advice from the server -- ❌ Batching API -- ❌ Bidirectional WebSocket emulation - -### Client-side features - -- ✅ Subscribe to a channel -- ✅ Protobuf transport +- ✅ Handle disconnect advice from the server +- ✅ Channel subscription - ✅ Setting subscription options - ✅ Automatic resubscribe with backoff algorithm - ✅ Subscription state changes - ✅ Subscription command-reply -- ✅ Subscription async pushes - ✅ Subscription token refresh - ✅ Handle unsubscribe advice from the server - ✅ Manage subscription registry - ✅ Publish data into a channel -- ✅ Enqueue methods - ✅ Set observer for hooking events & errors -- ✅ Metrics -- ❌ Optimistic subscriptions +- ✅ Metrics and stats +- ✅ Package errors +- ✅ Meta information about the library +- ✅ Web transport via extension type +- ❌ WASM compatibility - ❌ Run in separate isolate -- ❌ JSON transport +- ❌ JSON codec support +- ❌ Flutter package - ❌ DevTools extension +- ❌ Middleware support +- ❌ 95% test coverage +- ❌ Benchmarks +- ❌ Performance comparison with other libraries +- ❌ Batching API +- ❌ Bidirectional WebSocket emulation +- ❌ Optimistic subscriptions +- ❌ Delta compression ## Example @@ -86,7 +89,7 @@ Refer to the [Changelog](https://github.com/PlugFox/spinify/blob/master/CHANGELO ## Maintainers -- [Matiunin Mikhail aka Plague Fox](https://plugfox.dev) +- [Mike Matiunin aka Plague Fox](https://plugfox.dev) ## Funding diff --git a/build.yaml b/build.yaml index 25edb60..23ba1d9 100644 --- a/build.yaml +++ b/build.yaml @@ -4,6 +4,7 @@ targets: sources: - $package$ - lib/** + - test/** - pubspec.yaml builders: pubspec_generator: diff --git a/example/.fvm/fvm_config.json b/example/.fvm/fvm_config.json deleted file mode 100644 index 41959e9..0000000 --- a/example/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "stable", - "flavors": {} -} diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index 9e45942..0000000 --- a/example/.gitignore +++ /dev/null @@ -1,79 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# Databases -.sqlite -.db -.lock - -# Logs -log.txt - -# Emulator -emulator/ - -# Temporary files -temp/ -.temp/ - -# Env -.env* - -# Test -coverage/ - -# Flutter Version Manager -.fvm/flutter_sdk - -# Generated files - -# Screenshots -integration_test/screenshots/ - -# Firebase -.firebase/ - -# Config -config/local.json \ No newline at end of file diff --git a/example/.metadata b/example/.metadata deleted file mode 100644 index bd61a63..0000000 --- a/example/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled. - -version: - revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - channel: beta - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: android - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: ios - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: linux - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: macos - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: web - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - platform: windows - create_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - base_revision: 74e4b092e5212ebf8292dde2a48d3da960c0920b - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 436f3fd..0000000 --- a/example/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# spinifyapp - -Spinify App Example - -## Code generation - -```bash -$ make codegen -``` - -## Localization - -```bash -$ code lib/src/common/localization/intl_en.arb -$ make intl -``` - -## Recreating the project - -**! Warning: This will overwrite all files in the current directory.** - -```bash -fvm spawn beta create --overwrite -t app --project-name "spinifyapp" --org "dev.plugfox.spinify" --description "Spinify App Example" --platforms ios,android,windows,linux,macos,web . -``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml deleted file mode 100644 index 9c84a6e..0000000 --- a/example/analysis_options.yaml +++ /dev/null @@ -1,100 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Analyzer options -analyzer: - # Exclude files from analysis. Must be relative to the root of the package. - exclude: - # Build - - "build/**" - # Tests - - "test/**.mocks.dart" - - ".test_coverage.dart" - - "coverage/**" - # Assets - - "assets/**" - # Generated - - "lib/src/common/localization/generated/**" - - "lib/src/common/constants/pubspec.yaml.g.dart" - - "lib/src/common/model/generated/**" - - "**.g.dart" - - "**.gql.dart" - - "**.freezed.dart" - - "**.config.dart" - - "**.mocks.dart" - - "**.gen.dart" - - "**.pb.dart" - - "**.pbenum.dart" - - "**.pbjson.dart" - # Flutter Version Manager - - ".fvm/**" - # Tools - #- "tool/**" - - "scripts/**" - - ".dart_tool/**" - # Platform - - "ios/**" - - "android/**" - - "web/**" - - "macos/**" - - "windows/**" - - "linux/**" - - - # Enable the following options to enable strong mode. - language: - strict-casts: true - strict-raw-types: true - strict-inference: true - - # Enable the following options to enable new language features. - #enable-experiment: - # - patterns - # - sealed-class - # - records - # - class-modifiers - # - macros - # - const-functions - # - extension-types - # - inference-update-2 - # - inline-class - # - value-class - # - variance - - # Set the following options to true to enable additional analysis. - errors: - # Info - directives_ordering: info - always_declare_return_types: info - - # Warning - unsafe_html: warning - no_logic_in_create_state: warning - empty_catches: warning - close_sinks: warning - - # Error - always_use_package_imports: error - avoid_relative_lib_imports: error - avoid_slow_async_io: error - avoid_types_as_parameter_names: error - cancel_subscriptions: error - valid_regexps: error - always_require_non_null_named_parameters: error - - # Disable rules - -# Lint rules -linter: - rules: - # Public packages - public_member_api_docs: false - lines_longer_than_80_chars: false - - # Enabling rules - always_use_package_imports: true - avoid_relative_lib_imports: true - - # Disable rules - sort_pub_dependencies: false - prefer_relative_imports: false - curly_braces_in_flow_control_structures: false \ No newline at end of file diff --git a/example/android/.gitignore b/example/android/.gitignore deleted file mode 100644 index 6f56801..0000000 --- a/example/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 609c4f8..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "dev.plugfox.spinify.spinifyapp" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "dev.plugfox.spinify.spinifyapp" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 712541d..0000000 --- a/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/example/android/app/src/main/kotlin/dev/plugfox/spinify/spinifyapp/MainActivity.kt b/example/android/app/src/main/kotlin/dev/plugfox/spinify/spinifyapp/MainActivity.kt deleted file mode 100644 index 3bae9b8..0000000 --- a/example/android/app/src/main/kotlin/dev/plugfox/spinify/spinifyapp/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.plugfox.spinify.spinifyapp - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f..0000000 --- a/example/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be..0000000 --- a/example/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88..0000000 --- a/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index f7eb7f6..0000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/android/gradle.properties b/example/android/gradle.properties deleted file mode 100644 index 94adc3a..0000000 --- a/example/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c472b9..0000000 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 55c4ca8..0000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,20 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -include ":app" - -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/build.yaml b/example/build.yaml deleted file mode 100644 index 785f9a0..0000000 --- a/example/build.yaml +++ /dev/null @@ -1,11 +0,0 @@ -targets: - $default: - sources: - - $package$ - - lib/** - - pubspec.yaml - - test/** - builders: - pubspec_generator: - options: - output: lib/src/common/constant/pubspec.yaml.g.dart diff --git a/example/config/development.json b/example/config/development.json deleted file mode 100644 index 9cbc255..0000000 --- a/example/config/development.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ENVIRONMENT": "development", - "CENTRIFUGE_BASE_URL": "http://localhost:8000", - "CENTRIFUGE_CHANNEL": "chat", - "CENTRIFUGE_TIMEOUT": 8000 -} diff --git a/example/ios/.gitignore b/example/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/example/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 9625e10..0000000 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 11.0 - - diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6..0000000 --- a/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bf..0000000 --- a/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index d97f17e..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index be18bd5..0000000 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,614 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807E294A63A400263BE5 /* Frameworks */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 87131a0..0000000 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4..0000000 --- a/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e458972bab9d994556c8305db4c827017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e1120817fe9182483a228007b18ab6ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099ca80c806f8fe495613e8d6c69460d76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a01f64a61e2235dbe3f45b08f7729182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9bc882b461c96aadf492d1729e49e725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439225b78712f49115768196d8d76f6790..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27c705180eb716271f41b582e76dcbd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist deleted file mode 100644 index c1c8f38..0000000 --- a/example/ios/Runner/Info.plist +++ /dev/null @@ -1,51 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Spinify App - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - spinifyapp - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/example/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index f532f7c..0000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:l/l.dart'; -import 'package:spinifyapp/src/common/util/logger_util.dart'; -import 'package:spinifyapp/src/common/widget/app.dart'; -import 'package:spinifyapp/src/feature/dependencies/initialization/initialization.dart'; -import 'package:spinifyapp/src/feature/dependencies/widget/dependencies_scope.dart'; -import 'package:spinifyapp/src/feature/dependencies/widget/initialization_splash_screen.dart'; - -void main() => l.capture( - () => runZonedGuarded( - () { - final initialization = InitializationExecutor(); - runApp( - DependenciesScope( - initialization: initialization(), - splashScreen: InitializationSplashScreen( - progress: initialization, - ), - child: const App(), - ), - ); - }, - l.e, - ), - const LogOptions( - handlePrint: true, - messageFormatting: LoggerUtil.messageFormatting, - outputInRelease: false, - printColors: true, - ), - ); diff --git a/example/lib/src/common/constant/config.dart b/example/lib/src/common/constant/config.dart deleted file mode 100644 index 972977d..0000000 --- a/example/lib/src/common/constant/config.dart +++ /dev/null @@ -1,79 +0,0 @@ -/// Config for app. -abstract final class Config { - /// Environment flavor. - /// e.g. development, staging, production - static final EnvironmentFlavor environment = EnvironmentFlavor.from( - const String.fromEnvironment('ENVIRONMENT', defaultValue: 'development')); - - // --- Centrifuge --- // - - /// Centrifuge url. - /// e.g. https://domain.tld - static const String centrifugeBaseUrl = String.fromEnvironment( - 'CENTRIFUGE_BASE_URL', - defaultValue: 'http://127.0.0.1:8000'); - - /// Centrifuge timeout in milliseconds. - /// e.g. 15000 ms - static const Duration centrifugeTimeout = Duration( - milliseconds: - int.fromEnvironment('CENTRIFUGE_TIMEOUT', defaultValue: 15000)); - - /// Secret for HMAC token. - static const String centrifugeToken = - String.fromEnvironment('CENTRIFUGE_TOKEN_HMAC_SECRET'); - - /// Channel by default. - static const String centrifugeChannel = - String.fromEnvironment('CENTRIFUGE_CHANNEL'); - - /// Username by default. - static const String centrifugeUsername = - String.fromEnvironment('CENTRIFUGE_USERNAME'); - // --- Layout --- // - - /// Maximum screen layout width for screen with list view. - static const int maxScreenLayoutWidth = - int.fromEnvironment('MAX_LAYOUT_WIDTH', defaultValue: 768); -} - -/// Environment flavor. -/// e.g. development, staging, production -enum EnvironmentFlavor { - /// Local - local('local'), - - /// Development - development('development'), - - /// Staging - staging('staging'), - - /// Production - production('production'); - - const EnvironmentFlavor(this.value); - - factory EnvironmentFlavor.from(String? value) => - switch (value?.trim().toLowerCase()) { - 'local' || 'loc' || 'lcl' || 'l' => development, - 'development' || 'debug' || 'develop' || 'dev' || 'd' => development, - 'staging' || 'profile' || 'stage' || 'stg' || 's' => staging, - 'production' || 'release' || 'prod' || 'prd' || 'p' => production, - _ => const bool.fromEnvironment('dart.vm.product') - ? production - : development, - }; - - /// development, staging, production - final String value; - - /// Whether the environment is development. - bool get isDevelopment => this == development; - - /// Whether the environment is staging. - bool get isStaging => this == staging; - - /// Whether the environment is production. - bool get isProduction => this == production; -} diff --git a/example/lib/src/common/constant/pubspec.yaml.g.dart b/example/lib/src/common/constant/pubspec.yaml.g.dart deleted file mode 100644 index 8b1243b..0000000 --- a/example/lib/src/common/constant/pubspec.yaml.g.dart +++ /dev/null @@ -1,527 +0,0 @@ -// ignore_for_file: lines_longer_than_80_chars, unnecessary_raw_strings -// ignore_for_file: use_raw_strings, avoid_classes_with_only_static_members -// ignore_for_file: avoid_escaping_inner_quotes, prefer_single_quotes - -/// GENERATED CODE - DO NOT MODIFY BY HAND - -library pubspec; - -// ***************************************************************************** -// * pubspec_generator * -// ***************************************************************************** - -/* - - MIT License - - Copyright (c) 2023 Plague Fox - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -/// Given a version number MAJOR.MINOR.PATCH, increment the: -/// -/// 1. MAJOR version when you make incompatible API changes -/// 2. MINOR version when you add functionality in a backward compatible manner -/// 3. PATCH version when you make backward compatible bug fixes -/// -/// Additional labels for pre-release and build metadata are available -/// as extensions to the MAJOR.MINOR.PATCH format. -typedef PubspecVersion = ({ - String representation, - String canonical, - int major, - int minor, - int patch, - List preRelease, - List build -}); - -/// # The pubspec file -/// -/// Code generated pubspec.yaml.g.dart from pubspec.yaml -/// This class is generated from pubspec.yaml, do not edit directly. -/// -/// Every pub package needs some metadata so it can specify its dependencies. -/// Pub packages that are shared with others also need to provide some other -/// information so users can discover them. All of this metadata goes -/// in the package’s pubspec: -/// a file named pubspec.yaml that’s written in the YAML language. -/// -/// Read more: -/// - https://pub.dev/packages/pubspec_generator -/// - https://dart.dev/tools/pub/pubspec -sealed class Pubspec { - /// Version - /// - /// Current app [version] - /// - /// Every package has a version. - /// A version number is required to host your package on the pub.dev site, - /// but can be omitted for local-only packages. - /// If you omit it, your package is implicitly versioned 0.0.0. - /// - /// Versioning is necessary for reusing code while letting it evolve quickly. - /// A version number is three numbers separated by dots, like 0.2.43. - /// It can also optionally have a build ( +1, +2, +hotfix.oopsie) - /// or prerelease (-dev.4, -alpha.12, -beta.7, -rc.5) suffix. - /// - /// Each time you publish your package, you publish it at a specific version. - /// Once that’s been done, consider it hermetically sealed: - /// you can’t touch it anymore. To make more changes, - /// you’ll need a new version. - /// - /// When you select a version, - /// follow [semantic versioning](https://semver.org/). - static const PubspecVersion version = ( - /// Non-canonical string representation of the version as provided - /// in the pubspec.yaml file. - representation: r'1.0.0+1', - - /// Returns a 'canonicalized' representation - /// of the application version. - /// This represents the version string in accordance with - /// Semantic Versioning (SemVer) standards. - canonical: r'1.0.0+1', - - /// MAJOR version when you make incompatible API changes. - /// The major version number: 1 in "1.2.3". - major: 1, - - /// MINOR version when you add functionality - /// in a backward compatible manner. - /// The minor version number: 2 in "1.2.3". - minor: 0, - - /// PATCH version when you make backward compatible bug fixes. - /// The patch version number: 3 in "1.2.3". - patch: 0, - - /// The pre-release identifier: "foo" in "1.2.3-foo". - preRelease: [], - - /// The build identifier: "foo" in "1.2.3+foo". - build: [r'1'], - ); - - /// Build date and time (UTC) - static final DateTime timestamp = DateTime.utc( - 2023, - 8, - 4, - 9, - 6, - 50, - 841, - 552, - ); - - /// Name - /// - /// Current app [name] - /// - /// Every package needs a name. - /// It’s how other packages refer to yours, and how it appears to the world, - /// should you publish it. - /// - /// The name should be all lowercase, with underscores to separate words, - /// just_like_this. Use only basic Latin letters and Arabic digits: - /// [a-z0-9_]. Also, make sure the name is a valid Dart identifier—that - /// it doesn’t start with digits - /// and isn’t a [reserved word](https://dart.dev/language/keywords). - /// - /// Try to pick a name that is clear, terse, and not already in use. - /// A quick search of packages on the [pub.dev site](https://pub.dev/packages) - /// to make sure that nothing else is using your name is recommended. - static const String name = r'spinifyapp'; - - /// Description - /// - /// Current app [description] - /// - /// This is optional for your own personal packages, - /// but if you intend to publish your package you must provide a description, - /// which should be in English. - /// The description should be relatively short, from 60 to 180 characters - /// and tell a casual reader what they might want to know about your package. - /// - /// Think of the description as the sales pitch for your package. - /// Users see it when they [browse for packages](https://pub.dev/packages). - /// The description is plain text: no markdown or HTML. - static const String description = r'Spinify App Example'; - - /// Homepage - /// - /// Current app [homepage] - /// - /// This should be a URL pointing to the website for your package. - /// For [hosted packages](https://dart.dev/tools/pub/dependencies#hosted-packages), - /// this URL is linked from the package’s page. - /// While providing a homepage is optional, - /// please provide it or repository (or both). - /// It helps users understand where your package is coming from. - static const String homepage = r'https://centrifugal.dev'; - - /// Repository - /// - /// Current app [repository] - /// - /// Repository - /// The optional repository field should contain the URL for your package’s - /// source code repository—for example, - /// https://github.com//. - /// If you publish your package to the pub.dev site, - /// then your package’s page displays the repository URL. - /// While providing a repository is optional, - /// please provide it or homepage (or both). - /// It helps users understand where your package is coming from. - static const String repository = r'https://github.com/PlugFox/spinify'; - - /// Issue tracker - /// - /// Current app [issueTracker] - /// - /// The optional issue_tracker field should contain a URL for the package’s - /// issue tracker, where existing bugs can be viewed and new bugs can be filed. - /// The pub.dev site attempts to display a link - /// to each package’s issue tracker, using the value of this field. - /// If issue_tracker is missing but repository is present and points to GitHub, - /// then the pub.dev site uses the default issue tracker - /// (https://github.com///issues). - static const String issueTracker = - r'https://github.com/PlugFox/spinify/issues'; - - /// Documentation - /// - /// Current app [documentation] - /// - /// Some packages have a site that hosts documentation, - /// separate from the main homepage and from the Pub-generated API reference. - /// If your package has additional documentation, add a documentation: - /// field with that URL; pub shows a link to this documentation - /// on your package’s page. - static const String documentation = r''; - - /// Publish_to - /// - /// Current app [publishTo] - /// - /// The default uses the [pub.dev](https://pub.dev/) site. - /// Specify none to prevent a package from being published. - /// This setting can be used to specify a custom pub package server to publish. - /// - /// ```yaml - /// publish_to: none - /// ``` - static const String publishTo = r'none'; - - /// Funding - /// - /// Current app [funding] - /// - /// Package authors can use the funding property to specify - /// a list of URLs that provide information on how users - /// can help fund the development of the package. For example: - /// - /// ```yaml - /// funding: - /// - https://www.buymeacoffee.com/example_user - /// - https://www.patreon.com/some-account - /// ``` - /// - /// If published to [pub.dev](https://pub.dev/) the links are displayed on the package page. - /// This aims to help users fund the development of their dependencies. - static const List funding = [ - r'https://www.buymeacoffee.com/plugfox', - r'https://www.patreon.com/plugfox', - r'https://boosty.to/plugfox', - ]; - - /// False_secrets - /// - /// Current app [falseSecrets] - /// - /// When you try to publish a package, - /// pub conducts a search for potential leaks of secret credentials, - /// API keys, or cryptographic keys. - /// If pub detects a potential leak in a file that would be published, - /// then pub warns you and refuses to publish the package. - /// - /// Leak detection isn’t perfect. To avoid false positives, - /// you can tell pub not to search for leaks in certain files, - /// by creating an allowlist using gitignore - /// patterns under false_secrets in the pubspec. - /// - /// For example, the following entry causes pub not to look - /// for leaks in the file lib/src/hardcoded_api_key.dart - /// and in all .pem files in the test/localhost_certificates/ directory: - /// - /// ```yaml - /// false_secrets: - /// - /lib/src/hardcoded_api_key.dart - /// - /test/localhost_certificates/*.pem - /// ``` - /// - /// Starting a gitignore pattern with slash (/) ensures - /// that the pattern is considered relative to the package’s root directory. - static const List falseSecrets = []; - - /// Screenshots - /// - /// Current app [screenshots] - /// - /// Packages can showcase their widgets or other visual elements - /// using screenshots displayed on their pub.dev page. - /// To specify screenshots for the package to display, - /// use the screenshots field. - /// - /// A package can list up to 10 screenshots under the screenshots field. - /// Don’t include logos or other branding imagery in this section. - /// Each screenshot includes one description and one path. - /// The description explains what the screenshot depicts - /// in no more than 160 characters. For example: - /// - /// ```yaml - /// screenshots: - /// - description: 'This screenshot shows the transformation of a number of bytes - /// to a human-readable expression.' - /// path: path/to/image/in/package/500x500.webp - /// - description: 'This screenshot shows a stack trace returning a human-readable - /// representation.' - /// path: path/to/image/in/package.png - /// ``` - /// - /// Pub.dev limits screenshots to the following specifications: - /// - /// - File size: max 4 MB per image. - /// - File types: png, jpg, gif, or webp. - /// - Static and animated images are both allowed. - /// - /// Keep screenshot files small. Each download of the package - /// includes all screenshot files. - /// - /// Pub.dev generates the package’s thumbnail image from the first screenshot. - /// If this screenshot uses animation, pub.dev uses its first frame. - static const List screenshots = []; - - /// Topics - /// - /// Current app [topics] - /// - /// Package authors can use the topics field to categorize their package. Topics can be used to assist discoverability during search with filters on pub.dev. Pub.dev displays the topics on the package page as well as in the search results. - /// - /// The field consists of a list of names. For example: - /// - /// ```yaml - /// topics: - /// - network - /// - http - /// ``` - /// - /// Pub.dev requires topics to follow these specifications: - /// - /// - Tag each package with at most 5 topics. - /// - Write the topic name following these requirements: - /// 1) Use between 2 and 32 characters. - /// 2) Use only lowercase alphanumeric characters or hyphens (a-z, 0-9, -). - /// 3) Don’t use two consecutive hyphens (--). - /// 4) Start the name with lowercase alphabet characters (a-z). - /// 5) End with alphanumeric characters (a-z or 0-9). - /// - /// When choosing topics, consider if existing topics are relevant. - /// Tagging with existing topics helps users discover your package. - static const List topics = [ - r'spinify', - r'centrifugo', - r'centrifuge', - r'websocket', - r'cross-platform', - ]; - - /// Environment - static const Map environment = { - 'sdk': '>=3.1.0-63.1.beta <4.0.0', - 'flutter': '>=3.1.0-63.1.beta <4.0.0', - }; - - /// Platforms - /// - /// Current app [platforms] - /// - /// When you [publish a package](https://dart.dev/tools/pub/publishing), - /// pub.dev automatically detects the platforms that the package supports. - /// If this platform-support list is incorrect, - /// use platforms to explicitly declare which platforms your package supports. - /// - /// For example, the following platforms entry causes - /// pub.dev to list the package as supporting - /// Android, iOS, Linux, macOS, Web, and Windows: - /// - /// ```yaml - /// # This package supports all platforms listed below. - /// platforms: - /// android: - /// ios: - /// linux: - /// macos: - /// web: - /// windows: - /// ``` - /// - /// Here is an example of declaring that the package supports only Linux and macOS (and not, for example, Windows): - /// - /// ```yaml - /// # This package supports only Linux and macOS. - /// platforms: - /// linux: - /// macos: - /// ``` - static const Map platforms = { - 'android': r'', - 'ios': r'', - 'linux': r'', - 'macos': r'', - 'web': r'', - 'windows': r'', - }; - - /// Dependencies - /// - /// Current app [dependencies] - /// - /// [Dependencies](https://dart.dev/tools/pub/glossary#dependency) - /// are the pubspec’s `raison d’être`. - /// In this section you list each package that - /// your package needs in order to work. - /// - /// Dependencies fall into one of two types. - /// Regular dependencies are listed under dependencies: - /// these are packages that anyone using your package will also need. - /// Dependencies that are only needed in - /// the development phase of the package itself - /// are listed under dev_dependencies. - /// - /// During the development process, - /// you might need to temporarily override a dependency. - /// You can do so using dependency_overrides. - /// - /// For more information, - /// see [Package dependencies](https://dart.dev/tools/pub/dependencies). - static const Map dependencies = { - 'flutter': { - 'sdk': r'flutter', - }, - 'flutter_localizations': { - 'sdk': r'flutter', - }, - 'intl': r'any', - 'collection': r'any', - 'async': r'any', - 'meta': r'any', - 'path': r'any', - 'platform_info': r'^4.0.2', - 'win32': r'^5.0.6', - 'window_manager': r'^0.3.5', - 'l': r'^4.0.2', - 'spinify': { - 'path': r'../', - }, - 'cupertino_icons': r'^1.0.2', - }; - - /// Developer dependencies - static const Map devDependencies = { - 'flutter_test': { - 'sdk': r'flutter', - }, - 'integration_test': { - 'sdk': r'flutter', - }, - 'build_runner': r'^2.4.6', - 'pubspec_generator': r'>=4.0.0 <5.0.0', - 'flutter_lints': r'^2.0.1', - }; - - /// Dependency overrides - static const Map dependencyOverrides = {}; - - /// Executables - /// - /// Current app [executables] - /// - /// A package may expose one or more of its scripts as executables - /// that can be run directly from the command line. - /// To make a script publicly available, - /// list it under the executables field. - /// Entries are listed as key/value pairs: - /// - /// ```yaml - /// : - /// ``` - /// - /// For example, the following pubspec entry lists two scripts: - /// - /// ```yaml - /// executables: - /// slidy: main - /// fvm: - /// ``` - /// - /// Once the package is activated using pub global activate, - /// typing `slidy` executes `bin/main.dart`. - /// Typing `fvm` executes `bin/fvm.dart`. - /// If you don’t specify the value, it is inferred from the key. - /// - /// For more information, see pub global. - static const Map executables = {}; - - /// Source data from pubspec.yaml - static const Map source = { - 'name': name, - 'description': description, - 'repository': repository, - 'issue_tracker': issueTracker, - 'homepage': homepage, - 'documentation': documentation, - 'publish_to': publishTo, - 'version': version, - 'funding': funding, - 'false_secrets': falseSecrets, - 'screenshots': screenshots, - 'topics': topics, - 'platforms': platforms, - 'environment': environment, - 'dependencies': dependencies, - 'dev_dependencies': devDependencies, - 'dependency_overrides': dependencyOverrides, - 'flutter': { - 'generate': true, - 'uses-material-design': true, - }, - 'flutter_intl': { - 'enabled': true, - 'class_name': r'GeneratedLocalization', - 'main_locale': r'en', - 'arb_dir': r'lib/src/common/localization', - 'output_dir': r'lib/src/common/localization/generated', - 'use_deferred_loading': false, - }, - }; -} diff --git a/example/lib/src/common/controller/controller.dart b/example/lib/src/common/controller/controller.dart deleted file mode 100644 index 1864cbc..0000000 --- a/example/lib/src/common/controller/controller.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart' show Listenable, ChangeNotifier; -import 'package:meta/meta.dart'; - -/// {@template controller} -/// The controller responsible for processing the logic, -/// the connection of widgets and the date of the layer. -/// {@endtemplate} -abstract interface class IController implements Listenable { - /// Whether the controller is currently handling a requests - bool get isProcessing; - - /// Discards any resources used by the object. - /// - /// This method should only be called by the object's owner. - void dispose(); -} - -/// Controller observer -abstract interface class IControllerObserver { - /// Called when the controller is created. - void onCreate(IController controller); - - /// Called when the controller is disposed. - void onDispose(IController controller); - - /// Called on any state change in the controller. - void onStateChanged( - IController controller, Object prevState, Object nextState); - - /// Called on any error in the controller. - void onError(IController controller, Object error, StackTrace stackTrace); -} - -/// {@macro controller} -abstract base class Controller with ChangeNotifier implements IController { - /// {@macro controller} - Controller() { - runZonedGuarded( - () => Controller.observer?.onCreate(this), - (error, stackTrace) {/* ignore */}, - ); - } - - /// Controller observer - static IControllerObserver? observer; - - /// Whether the controller is disposed. - bool get isDisposed => _$isDisposed; - bool _$isDisposed = false; - - @protected - void onError(Object error, StackTrace stackTrace) => runZonedGuarded( - () => Controller.observer?.onError(this, error, stackTrace), - (error, stackTrace) {/* ignore */}, - ); - - @protected - void handle(FutureOr Function() handler); - - @override - @mustCallSuper - void dispose() { - _$isDisposed = true; - runZonedGuarded( - () => Controller.observer?.onDispose(this), - (error, stackTrace) {/* ignore */}, - ); - super.dispose(); - } - - @protected - @nonVirtual - @override - void notifyListeners() => super.notifyListeners(); -} diff --git a/example/lib/src/common/controller/controller_observer.dart b/example/lib/src/common/controller/controller_observer.dart deleted file mode 100644 index f288068..0000000 --- a/example/lib/src/common/controller/controller_observer.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:l/l.dart'; -import 'package:spinifyapp/src/common/controller/controller.dart'; - -class ControllerObserver implements IControllerObserver { - @override - void onCreate(IController controller) { - l.v6('Controller | ${controller.runtimeType} | Created'); - } - - @override - void onDispose(IController controller) { - l.v5('Controller | ${controller.runtimeType} | Disposed'); - } - - @override - void onStateChanged( - IController controller, Object prevState, Object nextState) { - l.d('Controller | ${controller.runtimeType} | $prevState -> $nextState'); - } - - @override - void onError(IController controller, Object error, StackTrace stackTrace) { - l.w('Controller | ${controller.runtimeType} | $error', stackTrace); - } -} diff --git a/example/lib/src/common/controller/droppable_controller_concurrency.dart b/example/lib/src/common/controller/droppable_controller_concurrency.dart deleted file mode 100644 index a029f6b..0000000 --- a/example/lib/src/common/controller/droppable_controller_concurrency.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import 'package:spinifyapp/src/common/controller/controller.dart'; - -base mixin DroppableControllerConcurrency on Controller { - @override - @nonVirtual - bool get isProcessing => _$processingCalls > 0; - int _$processingCalls = 0; - - @override - @protected - @mustCallSuper - void handle( - FutureOr Function() handler, [ - FutureOr Function(Object error, StackTrace stackTrace)? errorHandler, - FutureOr Function()? doneHandler, - ]) => - runZonedGuarded( - () async { - if (isDisposed || isProcessing) return; - _$processingCalls++; - try { - await handler(); - } on Object catch (error, stackTrace) { - onError(error, stackTrace); - await Future(() async { - await errorHandler?.call(error, stackTrace); - }).catchError(onError); - } finally { - await Future(() async { - await doneHandler?.call(); - }).catchError(onError); - _$processingCalls--; - } - }, - onError, - ); -} diff --git a/example/lib/src/common/controller/sequential_controller_concurrency.dart b/example/lib/src/common/controller/sequential_controller_concurrency.dart deleted file mode 100644 index 25abf47..0000000 --- a/example/lib/src/common/controller/sequential_controller_concurrency.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'package:meta/meta.dart'; -import 'package:spinifyapp/src/common/controller/controller.dart'; - -base mixin SequentialControllerConcurrency on Controller { - final _ControllerEventQueue _eventQueue = _ControllerEventQueue(); - - @override - @nonVirtual - bool get isProcessing => _eventQueue.length > 0; - - @override - @protected - @mustCallSuper - void handle( - FutureOr Function() handler, [ - FutureOr Function(Object error, StackTrace stackTrace)? errorHandler, - FutureOr Function()? doneHandler, - ]) => - _eventQueue.push( - () { - final completer = Completer(); - runZonedGuarded( - () async { - if (isDisposed) return; - try { - await handler(); - } on Object catch (error, stackTrace) { - onError(error, stackTrace); - await Future(() async { - await errorHandler?.call(error, stackTrace); - }).catchError(onError); - } finally { - await Future(() async { - await doneHandler?.call(); - }).catchError(onError); - completer.complete(); - } - }, - onError, - ); - return completer.future; - }, - ); -} - -final class _ControllerEventQueue { - _ControllerEventQueue(); - - final DoubleLinkedQueue<_SequentialTask> _queue = - DoubleLinkedQueue<_SequentialTask>(); - Future? _processing; - bool _isClosed = false; - - /// Event queue length. - int get length => _queue.length; - - /// Push it at the end of the queue. - Future push(FutureOr Function() fn) { - final task = _SequentialTask(fn); - _queue.add(task); - _exec(); - return task.future; - } - - /// Mark the queue as closed. - /// The queue will be processed until it's empty. - /// But all new and current events will be rejected with [WSClientClosed]. - FutureOr close() async { - _isClosed = true; - await _processing; - } - - /// Execute the queue. - void _exec() => _processing ??= Future.doWhile(() async { - final event = _queue.first; - try { - if (_isClosed) { - event.reject(StateError('Controller\'s event queue are disposed'), - StackTrace.current); - } else { - await event(); - } - } on Object catch (error, stackTrace) { - /* warning( - error, - stackTrace, - 'Error while processing event "${event.id}"', - ); */ - Future.sync(() => event.reject(error, stackTrace)).ignore(); - } - _queue.removeFirst(); - final isEmpty = _queue.isEmpty; - if (isEmpty) _processing = null; - return !isEmpty; - }); -} - -class _SequentialTask { - _SequentialTask(FutureOr Function() fn) - : _fn = fn, - _completer = Completer(); - - final Completer _completer; - - final FutureOr Function() _fn; - - Future get future => _completer.future; - - FutureOr call() async { - final result = await _fn(); - if (!_completer.isCompleted) { - _completer.complete(result); - } - return result; - } - - void reject(Object error, [StackTrace? stackTrace]) { - if (_completer.isCompleted) return; - _completer.completeError(error, stackTrace); - } -} diff --git a/example/lib/src/common/controller/state_consumer.dart b/example/lib/src/common/controller/state_consumer.dart deleted file mode 100644 index ad366a2..0000000 --- a/example/lib/src/common/controller/state_consumer.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:spinifyapp/src/common/controller/state_controller.dart'; - -/// Fire when the state changes. -typedef StateConsumerListener = void Function( - BuildContext context, S previous, S current); - -/// Build when the method returns true. -typedef StateConsumerCondition = bool Function(S previous, S current); - -/// Rebuild the widget when the state changes. -typedef StateConsumerBuilder = Widget Function( - BuildContext context, S state, Widget? child); - -/// {@template state_consumer} -/// StateBuilder widget. -/// {@endtemplate} -class StateConsumer extends StatefulWidget { - /// {@macro state_builder} - const StateConsumer({ - required this.controller, - this.listener, - this.buildWhen, - this.builder, - this.child, - super.key, - }); - - /// The controller responsible for processing the logic, - final IStateController controller; - - /// Takes the `BuildContext` along with the `state` - /// and is responsible for executing in response to `state` changes. - final StateConsumerListener? listener; - - /// Takes the previous `state` and the current `state` and is responsible for - /// returning a [bool] which determines whether or not to trigger - /// [builder] with the current `state`. - final StateConsumerCondition? buildWhen; - - /// The [builder] function which will be invoked on each widget build. - /// The [builder] takes the `BuildContext` and current `state` and - /// must return a widget. - /// This is analogous to the [builder] function in [StreamBuilder]. - final StateConsumerBuilder? builder; - - /// The child widget which will be passed to the [builder]. - final Widget? child; - - @override - State> createState() => _StateConsumerState(); -} - -class _StateConsumerState extends State> { - late IStateController _controller; - late S _previousState; - - @override - void initState() { - super.initState(); - _controller = widget.controller; - _previousState = _controller.state; - _subscribe(); - } - - @override - void didUpdateWidget(StateConsumer oldWidget) { - super.didUpdateWidget(oldWidget); - final oldController = oldWidget.controller, - newController = widget.controller; - if (identical(oldController, newController) || - oldController == newController) return; - _unsubscribe(); - _controller = newController; - _previousState = newController.state; - _subscribe(); - } - - @override - void dispose() { - _unsubscribe(); - super.dispose(); - } - - void _subscribe() => _controller.addListener(_valueChanged); - - void _unsubscribe() => _controller.removeListener(_valueChanged); - - void _valueChanged() { - final oldState = _previousState, newState = _controller.state; - if (!mounted || identical(oldState, newState)) return; - _previousState = newState; - widget.listener?.call(context, oldState, newState); - if (widget.buildWhen?.call(oldState, newState) ?? true) { - // Rebuild the widget when the state changes. - switch (SchedulerBinding.instance.schedulerPhase) { - case SchedulerPhase.idle: - case SchedulerPhase.transientCallbacks: - case SchedulerPhase.postFrameCallbacks: - setState(() {}); - case SchedulerPhase.persistentCallbacks: - case SchedulerPhase.midFrameMicrotasks: - SchedulerBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - setState(() {}); - }); - } - } - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) => - super.debugFillProperties(properties - ..add( - DiagnosticsProperty>('Controller', _controller)) - ..add(DiagnosticsProperty('State', _controller.state)) - ..add(FlagProperty('isProcessing', - value: _controller.isProcessing, - ifTrue: 'Processing', - ifFalse: 'Idle'))); - - @override - Widget build(BuildContext context) => - widget.builder?.call(context, _controller.state, widget.child) ?? - widget.child ?? - const SizedBox.shrink(); -} diff --git a/example/lib/src/common/controller/state_controller.dart b/example/lib/src/common/controller/state_controller.dart deleted file mode 100644 index e6dcc64..0000000 --- a/example/lib/src/common/controller/state_controller.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:spinifyapp/src/common/controller/controller.dart'; - -/// State controller -abstract interface class IStateController - implements IController { - /// The current state of the controller. - State get state; -} - -/// State controller -abstract base class StateController extends Controller - implements IStateController { - /// State controller - StateController({required State initialState}) : _$state = initialState; - - @override - @nonVirtual - State get state => _$state; - State _$state; - - @protected - @nonVirtual - void setState(State state) { - runZonedGuarded( - () => Controller.observer?.onStateChanged(this, _$state, state), - (error, stackTrace) {/* ignore */}, - ); - _$state = state; - if (isDisposed) return; - notifyListeners(); - } -} diff --git a/example/lib/src/common/localization/generated/intl/messages_all.dart b/example/lib/src/common/localization/generated/intl/messages_all.dart deleted file mode 100644 index 203415c..0000000 --- a/example/lib/src/common/localization/generated/intl/messages_all.dart +++ /dev/null @@ -1,63 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that looks up messages for specific locales by -// delegating to the appropriate library. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:implementation_imports, file_names, unnecessary_new -// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering -// ignore_for_file:argument_type_not_assignable, invalid_assignment -// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases -// ignore_for_file:comment_references - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; -import 'package:intl/src/intl_helpers.dart'; - -import 'messages_en.dart' as messages_en; - -typedef Future LibraryLoader(); -Map _deferredLibraries = { - 'en': () => new SynchronousFuture(null), -}; - -MessageLookupByLibrary? _findExact(String localeName) { - switch (localeName) { - case 'en': - return messages_en.messages; - default: - return null; - } -} - -/// User programs should call this before using [localeName] for messages. -Future initializeMessages(String localeName) { - var availableLocale = Intl.verifiedLocale( - localeName, (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); - if (availableLocale == null) { - return new SynchronousFuture(false); - } - var lib = _deferredLibraries[availableLocale]; - lib == null ? new SynchronousFuture(false) : lib(); - initializeInternalMessageLookup(() => new CompositeMessageLookup()); - messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new SynchronousFuture(true); -} - -bool _messagesExistFor(String locale) { - try { - return _findExact(locale) != null; - } catch (e) { - return false; - } -} - -MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { - var actualLocale = - Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); - if (actualLocale == null) return null; - return _findExact(actualLocale); -} diff --git a/example/lib/src/common/localization/generated/intl/messages_en.dart b/example/lib/src/common/localization/generated/intl/messages_en.dart deleted file mode 100644 index f998a81..0000000 --- a/example/lib/src/common/localization/generated/intl/messages_en.dart +++ /dev/null @@ -1,154 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a en locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'en'; - - final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "addButton": MessageLookupByLibrary.simpleMessage("Add"), - "addToStarredButton": - MessageLookupByLibrary.simpleMessage("Add to starred"), - "apiDomain": MessageLookupByLibrary.simpleMessage("API domain"), - "appLabel": MessageLookupByLibrary.simpleMessage("App"), - "applicationInformationLabel": - MessageLookupByLibrary.simpleMessage("Application information"), - "applicationLabel": MessageLookupByLibrary.simpleMessage("Application"), - "applicationVersionLabel": - MessageLookupByLibrary.simpleMessage("Application version"), - "authenticateLabel": - MessageLookupByLibrary.simpleMessage("Authenticate"), - "authenticatedLabel": - MessageLookupByLibrary.simpleMessage("Authenticated"), - "authenticationLabel": - MessageLookupByLibrary.simpleMessage("Authentication"), - "backButton": MessageLookupByLibrary.simpleMessage("Back"), - "cancelButton": MessageLookupByLibrary.simpleMessage("Cancel"), - "conectedDevicesLabel": - MessageLookupByLibrary.simpleMessage("Connected devices"), - "confirmButton": MessageLookupByLibrary.simpleMessage("Confirm"), - "continueButton": MessageLookupByLibrary.simpleMessage("Continue"), - "copiedLabel": MessageLookupByLibrary.simpleMessage("Copied"), - "copyButton": MessageLookupByLibrary.simpleMessage("Copy"), - "createButton": MessageLookupByLibrary.simpleMessage("Create"), - "currentLabel": MessageLookupByLibrary.simpleMessage("Current"), - "currentUserLabel": - MessageLookupByLibrary.simpleMessage("Current user"), - "currentVersionLabel": - MessageLookupByLibrary.simpleMessage("Current version"), - "databaseLabel": MessageLookupByLibrary.simpleMessage("Database"), - "dateLabel": MessageLookupByLibrary.simpleMessage("Date"), - "deleteButton": MessageLookupByLibrary.simpleMessage("Delete"), - "downloadButton": MessageLookupByLibrary.simpleMessage("Download"), - "editButton": MessageLookupByLibrary.simpleMessage("Edit"), - "emailLabel": MessageLookupByLibrary.simpleMessage("Email"), - "errAnErrorHasOccurred": - MessageLookupByLibrary.simpleMessage("An error has occurred"), - "errAnExceptionHasOccurred": - MessageLookupByLibrary.simpleMessage("An exception has occurred"), - "errAnUnknownErrorWasReceivedFromTheServer": - MessageLookupByLibrary.simpleMessage( - "An unknown error was received from the server"), - "errAssertionError": - MessageLookupByLibrary.simpleMessage("Assertion error"), - "errBadGateway": MessageLookupByLibrary.simpleMessage("Bad gateway"), - "errBadRequest": MessageLookupByLibrary.simpleMessage("Bad request"), - "errBadStateError": - MessageLookupByLibrary.simpleMessage("Bad state error"), - "errError": MessageLookupByLibrary.simpleMessage("Error"), - "errException": MessageLookupByLibrary.simpleMessage("Exception"), - "errFileSystemException": - MessageLookupByLibrary.simpleMessage("File system error"), - "errForbidden": MessageLookupByLibrary.simpleMessage("Forbidden"), - "errGatewayTimeout": - MessageLookupByLibrary.simpleMessage("Gateway timeout"), - "errInternalServerError": - MessageLookupByLibrary.simpleMessage("Internal server error"), - "errInvalidCredentials": - MessageLookupByLibrary.simpleMessage("Invalid credentials"), - "errInvalidFormat": - MessageLookupByLibrary.simpleMessage("Invalid format"), - "errNotAcceptable": - MessageLookupByLibrary.simpleMessage("Not acceptable"), - "errNotFound": MessageLookupByLibrary.simpleMessage("Not found"), - "errNotImplementedYet": - MessageLookupByLibrary.simpleMessage("Not implemented yet"), - "errRequestTimeout": - MessageLookupByLibrary.simpleMessage("Request timeout"), - "errServiceUnavailable": - MessageLookupByLibrary.simpleMessage("Service unavailable"), - "errSomethingWentWrong": - MessageLookupByLibrary.simpleMessage("Something went wrong"), - "errTimeOutExceeded": - MessageLookupByLibrary.simpleMessage("Time out exceeded"), - "errTooManyRequests": - MessageLookupByLibrary.simpleMessage("Too many requests"), - "errTryAgainLater": - MessageLookupByLibrary.simpleMessage("Please try again later."), - "errUnauthorized": MessageLookupByLibrary.simpleMessage("Unauthorized"), - "errUnimplemented": - MessageLookupByLibrary.simpleMessage("Unimplemented"), - "errUnknownServerError": - MessageLookupByLibrary.simpleMessage("Unknown server error"), - "errUnsupportedOperation": - MessageLookupByLibrary.simpleMessage("Unsupported operation"), - "exitButton": MessageLookupByLibrary.simpleMessage("Exit"), - "helpLabel": MessageLookupByLibrary.simpleMessage("Help"), - "language": MessageLookupByLibrary.simpleMessage("English"), - "languageCode": MessageLookupByLibrary.simpleMessage("en"), - "languageSelectionLabel": - MessageLookupByLibrary.simpleMessage("Language selection"), - "latestVersionLabel": - MessageLookupByLibrary.simpleMessage("Latest version"), - "localeName": MessageLookupByLibrary.simpleMessage("en_US"), - "logInButton": MessageLookupByLibrary.simpleMessage("Log In"), - "logOutButton": MessageLookupByLibrary.simpleMessage("Log Out"), - "logOutDescription": MessageLookupByLibrary.simpleMessage( - "You will be logged out from your account"), - "moveButton": MessageLookupByLibrary.simpleMessage("Move"), - "moveToTrashButton": - MessageLookupByLibrary.simpleMessage("Move to trash"), - "nameLabel": MessageLookupByLibrary.simpleMessage("Name"), - "navigationLabel": MessageLookupByLibrary.simpleMessage("Navigation"), - "removeFromStarredButton": - MessageLookupByLibrary.simpleMessage("Remove from starred"), - "renameButton": MessageLookupByLibrary.simpleMessage("Rename"), - "renewalDateLabel": - MessageLookupByLibrary.simpleMessage("Renewal date"), - "restoreButton": MessageLookupByLibrary.simpleMessage("Restore"), - "saveButton": MessageLookupByLibrary.simpleMessage("Save"), - "selectedLabel": MessageLookupByLibrary.simpleMessage("Selected"), - "shareButton": MessageLookupByLibrary.simpleMessage("Share"), - "shareLinkButton": MessageLookupByLibrary.simpleMessage("Share link"), - "signInButton": MessageLookupByLibrary.simpleMessage("Sign In"), - "signUpButton": MessageLookupByLibrary.simpleMessage("Sign Up"), - "sizeLabel": MessageLookupByLibrary.simpleMessage("Size"), - "statusLabel": MessageLookupByLibrary.simpleMessage("Status"), - "storageLabel": MessageLookupByLibrary.simpleMessage("Storage"), - "surnameLabel": MessageLookupByLibrary.simpleMessage("Surname"), - "timeLabel": MessageLookupByLibrary.simpleMessage("Time"), - "title": MessageLookupByLibrary.simpleMessage("Spinify"), - "typeLabel": MessageLookupByLibrary.simpleMessage("Type"), - "upgradeLabel": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadButton": MessageLookupByLibrary.simpleMessage("Upload"), - "usefulLinksLabel": - MessageLookupByLibrary.simpleMessage("Useful links"), - "versionLabel": MessageLookupByLibrary.simpleMessage("Version") - }; -} diff --git a/example/lib/src/common/localization/generated/l10n.dart b/example/lib/src/common/localization/generated/l10n.dart deleted file mode 100644 index 507d46d..0000000 --- a/example/lib/src/common/localization/generated/l10n.dart +++ /dev/null @@ -1,981 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'intl/messages_all.dart'; - -// ************************************************************************** -// Generator: Flutter Intl IDE plugin -// Made by Localizely -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars -// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each -// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes - -class GeneratedLocalization { - GeneratedLocalization(); - - static GeneratedLocalization? _current; - - static GeneratedLocalization get current { - assert(_current != null, - 'No instance of GeneratedLocalization was loaded. Try to initialize the GeneratedLocalization delegate before accessing GeneratedLocalization.current.'); - return _current!; - } - - static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); - - static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); - final localeName = Intl.canonicalizedLocale(name); - return initializeMessages(localeName).then((_) { - Intl.defaultLocale = localeName; - final instance = GeneratedLocalization(); - GeneratedLocalization._current = instance; - - return instance; - }); - } - - static GeneratedLocalization of(BuildContext context) { - final instance = GeneratedLocalization.maybeOf(context); - assert(instance != null, - 'No instance of GeneratedLocalization present in the widget tree. Did you add GeneratedLocalization.delegate in localizationsDelegates?'); - return instance!; - } - - static GeneratedLocalization? maybeOf(BuildContext context) { - return Localizations.of( - context, GeneratedLocalization); - } - - /// `en_US` - String get localeName { - return Intl.message( - 'en_US', - name: 'localeName', - desc: '', - args: [], - ); - } - - /// `en` - String get languageCode { - return Intl.message( - 'en', - name: 'languageCode', - desc: '', - args: [], - ); - } - - /// `English` - String get language { - return Intl.message( - 'English', - name: 'language', - desc: '', - args: [], - ); - } - - /// `Spinify` - String get title { - return Intl.message( - 'Spinify', - name: 'title', - desc: '', - args: [], - ); - } - - /// `Something went wrong` - String get errSomethingWentWrong { - return Intl.message( - 'Something went wrong', - name: 'errSomethingWentWrong', - desc: '', - args: [], - ); - } - - /// `Error` - String get errError { - return Intl.message( - 'Error', - name: 'errError', - desc: '', - args: [], - ); - } - - /// `Exception` - String get errException { - return Intl.message( - 'Exception', - name: 'errException', - desc: '', - args: [], - ); - } - - /// `An error has occurred` - String get errAnErrorHasOccurred { - return Intl.message( - 'An error has occurred', - name: 'errAnErrorHasOccurred', - desc: '', - args: [], - ); - } - - /// `An exception has occurred` - String get errAnExceptionHasOccurred { - return Intl.message( - 'An exception has occurred', - name: 'errAnExceptionHasOccurred', - desc: '', - args: [], - ); - } - - /// `Please try again later.` - String get errTryAgainLater { - return Intl.message( - 'Please try again later.', - name: 'errTryAgainLater', - desc: '', - args: [], - ); - } - - /// `Invalid format` - String get errInvalidFormat { - return Intl.message( - 'Invalid format', - name: 'errInvalidFormat', - desc: '', - args: [], - ); - } - - /// `Time out exceeded` - String get errTimeOutExceeded { - return Intl.message( - 'Time out exceeded', - name: 'errTimeOutExceeded', - desc: '', - args: [], - ); - } - - /// `Invalid credentials` - String get errInvalidCredentials { - return Intl.message( - 'Invalid credentials', - name: 'errInvalidCredentials', - desc: '', - args: [], - ); - } - - /// `Unimplemented` - String get errUnimplemented { - return Intl.message( - 'Unimplemented', - name: 'errUnimplemented', - desc: '', - args: [], - ); - } - - /// `Not implemented yet` - String get errNotImplementedYet { - return Intl.message( - 'Not implemented yet', - name: 'errNotImplementedYet', - desc: '', - args: [], - ); - } - - /// `Unsupported operation` - String get errUnsupportedOperation { - return Intl.message( - 'Unsupported operation', - name: 'errUnsupportedOperation', - desc: '', - args: [], - ); - } - - /// `File system error` - String get errFileSystemException { - return Intl.message( - 'File system error', - name: 'errFileSystemException', - desc: '', - args: [], - ); - } - - /// `Assertion error` - String get errAssertionError { - return Intl.message( - 'Assertion error', - name: 'errAssertionError', - desc: '', - args: [], - ); - } - - /// `Bad state error` - String get errBadStateError { - return Intl.message( - 'Bad state error', - name: 'errBadStateError', - desc: '', - args: [], - ); - } - - /// `Bad request` - String get errBadRequest { - return Intl.message( - 'Bad request', - name: 'errBadRequest', - desc: '', - args: [], - ); - } - - /// `Unauthorized` - String get errUnauthorized { - return Intl.message( - 'Unauthorized', - name: 'errUnauthorized', - desc: '', - args: [], - ); - } - - /// `Forbidden` - String get errForbidden { - return Intl.message( - 'Forbidden', - name: 'errForbidden', - desc: '', - args: [], - ); - } - - /// `Not found` - String get errNotFound { - return Intl.message( - 'Not found', - name: 'errNotFound', - desc: '', - args: [], - ); - } - - /// `Not acceptable` - String get errNotAcceptable { - return Intl.message( - 'Not acceptable', - name: 'errNotAcceptable', - desc: '', - args: [], - ); - } - - /// `Request timeout` - String get errRequestTimeout { - return Intl.message( - 'Request timeout', - name: 'errRequestTimeout', - desc: '', - args: [], - ); - } - - /// `Too many requests` - String get errTooManyRequests { - return Intl.message( - 'Too many requests', - name: 'errTooManyRequests', - desc: '', - args: [], - ); - } - - /// `Internal server error` - String get errInternalServerError { - return Intl.message( - 'Internal server error', - name: 'errInternalServerError', - desc: '', - args: [], - ); - } - - /// `Bad gateway` - String get errBadGateway { - return Intl.message( - 'Bad gateway', - name: 'errBadGateway', - desc: '', - args: [], - ); - } - - /// `Service unavailable` - String get errServiceUnavailable { - return Intl.message( - 'Service unavailable', - name: 'errServiceUnavailable', - desc: '', - args: [], - ); - } - - /// `Gateway timeout` - String get errGatewayTimeout { - return Intl.message( - 'Gateway timeout', - name: 'errGatewayTimeout', - desc: '', - args: [], - ); - } - - /// `Unknown server error` - String get errUnknownServerError { - return Intl.message( - 'Unknown server error', - name: 'errUnknownServerError', - desc: '', - args: [], - ); - } - - /// `An unknown error was received from the server` - String get errAnUnknownErrorWasReceivedFromTheServer { - return Intl.message( - 'An unknown error was received from the server', - name: 'errAnUnknownErrorWasReceivedFromTheServer', - desc: '', - args: [], - ); - } - - /// `Log Out` - String get logOutButton { - return Intl.message( - 'Log Out', - name: 'logOutButton', - desc: '', - args: [], - ); - } - - /// `Log In` - String get logInButton { - return Intl.message( - 'Log In', - name: 'logInButton', - desc: '', - args: [], - ); - } - - /// `Exit` - String get exitButton { - return Intl.message( - 'Exit', - name: 'exitButton', - desc: '', - args: [], - ); - } - - /// `Sign Up` - String get signUpButton { - return Intl.message( - 'Sign Up', - name: 'signUpButton', - desc: '', - args: [], - ); - } - - /// `Sign In` - String get signInButton { - return Intl.message( - 'Sign In', - name: 'signInButton', - desc: '', - args: [], - ); - } - - /// `Back` - String get backButton { - return Intl.message( - 'Back', - name: 'backButton', - desc: '', - args: [], - ); - } - - /// `Cancel` - String get cancelButton { - return Intl.message( - 'Cancel', - name: 'cancelButton', - desc: '', - args: [], - ); - } - - /// `Confirm` - String get confirmButton { - return Intl.message( - 'Confirm', - name: 'confirmButton', - desc: '', - args: [], - ); - } - - /// `Continue` - String get continueButton { - return Intl.message( - 'Continue', - name: 'continueButton', - desc: '', - args: [], - ); - } - - /// `Save` - String get saveButton { - return Intl.message( - 'Save', - name: 'saveButton', - desc: '', - args: [], - ); - } - - /// `Create` - String get createButton { - return Intl.message( - 'Create', - name: 'createButton', - desc: '', - args: [], - ); - } - - /// `Delete` - String get deleteButton { - return Intl.message( - 'Delete', - name: 'deleteButton', - desc: '', - args: [], - ); - } - - /// `Edit` - String get editButton { - return Intl.message( - 'Edit', - name: 'editButton', - desc: '', - args: [], - ); - } - - /// `Add` - String get addButton { - return Intl.message( - 'Add', - name: 'addButton', - desc: '', - args: [], - ); - } - - /// `Copy` - String get copyButton { - return Intl.message( - 'Copy', - name: 'copyButton', - desc: '', - args: [], - ); - } - - /// `Move` - String get moveButton { - return Intl.message( - 'Move', - name: 'moveButton', - desc: '', - args: [], - ); - } - - /// `Rename` - String get renameButton { - return Intl.message( - 'Rename', - name: 'renameButton', - desc: '', - args: [], - ); - } - - /// `Upload` - String get uploadButton { - return Intl.message( - 'Upload', - name: 'uploadButton', - desc: '', - args: [], - ); - } - - /// `Download` - String get downloadButton { - return Intl.message( - 'Download', - name: 'downloadButton', - desc: '', - args: [], - ); - } - - /// `Share` - String get shareButton { - return Intl.message( - 'Share', - name: 'shareButton', - desc: '', - args: [], - ); - } - - /// `Share link` - String get shareLinkButton { - return Intl.message( - 'Share link', - name: 'shareLinkButton', - desc: '', - args: [], - ); - } - - /// `Remove from starred` - String get removeFromStarredButton { - return Intl.message( - 'Remove from starred', - name: 'removeFromStarredButton', - desc: '', - args: [], - ); - } - - /// `Add to starred` - String get addToStarredButton { - return Intl.message( - 'Add to starred', - name: 'addToStarredButton', - desc: '', - args: [], - ); - } - - /// `Move to trash` - String get moveToTrashButton { - return Intl.message( - 'Move to trash', - name: 'moveToTrashButton', - desc: '', - args: [], - ); - } - - /// `Restore` - String get restoreButton { - return Intl.message( - 'Restore', - name: 'restoreButton', - desc: '', - args: [], - ); - } - - /// `Email` - String get emailLabel { - return Intl.message( - 'Email', - name: 'emailLabel', - desc: '', - args: [], - ); - } - - /// `Name` - String get nameLabel { - return Intl.message( - 'Name', - name: 'nameLabel', - desc: '', - args: [], - ); - } - - /// `Surname` - String get surnameLabel { - return Intl.message( - 'Surname', - name: 'surnameLabel', - desc: '', - args: [], - ); - } - - /// `Language selection` - String get languageSelectionLabel { - return Intl.message( - 'Language selection', - name: 'languageSelectionLabel', - desc: '', - args: [], - ); - } - - /// `Upgrade` - String get upgradeLabel { - return Intl.message( - 'Upgrade', - name: 'upgradeLabel', - desc: '', - args: [], - ); - } - - /// `App` - String get appLabel { - return Intl.message( - 'App', - name: 'appLabel', - desc: '', - args: [], - ); - } - - /// `Application` - String get applicationLabel { - return Intl.message( - 'Application', - name: 'applicationLabel', - desc: '', - args: [], - ); - } - - /// `Authenticate` - String get authenticateLabel { - return Intl.message( - 'Authenticate', - name: 'authenticateLabel', - desc: '', - args: [], - ); - } - - /// `Authenticated` - String get authenticatedLabel { - return Intl.message( - 'Authenticated', - name: 'authenticatedLabel', - desc: '', - args: [], - ); - } - - /// `Authentication` - String get authenticationLabel { - return Intl.message( - 'Authentication', - name: 'authenticationLabel', - desc: '', - args: [], - ); - } - - /// `Navigation` - String get navigationLabel { - return Intl.message( - 'Navigation', - name: 'navigationLabel', - desc: '', - args: [], - ); - } - - /// `Database` - String get databaseLabel { - return Intl.message( - 'Database', - name: 'databaseLabel', - desc: '', - args: [], - ); - } - - /// `Copied` - String get copiedLabel { - return Intl.message( - 'Copied', - name: 'copiedLabel', - desc: '', - args: [], - ); - } - - /// `Useful links` - String get usefulLinksLabel { - return Intl.message( - 'Useful links', - name: 'usefulLinksLabel', - desc: '', - args: [], - ); - } - - /// `Current version` - String get currentVersionLabel { - return Intl.message( - 'Current version', - name: 'currentVersionLabel', - desc: '', - args: [], - ); - } - - /// `Latest version` - String get latestVersionLabel { - return Intl.message( - 'Latest version', - name: 'latestVersionLabel', - desc: '', - args: [], - ); - } - - /// `Version` - String get versionLabel { - return Intl.message( - 'Version', - name: 'versionLabel', - desc: '', - args: [], - ); - } - - /// `Size` - String get sizeLabel { - return Intl.message( - 'Size', - name: 'sizeLabel', - desc: '', - args: [], - ); - } - - /// `Type` - String get typeLabel { - return Intl.message( - 'Type', - name: 'typeLabel', - desc: '', - args: [], - ); - } - - /// `Date` - String get dateLabel { - return Intl.message( - 'Date', - name: 'dateLabel', - desc: '', - args: [], - ); - } - - /// `Time` - String get timeLabel { - return Intl.message( - 'Time', - name: 'timeLabel', - desc: '', - args: [], - ); - } - - /// `Status` - String get statusLabel { - return Intl.message( - 'Status', - name: 'statusLabel', - desc: '', - args: [], - ); - } - - /// `Current` - String get currentLabel { - return Intl.message( - 'Current', - name: 'currentLabel', - desc: '', - args: [], - ); - } - - /// `Current user` - String get currentUserLabel { - return Intl.message( - 'Current user', - name: 'currentUserLabel', - desc: '', - args: [], - ); - } - - /// `Application version` - String get applicationVersionLabel { - return Intl.message( - 'Application version', - name: 'applicationVersionLabel', - desc: '', - args: [], - ); - } - - /// `Application information` - String get applicationInformationLabel { - return Intl.message( - 'Application information', - name: 'applicationInformationLabel', - desc: '', - args: [], - ); - } - - /// `Connected devices` - String get conectedDevicesLabel { - return Intl.message( - 'Connected devices', - name: 'conectedDevicesLabel', - desc: '', - args: [], - ); - } - - /// `Renewal date` - String get renewalDateLabel { - return Intl.message( - 'Renewal date', - name: 'renewalDateLabel', - desc: '', - args: [], - ); - } - - /// `API domain` - String get apiDomain { - return Intl.message( - 'API domain', - name: 'apiDomain', - desc: '', - args: [], - ); - } - - /// `Storage` - String get storageLabel { - return Intl.message( - 'Storage', - name: 'storageLabel', - desc: '', - args: [], - ); - } - - /// `Help` - String get helpLabel { - return Intl.message( - 'Help', - name: 'helpLabel', - desc: '', - args: [], - ); - } - - /// `Selected` - String get selectedLabel { - return Intl.message( - 'Selected', - name: 'selectedLabel', - desc: '', - args: [], - ); - } - - /// `You will be logged out from your account` - String get logOutDescription { - return Intl.message( - 'You will be logged out from your account', - name: 'logOutDescription', - desc: '', - args: [], - ); - } -} - -class AppLocalizationDelegate - extends LocalizationsDelegate { - const AppLocalizationDelegate(); - - List get supportedLocales { - return const [ - Locale.fromSubtags(languageCode: 'en'), - ]; - } - - @override - bool isSupported(Locale locale) => _isSupported(locale); - @override - Future load(Locale locale) => - GeneratedLocalization.load(locale); - @override - bool shouldReload(AppLocalizationDelegate old) => false; - - bool _isSupported(Locale locale) { - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode) { - return true; - } - } - return false; - } -} diff --git a/example/lib/src/common/localization/intl_en.arb b/example/lib/src/common/localization/intl_en.arb deleted file mode 100644 index 9ab5962..0000000 --- a/example/lib/src/common/localization/intl_en.arb +++ /dev/null @@ -1,98 +0,0 @@ -{ - "@@locale": "en", - "localeName": "en_US", - "languageCode": "en", - "language": "English", - "@ Title": {}, - "title": "Spinify", - "@ ############# Errors #############": {}, - "errSomethingWentWrong": "Something went wrong", - "errError": "Error", - "errException": "Exception", - "errAnErrorHasOccurred": "An error has occurred", - "errAnExceptionHasOccurred": "An exception has occurred", - "errTryAgainLater": "Please try again later.", - "errInvalidFormat": "Invalid format", - "errTimeOutExceeded": "Time out exceeded", - "errInvalidCredentials": "Invalid credentials", - "errUnimplemented": "Unimplemented", - "errNotImplementedYet": "Not implemented yet", - "errUnsupportedOperation": "Unsupported operation", - "errFileSystemException": "File system error", - "errAssertionError": "Assertion error", - "errBadStateError": "Bad state error", - "errBadRequest": "Bad request", - "errUnauthorized": "Unauthorized", - "errForbidden": "Forbidden", - "errNotFound": "Not found", - "errNotAcceptable": "Not acceptable", - "errRequestTimeout": "Request timeout", - "errTooManyRequests": "Too many requests", - "errInternalServerError": "Internal server error", - "errBadGateway": "Bad gateway", - "errServiceUnavailable": "Service unavailable", - "errGatewayTimeout": "Gateway timeout", - "errUnknownServerError": "Unknown server error", - "errAnUnknownErrorWasReceivedFromTheServer": "An unknown error was received from the server", - "@ ############# Buttons #############": {}, - "logOutButton": "Log Out", - "logInButton": "Log In", - "exitButton": "Exit", - "signUpButton": "Sign Up", - "signInButton": "Sign In", - "backButton": "Back", - "cancelButton": "Cancel", - "confirmButton": "Confirm", - "continueButton": "Continue", - "saveButton": "Save", - "createButton": "Create", - "deleteButton": "Delete", - "editButton": "Edit", - "addButton": "Add", - "copyButton": "Copy", - "moveButton": "Move", - "renameButton": "Rename", - "uploadButton": "Upload", - "downloadButton": "Download", - "shareButton": "Share", - "shareLinkButton": "Share link", - "removeFromStarredButton": "Remove from starred", - "addToStarredButton": "Add to starred", - "moveToTrashButton": "Move to trash", - "restoreButton": "Restore", - "@ ############# Labels #############": {}, - "emailLabel": "Email", - "nameLabel": "Name", - "surnameLabel": "Surname", - "languageSelectionLabel": "Language selection", - "upgradeLabel": "Upgrade", - "appLabel": "App", - "applicationLabel": "Application", - "authenticateLabel": "Authenticate", - "authenticatedLabel": "Authenticated", - "authenticationLabel": "Authentication", - "navigationLabel": "Navigation", - "databaseLabel": "Database", - "copiedLabel": "Copied", - "usefulLinksLabel": "Useful links", - "currentVersionLabel": "Current version", - "latestVersionLabel": "Latest version", - "versionLabel": "Version", - "sizeLabel": "Size", - "typeLabel": "Type", - "dateLabel": "Date", - "timeLabel": "Time", - "statusLabel": "Status", - "currentLabel": "Current", - "currentUserLabel": "Current user", - "applicationVersionLabel": "Application version", - "applicationInformationLabel": "Application information", - "conectedDevicesLabel": "Connected devices", - "renewalDateLabel": "Renewal date", - "apiDomain": "API domain", - "storageLabel": "Storage", - "helpLabel": "Help", - "selectedLabel": "Selected", - "@ ############# Descriptions #############": {}, - "logOutDescription": "You will be logged out from your account" -} \ No newline at end of file diff --git a/example/lib/src/common/localization/localization.dart b/example/lib/src/common/localization/localization.dart deleted file mode 100644 index 80600fc..0000000 --- a/example/lib/src/common/localization/localization.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; -import 'package:spinifyapp/src/common/localization/generated/l10n.dart' - as generated show GeneratedLocalization, AppLocalizationDelegate; - -/// Localization. -final class Localization extends generated.GeneratedLocalization { - Localization._(this.locale); - - final Locale locale; - - /// Localization delegate. - static const LocalizationsDelegate delegate = - _LocalizationView(generated.AppLocalizationDelegate()); - - /// Current localization instance. - static Localization get current => _current; - static late Localization _current; - - /// Get localization instance for the widget structure. - static Localization of(BuildContext context) => - switch (Localizations.of(context, Localization)) { - Localization localization => localization, - _ => throw ArgumentError( - 'Out of scope, not found inherited widget ' - 'a Localization of the exact type', - 'out_of_scope', - ), - }; - - /// Get language by code. - static ({String name, String nativeName})? getLanguageByCode(String code) => - switch (_isoLangs[code]) { - (String name, String nativeName) => ( - name: name, - nativeName: nativeName - ), - _ => null, - }; - - /// Get supported locales. - static List get supportedLocales => - const generated.AppLocalizationDelegate().supportedLocales; -} - -@immutable -final class _LocalizationView extends LocalizationsDelegate { - @literal - const _LocalizationView( - LocalizationsDelegate delegate, - ) : _delegate = delegate; - - final LocalizationsDelegate _delegate; - - @override - bool isSupported(Locale locale) => _delegate.isSupported(locale); - - @override - Future load(Locale locale) => - generated.GeneratedLocalization.load(locale).then( - (localization) => Localization._current = Localization._(locale)); - - @override - bool shouldReload(covariant _LocalizationView old) => - _delegate.shouldReload(old._delegate); -} - -const Map _isoLangs = - { - "ab": ('Abkhaz', 'аҧсуа'), - "aa": ('Afar', 'Afaraf'), - "af": ('Afrikaans', 'Afrikaans'), - "ak": ('Akan', 'Akan'), - "sq": ('Albanian', 'Shqip'), - "am": ('Amharic', 'አማርኛ'), - "ar": ('Arabic', 'العربية'), - "an": ('Aragonese', 'Aragonés'), - "hy": ('Armenian', 'Հայերեն'), - "as": ('Assamese', 'অসমীয়া'), - "av": ('Avaric', 'авар мацӀ, магӀарул мацӀ'), - "ae": ('Avestan', 'avesta'), - "ay": ('Aymara', 'aymar aru'), - "az": ('Azerbaijani', 'azərbaycan dili'), - "bm": ('Bambara', 'bamanankan'), - "ba": ('Bashkir', 'башҡорт теле'), - "eu": ('Basque', 'euskara, euskera'), - "be": ('Belarusian', 'Беларуская'), - "bn": ('Bengali', 'বাংলা'), - "bh": ('Bihari', 'भोजपुरी'), - "bi": ('Bislama', 'Bislama'), - "bs": ('Bosnian', 'bosanski jezik'), - "br": ('Breton', 'brezhoneg'), - "bg": ('Bulgarian', 'български език'), - "my": ('Burmese', 'ဗမာစာ'), - "ca": ('Catalan, Valencian', 'Català'), - "ch": ('Chamorro', 'Chamoru'), - "ce": ('Chechen', 'нохчийн мотт'), - "ny": ('Chichewa, Chewa, Nyanja', 'chiCheŵa, chinyanja'), - "zh": ('Chinese', '中文 (Zhōngwén), 汉语, 漢語'), - "cv": ('Chuvash', 'чӑваш чӗлхи'), - "kw": ('Cornish', 'Kernewek'), - "co": ('Corsican', 'corsu, lingua corsa'), - "cr": ('Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'), - "hr": ('Croatian', 'hrvatski'), - "cs": ('Czech', 'česky, čeština'), - "da": ('Danish', 'dansk'), - "dv": ('Divehi, Dhivehi, Maldivian;', 'ދިވެހި'), - "nl": ('Dutch', 'Nederlands, Vlaams'), - "en": ('English', 'English'), - "eo": ('Esperanto', 'Esperanto'), - "et": ('Estonian', 'eesti, eesti keel'), - "fo": ('Faroese', 'føroyskt'), - "fj": ('Fijian', 'vosa Vakaviti'), - "fi": ('Finnish', 'suomi, suomen kieli'), - "fr": ('French', 'Français'), - "ff": ('Fula, Fulah, Pulaar, Pular', 'Fulfulde, Pulaar, Pular'), - "gl": ('Galician', 'Galego'), - "ka": ('Georgian', 'ქართული'), - "de": ('German', 'Deutsch'), - "el": ('Greek, Modern', 'Ελληνικά'), - "gn": ('Guaraní', 'Avañeẽ'), - "gu": ('Gujarati', 'ગુજરાતી'), - "ht": ('Haitian, Haitian Creole', 'Kreyòl ayisyen'), - "ha": ('Hausa', 'Hausa, هَوُسَ'), - "he": ('Hebrew (modern)', 'עברית'), - "hz": ('Herero', 'Otjiherero'), - "hi": ('Hindi', 'हिन्दी, हिंदी'), - "ho": ('Hiri Motu', 'Hiri Motu'), - "hu": ('Hungarian', 'Magyar'), - "ia": ('Interlingua', 'Interlingua'), - "id": ('Indonesian', 'Bahasa Indonesia'), - "ie": ('Interlingue', 'Interlingue'), - "ga": ('Irish', 'Gaeilge'), - "ig": ('Igbo', 'Asụsụ Igbo'), - "ik": ('Inupiaq', 'Iñupiaq, Iñupiatun'), - "io": ('Ido', 'Ido'), - "is": ('Icelandic', 'Íslenska'), - "it": ('Italian', 'Italiano'), - "iu": ('Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'), - "ja": ('Japanese', '日本語 (にほんご/にっぽんご)'), - "jv": ('Javanese', 'basa Jawa'), - "kl": ('Kalaallisut, Greenlandic', 'kalaallisut, kalaallit oqaasii'), - "kn": ('Kannada', 'ಕನ್ನಡ'), - "kr": ('Kanuri', 'Kanuri'), - "kk": ('Kazakh', 'Қазақ тілі'), - "km": ('Khmer', 'ភាសាខ្មែរ'), - "ki": ('Kikuyu, Gikuyu', 'Gĩkũyũ'), - "rw": ('Kinyarwanda', 'Ikinyarwanda'), - "ky": ('Kirghiz, Kyrgyz', 'кыргыз тили'), - "kv": ('Komi', 'коми кыв'), - "kg": ('Kongo', 'KiKongo'), - "ko": ('Korean', '한국어 (韓國語), 조선말 (朝鮮語)'), - "kj": ('Kwanyama, Kuanyama', 'Kuanyama'), - "la": ('Latin', 'latine, lingua latina'), - "lb": ('Luxembourgish', 'Lëtzebuergesch'), - "lg": ('Luganda', 'Luganda'), - "li": ('Limburgish, Limburgan, Limburger', 'Limburgs'), - "ln": ('Lingala', 'Lingála'), - "lo": ('Lao', 'ພາສາລາວ'), - "lt": ('Lithuanian', 'lietuvių kalba'), - "lu": ('Luba-Katanga', ''), - "lv": ('Latvian', 'latviešu valoda'), - "gv": ('Manx', 'Gaelg, Gailck'), - "mk": ('Macedonian', 'македонски јазик'), - "mg": ('Malagasy', 'Malagasy fiteny'), - "ml": ('Malayalam', 'മലയാളം'), - "mt": ('Maltese', 'Malti'), - "mi": ('Māori', 'te reo Māori'), - "mr": ('Marathi (Marāṭhī)', 'मराठी'), - "mh": ('Marshallese', 'Kajin M̧ajeļ'), - "mn": ('Mongolian', 'монгол'), - "na": ('Nauru', 'Ekakairũ Naoero'), - "nb": ('Norwegian Bokmål', 'Norsk bokmål'), - "nd": ('North Ndebele', 'isiNdebele'), - "ne": ('Nepali', 'नेपाली'), - "ng": ('Ndonga', 'Owambo'), - "nn": ('Norwegian Nynorsk', 'Norsk nynorsk'), - "no": ('Norwegian', 'Norsk'), - "ii": ('Nuosu', 'ꆈꌠ꒿ Nuosuhxop'), - "nr": ('South Ndebele', 'isiNdebele'), - "oc": ('Occitan', 'Occitan'), - "oj": ('Ojibwe, Ojibwa', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'), - "om": ('Oromo', 'Afaan Oromoo'), - "or": ('Oriya', 'ଓଡ଼ିଆ'), - "pi": ('Pāli', 'पाऴि'), - "fa": ('Persian', 'فارسی'), - "pl": ('Polish', 'Polski'), - "ps": ('Pashto, Pushto', 'پښتو'), - "pt": ('Portuguese', 'Português'), - "qu": ('Quechua', 'Runa Simi, Kichwa'), - "rm": ('Romansh', 'rumantsch grischun'), - "rn": ('Kirundi', 'kiRundi'), - "ro": ('Romanian, Moldavian, Moldovan', 'română'), - "ru": ('Russian', 'Русский'), - "sa": ('Sanskrit (Saṁskṛta)', 'संस्कृतम्'), - "sc": ('Sardinian', 'sardu'), - "se": ('Northern Sami', 'Davvisámegiella'), - "sm": ('Samoan', 'gagana faa Samoa'), - "sg": ('Sango', 'yângâ tî sängö'), - "sr": ('Serbian', 'српски језик'), - "gd": ('Scottish Gaelic, Gaelic', 'Gàidhlig'), - "sn": ('Shona', 'chiShona'), - "si": ('Sinhala, Sinhalese', 'සිංහල'), - "sk": ('Slovak', 'slovenčina'), - "sl": ('Slovene', 'slovenščina'), - "so": ('Somali', 'Soomaaliga, af Soomaali'), - "st": ('Southern Sotho', 'Sesotho'), - "es": ('Spanish', 'Español'), - "su": ('Sundanese', 'Basa Sunda'), - "sw": ('Swahili', 'Kiswahili'), - "ss": ('Swati', 'SiSwati'), - "sv": ('Swedish', 'svenska'), - "ta": ('Tamil', 'தமிழ்'), - "te": ('Telugu', 'తెలుగు'), - "th": ('Thai', 'ไทย'), - "ti": ('Tigrinya', 'ትግርኛ'), - "bo": ('Tibetan', 'བོད་ཡིག'), - "tk": ('Turkmen', 'Türkmen, Түркмен'), - "tn": ('Tswana', 'Setswana'), - "to": ('Tonga (Tonga Islands)', 'faka Tonga'), - "tr": ('Turkish', 'Türkçe'), - "ts": ('Tsonga', 'Xitsonga'), - "tw": ('Twi', 'Twi'), - "ty": ('Tahitian', 'Reo Tahiti'), - "uk": ('Ukrainian', 'українська'), - "ur": ('Urdu', 'اردو'), - "ve": ('Venda', 'Tshivenḓa'), - "vi": ('Vietnamese', 'Tiếng Việt'), - "vo": ('Volapük', 'Volapük'), - "wa": ('Walloon', 'Walon'), - "cy": ('Welsh', 'Cymraeg'), - "wo": ('Wolof', 'Wollof'), - "fy": ('Western Frisian', 'Frysk'), - "xh": ('Xhosa', 'isiXhosa'), - "yi": ('Yiddish', 'ייִדיש'), - "yo": ('Yoruba', 'Yorùbá'), -}; diff --git a/example/lib/src/common/util/color_util.dart b/example/lib/src/common/util/color_util.dart deleted file mode 100644 index 5c16622..0000000 --- a/example/lib/src/common/util/color_util.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -sealed class ColorUtil { - /// Get list of colors with length [count]. - static List getColors(int count) { - final primariesLength = Colors.primaries.length; - if (count <= primariesLength) return Colors.primaries.take(count).toList(); - - final colors = List.filled(count, Colors.transparent); - final step = count / (primariesLength - 1); - - var index = 0; - for (var i = 0; i < primariesLength - 1; i++) { - for (var j = 0; j < step; j++) { - final color1 = Colors.primaries[i], color2 = Colors.primaries[i + 1]; - colors[index] = Color.lerp(color1, color2, j / step)!; - index++; - if (index == count) return colors; - } - } - - while (index < count) { - colors[index] = Colors.primaries.last; - index++; - } - - return colors; - } -} diff --git a/example/lib/src/common/util/date_util.dart b/example/lib/src/common/util/date_util.dart deleted file mode 100644 index cd9945e..0000000 --- a/example/lib/src/common/util/date_util.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:intl/intl.dart' as intl; - -sealed class DateUtil { - /// Format date - static String format( - DateTime? date, { - intl.DateFormat? format, - String fallback = '-', - }) { - if (date == null) return fallback; - if (format != null) return format.format(date); - final now = DateTime.now(); - final today = now.copyWith( - hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0); - if (date.isAfter(today)) return intl.DateFormat.Hms().format(date); - if (date.isAfter(today.subtract(const Duration(days: 7)))) { - return intl.DateFormat(intl.DateFormat.WEEKDAY).format(date); - } - return intl.DateFormat.yMd().format(date); - } -} - -extension DateUtilX on DateTime { - /// Format date - String format({intl.DateFormat? format}) => - DateUtil.format(this, format: format); -} diff --git a/example/lib/src/common/util/error_util.dart b/example/lib/src/common/util/error_util.dart deleted file mode 100644 index 38010d7..0000000 --- a/example/lib/src/common/util/error_util.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart' - show BuildContext, Colors, ScaffoldMessenger, SnackBar, Text; -import 'package:l/l.dart'; -import 'package:spinifyapp/src/common/localization/localization.dart'; -import 'package:spinifyapp/src/common/util/platform/error_util_vm.dart' - // ignore: uri_does_not_exist - if (dart.library.js_util) 'package:spinifyapp/src/common/util/platform/error_util_js.dart'; - -/// Error util. -sealed class ErrorUtil { - /// Log the error to the console and to Crashlytics. - static Future logError( - Object exception, - StackTrace stackTrace, { - String? hint, - bool fatal = false, - }) async { - try { - if (exception is String) { - return await logMessage( - exception, - stackTrace: stackTrace, - hint: hint, - warning: true, - ); - } - $captureException(exception, stackTrace, hint, fatal).ignore(); - l.e(exception, stackTrace); - } on Object catch (error, stackTrace) { - l.e( - 'Error while logging error "$error" inside ErrorUtil.logError', - stackTrace, - ); - } - } - - /// Logs a message to the console and to Crashlytics. - static Future logMessage( - String message, { - StackTrace? stackTrace, - String? hint, - bool warning = false, - }) async { - try { - l.e(message, stackTrace ?? StackTrace.current); - $captureMessage(message, stackTrace, hint, warning).ignore(); - } on Object catch (error, stackTrace) { - l.e( - 'Error while logging error "$error" inside ErrorUtil.logMessage', - stackTrace, - ); - } - } - - /// Rethrows the error with the stack trace. - static Never throwWithStackTrace(Object error, StackTrace stackTrace) => - Error.throwWithStackTrace(error, stackTrace); - - @pragma('vm:prefer-inline') - static String _localizedError( - String fallback, String Function(Localization l) localize) { - try { - return localize(Localization.current); - } on Object { - return fallback; - } - } - - // Also we can add current localization to this method - static String formatMessage( - Object error, [ - String fallback = 'An error has occurred', - ]) => - switch (error) { - String e => e, - FormatException _ => - _localizedError('Invalid format', (lcl) => lcl.errInvalidFormat), - TimeoutException _ => - _localizedError('Timeout exceeded', (lcl) => lcl.errTimeOutExceeded), - UnimplementedError _ => _localizedError( - 'Not implemented yet', (lcl) => lcl.errNotImplementedYet), - UnsupportedError _ => _localizedError( - 'Unsupported operation', (lcl) => lcl.errUnsupportedOperation), - FileSystemException _ => _localizedError( - 'File system error', (lcl) => lcl.errFileSystemException), - AssertionError _ => - _localizedError('Assertion error', (lcl) => lcl.errAssertionError), - Error _ => _localizedError( - 'An error has occurred', (lcl) => lcl.errAnErrorHasOccurred), - Exception _ => _localizedError('An exception has occurred', - (lcl) => lcl.errAnExceptionHasOccurred), - _ => fallback, - }; - - /// Shows a error snackbar with the given message. - static void showSnackBar(BuildContext context, Object message) => - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(formatMessage(message)), - backgroundColor: Colors.red, - ), - ); -} diff --git a/example/lib/src/common/util/log_buffer.dart b/example/lib/src/common/util/log_buffer.dart deleted file mode 100644 index 94c0730..0000000 --- a/example/lib/src/common/util/log_buffer.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:collection' show Queue; - -import 'package:flutter/foundation.dart' show ChangeNotifier; -import 'package:l/l.dart'; - -/// LogBuffer Singleton class -class LogBuffer with ChangeNotifier { - static final LogBuffer _internalSingleton = LogBuffer._internal(); - static LogBuffer get instance => _internalSingleton; - LogBuffer._internal(); - - static const int bufferLimit = 10000; - final Queue _queue = Queue(); - - /// Get the logs - Iterable get logs => _queue; - - /// Clear the logs - void clear() { - _queue.clear(); - notifyListeners(); - } - - /// Add a log to the buffer - void add(LogMessage log) { - if (_queue.length >= bufferLimit) _queue.removeFirst(); - _queue.add(log); - notifyListeners(); - } - - /// Add a list of logs to the buffer - void addAll(List logs) { - logs = logs.take(bufferLimit).toList(); - if (_queue.length + logs.length >= bufferLimit) { - final toRemove = _queue.length + logs.length - bufferLimit; - for (var i = 0; i < toRemove; i++) _queue.removeFirst(); - } - _queue.addAll(logs); - notifyListeners(); - } - - @override - void dispose() { - _queue.clear(); - super.dispose(); - } -} diff --git a/example/lib/src/common/util/logger_util.dart b/example/lib/src/common/util/logger_util.dart deleted file mode 100644 index aa7dbdd..0000000 --- a/example/lib/src/common/util/logger_util.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:l/l.dart'; - -sealed class LoggerUtil { - /// Formats the log message. - static Object messageFormatting( - Object message, LogLevel logLevel, DateTime now) => - '${timeFormat(now)} | $message'; - - /// Formats the time. - static String timeFormat(DateTime time) => - '${time.hour}:${time.minute.toString().padLeft(2, '0')}'; -} diff --git a/example/lib/src/common/util/platform/error_util_js.dart b/example/lib/src/common/util/platform/error_util_js.dart deleted file mode 100644 index 6e966ea..0000000 --- a/example/lib/src/common/util/platform/error_util_js.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: avoid_positional_boolean_parameters - -Future $captureException( - Object exception, - StackTrace stackTrace, - String? hint, - bool fatal, -) => - Future.value(null); - -Future $captureMessage( - String message, - StackTrace? stackTrace, - String? hint, - bool warning, -) => - Future.value(null); diff --git a/example/lib/src/common/util/platform/error_util_vm.dart b/example/lib/src/common/util/platform/error_util_vm.dart deleted file mode 100644 index a3a83bf..0000000 --- a/example/lib/src/common/util/platform/error_util_vm.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: avoid_positional_boolean_parameters -//import 'package:firebase_crashlytics/firebase_crashlytics.dart'; - -/* - * Sentry.captureException(exception, stackTrace: stackTrace, hint: hint); - * FirebaseCrashlytics.instance - * .recordError(exception, stackTrace ?? StackTrace.current, reason: hint, fatal: fatal); - * */ -Future $captureException( - Object exception, - StackTrace stackTrace, - String? hint, - bool fatal, -) => - Future.value(); -// FirebaseCrashlytics.instance.recordError(exception, stackTrace, reason: hint, fatal: fatal); - -/* - * Sentry.captureMessage( - * message, - * level: warning ? SentryLevel.warning : SentryLevel.info, - * hint: hint, - * params: [ - * ...?params, - * if (stackTrace != null) 'StackTrace: $stackTrace', - * ], - * ); - * (warning || stackTrace != null) - * ? FirebaseCrashlytics.instance.recordError(message, stackTrace ?? StackTrace.current); - * : FirebaseCrashlytics.instance.log('$message${hint != null ? '\r\n$hint' : ''}'); - * */ -Future $captureMessage( - String message, - StackTrace? stackTrace, - String? hint, - bool warning, -) => - Future.value(); -/* warning || stackTrace != null - ? FirebaseCrashlytics.instance.recordError( - message, - stackTrace ?? StackTrace.current, - reason: hint, - fatal: false, - ) - : FirebaseCrashlytics.instance.log('$message' - '${stackTrace != null ? '\nHint: $hint' : ''}'); */ diff --git a/example/lib/src/common/util/screen_util.dart b/example/lib/src/common/util/screen_util.dart deleted file mode 100644 index 0bc1659..0000000 --- a/example/lib/src/common/util/screen_util.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; - -/// {@macro screen_util} -extension ScreenUtilExtension on BuildContext { - /// Get current screen logical size representation - /// - /// phone | <= 600 dp | 4 column - /// tablet | 600..1023 dp | 8 column - /// desktop | >= 1024 dp | 12 column - ScreenSize get screenSize => ScreenUtil.screenSizeOf(this); - - /// Portrait or Landscape - Orientation get orientation => ScreenUtil.orientationOf(this); - - /// Evaluate the result of the first matching callback. - /// - /// phone | <= 600 dp | 4 column - /// tablet | 600..1023 dp | 8 column - /// desktop | >= 1024 dp | 12 column - ScreenSizeWhenResult screenSizeWhen({ - required final ScreenSizeWhenResult Function() phone, - required final ScreenSizeWhenResult Function() tablet, - required final ScreenSizeWhenResult Function() desktop, - }) => - ScreenUtil.screenSizeOf(this).when( - phone: phone, - tablet: tablet, - desktop: desktop, - ); - - /// The [screenSizeMaybeWhen] method is equivalent to [screenSizeWhen], - /// but doesn't require all callbacks to be specified. - /// - /// On the other hand, it adds an extra [orElse] required parameter, - /// for fallback behavior. - ScreenSizeWhenResult - screenSizeMaybeWhen({ - required final ScreenSizeWhenResult Function() orElse, - final ScreenSizeWhenResult Function()? phone, - final ScreenSizeWhenResult Function()? tablet, - final ScreenSizeWhenResult Function()? desktop, - }) => - ScreenUtil.screenSizeOf(this).maybeWhen( - phone: phone, - tablet: tablet, - desktop: desktop, - orElse: orElse, - ); -} - -/// {@template screen_util} -/// Screen logical size representation -/// -/// phone | <= 600 dp | 4 column -/// tablet | 600..1023 dp | 8 column -/// desktop | >= 1024 dp | 12 column -/// {@endtemplate} -sealed class ScreenUtil { - /// {@macro screen_util} - static ScreenSize screenSize() { - final view = ui.PlatformDispatcher.instance.implicitView; - if (view == null) return ScreenSize.phone; - final size = view.physicalSize ~/ view.devicePixelRatio; - return _screenSizeFromSize(size); - } - - static ScreenSize from(Size size) => _screenSizeFromSize(size); - - /// {@macro screen_util} - static ScreenSize screenSizeOf(final BuildContext context) { - final size = MediaQuery.of(context).size; - return _screenSizeFromSize(size); - } - - static ScreenSize _screenSizeFromSize(final Size size) => - switch (size.width) { - >= 1024 => ScreenSize.desktop, - <= 600 => ScreenSize.phone, - _ => ScreenSize.tablet, - }; - - /// Portrait or Landscape - static Orientation orientation() { - final view = ui.PlatformDispatcher.instance.implicitView; - final size = view?.physicalSize; - return size == null || size.height > size.width - ? Orientation.portrait - : Orientation.landscape; - } - - /// Portrait or Landscape - static Orientation orientationOf(BuildContext context) => - MediaQuery.of(context).orientation; -} - -/// {@macro screen_util} -@immutable -sealed class ScreenSize { - /// {@macro screen_util} - @literal - const ScreenSize._(this.representation, this.min, this.max); - - /// Phone - static const ScreenSize phone = ScreenSize$Phone(); - - /// Tablet - static const ScreenSize tablet = ScreenSize$Tablet(); - - /// Large desktop - static const ScreenSize desktop = ScreenSize$Desktop(); - - /// Minimum width in logical pixels - final double min; - - /// Maximum width in logical pixels - final double max; - - /// String representation - final String representation; - - /// Is phone - abstract final bool isPhone; - - /// Is tablet - abstract final bool isTablet; - - /// Is desktop - abstract final bool isDesktop; - - /// Evaluate the result of the first matching callback. - /// - /// phone | <= 600 dp | 4 column - /// tablet | 600..1023 dp | 8 column - /// desktop | >= 1024 dp | 12 column - ScreenSizeWhenResult when({ - required final ScreenSizeWhenResult Function() phone, - required final ScreenSizeWhenResult Function() tablet, - required final ScreenSizeWhenResult Function() desktop, - }); - - /// The [maybeWhen] method is equivalent to [when], - /// but doesn't require all callbacks to be specified. - /// - /// On the other hand, it adds an extra [orElse] required parameter, - /// for fallback behavior. - ScreenSizeWhenResult maybeWhen({ - required final ScreenSizeWhenResult Function() orElse, - final ScreenSizeWhenResult Function()? phone, - final ScreenSizeWhenResult Function()? tablet, - final ScreenSizeWhenResult Function()? desktop, - }) => - when( - phone: phone ?? orElse, - tablet: tablet ?? orElse, - desktop: desktop ?? orElse, - ); - - @override - String toString() => representation; -} - -/// {@macro screen_util} -final class ScreenSize$Phone extends ScreenSize { - /// {@macro screen_util} - @literal - const ScreenSize$Phone() : super._('Phone', 0, 599); - - @override - ScreenSizeWhenResult when({ - required final ScreenSizeWhenResult Function() phone, - required final ScreenSizeWhenResult Function() tablet, - required final ScreenSizeWhenResult Function() desktop, - }) => - phone(); - - @override - final bool isPhone = true; - - @override - final bool isTablet = false; - - @override - final bool isDesktop = false; - - @override - int get hashCode => 0; - - @override - bool operator ==(final Object other) => - identical(other, this) || other is ScreenSize$Phone; -} - -/// {@macro screen_util} -final class ScreenSize$Tablet extends ScreenSize { - /// {@macro screen_util} - @literal - const ScreenSize$Tablet() : super._('Tablet', 600, 1023); - - @override - ScreenSizeWhenResult when({ - required final ScreenSizeWhenResult Function() phone, - required final ScreenSizeWhenResult Function() tablet, - required final ScreenSizeWhenResult Function() desktop, - }) => - tablet(); - - @override - final bool isPhone = false; - - @override - final bool isTablet = true; - - @override - final bool isDesktop = false; - - @override - int get hashCode => 1; - - @override - bool operator ==(final Object other) => - identical(other, this) || other is ScreenSize$Tablet; -} - -/// {@macro screen_util} -final class ScreenSize$Desktop extends ScreenSize { - /// {@macro screen_util} - @literal - const ScreenSize$Desktop() : super._('Desktop', 1024, double.infinity); - - @override - ScreenSizeWhenResult when({ - required final ScreenSizeWhenResult Function() phone, - required final ScreenSizeWhenResult Function() tablet, - required final ScreenSizeWhenResult Function() desktop, - }) => - desktop(); - - @override - final bool isPhone = false; - - @override - final bool isTablet = false; - - @override - final bool isDesktop = true; - - @override - int get hashCode => 2; - - @override - bool operator ==(final Object other) => - identical(other, this) || other is ScreenSize$Desktop; -} diff --git a/example/lib/src/common/util/timeouts.dart b/example/lib/src/common/util/timeouts.dart deleted file mode 100644 index 94de7f7..0000000 --- a/example/lib/src/common/util/timeouts.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -/// Extension methods for [Future]. -extension TimeoutsExtension on Future { - /// Returns a [Future] that completes with this future's result, or with the - /// result of calling the [onTimeout] function, if this future doesn't - /// complete before the timeout is exceeded. - /// - /// The [onTimeout] function must return a [Future] which will be used as the - /// result of the returned [Future], and must not throw. - Future logicTimeout({ - double coefficient = 1, - FutureOr Function()? onTimeout, - }) => - timeout( - const Duration(milliseconds: 20000) * coefficient, - onTimeout: onTimeout, - ); -} diff --git a/example/lib/src/common/widget/app.dart b/example/lib/src/common/widget/app.dart deleted file mode 100644 index 59046b9..0000000 --- a/example/lib/src/common/widget/app.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:platform_info/platform_info.dart'; -import 'package:spinifyapp/src/common/localization/localization.dart'; -import 'package:spinifyapp/src/common/widget/window_scope.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/sign_in_form.dart'; -import 'package:spinifyapp/src/feature/chat/widget/chat_screen.dart'; - -/// {@template app} -/// App widget. -/// {@endtemplate} -class App extends StatelessWidget { - /// {@macro app} - const App({super.key}); - - @override - Widget build(BuildContext context) => MaterialApp( - title: 'Spinify', - debugShowCheckedModeBanner: false, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - Localization.delegate, - ], - theme: View.of(context).platformDispatcher.platformBrightness == - Brightness.dark - ? ThemeData.dark(useMaterial3: true) - : ThemeData.light(useMaterial3: true), - /* themeMode: ThemeMode.system, */ - home: const AuthenticationScope( - signInForm: SignInForm(), - child: ChatScreen(), - ), - supportedLocales: Localization.supportedLocales, - locale: Localization.supportedLocales - .firstWhereOrNull((e) => e.languageCode == platform.locale) ?? - const Locale('en', 'US'), - builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith( - /* textScaler: TextScaler.noScaling, */ - textScaler: const TextScaler.linear(1), - ), - child: WindowScope( - /* title: Localization.of(context).title, */ - child: child ?? const SizedBox.shrink(), - ), - ), - ); -} diff --git a/example/lib/src/common/widget/radial_progress_indicator.dart b/example/lib/src/common/widget/radial_progress_indicator.dart deleted file mode 100644 index 44d4fb1..0000000 --- a/example/lib/src/common/widget/radial_progress_indicator.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/material.dart'; - -/// {@template radial_progress_indicator} -/// RadialProgressIndicator widget -/// {@endtemplate} -class RadialProgressIndicator extends StatefulWidget { - /// {@macro radial_progress_indicator} - const RadialProgressIndicator({ - this.size = 64, - this.child, - super.key, - }); - - /// The size of the progress indicator - final double size; - - /// The child widget - final Widget? child; - - @override - State createState() => - _RadialProgressIndicatorState(); -} - -class _RadialProgressIndicatorState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _sweepController; - late final Animation _curvedAnimation; - - @override - void initState() { - super.initState(); - _sweepController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1000), - )..repeat(); - _curvedAnimation = CurvedAnimation( - parent: _sweepController, - curve: Curves.ease, - ); - } - - @override - void dispose() { - _sweepController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => Center( - child: SizedBox.square( - dimension: widget.size, - child: RepaintBoundary( - child: CustomPaint( - painter: _RadialProgressIndicatorPainter( - animation: _curvedAnimation, - color: Theme.of(context).indicatorColor, - ), - child: Center( - child: widget.child, - ), - ), - ), - ), - ); -} - -class _RadialProgressIndicatorPainter extends CustomPainter { - _RadialProgressIndicatorPainter({ - required Animation animation, - Color color = Colors.blue, - }) : _animation = animation, - _arcPaint = Paint() - ..strokeCap = StrokeCap.round - ..style = PaintingStyle.stroke - ..color = color, - super(repaint: animation); - - final Animation _animation; - final Paint _arcPaint; - - @override - void paint(Canvas canvas, Size size) { - _arcPaint.strokeWidth = size.shortestSide / 8; - final progress = _animation.value; - final rect = Rect.fromCircle( - center: size.center(Offset.zero), - radius: size.shortestSide / 2 - _arcPaint.strokeWidth / 2, - ); - final rotate = math.pow(progress, 2) * math.pi * 2; - final sweep = math.sin(progress * math.pi) * 3 + math.pi * .25; - - canvas.drawArc(rect, rotate, sweep, false, _arcPaint); - } - - @override - bool shouldRepaint(covariant _RadialProgressIndicatorPainter oldDelegate) => - _animation.value != oldDelegate._animation.value; - - @override - bool shouldRebuildSemantics( - covariant _RadialProgressIndicatorPainter oldDelegate) => - false; -} diff --git a/example/lib/src/common/widget/window_scope.dart b/example/lib/src/common/widget/window_scope.dart deleted file mode 100644 index 9461b8e..0000000 --- a/example/lib/src/common/widget/window_scope.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'dart:io' as io; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:window_manager/window_manager.dart'; - -/// {@template window_scope} -/// WindowScope widget. -/// {@endtemplate} -class WindowScope extends StatefulWidget { - /// {@macro window_scope} - const WindowScope({ - required this.child, - this.title, - super.key, - }); - - /// Title of the window. - final String? title; - - /// The widget below this widget in the tree. - final Widget child; - - @override - State createState() => _WindowScopeState(); -} - -class _WindowScopeState extends State { - @override - Widget build(BuildContext context) => - kIsWeb || io.Platform.isAndroid || io.Platform.isIOS - ? widget.child - : Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const _WindowTitle(), - Expanded( - child: widget.child, - ), - ], - ); -} - -class _WindowTitle extends StatefulWidget { - const _WindowTitle(); - - @override - State<_WindowTitle> createState() => _WindowTitleState(); -} - -class _WindowTitleState extends State<_WindowTitle> with WindowListener { - final ValueNotifier _isFullScreen = ValueNotifier(false); - final ValueNotifier _isAlwaysOnTop = ValueNotifier(false); - - @override - void initState() { - windowManager.addListener(this); - super.initState(); - } - - @override - void dispose() { - windowManager.removeListener(this); - super.dispose(); - } - - @override - void onWindowEnterFullScreen() { - super.onWindowEnterFullScreen(); - _isFullScreen.value = true; - } - - @override - void onWindowLeaveFullScreen() { - super.onWindowLeaveFullScreen(); - _isFullScreen.value = false; - } - - @override - void onWindowFocus() { - // Make sure to call once. - setState(() {}); - // do something - } - - void setAlwaysOnTop(bool value) { - Future(() async { - await windowManager.setAlwaysOnTop(value); - _isAlwaysOnTop.value = await windowManager.isAlwaysOnTop(); - }).ignore(); - } - - @override - Widget build(BuildContext context) { - final title = context.findAncestorWidgetOfExactType()?.title; - return SizedBox( - height: 24, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanStart: (details) => windowManager.startDragging(), - onDoubleTap: null, - /* () async { - bool isMaximized = await windowManager.isMaximized(); - if (!isMaximized) { - windowManager.maximize(); - } else { - windowManager.unmaximize(); - } - }, */ - child: Material( - color: Theme.of(context).primaryColor, - child: Stack( - alignment: Alignment.center, - children: [ - if (title != null) - Builder( - builder: (context) { - final size = MediaQuery.of(context).size; - return AnimatedPositioned( - duration: const Duration(milliseconds: 350), - left: size.width < 800 ? 8 : 78, - right: 78, - top: 0, - bottom: 0, - child: Center( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (child, animation) => - FadeTransition( - opacity: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ), - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(height: 1), - ), - ), - ), - ); - }, - ), - _WindowButtons$Windows( - isFullScreen: _isFullScreen, - isAlwaysOnTop: _isAlwaysOnTop, - setAlwaysOnTop: setAlwaysOnTop, - ), - ], - ), - ), - ), - ); - } -} - -class _WindowButtons$Windows extends StatelessWidget { - const _WindowButtons$Windows({ - required ValueListenable isFullScreen, - required ValueListenable isAlwaysOnTop, - required this.setAlwaysOnTop, - }) : _isFullScreen = isFullScreen, - _isAlwaysOnTop = isAlwaysOnTop; - - // ignore: unused_field - final ValueListenable _isFullScreen; - final ValueListenable _isAlwaysOnTop; - - final ValueChanged setAlwaysOnTop; - - @override - Widget build(BuildContext context) => Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Is always on top - ValueListenableBuilder( - valueListenable: _isAlwaysOnTop, - builder: (context, isAlwaysOnTop, _) => _WindowButton( - onPressed: () => setAlwaysOnTop(!isAlwaysOnTop), - icon: isAlwaysOnTop ? Icons.push_pin : Icons.push_pin_outlined, - ), - ), - - // Minimize - _WindowButton( - onPressed: () => windowManager.minimize(), - icon: Icons.minimize, - ), - - /* ValueListenableBuilder( - valueListenable: _isFullScreen, - builder: (context, isFullScreen, _) => _WindowButton( - onPressed: () => windowManager.setFullScreen(!isFullScreen), - icon: isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, - )), */ - - // Close - _WindowButton( - onPressed: () => windowManager.close(), - icon: Icons.close, - ), - const SizedBox(width: 4), - ], - ), - ); -} - -class _WindowButton extends StatelessWidget { - const _WindowButton({ - required this.onPressed, - required this.icon, - }); - - final VoidCallback onPressed; - final IconData icon; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: IconButton( - onPressed: onPressed, - icon: Icon(icon), - iconSize: 16, - alignment: Alignment.center, - padding: EdgeInsets.zero, - splashRadius: 12, - constraints: const BoxConstraints.tightFor(width: 24, height: 24), - ), - ); -} diff --git a/example/lib/src/feature/authentication/controller/authentication_controller.dart b/example/lib/src/feature/authentication/controller/authentication_controller.dart deleted file mode 100644 index c0c8d29..0000000 --- a/example/lib/src/feature/authentication/controller/authentication_controller.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; - -import 'package:spinifyapp/src/common/controller/droppable_controller_concurrency.dart'; -import 'package:spinifyapp/src/common/controller/state_controller.dart'; -import 'package:spinifyapp/src/common/util/error_util.dart'; -import 'package:spinifyapp/src/feature/authentication/controller/authentication_state.dart'; -import 'package:spinifyapp/src/feature/authentication/data/authentication_repository.dart'; -import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; - -final class AuthenticationController - extends StateController - with DroppableControllerConcurrency { - AuthenticationController( - {required IAuthenticationRepository repository, - super.initialState = - const AuthenticationState.idle(user: User.unauthenticated())}) - : _repository = repository { - _userSubscription = repository - .userChanges() - .map((u) => AuthenticationState.idle(user: u)) - .where((newState) => - state.isProcessing || !identical(newState.user, state.user)) - .listen(setState); - } - - final IAuthenticationRepository _repository; - StreamSubscription? _userSubscription; - - void signIn(SignInData data) => handle( - () async { - setState( - AuthenticationState.processing( - user: state.user, - message: 'Logging in...', - ), - ); - await _repository.signIn(data); - }, - (error, _) => setState( - AuthenticationState.idle( - user: state.user, - error: ErrorUtil.formatMessage(error), - ), - ), - () => setState( - AuthenticationState.idle(user: state.user), - ), - ); - - void signOut() => handle( - () async { - setState( - AuthenticationState.processing( - user: state.user, - message: 'Logging out...', - ), - ); - await _repository.signOut(); - }, - (error, _) => setState( - AuthenticationState.idle( - user: state.user, - error: ErrorUtil.formatMessage(error), - ), - ), - () => setState( - const AuthenticationState.idle( - user: User.unauthenticated(), - ), - ), - ); - - @override - void dispose() { - _userSubscription?.cancel(); - super.dispose(); - } -} diff --git a/example/lib/src/feature/authentication/controller/authentication_state.dart b/example/lib/src/feature/authentication/controller/authentication_state.dart deleted file mode 100644 index 4ffa238..0000000 --- a/example/lib/src/feature/authentication/controller/authentication_state.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; - -/// {@template authentication_state} -/// AuthenticationState. -/// {@endtemplate} -sealed class AuthenticationState extends _$AuthenticationStateBase { - /// Idling state - /// {@macro authentication_state} - const factory AuthenticationState.idle({ - required User user, - String message, - String? error, - }) = AuthenticationState$Idle; - - /// Processing - /// {@macro authentication_state} - const factory AuthenticationState.processing({ - required User user, - String message, - }) = AuthenticationState$Processing; - - /// {@macro authentication_state} - const AuthenticationState({required super.user, required super.message}); -} - -/// Idling state -final class AuthenticationState$Idle extends AuthenticationState - with _$AuthenticationState { - const AuthenticationState$Idle( - {required super.user, super.message = 'Idling', this.error}); - - @override - final String? error; -} - -/// Processing -final class AuthenticationState$Processing extends AuthenticationState - with _$AuthenticationState { - const AuthenticationState$Processing( - {required super.user, super.message = 'Processing'}); - - @override - String? get error => null; -} - -base mixin _$AuthenticationState on AuthenticationState {} - -/// Pattern matching for [AuthenticationState]. -typedef AuthenticationStateMatch = R Function( - S state); - -@immutable -abstract base class _$AuthenticationStateBase { - const _$AuthenticationStateBase({required this.user, required this.message}); - - /// Data entity payload. - @nonVirtual - final User user; - - /// Message or state description. - @nonVirtual - final String message; - - /// Error message. - abstract final String? error; - - /// If an error has occurred? - bool get hasError => error != null; - - /// Is in progress state? - bool get isProcessing => - maybeMap(orElse: () => false, processing: (_) => true); - - /// Is in idle state? - bool get isIdling => !isProcessing; - - /// Pattern matching for [AuthenticationState]. - R map({ - required AuthenticationStateMatch idle, - required AuthenticationStateMatch - processing, - }) => - switch (this) { - AuthenticationState$Idle s => idle(s), - AuthenticationState$Processing s => processing(s), - _ => throw AssertionError(), - }; - - /// Pattern matching for [AuthenticationState]. - R maybeMap({ - AuthenticationStateMatch? idle, - AuthenticationStateMatch? processing, - required R Function() orElse, - }) => - map( - idle: idle ?? (_) => orElse(), - processing: processing ?? (_) => orElse(), - ); - - /// Pattern matching for [AuthenticationState]. - R? mapOrNull({ - AuthenticationStateMatch? idle, - AuthenticationStateMatch? processing, - }) => - map( - idle: idle ?? (_) => null, - processing: processing ?? (_) => null, - ); - - @override - int get hashCode => user.hashCode; - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() { - final buffer = StringBuffer() - ..write('AuthenticationState(') - ..write('user: $user, '); - if (error != null) buffer.write('error: $error, '); - buffer - ..write('message: $message') - ..write(')'); - return buffer.toString(); - } -} diff --git a/example/lib/src/feature/authentication/data/authentication_repository.dart b/example/lib/src/feature/authentication/data/authentication_repository.dart deleted file mode 100644 index cf99cca..0000000 --- a/example/lib/src/feature/authentication/data/authentication_repository.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; -import 'package:spinify/spinify.dart'; -import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; - -abstract interface class IAuthenticationRepository { - Stream userChanges(); - FutureOr getUser(); - FutureOr getToken(); - Future signIn(SignInData data); - Future signOut(); -} - -class AuthenticationRepositoryImpl implements IAuthenticationRepository { - AuthenticationRepositoryImpl(); - - final StreamController _userController = - StreamController.broadcast(); - User _user = const User.unauthenticated(); - - @override - FutureOr getUser() => _user; - - @override - Future getToken() async { - switch (_user) { - case AuthenticatedUser user: - final AuthenticatedUser( - :String username, - :String token, - :String channel - ) = user; - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - SpinifyJWT jwt = SpinifyJWT( - sub: username, - exp: now + (24 * 60 * 60), - iat: now, - info: { - 'username': username, - }, - channels: [channel], - ); - return jwt.encode(token); - case UnauthenticatedUser _: - throw Exception('User is not authenticated'); - } - } - - @override - Stream userChanges() => _userController.stream; - - @override - Future signIn(SignInData data) { - String buildChannelName(String channel, [String? secret]) => - switch (secret) { - null || '' => channel, - String secret => '$channel' - '#' - '${hex.encode(utf8.encoder.fuse(sha256).convert(secret).bytes)}', - }; - return Future.sync( - () => _userController.add( - _user = User.authenticated( - username: data.username, - endpoint: data.endpoint, - token: data.token, - channel: buildChannelName(data.channel, data.secret), - secret: switch (data.secret) { - null || '' => null, - String secret => secret, - }), - ), - ); - } - - @override - Future signOut() => Future.sync( - () => _userController.add( - _user = const User.unauthenticated(), - ), - ); -} diff --git a/example/lib/src/feature/authentication/model/sign_in_data.dart b/example/lib/src/feature/authentication/model/sign_in_data.dart deleted file mode 100644 index 77a8a9a..0000000 --- a/example/lib/src/feature/authentication/model/sign_in_data.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; - -@immutable -final class SignInData { - SignInData({ - required this.endpoint, - required this.token, - required this.username, - required this.channel, - String? secret, - }) : secret = secret == null || secret.isEmpty ? null : secret; - - /// Centrifuge endpoint - final String endpoint; - - /// Centrifuge HMAC token for JWT authentication. - /// **BEWARE**: You should not store the token in the real app! - final String token; - - /// Centrifuge username. - final String username; - - /// Centrifuge channel. - final String channel; - - /// Centrifuge secret (optional) - final String? secret; - - static final RegExp _urlValidator = RegExp( - r'^(https?|ws|wss):\/\/(localhost|((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3})))?(:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$', - caseSensitive: false, - multiLine: false, - ); - String? isValidEndpoint() { - if (endpoint.isEmpty) return 'Endpoint is required'; - if (endpoint.length < 6) return 'Endpoint is too short'; - if (endpoint.length > 1024) return 'Endpoint is too long'; - if (!_urlValidator.hasMatch(endpoint)) return 'Endpoint is invalid'; - return null; - } - - String? isValidToken() { - if (token.isEmpty) return 'Token is required'; - if (token.length < 6) return 'Token is too short'; - if (token.length > 64) return 'Token is too long'; - return null; - } - - static final RegExp _usernameValidator = RegExp( - r'\@|[A-Z]|[a-z]|[0-9]|\.|\-|\_|\+', - caseSensitive: false, - multiLine: false, - ); - String? isValidUsername() { - if (username.isEmpty) return 'Username is required'; - if (username.length < 4) return 'Username is too short'; - if (username.length > 64) return 'Username is too long'; - if (!_usernameValidator.hasMatch(username)) return 'Username is invalid'; - return null; - } - - static final RegExp _channelValidator = RegExp( - r'^[a-zA-Z0-9_-]+$', - caseSensitive: false, - multiLine: false, - ); - String? isValidChannel() { - if (channel.isEmpty) return 'Channel is required'; - if (channel.length < 4) return 'Channel is too short'; - if (channel.length > 64) return 'Channel is too long'; - if (!_channelValidator.hasMatch(channel)) return 'Channel is invalid'; - return null; - } - - String? isValidSecret() { - final secret = this.secret; - if (secret == null || secret.isEmpty) return null; - if (secret.length < 4) return 'Secret is too short'; - if (secret.length > 64) return 'Secret is too long'; - return null; - } -} diff --git a/example/lib/src/feature/authentication/model/user.dart b/example/lib/src/feature/authentication/model/user.dart deleted file mode 100644 index 7c2d0f4..0000000 --- a/example/lib/src/feature/authentication/model/user.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:meta/meta.dart'; - -/// User username type. -typedef Username = String; - -/// {@template user} -/// The user entry model. -/// {@endtemplate} -sealed class User with _UserPatternMatching, _UserShortcuts { - /// {@macro user} - const User._(); - - /// {@macro user} - @literal - const factory User.unauthenticated() = UnauthenticatedUser; - - /// {@macro user} - const factory User.authenticated({ - required Username username, - required String endpoint, - required String token, - required String channel, - String? secret, - }) = AuthenticatedUser; - - /// The user's username. - abstract final Username? username; -} - -/// {@macro user} -/// -/// Unauthenticated user. -class UnauthenticatedUser extends User { - /// {@macro user} - const UnauthenticatedUser() : super._(); - - @override - Username? get username => null; - - @override - @nonVirtual - bool get isAuthenticated => false; - - @override - T map({ - required T Function(UnauthenticatedUser user) unauthenticated, - required T Function(AuthenticatedUser user) authenticated, - }) => - unauthenticated(this); - - @override - int get hashCode => -2; - - @override - bool operator ==(Object other) => - identical(this, other) || other is UnauthenticatedUser; - - @override - String toString() => 'UnauthenticatedUser()'; -} - -final class AuthenticatedUser extends User { - const AuthenticatedUser({ - required this.username, - required this.endpoint, - required this.token, - required this.channel, - this.secret, - }) : super._(); - - factory AuthenticatedUser.fromJson(Map json) { - if (json.isEmpty) throw FormatException('Json is empty', json); - if (json - case { - 'username': Username username, - 'endpoint': String endpoint, - 'token': String token, - 'channel': String channel, - 'secret': String? secret, - }) - return AuthenticatedUser( - username: username, - endpoint: endpoint, - token: token, - channel: channel, - secret: secret, - ); - throw FormatException('Invalid json format', json); - } - - @override - @nonVirtual - final Username username; - - /// Centrifuge endpoint - final String endpoint; - - /// Centrifuge HMAC token for JWT authentication. - /// **BEWARE**: You should not store the token in the real app! - final String token; - - /// Centrifuge channel. - final String channel; - - /// Centrifuge secret (optional) - final String? secret; - - @override - @nonVirtual - bool get isAuthenticated => true; - - @override - T map({ - required T Function(UnauthenticatedUser user) unauthenticated, - required T Function(AuthenticatedUser user) authenticated, - }) => - authenticated(this); - - Map toJson() => { - 'username': username, - }; - - @override - int get hashCode => username.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AuthenticatedUser && - username == other.username && - endpoint == other.endpoint && - token == other.token && - channel == other.channel && - secret == other.secret; - - @override - String toString() => 'AuthenticatedUser(username: $username)'; -} - -mixin _UserPatternMatching { - /// Pattern matching on [User] subclasses. - T map({ - required T Function(UnauthenticatedUser user) unauthenticated, - required T Function(AuthenticatedUser user) authenticated, - }); - - /// Pattern matching on [User] subclasses. - T maybeMap({ - required T Function() orElse, - T Function(UnauthenticatedUser user)? unauthenticated, - T Function(AuthenticatedUser user)? authenticated, - }) => - map( - unauthenticated: (user) => unauthenticated?.call(user) ?? orElse(), - authenticated: (user) => authenticated?.call(user) ?? orElse(), - ); - - /// Pattern matching on [User] subclasses. - T? mapOrNull({ - T Function(UnauthenticatedUser user)? unauthenticated, - T Function(AuthenticatedUser user)? authenticated, - }) => - map( - unauthenticated: (user) => unauthenticated?.call(user), - authenticated: (user) => authenticated?.call(user), - ); -} - -mixin _UserShortcuts on _UserPatternMatching { - /// User is authenticated. - bool get isAuthenticated; - - /// User is not authenticated. - bool get isNotAuthenticated => !isAuthenticated; -} diff --git a/example/lib/src/feature/authentication/widget/authentication_scope.dart b/example/lib/src/feature/authentication/widget/authentication_scope.dart deleted file mode 100644 index 87a70dd..0000000 --- a/example/lib/src/feature/authentication/widget/authentication_scope.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/widgets.dart'; -import 'package:spinifyapp/src/feature/authentication/controller/authentication_controller.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; -import 'package:spinifyapp/src/feature/dependencies/widget/dependencies_scope.dart'; - -/// {@template authentication_scope} -/// AuthenticationScope widget. -/// {@endtemplate} -class AuthenticationScope extends StatefulWidget { - /// {@macro authentication_scope} - const AuthenticationScope({ - required this.signInForm, - required this.child, - super.key, - }); - - /// Sign In form for unauthenticated users. - final Widget signInForm; - - /// The widget below this widget in the tree. - final Widget child; - - /// User of the authentication scope. - static User userOf(BuildContext context, {bool listen = true}) => - _InheritedAuthenticationScope.of(context, listen: listen).user; - - /// Authentication controller of the authentication scope. - static AuthenticationController controllerOf(BuildContext context) => - _InheritedAuthenticationScope.of(context, listen: false).controller; - - @override - State createState() => _AuthenticationScopeState(); -} - -/// State for widget AuthenticationScope. -class _AuthenticationScopeState extends State { - late final AuthenticationController _authenticationController; - User _user = const User.unauthenticated(); - bool _showForm = true; - - @override - void initState() { - super.initState(); - _authenticationController = AuthenticationController( - repository: DependenciesScope.of(context).authenticationRepository, - )..addListener(_onAuthenticationControllerChanged); - } - - @override - void dispose() { - _authenticationController - ..removeListener(_onAuthenticationControllerChanged) - ..dispose(); - super.dispose(); - } - - void _onAuthenticationControllerChanged() { - final user = _authenticationController.state.user; - if (!identical(_user, user)) { - if (user.isNotAuthenticated) _showForm = true; - setState(() => _user = user); - } - } - - @override - Widget build(BuildContext context) => _InheritedAuthenticationScope( - controller: _authenticationController, - user: _user, - child: ClipRect( - child: StatefulBuilder( - builder: (context, setState) => Stack( - children: [ - Positioned.fill( - key: const ValueKey('child'), - child: IgnorePointer( - ignoring: _user.isNotAuthenticated, - child: widget.child, - ), - ), - if (_showForm) - Positioned.fill( - key: const ValueKey('authentication-form'), - child: IgnorePointer( - ignoring: _user.isAuthenticated, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 350), - onEnd: () => setState(() => _showForm = false), - curve: Curves.easeInOut, - opacity: _user.isNotAuthenticated ? 1 : 0, - child: RepaintBoundary( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 2.5, - sigmaY: 2.5, - ), - child: Center( - child: widget.signInForm, - ), - ), - ), - ), - ), - ), - ], - )), - ), - ); -} - -/// Inherited widget for quick access in the element tree. -class _InheritedAuthenticationScope extends InheritedWidget { - const _InheritedAuthenticationScope({ - required this.controller, - required this.user, - required super.child, - }); - - final AuthenticationController controller; - final User user; - - /// The state from the closest instance of this class - /// that encloses the given context, if any. - /// For example: `AuthenticationScope.maybeOf(context)`. - static _InheritedAuthenticationScope? maybeOf(BuildContext context, - {bool listen = true}) => - listen - ? context.dependOnInheritedWidgetOfExactType< - _InheritedAuthenticationScope>() - : context - .getInheritedWidgetOfExactType<_InheritedAuthenticationScope>(); - - static Never _notFoundInheritedWidgetOfExactType() => throw ArgumentError( - 'Out of scope, not found inherited widget ' - 'a _InheritedAuthenticationScope of the exact type', - 'out_of_scope', - ); - - /// The state from the closest instance of this class - /// that encloses the given context. - /// For example: `AuthenticationScope.of(context)`. - static _InheritedAuthenticationScope of(BuildContext context, - {bool listen = true}) => - maybeOf(context, listen: listen) ?? _notFoundInheritedWidgetOfExactType(); - - @override - bool updateShouldNotify(covariant _InheritedAuthenticationScope oldWidget) => - !identical(user, oldWidget.user); -} diff --git a/example/lib/src/feature/authentication/widget/authentication_screen.dart b/example/lib/src/feature/authentication/widget/authentication_screen.dart deleted file mode 100644 index 08691b6..0000000 --- a/example/lib/src/feature/authentication/widget/authentication_screen.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; - -/// {@template authentication_screen} -/// AuthenticationScreen widget. -/// {@endtemplate} -class AuthenticationScreen extends StatelessWidget { - /// {@macro authentication_screen} - const AuthenticationScreen({super.key}); - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Authentication'), - ), - body: const Center( - child: Text('Authentication'), - ), - ); -} diff --git a/example/lib/src/feature/authentication/widget/sign_in_form.dart b/example/lib/src/feature/authentication/widget/sign_in_form.dart deleted file mode 100644 index 92ad085..0000000 --- a/example/lib/src/feature/authentication/widget/sign_in_form.dart +++ /dev/null @@ -1,406 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:spinifyapp/src/common/constant/config.dart'; -import 'package:spinifyapp/src/common/controller/state_consumer.dart'; -import 'package:spinifyapp/src/common/localization/localization.dart'; -import 'package:spinifyapp/src/feature/authentication/controller/authentication_controller.dart'; -import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; - -/// {@template sign_in_form} -/// SignInScreen widget. -/// {@endtemplate} -class SignInForm extends StatelessWidget implements PreferredSizeWidget { - /// {@macro sign_in_form} - const SignInForm({super.key}); - - /// Width of the sign in form. - static const double width = 480; - - /// Height of the sign in form. - static const double height = 720; - - @override - Size get preferredSize => const Size(width, height); - - @override - Widget build(BuildContext context) => - LayoutBuilder(builder: (context, constraints) { - final space = math.min(constraints.maxHeight - preferredSize.height, - constraints.maxWidth - preferredSize.width); - final padding = switch (space) { - > 32 => 24.0, - > 24 => 16.0, - > 16 => 8.0, - _ => 0.0, - }; - Widget wrap({required Widget child}) => padding > 0 - ? SizedBox( - width: width, - height: height, - child: child, - ) - : SizedBox.expand( - child: child, - ); - return wrap( - child: Card( - elevation: padding > 0 ? 8 : 0, - margin: EdgeInsets.all(padding), - shape: padding > 0 - ? RoundedRectangleBorder( - borderRadius: BorderRadius.circular(padding), - ) - : const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - child: const _SignInForm(), - ), - ); - }); -} - -class _SignInForm extends StatefulWidget { - const _SignInForm(); - - @override - State<_SignInForm> createState() => _SignInFormState(); -} - -/// State for widget _SignInForm. -class _SignInFormState extends State<_SignInForm> { - // Make it static so that it doesn't get disposed when the widget is rebuilt. - static final TextEditingController _endpointController = - TextEditingController(text: Config.centrifugeBaseUrl), - _tokenController = TextEditingController(text: Config.centrifugeToken), - _channelController = - TextEditingController(text: Config.centrifugeChannel), - _usernameController = - TextEditingController(text: Config.centrifugeUsername), - _secretController = TextEditingController(); - - final FocusNode _endpointFocusNode = FocusNode(), - _tokenFocusNode = FocusNode(), - _channelFocusNode = FocusNode(), - _usernameFocusNode = FocusNode(), - _secretFocusNode = FocusNode(); - - final ValueNotifier _endpointError = ValueNotifier(null), - _tokenError = ValueNotifier(null), - _channelError = ValueNotifier(null), - _usernameError = ValueNotifier(null), - _secretError = ValueNotifier(null); - - final ValueNotifier _validNotifier = ValueNotifier(false); - - late final AuthenticationController authenticationController; - late final Listenable _observer; - - @override - void initState() { - super.initState(); - authenticationController = AuthenticationScope.controllerOf(context); - _observer = Listenable.merge([ - _endpointController, - _tokenController, - _channelController, - _usernameController, - _secretController, - ]) - ..addListener(_onChanged); - _onChanged(); - } - - @override - void dispose() { - _observer.removeListener(_onChanged); - _validNotifier.dispose(); - super.dispose(); - } - - late SignInData _data; - - void _onChanged() { - if (!mounted) return; - _data = SignInData( - endpoint: _endpointController.text, - token: _tokenController.text, - channel: _channelController.text, - username: _usernameController.text, - secret: _secretController.text, - ); - _validNotifier.value = _validate(_data); - } - - late final List _validators = - [ - (data) => _endpointError.value = data.isValidEndpoint(), - (data) => _tokenError.value = data.isValidToken(), - (data) => _usernameError.value = data.isValidUsername(), - (data) => _channelError.value = data.isValidChannel(), - (data) => _secretError.value = data.isValidSecret(), - ]; - bool _validate(SignInData data) { - for (final validator in _validators) { - if (validator(data) != null) return false; - } - return true; - } - - void _submit() { - final data = _data; - if (!_validate(data)) return; - authenticationController.signIn(data); - } - - @override - Widget build(BuildContext context) => FocusScope( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: ListView( - padding: const EdgeInsets.all(16), - shrinkWrap: true, - children: [ - SizedBox( - width: double.infinity, - height: 48, - child: Text( - Localization.of(context).signInButton, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith(height: 1), - ), - ), - const SizedBox(height: 12), - SignInTextField( - focusNode: _endpointFocusNode, - controller: _endpointController, - error: _endpointError, - autofillHints: const [ - AutofillHints.url, - ], - maxLength: 1024, - keyboardType: TextInputType.url, - labelText: 'Endpoint', - hintText: 'Enter your endpoint', - ), - SignInTextField( - focusNode: _tokenFocusNode, - controller: _tokenController, - error: _tokenError, - maxLength: 64, - autofillHints: const [ - AutofillHints.password, - ], - keyboardType: TextInputType.visiblePassword, - labelText: 'Token', - hintText: 'Enter HMAC secret token', - obscureText: true, - ), - SignInTextField( - focusNode: _channelFocusNode, - controller: _channelController, - error: _channelError, - maxLength: 64, - labelText: 'Channel', - hintText: 'Enter your channel', - autofillHints: const [ - AutofillHints.username, - ], - keyboardType: TextInputType.name, - formatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^[a-zA-Z0-9_-]+$'), - ), - ], - ), - SignInTextField( - focusNode: _usernameFocusNode, - controller: _usernameController, - error: _usernameError, - maxLength: 64, - labelText: 'Username', - hintText: 'Select your username', - autofillHints: const [ - AutofillHints.username, - ], - keyboardType: TextInputType.name, - formatters: [ - FilteringTextInputFormatter.allow( - /// Allow only letters, numbers, - /// and the following characters: @.-_+ - RegExp(r'\@|[A-Z]|[a-z]|[0-9]|\.|\-|\_|\+'), - ), - ], - ), - SignInTextField( - focusNode: _secretFocusNode, - controller: _secretController, - error: _secretError, - maxLength: 64, - autofillHints: const [ - AutofillHints.password, - ], - keyboardType: TextInputType.visiblePassword, - labelText: 'Secret (optional)', - hintText: 'For private channels only', - obscureText: true, - ), - ], - ), - ), - const Divider(height: 1, thickness: 1, indent: 16, endIndent: 16), - Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: SizedBox( - width: 320, - height: 64, - child: ValueListenableBuilder( - valueListenable: _validNotifier, - builder: (context, valid, _) => AnimatedOpacity( - opacity: valid ? 1 : .5, - duration: const Duration(milliseconds: 350), - child: ElevatedButton( - onPressed: valid ? _submit : null, - style: ElevatedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - bottomLeft: Radius.circular(8), - bottomRight: Radius.circular(24), - topRight: Radius.circular(8), - ), - ), - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - child: const Text('Sign In'), - ), - ), - ), - ), - ), - ), - ], - ), - ); -} - -class SignInTextField extends StatefulWidget { - const SignInTextField({ - required this.controller, - this.formatters, - this.focusNode, - this.error, - this.autofillHints, - this.labelText, - this.hintText, - this.obscureText = false, - this.keyboardType, - this.maxLength, - super.key, - }); - - final TextEditingController controller; - final List? formatters; - final FocusNode? focusNode; - final ValueListenable? error; - final List? autofillHints; - final String? labelText; - final String? hintText; - final bool obscureText; - final TextInputType? keyboardType; - final int? maxLength; - - @override - State createState() => _SignInTextFieldState(); -} - -class _SignInTextFieldState extends State { - bool _obscurePassword = false; - FocusNode? focusNode; - - @override - void initState() { - super.initState(); - _obscurePassword = widget.obscureText; - focusNode = widget.focusNode?..addListener(_onFocusChanged); - } - - @override - void dispose() { - focusNode?.removeListener(_onFocusChanged); - super.dispose(); - } - - void _onFocusChanged() { - if (focusNode?.hasFocus == false && - mounted && - widget.obscureText && - !_obscurePassword) { - setState(() => _obscurePassword = true); - } - } - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: StateConsumer( - controller: AuthenticationScope.controllerOf(context), - builder: (context, state, _) => AnimatedOpacity( - opacity: state.isIdling ? 1 : .5, - duration: const Duration(milliseconds: 250), - child: ValueListenableBuilder( - valueListenable: widget.error ?? ValueNotifier(null), - builder: (context, error, child) => StatefulBuilder( - builder: (context, setState) => TextField( - focusNode: widget.focusNode, - enabled: state.isIdling, - maxLines: 1, - minLines: 1, - maxLength: widget.maxLength, - controller: widget.controller, - autocorrect: false, - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - inputFormatters: widget.formatters, - obscureText: _obscurePassword, - decoration: InputDecoration( - constraints: const BoxConstraints(maxHeight: 84), - labelText: widget.labelText, - hintText: widget.hintText, - helperText: '', - helperMaxLines: 1, - errorText: error ?? state.error, - errorMaxLines: 1, - suffixIcon: widget.obscureText - ? IconButton( - icon: Icon(_obscurePassword - ? Icons.visibility - : Icons.visibility_off), - onPressed: () => setState( - () => _obscurePassword = !_obscurePassword, - ), - ) - : null, - border: const OutlineInputBorder(), - ), - ), - ), - ), - ), - ), - ); -} diff --git a/example/lib/src/feature/chat/controller/chat_connection_controller.dart b/example/lib/src/feature/chat/controller/chat_connection_controller.dart deleted file mode 100644 index 9495966..0000000 --- a/example/lib/src/feature/chat/controller/chat_connection_controller.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:spinifyapp/src/common/controller/droppable_controller_concurrency.dart'; -import 'package:spinifyapp/src/common/controller/state_controller.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_connection_state.dart'; -import 'package:spinifyapp/src/feature/chat/data/chat_repository.dart'; - -final class ChatConnectionController - extends StateController - with DroppableControllerConcurrency { - ChatConnectionController({required IChatRepository repository}) - : _repository = repository, - super(initialState: repository.connectionState) { - _repository.connectionStates.distinct().listen(setState); - } - - final IChatRepository _repository; - - void connect(String url) => handle(() => _repository.connect(url)); - - void disconnect() => handle(_repository.disconnect); - - @override - void dispose() { - _repository.disconnect(); - super.dispose(); - } -} diff --git a/example/lib/src/feature/chat/controller/chat_connection_state.dart b/example/lib/src/feature/chat/controller/chat_connection_state.dart deleted file mode 100644 index 16f6468..0000000 --- a/example/lib/src/feature/chat/controller/chat_connection_state.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template chat_connection_state} -/// ChatConnectionState. -/// {@endtemplate} -sealed class ChatConnectionState extends _$ChatConnectionStateBase { - /// Disconnected - /// {@macro chat_connection_state} - const factory ChatConnectionState.disconnected({ - String message, - }) = ChatConnectionState$Disconnected; - - /// Connecting - /// {@macro chat_connection_state} - const factory ChatConnectionState.connecting({ - String message, - }) = ChatConnectionState$Connecting; - - /// Connected - /// {@macro chat_connection_state} - const factory ChatConnectionState.connected({ - String message, - }) = ChatConnectionState$Connected; - - /// {@macro chat_connection_state} - const ChatConnectionState({required super.message}); -} - -/// Disconnected -final class ChatConnectionState$Disconnected extends ChatConnectionState { - const ChatConnectionState$Disconnected({super.message = 'Disconnected'}); -} - -/// Connecting -final class ChatConnectionState$Connecting extends ChatConnectionState { - const ChatConnectionState$Connecting({super.message = 'Connecting'}); -} - -/// Connected -final class ChatConnectionState$Connected extends ChatConnectionState { - const ChatConnectionState$Connected({super.message = 'Connected'}); -} - -/// Pattern matching for [ChatConnectionState]. -typedef ChatConnectionStateMatch = R Function( - S state); - -@immutable -abstract base class _$ChatConnectionStateBase { - const _$ChatConnectionStateBase({required this.message}); - - /// Message or state description. - @nonVirtual - final String message; - - /// Is connecting? - bool get isConnecting => - maybeMap(orElse: () => false, connecting: (_) => true); - - /// Is connected? - bool get isConnected => - maybeMap(orElse: () => false, connected: (_) => true); - - /// Is disconnected? - bool get isDisconnected => - maybeMap(orElse: () => false, disconnected: (_) => true); - - /// Pattern matching for [ChatConnectionState]. - R map({ - required ChatConnectionStateMatch - disconnected, - required ChatConnectionStateMatch - connecting, - required ChatConnectionStateMatch - connected, - }) => - switch (this) { - ChatConnectionState$Disconnected s => disconnected(s), - ChatConnectionState$Connecting s => connecting(s), - ChatConnectionState$Connected s => connected(s), - _ => throw AssertionError(), - }; - - /// Pattern matching for [ChatConnectionState]. - R maybeMap({ - ChatConnectionStateMatch? disconnected, - ChatConnectionStateMatch? connecting, - ChatConnectionStateMatch? connected, - required R Function() orElse, - }) => - map( - disconnected: disconnected ?? (_) => orElse(), - connecting: connecting ?? (_) => orElse(), - connected: connected ?? (_) => orElse(), - ); - - /// Pattern matching for [ChatConnectionState]. - R? mapOrNull({ - ChatConnectionStateMatch? disconnected, - ChatConnectionStateMatch? connecting, - ChatConnectionStateMatch? connected, - }) => - map( - disconnected: disconnected ?? (_) => null, - connecting: connecting ?? (_) => null, - connected: connected ?? (_) => null, - ); - - @override - String toString() => message; -} diff --git a/example/lib/src/feature/chat/controller/chat_messages_controller.dart b/example/lib/src/feature/chat/controller/chat_messages_controller.dart deleted file mode 100644 index 295da6b..0000000 --- a/example/lib/src/feature/chat/controller/chat_messages_controller.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'package:l/l.dart'; -import 'package:spinifyapp/src/common/controller/droppable_controller_concurrency.dart'; -import 'package:spinifyapp/src/common/controller/state_controller.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_messages_state.dart'; -import 'package:spinifyapp/src/feature/chat/data/chat_repository.dart'; -import 'package:spinifyapp/src/feature/chat/model/message.dart'; - -final class ChatMessagesController extends StateController - with DroppableControllerConcurrency { - ChatMessagesController( - {required AuthenticatedUser user, required IChatRepository repository}) - : _user = user, - _repository = repository, - super(initialState: ChatMessagesState.initial) { - final AuthenticatedUser(:channel, :secret) = user; - _messagesSubscription = - _repository.getMessages(channel, secret).listen(_onMessage); - } - - final AuthenticatedUser _user; - final IChatRepository _repository; - late final StreamSubscription _messagesSubscription; - late final Set _messages = SplayTreeSet.of( - state.data, - (a, b) => b.compareTo(a), - ); - - void _onMessage(Message message) { - setState(state.copyWith( - data: (_messages..add(message)).toList(growable: false))); - } - - void sendMessage(String message) => handle( - () async { - l.v6('Sending message'); - await _repository.sendMessage(_user, message); - setState(ChatMessagesState.successful( - data: state.data, message: 'Message sent')); - l.v6('Message sent'); - }, - (error, stackTrace) { - l.w('Error sending message: $error', stackTrace); - setState( - ChatMessagesState.error( - data: state.data, message: 'Error sending message'), - ); - }, - () => setState(ChatMessagesState.idle(data: state.data)), - ); - - void disconnect() => handle(_repository.disconnect); - - @override - void dispose() { - _messagesSubscription.cancel(); - _repository.disconnect(); - super.dispose(); - } -} diff --git a/example/lib/src/feature/chat/controller/chat_messages_state.dart b/example/lib/src/feature/chat/controller/chat_messages_state.dart deleted file mode 100644 index 93e9ce3..0000000 --- a/example/lib/src/feature/chat/controller/chat_messages_state.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:spinifyapp/src/feature/chat/model/message.dart'; - -/// Chat messages entity. -typedef ChatMessages = List; - -/// {@template chat_messages_state} -/// ChatMessagesState. -/// {@endtemplate} -sealed class ChatMessagesState extends _$ChatMessagesStateBase { - /// Idling state - /// {@macro chat_messages_state} - const factory ChatMessagesState.idle({ - required ChatMessages data, - String message, - }) = ChatMessagesState$Idle; - - /// Processing - /// {@macro chat_messages_state} - const factory ChatMessagesState.processing({ - required ChatMessages data, - String message, - }) = ChatMessagesState$Processing; - - /// Successful - /// {@macro chat_messages_state} - const factory ChatMessagesState.successful({ - required ChatMessages data, - String message, - }) = ChatMessagesState$Successful; - - /// An error has occurred - /// {@macro chat_messages_state} - const factory ChatMessagesState.error({ - required ChatMessages data, - String message, - }) = ChatMessagesState$Error; - - /// {@macro chat_messages_state} - const ChatMessagesState({required super.data, required super.message}); - - static ChatMessagesState get initial => - const ChatMessagesState.idle(data: []); -} - -/// Idling state -final class ChatMessagesState$Idle extends ChatMessagesState { - const ChatMessagesState$Idle({required super.data, super.message = 'Idling'}); - - @override - ChatMessagesState$Idle copyWith({ - ChatMessages? data, - String? message, - }) => - ChatMessagesState$Idle( - data: data ?? this.data, - message: message ?? this.message, - ); -} - -/// Processing -final class ChatMessagesState$Processing extends ChatMessagesState { - const ChatMessagesState$Processing( - {required super.data, super.message = 'Processing'}); - - @override - ChatMessagesState$Processing copyWith({ - ChatMessages? data, - String? message, - }) => - ChatMessagesState$Processing( - data: data ?? this.data, - message: message ?? this.message, - ); -} - -/// Successful -final class ChatMessagesState$Successful extends ChatMessagesState { - const ChatMessagesState$Successful( - {required super.data, super.message = 'Successful'}); - - @override - ChatMessagesState$Successful copyWith({ - ChatMessages? data, - String? message, - }) => - ChatMessagesState$Successful( - data: data ?? this.data, - message: message ?? this.message, - ); -} - -/// Error -final class ChatMessagesState$Error extends ChatMessagesState { - const ChatMessagesState$Error( - {required super.data, super.message = 'An error has occurred.'}); - - @override - ChatMessagesState$Error copyWith({ - ChatMessages? data, - String? message, - }) => - ChatMessagesState$Error( - data: data ?? this.data, - message: message ?? this.message, - ); -} - -/// Pattern matching for [ChatMessagesState]. -typedef ChatMessagesStateMatch = R Function( - S state); - -@immutable -abstract base class _$ChatMessagesStateBase { - const _$ChatMessagesStateBase({required this.data, required this.message}); - - /// Data entity payload. - @nonVirtual - final ChatMessages data; - - /// Message or state description. - @nonVirtual - final String message; - - /// If an error has occurred? - bool get hasError => maybeMap(orElse: () => false, error: (_) => true); - - /// Is in progress state? - bool get isProcessing => - maybeMap(orElse: () => false, processing: (_) => true); - - /// Is in idle state? - bool get isIdling => !isProcessing; - - /// Copy with new data. - ChatMessagesState copyWith({ - ChatMessages? data, - String? message, - }); - - /// Pattern matching for [ChatMessagesState]. - R map({ - required ChatMessagesStateMatch idle, - required ChatMessagesStateMatch processing, - required ChatMessagesStateMatch successful, - required ChatMessagesStateMatch error, - }) => - switch (this) { - ChatMessagesState$Idle s => idle(s), - ChatMessagesState$Processing s => processing(s), - ChatMessagesState$Successful s => successful(s), - ChatMessagesState$Error s => error(s), - _ => throw AssertionError(), - }; - - /// Pattern matching for [ChatMessagesState]. - R maybeMap({ - ChatMessagesStateMatch? idle, - ChatMessagesStateMatch? processing, - ChatMessagesStateMatch? successful, - ChatMessagesStateMatch? error, - required R Function() orElse, - }) => - map( - idle: idle ?? (_) => orElse(), - processing: processing ?? (_) => orElse(), - successful: successful ?? (_) => orElse(), - error: error ?? (_) => orElse(), - ); - - /// Pattern matching for [ChatMessagesState]. - R? mapOrNull({ - ChatMessagesStateMatch? idle, - ChatMessagesStateMatch? processing, - ChatMessagesStateMatch? successful, - ChatMessagesStateMatch? error, - }) => - map( - idle: idle ?? (_) => null, - processing: processing ?? (_) => null, - successful: successful ?? (_) => null, - error: error ?? (_) => null, - ); - - @override - int get hashCode => data.hashCode; - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() => 'ChatMessagesState(data: $data, message: $message)'; -} diff --git a/example/lib/src/feature/chat/data/chat_repository.dart b/example/lib/src/feature/chat/data/chat_repository.dart deleted file mode 100644 index c078d64..0000000 --- a/example/lib/src/feature/chat/data/chat_repository.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:l/l.dart'; -import 'package:spinify/spinify.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_connection_state.dart'; -import 'package:spinifyapp/src/feature/chat/model/message.dart'; - -/// Chat repository -abstract interface class IChatRepository { - /// Receive messages stream - Stream getMessages(String channel, [String? secret]); - - /// Connection state - ChatConnectionState get connectionState; - - /// Connection states stream - Stream get connectionStates; - - /// Connect to chat server - Future connect(String url); - - /// Disconnect from chat server - Future disconnect(); - - /// Send message to chat server - Future sendMessage(AuthenticatedUser user, String message); -} - -final class ChatRepositorySpinifyImpl implements IChatRepository { - ChatRepositorySpinifyImpl({required Spinify spinify}) : _spinify = spinify; - - /// Centrifugo client - final Spinify _spinify; - - @override - ChatConnectionState get connectionState => - _spinifyStateToConnectionState(_spinify.state); - - @override - late final Stream connectionStates = - _spinify.states.map(_spinifyStateToConnectionState); - - ChatConnectionState _spinifyStateToConnectionState(SpinifyState state) => - switch (state) { - SpinifyState$Connected _ => const ChatConnectionState.connected(), - SpinifyState$Connecting _ => const ChatConnectionState.connecting(), - _ => const ChatConnectionState.disconnected(), - }; - - @override - Stream getMessages(String channel, [String? secret]) { - void ignoreErrors(Object error, StackTrace? stackTrace) { - l.w('Error receiving message: $error', stackTrace); - } - - final Converter, Message> decoder; - if (secret != null && secret.isNotEmpty) { - decoder = EncryptedMessageDecoder(secretKey: secret); - } else { - decoder = const PlainMessageDecoder(); - } - - return _spinify.stream.publications - .where((event) => event.channel == channel) - .map>((event) => event.data) - .map(decoder.convert) - .handleError(ignoreErrors); - } - - @override - Future connect(String url) => _spinify.connect(url); - - @override - Future disconnect() => _spinify.disconnect(); - - @override - Future sendMessage(AuthenticatedUser user, String message) async { - if (!_spinify.state.isConnected) { - throw Exception('Spinify is not connected'); - } - final serverChannels = _spinify.subscriptions.server.values.toList(); - final AuthenticatedUser(:channel, :username, :secret) = user; - if (!serverChannels.any((c) => c.channel == channel)) - throw Exception('Spinify server channel is not set'); - List data; - switch (secret) { - case null || '': - data = const PlainMessageEncoder().convert( - PlainMessage( - author: username, - text: message, - createdAt: DateTime.now(), - version: 1, - ), - ); - case String secret: - data = EncryptedMessageEncoder(secretKey: secret).convert( - EncryptedMessage( - author: username, - text: message, - createdAt: DateTime.now(), - version: 1, - ), - ); - } - await _spinify.publish(channel, data); - } -} diff --git a/example/lib/src/feature/chat/model/message.dart b/example/lib/src/feature/chat/model/message.dart deleted file mode 100644 index 4367fa9..0000000 --- a/example/lib/src/feature/chat/model/message.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; -import 'package:meta/meta.dart'; - -@immutable -sealed class Message implements Comparable { - const Message({ - required this.author, - required this.text, - required this.version, - required this.createdAt, - }); - - /// The type of the message. - abstract final String type; - - /// The author of the message. - final String author; - - /// The text of the message. - final String text; - - /// The version of the message. - final int version; - - /// The time the message was created. - final DateTime createdAt; - - Map toJson(); -} - -final class PlainMessage extends Message { - const PlainMessage({ - required super.author, - required super.text, - required super.version, - required super.createdAt, - }); - - factory PlainMessage.fromJson(Map json) { - if (json - case { - 'type': 'plain', - 'author': String author, - 'text': String text, - 'version': int version, - 'createdAt': int createdAt, - }) - return PlainMessage( - author: author, - text: text, - version: version, - createdAt: DateTime.fromMillisecondsSinceEpoch(createdAt * 1000), - ); - throw const FormatException('Invalid message type'); - } - - @override - String get type => 'plain'; - - @override - int compareTo(Message other) => createdAt.compareTo(other.createdAt); - - @override - Map toJson() => { - 'type': type, - 'author': author, - 'text': text, - 'version': version, - 'createdAt': createdAt.millisecondsSinceEpoch ~/ 1000, - }; - - @override - String toString() => '$author: $text'; -} - -final class EncryptedMessage extends Message { - const EncryptedMessage({ - required super.author, - required super.text, - required super.version, - required super.createdAt, - }); - - factory EncryptedMessage.fromJson(Map json) { - if (json - case { - 'type': 'encrypted', - 'author': String author, - 'text': String text, - 'version': int version, - 'createdAt': int createdAt, - }) - return EncryptedMessage( - author: author, - text: text, - version: version, - createdAt: DateTime.fromMillisecondsSinceEpoch(createdAt * 1000), - ); - throw const FormatException('Invalid message type'); - } - - @override - int compareTo(Message other) => createdAt.compareTo(other.createdAt); - - @override - String get type => 'encrypted'; - - @override - Map toJson() => { - 'type': type, - 'author': author, - 'text': text, - 'version': version, - 'createdAt': createdAt.millisecondsSinceEpoch ~/ 1000, - }; - - @override - String toString() => '$author: $text'; -} - -@immutable -final class PlainMessageCodec extends Codec> { - const PlainMessageCodec(); - - @override - Converter, PlainMessage> get decoder => const PlainMessageDecoder(); - - @override - Converter> get encoder => const PlainMessageEncoder(); -} - -final class PlainMessageDecoder extends Converter, PlainMessage> { - const PlainMessageDecoder(); - - @override - PlainMessage convert(List input) => - PlainMessage.fromJson(_$bytesDecoder.convert(input)); -} - -final class PlainMessageEncoder extends Converter> { - const PlainMessageEncoder(); - - @override - List convert(PlainMessage input) => - _$bytesEncoder.convert(input.toJson()); -} - -@immutable -final class EncryptedMessageCodec extends Codec> { - const EncryptedMessageCodec({required this.secretKey}); - - final String secretKey; - - @override - Converter, EncryptedMessage> get decoder => - EncryptedMessageDecoder(secretKey: secretKey); - - @override - Converter> get encoder => - EncryptedMessageEncoder(secretKey: secretKey); -} - -final class EncryptedMessageDecoder - extends Converter, EncryptedMessage> { - EncryptedMessageDecoder({required String secretKey}) - : _secret = utf8.encode(secretKey); - - final List _secret; - late final int secretLength = _secret.length; - late final Digest _digest = sha256.convert(_secret); - - @override - EncryptedMessage convert(List input) { - if (input.length < 32) throw const FormatException('Message too short'); - final signature = input.sublist(input.length - 32); - if (_digest != Digest(signature)) - throw const FormatException('Invalid signature'); - final bytes = input.sublist(0, input.length - 32); - for (var i = 0; i < bytes.length; i++) - bytes[i] ^= _secret[i % secretLength]; - return EncryptedMessage.fromJson(_$bytesDecoder.convert(bytes)); - } -} - -final class EncryptedMessageEncoder - extends Converter> { - EncryptedMessageEncoder({required String secretKey}) - : _secret = utf8.encode(secretKey); - - final List _secret; - late final int secretLength = _secret.length; - late final Digest _digest = sha256.convert(_secret); - - @override - List convert(EncryptedMessage input) { - final bytes = _$bytesEncoder.convert(input.toJson()); - for (var i = 0; i < bytes.length; i++) - bytes[i] ^= _secret[i % secretLength]; - return bytes + _digest.bytes; - } -} - -final Converter, Map> _$bytesDecoder = - const Utf8Decoder() - .fuse(const JsonDecoder().cast>()); - -final Converter, List> _$bytesEncoder = - const JsonEncoder() - .cast, String>() - .fuse(const Utf8Encoder()); diff --git a/example/lib/src/feature/chat/widget/chat_room.dart b/example/lib/src/feature/chat/widget/chat_room.dart deleted file mode 100644 index 488f736..0000000 --- a/example/lib/src/feature/chat/widget/chat_room.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spinifyapp/src/common/controller/state_consumer.dart'; -import 'package:spinifyapp/src/common/util/date_util.dart'; -import 'package:spinifyapp/src/feature/authentication/model/user.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_connection_controller.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_connection_state.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_messages_controller.dart'; -import 'package:spinifyapp/src/feature/chat/controller/chat_messages_state.dart'; -import 'package:spinifyapp/src/feature/chat/model/message.dart'; -import 'package:spinifyapp/src/feature/dependencies/widget/dependencies_scope.dart'; - -/// {@template chat_screen} -/// ChatRoom widget. -/// {@endtemplate} -class ChatRoom extends StatefulWidget { - /// {@macro chat_screen} - const ChatRoom({required this.user, super.key}); - - /// The user that is currently logged in - final AuthenticatedUser user; - - @override - State createState() => _ChatRoomState(); -} - -/// State for widget ChatRoom. -class _ChatRoomState extends State { - late final ChatConnectionController _connectionController; - late ChatMessagesController _messagesController; - final TextEditingController _textEditingController = TextEditingController(); - - @override - void initState() { - super.initState(); - final repository = DependenciesScope.of(context).chatRepository; - _connectionController = ChatConnectionController(repository: repository); - _messagesController = - ChatMessagesController(user: widget.user, repository: repository); - _connectionController.connect(widget.user.endpoint); - } - - @override - void didUpdateWidget(covariant ChatRoom oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.user != widget.user) { - _connectionController.disconnect(); - _connectionController.connect(widget.user.endpoint); - _messagesController.dispose(); - _messagesController = ChatMessagesController( - user: widget.user, - repository: DependenciesScope.of(context).chatRepository, - ); - } - } - - @override - void dispose() { - _messagesController.dispose(); - _connectionController.dispose(); - _textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => Column( - children: [ - Expanded( - child: StateConsumer( - controller: _messagesController, - builder: (context, messagesState, child) => RepaintBoundary( - child: ListView.builder( - scrollDirection: Axis.vertical, - padding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 8), - reverse: true, - itemCount: messagesState.data.length, - itemBuilder: (context, index) => ChatMessageBubble( - message: messagesState.data[index], - currentUser: widget.user, - ), - ), - ), - ), - ), - const Divider(height: 1, thickness: .5), - RepaintBoundary( - child: SizedBox( - height: 64, - width: double.infinity, - child: ColoredBox( - color: Colors.grey.withOpacity(0.2), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StateConsumer( - controller: _connectionController, - builder: (context, connectionState, _) => - StateConsumer( - controller: _messagesController, - buildWhen: (previous, current) => - !(previous.isIdling && current.isIdling), - listener: (context, previous, current) { - switch (current) { - case ChatMessagesState$Successful _: - _textEditingController.clear(); - case ChatMessagesState$Error state: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.red, - ), - ); - default: - break; - } - }, - builder: (context, messagesState, child) => Row( - children: [ - Expanded( - child: TextField( - controller: _textEditingController, - enabled: connectionState.isConnected, - maxLength: 128, - maxLines: 1, - decoration: const InputDecoration( - border: InputBorder.none, - counterText: '', - hintText: 'Write a message...', - ), - ), - ), - ValueListenableBuilder( - valueListenable: _textEditingController, - builder: (context, value, _) { - final enabled = connectionState.isConnected && - messagesState.isIdling && - value.text.isNotEmpty; - return IconButton( - icon: AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - child: switch (connectionState) { - ChatConnectionState$Connecting _ => - const CircularProgressIndicator(), - ChatConnectionState$Connected _ => - const Icon(Icons.send), - ChatConnectionState$Disconnected _ => - const Icon(Icons.send_outlined), - }, - ), - onPressed: enabled - ? () => _messagesController - .sendMessage(value.text) - : null, - ); - }, - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ); -} - -/// {@template chat_room} -/// ChatMessageBubble widget. -/// {@endtemplate} -class ChatMessageBubble extends StatelessWidget { - /// {@macro chat_room} - const ChatMessageBubble( - {required this.message, required this.currentUser, super.key}); - - final Message message; - final AuthenticatedUser currentUser; - - static const List _$colors = Colors.primaries; - static Color _getColorForUsername(String username) => - _$colors[username.codeUnitAt(0) % _$colors.length]; - - @override - Widget build(BuildContext context) => Align( - alignment: message.author == currentUser.username - ? Alignment.centerRight - : Alignment.centerLeft, - child: ConstrainedBox( - constraints: BoxConstraints.loose( - const Size.fromWidth(512), - ), - child: Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: Stack( - fit: StackFit.loose, - children: [ - Positioned( - top: 4, - left: 8, - child: Text( - message.author, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: _getColorForUsername(message.author), - letterSpacing: 1, - height: 1, - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 16, 8, 14), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 128), - child: Text(message.text), - ), - ), - Positioned( - bottom: 4, - right: 4, - child: Text( - message.createdAt.format(), - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: const TextStyle( - fontSize: 10, - color: Colors.grey, - letterSpacing: 1.2, - height: 1, - ), - ), - ), - ], - ), - ), - ), - ); -} diff --git a/example/lib/src/feature/chat/widget/chat_screen.dart b/example/lib/src/feature/chat/widget/chat_screen.dart deleted file mode 100644 index e8860ff..0000000 --- a/example/lib/src/feature/chat/widget/chat_screen.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spinifyapp/src/common/controller/state_consumer.dart'; -import 'package:spinifyapp/src/common/localization/localization.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; -import 'package:spinifyapp/src/feature/chat/widget/chat_room.dart'; - -/// {@template chat_screen} -/// ChatScreen widget. -/// {@endtemplate} -class ChatScreen extends StatelessWidget { - /// {@macro chat_screen} - const ChatScreen({super.key}); - - @override - Widget build(BuildContext context) { - final authController = AuthenticationScope.controllerOf(context); - return StateConsumer( - controller: authController, - builder: (context, state, _) => Scaffold( - appBar: AppBar( - title: Text(Localization.of(context).title), - centerTitle: true, - automaticallyImplyLeading: false, - /* elevation: 4, */ - /* pinned: MediaQuery.of(context).size.height > 600, */ - actions: [ - IconButton( - onPressed: () => - AuthenticationScope.controllerOf(context).signOut(), - icon: const Icon(Icons.logout), - ), - const SizedBox(width: 16), - ], - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: state.user.map( - authenticated: (user) => ChatRoom(user: user), - unauthenticated: (_) => const SizedBox.expand(), - ), - ), - ), - ); - } -} diff --git a/example/lib/src/feature/dependencies/initialization/initialization.dart b/example/lib/src/feature/dependencies/initialization/initialization.dart deleted file mode 100644 index 092ef58..0000000 --- a/example/lib/src/feature/dependencies/initialization/initialization.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart' - show ChangeNotifier, FlutterError, PlatformDispatcher, ValueListenable; -import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation; -import 'package:flutter/widgets.dart' - show WidgetsBinding, WidgetsFlutterBinding; -import 'package:spinifyapp/src/common/util/error_util.dart'; -import 'package:spinifyapp/src/feature/dependencies/initialization/initialize_dependencies.dart'; -import 'package:spinifyapp/src/feature/dependencies/model/dependencies.dart'; - -typedef InitializationProgressTuple = ({int progress, String message}); - -abstract interface class InitializationProgressListenable - implements ValueListenable {} - -class InitializationExecutor - with ChangeNotifier, InitializeDependencies - implements InitializationProgressListenable { - InitializationExecutor(); - - /// Ephemerally initializes the app and prepares it for use. - Future? _$currentInitialization; - - @override - InitializationProgressTuple get value => _value; - InitializationProgressTuple _value = (progress: 0, message: ''); - - /// Initializes the app and prepares it for use. - Future call({ - bool deferFirstFrame = false, - List? orientations, - void Function(int progress, String message)? onProgress, - void Function(Dependencies dependencies)? onSuccess, - void Function(Object error, StackTrace stackTrace)? onError, - }) => - _$currentInitialization ??= Future(() async { - late final WidgetsBinding binding; - final stopwatch = Stopwatch()..start(); - void notifyProgress(int progress, String message) { - _value = (progress: progress.clamp(0, 100), message: message); - onProgress?.call(_value.progress, _value.message); - notifyListeners(); - } - - notifyProgress(0, 'Initializing'); - try { - binding = WidgetsFlutterBinding.ensureInitialized(); - if (deferFirstFrame) binding.deferFirstFrame(); - await _catchExceptions(); - if (orientations != null) - await SystemChrome.setPreferredOrientations(orientations); - final dependencies = - await $initializeDependencies(onProgress: notifyProgress) - .timeout(const Duration(minutes: 5)); - notifyProgress(100, 'Done'); - onSuccess?.call(dependencies); - return dependencies; - } on Object catch (error, stackTrace) { - onError?.call(error, stackTrace); - ErrorUtil.logError( - error, - stackTrace, - hint: 'Failed to initialize app', - ).ignore(); - rethrow; - } finally { - stopwatch.stop(); - binding.addPostFrameCallback((_) { - // Closes splash screen, and show the app layout. - if (deferFirstFrame) binding.allowFirstFrame(); - //final context = binding.renderViewElement; - }); - _$currentInitialization = null; - } - }); - - Future _catchExceptions() async { - try { - PlatformDispatcher.instance.onError = (error, stackTrace) { - ErrorUtil.logError( - error, - stackTrace, - hint: 'ROOT | ${Error.safeToString(error)}', - ).ignore(); - return true; - }; - - final sourceFlutterError = FlutterError.onError; - FlutterError.onError = (final details) { - ErrorUtil.logError( - details.exception, - details.stack ?? StackTrace.current, - hint: 'FLUTTER ERROR\r\n$details', - ).ignore(); - // FlutterError.presentError(details); - sourceFlutterError?.call(details); - }; - } on Object catch (error, stackTrace) { - ErrorUtil.logError(error, stackTrace).ignore(); - } - } -} diff --git a/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart b/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart deleted file mode 100644 index 6caeebd..0000000 --- a/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:async'; - -import 'package:l/l.dart'; -import 'package:meta/meta.dart'; -import 'package:platform_info/platform_info.dart'; -import 'package:spinify/spinify.dart'; -import 'package:spinifyapp/src/common/constant/config.dart'; -import 'package:spinifyapp/src/common/constant/pubspec.yaml.g.dart'; -import 'package:spinifyapp/src/common/controller/controller.dart'; -import 'package:spinifyapp/src/common/controller/controller_observer.dart'; -import 'package:spinifyapp/src/common/util/screen_util.dart'; -import 'package:spinifyapp/src/feature/authentication/data/authentication_repository.dart'; -import 'package:spinifyapp/src/feature/chat/data/chat_repository.dart'; -import 'package:spinifyapp/src/feature/dependencies/initialization/platform/initialization_vm.dart' - // ignore: uri_does_not_exist - if (dart.library.js_util) 'package:spinifyapp/src/feature/dependencies/initialization/platform/initialization_js.dart'; -import 'package:spinifyapp/src/feature/dependencies/model/app_metadata.dart'; -import 'package:spinifyapp/src/feature/dependencies/model/dependencies.dart'; - -typedef _InitializationStep = FutureOr Function( - _MutableDependencies dependencies); - -class _MutableDependencies implements Dependencies { - @override - late AppMetadata appMetadata; - - @override - late IAuthenticationRepository authenticationRepository; - - @override - late IChatRepository chatRepository; -} - -@internal -mixin InitializeDependencies { - /// Initializes the app and returns a [Dependencies] object - @protected - Future $initializeDependencies({ - void Function(int progress, String message)? onProgress, - }) async { - final steps = _initializationSteps; - final dependencies = _MutableDependencies(); - final totalSteps = steps.length; - for (var currentStep = 0; currentStep < totalSteps; currentStep++) { - final step = steps[currentStep]; - final percent = (currentStep * 100 ~/ totalSteps).clamp(0, 100); - onProgress?.call(percent, step.$1); - l.v6( - 'Initialization | $currentStep/$totalSteps ($percent%) | "${step.$1}"'); - await step.$2(dependencies); - } - return dependencies; - } - - List<(String, _InitializationStep)> get _initializationSteps => - <(String, _InitializationStep)>[ - ( - 'Platform pre-initialization', - (_) => $platformInitialization(), - ), - ( - 'Creating app metadata', - (dependencies) => dependencies.appMetadata = AppMetadata( - environment: Config.environment.value, - isWeb: platform.isWeb, - isRelease: platform.buildMode.isRelease, - appName: Pubspec.name, - appVersion: Pubspec.version.canonical, - appVersionMajor: Pubspec.version.major, - appVersionMinor: Pubspec.version.minor, - appVersionPatch: Pubspec.version.patch, - appBuildTimestamp: Pubspec.version.build.isNotEmpty - ? (int.tryParse( - Pubspec.version.build.firstOrNull ?? '-1') ?? - -1) - : -1, - operatingSystem: platform.operatingSystem.name, - processorsCount: platform.numberOfProcessors, - appLaunchedTimestamp: DateTime.now(), - locale: platform.locale, - deviceVersion: platform.version, - deviceScreenSize: ScreenUtil.screenSize().representation, - ), - ), - ( - 'Observer state managment', - (_) => Controller.observer = ControllerObserver(), - ), - ( - 'Initializing analytics', - (_) {}, - ), - ( - 'Log app open', - (_) {}, - ), - ( - 'Get remote config', - (_) {}, - ), - ( - 'Authentication repository', - (dependencies) => dependencies.authenticationRepository = - AuthenticationRepositoryImpl(), - ), - ( - 'Chat repository', - (dependencies) => - dependencies.chatRepository = ChatRepositorySpinifyImpl( - spinify: Spinify( - config: SpinifyConfig( - getToken: dependencies.authenticationRepository.getToken, - ), - ), - ), - ), - ]; -} diff --git a/example/lib/src/feature/dependencies/initialization/platform/initialization_js.dart b/example/lib/src/feature/dependencies/initialization/platform/initialization_js.dart deleted file mode 100644 index 611f64e..0000000 --- a/example/lib/src/feature/dependencies/initialization/platform/initialization_js.dart +++ /dev/null @@ -1,21 +0,0 @@ -// ignore_for_file: avoid_web_libraries_in_flutter - -import 'dart:html' as html; - -//import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -Future $platformInitialization() async { - //setUrlStrategy(const HashUrlStrategy()); - Future.delayed( - const Duration(seconds: 1), - () { - html.document.getElementById('splash')?.remove(); - html.document.getElementById('splash-branding')?.remove(); - html.document.body?.style.background = 'transparent'; - html.document - .getElementsByClassName('splash-loading') - .toList(growable: false) - .forEach((element) => element.remove()); - }, - ); -} diff --git a/example/lib/src/feature/dependencies/initialization/platform/initialization_vm.dart b/example/lib/src/feature/dependencies/initialization/platform/initialization_vm.dart deleted file mode 100644 index 76ba8d0..0000000 --- a/example/lib/src/feature/dependencies/initialization/platform/initialization_vm.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io' as io; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:window_manager/window_manager.dart'; - -Future $platformInitialization() => - io.Platform.isAndroid || io.Platform.isIOS - ? _mobileInitialization() - : _desktopInitialization(); - -Future _mobileInitialization() async {} - -Future _desktopInitialization() async { - // Must add this line. - await windowManager.ensureInitialized(); - final windowOptions = WindowOptions( - minimumSize: const Size(360, 480), - size: const Size(960, 800), - maximumSize: const Size(1440, 1080), - center: true, - backgroundColor: - PlatformDispatcher.instance.platformBrightness == Brightness.dark - ? ThemeData.dark().colorScheme.background - : ThemeData.light().colorScheme.background, - skipTaskbar: false, - titleBarStyle: TitleBarStyle.hidden, - /* alwaysOnTop: true, */ - windowButtonVisibility: false, - fullScreen: false, - title: 'Chat App', - ); - await windowManager.waitUntilReadyToShow( - windowOptions, - () async { - if (io.Platform.isMacOS) { - await windowManager.setMovable(true); - } - await windowManager.setMaximizable(false); - await windowManager.show(); - await windowManager.focus(); - }, - ); -} diff --git a/example/lib/src/feature/dependencies/model/app_metadata.dart b/example/lib/src/feature/dependencies/model/app_metadata.dart deleted file mode 100644 index eeb599f..0000000 --- a/example/lib/src/feature/dependencies/model/app_metadata.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template app_metadata} -/// App metadata -/// {@endtemplate} -@immutable -class AppMetadata { - /// {@macro app_metadata} - const AppMetadata({ - required this.environment, - required this.isWeb, - required this.isRelease, - required this.appVersion, - required this.appVersionMajor, - required this.appVersionMinor, - required this.appVersionPatch, - required this.appBuildTimestamp, - required this.appName, - required this.operatingSystem, - required this.processorsCount, - required this.locale, - required this.deviceVersion, - required this.deviceScreenSize, - required this.appLaunchedTimestamp, - }); - - /// Environment - /// Possible values: development, staging, production - final String environment; - - /// Is web platform - final bool isWeb; - - /// Is release build - final bool isRelease; - - /// App version - final String appVersion; - - /// App version major - final int appVersionMajor; - - /// App version minor - final int appVersionMinor; - - /// App version patch - final int appVersionPatch; - - /// App build timestamp - final int appBuildTimestamp; - - /// App name - final String appName; - - /// Operating system - final String operatingSystem; - - /// Processors count - final int processorsCount; - - /// Locale - final String locale; - - /// Device representation - final String deviceVersion; - - /// Device logical screen size - final String deviceScreenSize; - - /// App launched timestamp - final DateTime appLaunchedTimestamp; - - /// Convert to headers - Map toHeaders() => { - 'X-Meta-Environment': environment, - 'X-Meta-Is-Web': isWeb ? 'true' : 'false', - 'X-Meta-Is-Release': isRelease ? 'true' : 'false', - 'X-Meta-App-Version': appVersion, - 'X-Meta-App-Version-Major': appVersionMajor.toString(), - 'X-Meta-App-Version-Minor': appVersionMinor.toString(), - 'X-Meta-App-Version-Patch': appVersionPatch.toString(), - 'X-Meta-App-Build-Timestamp': appBuildTimestamp.toString(), - 'X-Meta-App-Name': appName, - 'X-Meta-Operating-System': operatingSystem, - 'X-Meta-Processors-Count': processorsCount.toString(), - 'X-Meta-Locale': locale, - 'X-Meta-Device-Version': deviceVersion, - 'X-Meta-Device-Screen-Size': deviceScreenSize, - 'X-Meta-App-Launched-Timestamp': - appLaunchedTimestamp.millisecondsSinceEpoch.toString(), - }; -} diff --git a/example/lib/src/feature/dependencies/model/dependencies.dart b/example/lib/src/feature/dependencies/model/dependencies.dart deleted file mode 100644 index 3087822..0000000 --- a/example/lib/src/feature/dependencies/model/dependencies.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:spinifyapp/src/feature/authentication/data/authentication_repository.dart'; -import 'package:spinifyapp/src/feature/chat/data/chat_repository.dart'; -import 'package:spinifyapp/src/feature/dependencies/model/app_metadata.dart'; - -abstract interface class Dependencies { - /// App metadata - abstract final AppMetadata appMetadata; - - /// Authentication repository - abstract final IAuthenticationRepository authenticationRepository; - - /// Chat repository - abstract final IChatRepository chatRepository; -} diff --git a/example/lib/src/feature/dependencies/widget/dependencies_scope.dart b/example/lib/src/feature/dependencies/widget/dependencies_scope.dart deleted file mode 100644 index d44d74e..0000000 --- a/example/lib/src/feature/dependencies/widget/dependencies_scope.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:spinifyapp/src/feature/dependencies/model/dependencies.dart'; - -/// {@template dependencies_scope} -/// DependenciesScope widget. -/// {@endtemplate} -class DependenciesScope extends StatelessWidget { - /// {@macro dependencies_scope} - const DependenciesScope({ - required this.initialization, - required this.splashScreen, - required this.child, - this.errorBuilder, - super.key, - }); - - /// The state from the closest instance of this class - /// that encloses the given context, if any. - /// e.g. `DependenciesScope.maybeOf(context)`. - static Dependencies? maybeOf(BuildContext context) => switch (context - .getElementForInheritedWidgetOfExactType<_InheritedDependencies>() - ?.widget) { - _InheritedDependencies inheritedDependencies => - inheritedDependencies.dependencies, - _ => null, - }; - - static Never _notFoundInheritedWidgetOfExactType() => throw ArgumentError( - 'Out of scope, not found inherited widget ' - 'a DependenciesScope of the exact type', - 'out_of_scope', - ); - - /// The state from the closest instance of this class - /// that encloses the given context. - /// e.g. `DependenciesScope.of(context)` - static Dependencies of(BuildContext context) => - maybeOf(context) ?? _notFoundInheritedWidgetOfExactType(); - - /// Initialization of the dependencies. - final Future initialization; - - /// Splash screen widget. - final Widget splashScreen; - - /// Error widget. - final Widget Function(Object error, StackTrace? stackTrace)? errorBuilder; - - /// The widget below this widget in the tree. - final Widget child; - - @override - Widget build(BuildContext context) => FutureBuilder( - future: initialization, - builder: (context, snapshot) => - switch ((snapshot.data, snapshot.error, snapshot.stackTrace)) { - (Dependencies dependencies, null, null) => _InheritedDependencies( - dependencies: dependencies, - child: child, - ), - (_, Object error, StackTrace? stackTrace) => - errorBuilder?.call(error, stackTrace) ?? ErrorWidget(error), - _ => splashScreen, - }, - ); -} - -/// {@template inherited_dependencies} -/// InheritedDependencies widget. -/// {@endtemplate} -class _InheritedDependencies extends InheritedWidget { - /// {@macro inherited_dependencies} - const _InheritedDependencies({ - required this.dependencies, - required super.child, - }); - - final Dependencies dependencies; - - @override - bool updateShouldNotify(covariant _InheritedDependencies oldWidget) => false; -} diff --git a/example/lib/src/feature/dependencies/widget/initialization_splash_screen.dart b/example/lib/src/feature/dependencies/widget/initialization_splash_screen.dart deleted file mode 100644 index 1e5139c..0000000 --- a/example/lib/src/feature/dependencies/widget/initialization_splash_screen.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:spinifyapp/src/common/widget/radial_progress_indicator.dart'; - -class InitializationSplashScreen extends StatelessWidget { - const InitializationSplashScreen({required this.progress, super.key}); - - final ValueListenable<({int progress, String message})> progress; - - @override - Widget build(BuildContext context) { - final theme = View.of(context).platformDispatcher.platformBrightness == - Brightness.dark - ? ThemeData.dark() - : ThemeData.light(); - return Material( - color: theme.primaryColor, - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: ListView( - shrinkWrap: true, - children: [ - RadialProgressIndicator( - size: 128, - child: ValueListenableBuilder<({String message, int progress})>( - valueListenable: progress, - builder: (context, value, _) => Text( - '${value.progress}%', - overflow: TextOverflow.ellipsis, - maxLines: 1, - textAlign: TextAlign.center, - style: theme.textTheme.titleLarge?.copyWith( - height: 1, - fontSize: 32, - ), - ), - ), - ), - const SizedBox(height: 16), - Opacity( - opacity: .25, - child: ValueListenableBuilder<({String message, int progress})>( - valueListenable: progress, - builder: (context, value, _) => Text( - value.message, - overflow: TextOverflow.ellipsis, - maxLines: 3, - textAlign: TextAlign.center, - style: theme.textTheme.labelSmall?.copyWith( - height: 1, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/example/linux/.gitignore b/example/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/example/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt deleted file mode 100644 index ac24608..0000000 --- a/example/linux/CMakeLists.txt +++ /dev/null @@ -1,139 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "spinifyapp") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "dev.plugfox.spinify.spinifyapp") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Define the application target. To change its name, change BINARY_NAME above, -# not the value here, or `flutter run` will no longer work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/example/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index b7a6d08..0000000 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,19 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) screen_retriever_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); - screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); - g_autoptr(FlPluginRegistrar) window_manager_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); - window_manager_plugin_register_with_registrar(window_manager_registrar); -} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 913ac71..0000000 --- a/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - screen_retriever - window_manager -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/example/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc deleted file mode 100644 index c44e7ba..0000000 --- a/example/linux/my_application.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char **dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication *application) { - MyApplication *self = MY_APPLICATION(application); - GtkWindow *window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen *screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "spinifyapp"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "spinifyapp"); - } - - gtk_window_set_default_size(window, 1280, 720); - // gtk_widget_show(GTK_WIDGET(window)); - gtk_widget_realize(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView *view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication *application, - gchar ***arguments, - int *exit_status) { - MyApplication *self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject *object) { - MyApplication *self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass *klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -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, nullptr)); -} diff --git a/example/linux/my_application.h b/example/linux/my_application.h deleted file mode 100644 index 72271d5..0000000 --- a/example/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b..0000000 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d1..0000000 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index b622947..0000000 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import screen_retriever -import window_manager - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) - WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) -} diff --git a/example/macos/Podfile b/example/macos/Podfile deleted file mode 100644 index c795730..0000000 --- a/example/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '10.14' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index b26d7a9..0000000 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,695 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* spinifyapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "spinifyapp.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* spinifyapp.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* spinifyapp.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/spinifyapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/spinifyapp"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/spinifyapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/spinifyapp"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/spinifyapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/spinifyapp"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index c30f04d..0000000 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index a8565c3..0000000 --- a/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - //return true - return false - } -} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z zKpxF`G*X(YOz2^@7i#D+s-~A1E;3&x%%qL5hkiy^JhYjJ74{hvVmAx*6BH`M`!qGC zO9pjEsR)A-n1`6KLACSL%FS_Kcm+?4*z-V?WAZPs?RkzoijIr~I+oh1^~T`q^dCFvG$Gbd8AnTYBjLKYUmayaQz#S1le7Q^Hyr#;X&h*1wDpm+gZC!rSKom zq|+o&UGpeXtlQ1;?@JukKG!8PGS1Io0z6O}ZeL&DsON^I0K+>Mxv#ohK+;ByAZ`Eb z2orY{j0Pa3edA(#-pJA0AaJ6h& z81Gl(pd#j~mrizktoid14K5ig7u8FvZmLLP%l@dl05IprCyqDB?mA2fc*6UB+49lb zZ8`V9epdo=OeZoiY%zw-w`8DNwTORV_>>3T{r)1-YsGSo0E2s>tix9OBqKFBjg#}G z`pgkCblKMYs!Z)r^(qT_c+}gLhR|gnq!1~Qr|~kt&2@_yswx{i$KEn`8J1W8BGljl zr@GEG#W(s#AKKyuqLp+cl1C}7%`m#-!$15XF{M(M*-fD%+i#mFbP35jlgN3{8#A-dmj&OQtG)!031jTwGMal=&YtPfq2AUWekP9J-JT(p099!L`+yen$ zVH1?kRrhV7(mGKkm_jPP_U@Xd;x=ppk}4WY0Rbr> z0MJM_;$GGxL*P68y%KBqHntF{>X&<{aeI4m6+{TQ%~Zp}v%Pujr)zg5mV;cFKqeA- zQm5`#Sd{B6Rc*4PS-rO(vf>YEdXmOK?>K@`L5}|9q}#t_IE%g+U<-1qw3mr5&v;2A zCQ}BEn9_u;;>n5N#dP0RhCF-_UplC+U(i~Zjh>U5+b8%@p3HK(R*IMQwE!uritb}< zF)AK2?+0@-aE3LYkg`B*&N&m~JWB9>(Z>`aqRwgioU)0w{U1K4?>-#i|ZfhNa9hV)2)(%ch zJMH1twoeZWwkE@I!dz$ma+;9GeACv>Ncupl@+gBSeU_uzfj!$+h&@EACkZG_vwLGA z(?^;rcJu1$5H~xI@6lHIYC-$+b&hF1p`AoAOKqw{t0Fu#X`OGt$)7Q!nmJ=&)xjq@ zHoxT4pcYKSPT5(4yzIuQ^S*N2NJpR4v0?rB-^JuaXNLis?E(l>Jo8mUw(gsFLLOy? zEszHWGaCn|lw$LSwoj{G7Uq(zK0W^VVWu#ms8BMRlF2z%-g`fOXmndgC(na8fc)s` zz$GAoxP+l|+T_S4$r1sLwkV77ew1Gug*`|HiE*?FGLm1q; z^p0A0eqqbmk3?|!CB9DBN1Zof6d7+ zJSn!`VD~tVaqy<*Mw^8dM5v3Bvj2VdVFb=)U3L2eDM3@>n(P z?Rr_=I17+r4fE{>1LBQG0&o97nef67n-aNnVP<{dd6*B!Q344 zZbsAof&jw+;CLeK2d87t9s~YZ5?6Qwf&{NPEBN+)LbjOcZRXNcR&h)x`TtdpI+b!>$E~h0o1L*2OddpR9!Gw~-E^Cj(7i69S<66ak$)AYMv|xG+;uR(`;h zGIV3}?+Qxdjz)s;s}jHY{JPmeo@-tN$H@hxaV@)}K?y~ts~E6H(F|SlsN5oH8g7*h zGiC!8c1doE3U|D}Vul1yPmXuCk*hmyU4MG2ml#V0+(G5I+`L_=3cD$%$I=@*8m-LU-!fn&-sZO1%ls63+w}AiAK`Jv z>`q~ztr&&(gCkFpci+*1Ekdv*MhBCzGfPBj9dM|YEjZk(tWBuz4?MGeq+*)t>Q=z6UXF_w z{QDUT4^JQ8J%hW;d2xGB>Fl4Y-bRT!ttP2GE5jYoI1e(eVK0&V5W+>zludt=nf|UN zi1IV;MK$Fy%$yw<oGeW?JIGjmfGLH$Y;l|T0p1V!N*Jvu zHSAG0WpwPip0vm7%VRq8$2O2>P5b!WBfTz*6dZ4Wd6O9Y(8A;nOuG((y?F`ac_u2( z#~17CoTK)1G<~~Z4jXlout{e&nZbDHyHf(=a?OtaJ(2Q(!g#)Ugw-QQ?A?mN#yN%T zBtJ`sA6Lpg`k>Pi8a7GssiY$eG0Be8LCoQL{GDqi-;j0pLmT!Z)szldvbN7GVcu*S zzb1rEq|M)1qa7rM*I8!<#w7FnQ?{v^? z0`MlS3+`#ZB5$DT4+`7e-Hlp_2G0`*F@STbRJ|!tk3cC~1T%NR-p4s=sTT+RqsMjF zyrp-Jv?CD4Y3N&Zb1gr=%`MFR8;|r)uxQ6*X{OpEhQ~+tu}^n8Wijiy`pSMw0uKNi zSNX^Z1y;WirM0o_x%zft0U2GcLm_2BS`b{Z>g|9VOVr%QF*R?pTpiJsEbj4jLVAyd zTA;x15=f~b0^(e*Vo;Tn;WTJSxpI9LmL($Lxob<^S!k7mGhnnVNnAC*g!$ms0#Q|q zs=25I0<>fUw_&+KU`}5P9wlmjRWdMYh%Np6n?AAHQ;JzG?s(Z9UR`pNh79Nzk~DF+ zX~jy>>f-2bl?drlM8 z3NfIQnrT@pLmv+QA6efWPv!sqe;mh3_RcOj5>Ya;4hhN13dtx*_TJ-=kX_kZQDkPz zIw}#e_dK%au@1*L&iUP^cfH?zf1iK)tHv=t|>-9mMT!;;Vg|svSzWkN7q#t$c4N$Q;tl3EYwef_4q>GO<#I89VhY;`X*hz$n*GZ%f+;uViG z?uLlxD1OIeid}0r9%Ssoc7@vJjZIsZlU9zvYpjhYiOrzD5sq3OC zpf-X;Nb!DLpxqX^zDIK%=46-Z3%i-bac`RIBS5*wcw5Pu>G|kF>TQP$dGRYh#1hwD z{|cbbTOKL>Gb1-;X6?vWLC+KJ_^Ij?KzJ7eZ?^8XNgoYU9^z&>d zsIjX*uOK`#Wu!`>L@y!=XpQcW+mBaRjm|XrB@etLdr}Ob57e7EkE;7a*t7=M#XFL6 za;KHHk-rBNTjp-gS^;ehKNv>K>+_jPQ45J%4><1HyKJ?;T9#~k_23?xD}B&@Wp{%H z($hU+nWR?g!9dsJkgVz(J_Yrdns+m~9V_gQ7Sb`&F4wZZ!k}##j$>O{4{?avCbCZfyW zO$)m7LE=P?$CXHDU_RUD+sYwT;nKI7 zSs_XTv!BuxpJ!7(b~uYfsgzt~mj5(vf2r~`LHwpePs!o2A3zEr@#sxo8HEe8>V||d zBiz0@e&6}p*}!6jsm}I0bN9Mc2(c#jg@;Nu6!Kv&4&P8-UcQ-00WJIO%4OuUn;^jU z;I3r=T3KQtiMQ7&x32eVtB`mCe)9ws^7u%2P`B%Xc}=Qc&O^{FmS^{~Rho}^s`B+H z=1_T);9LRK?{$Vx22!5m)Er8aoPOA8&{7fyt`t@~Vw%gtx~+g3qs8LFR%(2Uny28A6dFYnNQgcUa>Sq=%alFh&8#@1o_qgwve* zVFimnUtL{4aHP6s?FB%bu2SP=e*VGqXC8iuZ-JOc{5%Lx0g|VvyWkdh&FD^Gkc!0N zhoolXvp6GC8wj?Y+V;r*EN+<1ac`-+!8Mqb@Nz)=OqV?4gxhR^t7*+^+AfxxVt(n{ z+fkk|-xSGqmkZa@Q%`;;r`-Z|? z0fR6b@l%pTwK*@xY+(MwBUwf^z+F*~piC64BWTrz}-HS1-XF-IA%?Zs_#F8 zcmUuEZ6Of>YIJOe$&{V;3vIBw7|jSGPeS6cvTMdj96Y~pI-z7InGW;(DhFqaiTTO9@KWvQi9__j0btLZ9 zAa~-Po%^sDFfme4@Yiq}r`BgnYK2eTwCjg9_zC4V{{&_GTm-!qHGVR6JXDjw;}GzF z6lXA{xo1+tQM{9vwb1&sRXPdGDHbEMbnwh}t+%tvcw5p4J4r#hEpDl=A{;Mjc%0)T zsG}v<$^HhdcE)5IJ^iBWK{7?Zn)vb%c!5eIj4 zbT}CGO*u)Od@^LuIC@_2{=AP2-O99NglFudj{!T}0e8wtTQcB@F9QW6$J!0Ye`T+U zXDx84b$!hD#4YzSyZLy~!IIZuFa3%eU zG4eg5?}sZ6Yj29P^-PcXG*8%VzLL$0!oL?c(!oQ+G!kORsa+lsf5YER>PX83R4LgF zgPNQJ#Bo#)MXU%J9k?RWD;c>|as5b5p>xAwau=X5XbERX`_ZHB8_XSNDe`s?n(e>) zGF$G%n6o+W{6A-@4hsIK0*J%jpB#Y*G^B48eQD(CDZR5oBl-P=)r7fH^PLf?!aK6V zwkIM35?l*I6p@;^H}JIDNs-fF*IFN?k?kj(M)QKM%%?dSkf1d$Nly2z(>)oq8z}0H zH?Qa{x&36#W@y04!9zx@x7un@ob$&)V8#f~0n1|jF0kFs4aZ{ND1~QjWHToIY5)LY zrgKDCj@dFCx&-w$QMi=CqD*=`$NqC~2k366pPXl#>Y7A=iQD}f`)+B-pS@LIW_M?9 zlBS_)(vGz!L$#P`?<3Hvonw@B1uJ244y)M?0)z0-hq++sJ0GZ+{oiiH;lFi&wy(C! z0Bv9z^M;`4@)USP)7dhg@K5K&U&|7&-@I0Sk>I+ZH75_xEn>qh9qmc%aA@NEKBsVBgUuK zC=b{w-0oU|)~tAVI zyJ3BAB}%rsjz7qZ?x_XCWe6!_u-{e_3u68Asso0IvwKdxq1lN#%4w>J zi>}P;$JZ>58(ZAjsmSJl6BWUTe`0eGEf3f_yS#H6vx;UJWO7CCK!{)4C}`C$j5gNj|k znb$4QRurEE3tPEe!JzG-a0DmvXePO zSD#Q-qOAjTMm|=aBSnvwHoEbgyVIz@J$hT*legak-hhb}e#%cm2$nR2 zV9A{kc)WT$np=5coPQIskbGMO@Fn2NxPv$@SJZdG6}jV;+%(cH+*RFQ(+DjsJlman zy`D(yN?8MCtjWD3w}Q|jQccb$}BDW%M$zZZnri2+5ls)@@(wQD`jt_GpTKL_^CO&SSCcHbfMX#JXYFI^*947 zPh&S-G=l*C@`E5CU1$m7ao(Q&oSmY7)ZZ#5_fEyYzLsFJwJ%GfErFeRN@7lUbUrL| z$6;gQSNsI91LJvT+$Zb0>g<4g8T{B!U05lfKmoSRH^pB^^8sJ3{8PzVq0NeypMF5k zU3qOqksdq{>AUjm3O~dZx^vS6C$ldgCWszl?xd8-sJ;-kPnISB*-f=L*8XggOx$?u zg%B-QovSjBbj}%sShZv~r?`*6PiiQW;nee<-=+y4}S#}q_BgXIJoSOf$YbE7vXt4;Np zrKzZf6Ny0aES8(-cqmnIGMg&ieYWryBZ0VTB=4<*@auP4NdIk&q(Mt(OLPm|Yl za!0OpC9sA#tk>OsaCSx0;!$5r6naw ztzLBo>#LKaxxsO=yWe%yGilL`A|6E#TK! z+1VRQlo*D?(k0-mlRM+`OMT8kVB*-%ZGv}Aj1u^j!wu*~>L<-T+u?6sX!3C}lQte- zk(6_=iwXsQ0JbRvJDwMnk!c99w~s~uD_4vMB=m~-ft-*|z~$*g4g;pgG~Ap1m@@Fx zWS)8IKSN6`^vVQ8hv^Oc+O(Rt7!U%wVsGP+Y6fyS%GG+v+dIdVfCXPzAV~~li+3m5 ztFQmbE)(#2#Oi@k$1#zUS6ijD_yYsa{+BHZAw+^zAEI3bc(h0qm?|pNf?oS}Km#OG zrOfCKn_-CVO;}DXu|5YE#d8I2o>}vUxYlv&>=+I28WY>a1;uI)HUM_IvpF;Ln4ROT zf!=1rpKihNFUo=R@sD-pT!EOm%%ncl43f;aem^;|A#s3`b6vjeAzO!M-gwc`-Kj~{ zBX)tq64*kJl#TrgW4o%hTY3x$P01nD6a6s2#MmwM$vyX5PU|YngU*wXGK*?f?#Eg$~^OWW3I@of-=XVuu-b%A1Z|nqY_2 z;~jD&=QnB#WGU>;RwFq(I< z34K1fCMwf9F}G%k(&?~2EY&)W*-_z0ReS$;7+I1)zz`)M zpAF{5ZHLPMJhYU z;GE*@hM1NM{G{L94dL$!Y-h6A9K9W=I6AYb`Y=v{(tpyLQz^^Aibea(q()R*TU|-m zozpyr!|-BZ_Dn+$*2|vq2Y@ghHo!-`WjVtU-bab(SJp2*2i-}$UP9^qnF_OIFS~-< zYj^VS!)Wu}vn6!LDIt!HJ1SU-@ce>z8f4cT4R9V@O^Xg9)4`VpjsXm*~@%l^Ux;Rf#Zck`BNXu0Y(!C zj%Z}UAmD00nsOS%Uull)dU(fZgJ$bo>3Oa`8h~Wt)EM?v(ndlTS1p0|E9Pg>=&>58 zghD~%R;YpqZAw;F;M(lx5b_wkVbnd+ER+6A-SYj^1XUgNGn0I~ES|f|5emjyPIW)S z0z8i6)BZt&h(qQxih4HbFYa6~jyeKbc_`QEdLD@9SBGButjw|b^l*oQjDk<7Nig08IK zb`ATVGzK%LP+>9aFM0hr8t+m`uNr?h&8o3Rp$T&ql||K}7GgobFhCViaDH~+F#yC- zt>7T3&_PZ*feTKTyd6vlF~JmEA1f+*>CCE4ex}5N^$4o)YuxX&3T$P0(IS!+kan^J z_p>v#1J8bWELml|S02YAQe-&yVew+kipZr~H-I@yc$=8#rZ-8L<_nDx&Qv3dJDwUX z!)@=h1`~R2M{$J8bM^1O&Gy2oxe1T;K?NA{iv_eYuhpLyc3%xu%z`dVc}Z}%cHGHQ<7P!Q|e?dwnSpL!AUf!B^!?#^Q#W!Ry+7ofwPZ1mZq z(Id0{htmX1W?2cAYWZo_lOtT#+Us-nlP$=CGK|Ri4x0Xh>(|iN9y1 z=9y26A4Y}ViRi9Fxzm{>J`YM>GX1D|$4BY9xJrY{oY2~Z&};B{Zq9Pp!pox`8e#0C z-h~@fohA74(#ws!{7kIe4v6XUX<)9bd)g66Bz%^Y4p0~OF+rY;l$v&7T<3~4y!bv> zR$r#LblZcVgy2lq!ff+>yuR4qCcljQa03x|dTcG7`CHcxh#POtGKt6ymNd_0qF7Wf zBj_KC8{jl!zZ>0neDp19n3sD?HC=|WM3!}cK4zCnu6Uoj*hbV1<#F2BD)@A~y%@VXx+u}Hcn=_s-({PxzmMZ^xJ1SV zoZMY*FarYvO_@z8Lr2ep)%HgIL7rhYa~#X&&V8oYSw zA4m{3{hw1Vb~~26K^xro&e7i9eg^SqK0i}kG3z(!_~E?sjJlSWIWXJqKiHAWTG*SpPcCMD`kEc1gx`R^YkYWz zEN4vEIkj@&e4tC!(_~x`-K$w6CU%X7U2Y z)Y}T5stEyoSsB{H{+xfST3tov~6@lO}2gx#N(rHXiOAHT!dp6FiV8V)B4{L_P_% zmX0rPa^-{1xG6|#uEGo+!v)QAOjRe|jg2ICcXU!|Cr+LMbLHlhJ)ErR*P9*z$NLlt zmYjAUbljq004ZyOco?HJovV7M*Wb2nF8vT2D;3kGi%F)6Kr#TVW>}zTHnUQxoGmD0CY9J`|d%8@}n;_co2q zWr98`R_c@PQbMi}x3bWo4XZj{it6qYj+o*XvNoS4>rF;7WNn;vA*|A!3H}Wh-uk@n z*hV0S+XnX;K;BOoz?&*9_{NnM25s4^^QUt|>R!()^Z6#G3OmL{CU^-IG_M7_a~B+& zCrV;ouC1ljbK(K=ygqAE_-}ewnH2&&t0enS7}I4i0wJgNvCf|P$`|DHku`K`HfDa2=n@DCg8MRi_)vpMR2Mxy4PE2Qe! zD||kNXy=0WeU(43v%md9Hg9Zu#CP%d%C67gk_#pfXs8lf>M=betm(}0fdDKq0{26# z_c?J!Cgo-~*=wswLXkR|W8d+rDdV00`22Ouv=_Hod9bmB!=D$I4r@7DZX7e+0tO!9 zR{0d}A6^K#yRx@ykotO4(WUJsmFvN)d-o-wZ(wcDSUS`8jO-JSAMa4y@MK4fDP`(P zzxQ2})ofiauWKj9{Rm$Yw^?g=?`oO(Vf|T^I+-A+o1#F`>tn59d=FtgVJAV=y;G&` z0GMvtEeil5;e$Ln8-41(UeMl2kYLk%vPl?0+Egg_;g)494o5FsvdeZKP;&&fjw7o{ z|B+e%Z|)8Ts?=>@p|hr!nYXgV=ZjI4Cp#$E>+g^6r7Nd3<>-t=G%B5IyZUI{e{49G zqnIXEB=M@5Ndf1J#l5YWcLG=A4ufF8S{z5Kz-uM?Ni{{%mr);=l0=473h#cIc{K3> zZ-VUw_Ng5^HgWQhs5tQU@qv-YBej9`R$a^|lknX<*+sSVXue8M0#EPBJ6_Liwl*8l z_zoD#!l%WIXJZ$jm?|zUu0LdeP&8IW*(|39&QzKGnem$6--u{ZGtHt#Hro*h)?lu zXGKo-4Hv1WP*VLj;uA6UwGSV*6ro%PRbwR{@tXoCOb=OFTB4ru-|Id!rP5Y6LF*-D zy|t0qDSVPo$ffyoj#CIZV?l3VsPRYye$F^xxv~Z78_fwlCWbwW!nYCR2nx0_+@tg3C_UDMVa2Br=X3hfP}^Cp4Yg=#OK}K zKYVY`V9jEKD!UrCbSX6Xym2T-cg}!n;?;o{mM|zWj0P@D|FO-rQ zKt#ApEh#AX%_f%9!G6`I*K=bSnMIhQ%W5&BOMntzVr*eS;WR;FgM)+k`#+Vze*z&V zkU^I-R|!Nwy<~>eeQ~hJqa2|DdpX15kD=6U73Du;T|VarycBP^n#IZeIJ&H3S9#@oec~poZELqX$DAc>XZyuIqd^GK0Jq~0kI=d zA7gMo8%zmkEdnqMh)tkp?V0I;Tm3`>aU3^~dXw zlhdd3=iygnUgYu#GRhxln}4D?Gokczq?T;RjCk0=fUHy18$lt!-q!%sNxee7No^+N$9d?Es*``)0UJ4SC&FNY0pf z_MlbGdUy$|F}YDvJ9GTCkZbsNKj3DL5;=BGBx8xI;n)=A0d0j6MP7Mi6MQdk@Tux2Qy`oI_&*%EQ0bE?|R>P$rDhcFa8O?JIK zPOpFDa?-L*+Q7RrCg#y5z$l0d>n@+OYo3g>-Z*x&`Jj5|=*UOYaJer6;FAbdtt0O? zrFGUE?!XeUG}G8wMgeTs%+r;3uUU;Nq5EuU{h-g&UOBKhdS`;J=m!~xn*ztv_p@dD zR)tR!P=~5kX)FRsx9)uyuu?0dh%Ht7`PTM@e#Cq!z2ts;O;L)tQ1ipDiWqbGz@o_p z^D=UKR#`S7HAt4vQtD(_SeWyj_av~#tJKlb9>-s5Ykuzx_E1ZNl4)~f=zG$*;-y=T z2ozmFva9az<{2&63fQ?(Q8{IPx@t1LuFcxP-LXVctWh3AwazVTt2)w^*Zn-#eB`bD zSHoAusjOBK5(>uQPGj=ijdOH3jqG?(<5#C{*JQ?Lt~@zow=Ii4Al$Vr!#+Cf-gx)A z`_h(>b@7?*6bYM8%628gGW^rwWoG$mK_eCk`}B&llStfwHf12*{5spmTeNH$4{gCY z@Yuwr*k@%m;T<60bw9z6^WpWi@Bu^qe-g;YAzI+VjgsuZaGA=^G*I{KLy@rIjSpWb zFQNsCp2T;S$VaJtZ<(waRu8y7^X;>YhsWp zM)mKgCeE@K;J4vQSV z&-(Gl5AJCp>K*2-`U|4i;u3p8xo6(isu-38>cY zml1Eo&FBBKJpour?}q&nggpFiGM%m+YX`ng8P+uRnJiMyWcv*_AZ8KAB$w;rfmN8C z<-2EB6TqZO>A~P{*<);wYqZgxQS8E*syOXvGkGxF@s(scud0uv?T)fQ z(DGrwM7lvpitUG~6!*}kZUpBn9PuP`5^nMK@($xI^0Q~axP5qU>L~uF{R_<9&m z({}$$WuD1y-QzMVb3jLPk`~bDJNkw(Dv-6cKUb4uzD= z-w?i0NZ2K}AbT}Zi^uOZ32xmSxJw+6(3j%a!~Tdy-@RxVx6YUw2|V6JX+mSJNclfl zF~SD#eo+lnB=ZpHLl{)E+`sI^-V1Vn!6#Ml_W4aH*Pe(++sNI`M=5L3?X1z0;CJeE zJiX5Mp6JH*=R9W0t(1@>>1y=lP^F=yJil6JxU~I}EpTsBx?rJ5LbCbQ zuLBmmX1MO&!E}khx=+#hCesIB53`IWwqyFtR{AUv7vJ{Q^dn1S0@*^UOmRwctFy&> zd={(J@avBzmu$MbyamRMt_$kfHY<*v)%%&nY4hUDH=$k)$8LHlUG0G3Kv#T~-vQjw z)hXbsNIg?~b-jRw)ir5Q(gfwM+Zk+0haf z+4ER%>T8RnKAoJ-(s&tu&-iZ@A?^J|d z6md=9C4am*v2r=aa&a?~37bc($n#wQ<8UGXL+!RtrRXGSj-2INJ#+3J=}e6nOC}G8 zN~lvCS@rxoq7w$CLg-wx!%V%ymw>~xhUw4cADX*$A}D~{21F$!Y61aHwpdL!QcrsN zl~$s5kk%7HWHkZ43%mOcwlk3RcbKGQ*}K(Fxput)rpE0zH0vY(EyY=blQZ`odG#hD z)~{&r6XkSE(^csqsaMm>2c%xsT2&g_Nab1bTY%fIoNHatDY@C@Ei~v@19|F?szU6SWRS)uDXqNY!48RlAb;S*ijqus; zp;bteR835>3BXML2CewOM<^q3M*ubU`}gnI-oS&(vf=GF|JJB-inGOH_dc1xb|iqR zWgrcNy?1*8)vAlAaiBE%K3Q>5Ygy-#Wf$>FqL|Kvgb&6H?iQC*Z|PN)xZJhH#d#=a z@s9O0oea6Lg}submzNZ{iZ*_okZ$6G*h5YO!dE=7c4=YA9g$y%1xjkVl#|1DShEjM zH3(sS?uRfB3mhW5Wrm} zrY>KpBxM&CC;s5Ie_{o}upN{vdb8x<_$5iiQN49`z`+Zz`&E`yLAim;X&}$HAfKmT zkO2Dgdno95mWMH~h2c4);H=MigT8hyzl|4g;dU7F;p^X>w!fa0zf{^rf?>~ z0w{=F_R}ru{g5i@&xwC%R-!-1x|(k6pSb5_)$f`zyErIvSCs{z`iVvU4x_znFKti!!av6BkRX_=+kEc;*`_rla zB`g4ruCJGT3XVTTrlh3Yj>1>PNIy?sV%Yo*=qaBIOY87_?P04yx6TV?_{~K? zOHEo3|2EA2JAMPYZM!H<{|!s-$r>l5{19icxV`Wf-{<0I>{v&H4FZaCy$B6Ludz{v zRH!!HV#JGP?5(L!Zp#}NlOODgWqjO+yo~+LasPYxH+ht2KjdfCFQr(oovP3?vkFK^5FvPJ4^LD=DpYQi4tUXuY1;erJaBQ79 zHcp(>mKvoD+)bq5SX9siR>(%CL??*D>Snn%p}NfGO4(RY^puLI+j$Pw)NZLb5bKo{s|0L~ z-A3R~;QHMg0bHSgESOM&N&@oF4|8gkPF-nVM=sQ;d}wcS{{!iW-)yQ``D6t#xlh(O zRF0Z@O>0uMz9g)u{P))ptV5lH2(gC8I5i(FDRG5Gp1bgBydKgxJy5gBfK(#D7NzZU zatG}S^z#KL*Do5=K*F7hk(`mbdgI1XoM!8*-};#UzNtEG@Nki#`7)GfV;VlfW^)=` zBaAjK5>gx@wf_D!B!2C6xBK^K4%x|+#?P@5N7tlfWo6xWJD~Wz^cnPfFF($Ixt4!j z9%x^1$on56XZB0Irm^kw-*rd1YVO;(*LbB21@7OPJspo%WO676#~oUMws(zP#+shG+$ns0IC3W z_{kYU>N5<_6=j>*0d}r-?8U+--eXfy2M+opoYL|=I932TMp=&k#tzJ^72OtRJ8BVOvTYPh;@EE=LJLeOk`y?d|Dd9%fWlhON^LnB^6x0LyZqz@imyogJ`$C@Lr9Z4o)ZQz>NCavG$$@e2#r3 z4I=}I5KgV>wl)~_Ja7gLQGju0c1{h%cV&6c`doWWv$>q*=ZLc8J{hBiKXNK?zx2Nr zz!pph;BLU2OaZTv>Pzj(VpSp2&OWNCF<~>NgL!nezhxEgj;&2 zl>z@V#>sykFCnFL?|(j)J3SFr|FFa`n@KbhC2pZB7 z#3>qIn&~mG_Vki=p8_x&CFeD4V7MvgJlk^G7H;(apFxr+7Gc0+1KfI6$@aeF+d7DJ~_-A|H=0?Da#&^Cqb=!=fVz>giW5nw=jWQBS%L^t1EZ@ zCm9;qlG{($@0W3T&l17ownc5pWhfM8Mwn-fLtb7H|IYl)8@QikEc_Le+s60x?&B*m z5kObB5{BD}gGr7l84~vP{N)C~3V;xhBWd%=^j0&KBw3T3-HU`;hqWA3OWW~<8nl-M zfYn-BI0_?g`3$_;&Exw<(G{QM|8)Kq28x9NF-F$>r@_BO)t^T*i-U1bX01<)zC_uE zR@8qEQQ#cm$YbXIUPVO?z7KI$pw@r=-V{V@>dC9Hn==1QBVy_b;#*jR+&f*$AwCl?o&G?2Uk4=*Ej zFK^Yvw*HTO9n!XRBWe++o3)4O!OC9PC=_l_<$M(W8(Akk`zv5?nJifb^rH3N?Hhio zo$=nNmSEz_QFHj|XF!vQEcdqPyZz_4|M_GBH)k)KA9XGRlTJD;3*y1c#?ZWkeaQM* z^`Bf04#Z)ARgrE4rMmlk8E5F=NpaW8xKNd3)-orW$m+kh(W12jQbQ7oi z)=#qbmhkplt}u`FC0sV9sdnb5$E!zX_xlA{4wW&j0*DCm`=1;Sh_sB1xiH@C89Z93;8d)EUk=lPNIZ`o3H`Vd+Ig`=CV}#?PAXvzWk{x96fn z0(rYh<>?PJ>Hd8v@c8=*vm+)>P1k@i2>yMaKw2nihLV6Z;wcdc*E2{8=xNh(FkEe3 zq_pc;ISw&}`?lqKx<4vIa67!xu|P}G$c3MDyg?u^InS?uM6Zzys0QM9ChW>g-ypzA zkOUSfvhTTWq{_>TJ{+kpgwX{@>P5ptiJ1NTO5)8 z8BiLUY_!*AJ$V386^TicK@z0qOPWP#Ea5?}!$_&fQ zOcRKuR^tLX*&CM(ahYftiNg!a=uU|He)2nU2(~iX@Yo|foZp906;o=d%aK09YEW7_ z-yX*;XE#z@?zZ&fQ?2fYX!T8@-$(K5Jo+AkyOM+(944x4B%2NR&avFFJY^9_br5UtzSX5@gmYYm@ z@S$jtqFn18bXQr0IYhQ=+2~ZDB_DRW3d=*B+3q`-*1P$i!GVIG(AMp=vBQ#^_mNxp z(;4Iz#_~&9jZ}}7oW?R;_x8&h?b0N326NJq4~>W^TeI^!o4=G5G{|9ff|`NN5+?ns zL@IWva(*@PXPmVGQ#rgIOY*nnoqNDDy$hd2uMT>wBgzg>YT&BV2U{k1ah1(1j_v0` z@o;6~SUGW=!+j!oa9ko_2^G75?VolPmWk=Pb-h{k=phZga( z88Rp7QzbHkpYG!aug9e^DF63Bi|1#CeAW^CpakO9DTT!p$yhuT8Aq10^cl2O@Zl-2RXr`+zCPj#_FqXs}W2{Qvn2Y{BmNsG45? zB{BF_rVgT$u0 zE8o6|@C>uOK1Ba}!V zx!M$9J1B7#_JSs90cKlucib?T&HqQpLE9YV1?v{gh2NWKEt9FX8;3DePnCL5Z=k)Flp=?-i$<5H4zc z`?2ZZ+p~Y8FYr;m3Vn2(u5Z`Av6#S}zkpQpZ|vNP0DY^I-oa$HXzg+ajQC7%wldRN zfOAL!UwFtuphqqR41v|3He4cQF5;UU9M~lti-k<HSTs^#>-Tf|C2&~#m%6WZAy1jz!Q_-IbpZP z8ht8}UG13lz+N-7+01+RlE)6OT^3px7fn@1|_b7^{bhPet}< z_)77(<^>8-qQ2X(n4faVhm@T0@Z{5HFSWs~EDXtV@7IAMbVUP6;v8^%l3PZ#wOZ-* z*Vk4lRj6OYpAZ_$*`t|tYKmLar&&{5{d+5cst)rQTn`n8>Xi+0zXc6YbTPMgzewFg z23F=+`8=FXXF6b*CDVN$v3|6iy;TSFSYh$qrbhKDcT^U9l zj}3g#zty{k*>s8S+>t|cng#3@Rz`z}njy{*?90mV6_Mkvv=iL9pb0ttHf$7;TxkX1 z-klTGb`2~-Mxx6~+{b-KiFd3XG`p?+6-0PMorB#Q@TY_CH5)En#5WrmHqj;@Fvi1A zeGpO@wuYIPOgRY&02e-U+j7!$LZ#5mS72R3MJS^gfheL5`kQV_n{8}KXaj)V%4b~As zFrQ7yZal}~{ELX@8c#V?2LlM@)g(|;VvcBjEuTJ=`WkOem{DL!+7Lr!U;F!mGm_^~ z+V^T?%bz+8noq9{ybcq16Gzd^fS2`skac)@6|;8X8l6Q19epZ@l^3@1ES!x2XLNA4 z_FI8#x5sq7hXVr83D;_5$sU!*Ye}zyx1wMC?Q{DSgrUx#fM?_Fj@{syA2x2yL^J{S zPPLkQ#O+9E9a^H*USdriL6rGHDt$B!vu~t7^)@_e=(<|SVd!MenX48AP(Z$4WoC9_ zeN;I;hEAr{ZvB^gK*1AWfI~5H0a{Y#2UBjn9`7;3JDrI5leeufemoZol*pDlVTSHP z3#8@6kxsJwUFg9(;)>Xm!{nsFC<7}Xwv_?o=eP)$>vvvj>yw z=YS7{pIOg(u@mJ%G0G^TM@L6>l)?_{_e`(yLxmX%h*D zMJS13@e!}HFR{?GNtq;%=4#zUgfFP^$g|Ax1<`vC&qIPbwGNo}3>ZM?=Evk6r|J&S zi$UD-za)A$kcqu)8)1mG z{FI*zS4{wM6S3;RP-!$0&8!6*;>|%T%HJxZt}cmap#~4vD0Pkx22gBbPo~=2iEMFa zSN<~qRz>jf54?e)>3%j;Gc6C1_YO0C|CDQDt7+bE({$0($tizZ)xn2L?@6_ zR3$`yiwH?E%X*^k*^oQ=z!1GA|E&fXHPR=rIEGq4%0=SGvror2Y%k#d`aPmx5@~7a zdkmPa1d-<`6M%& zp9rn|?C(5SRowEcasXoE$)s`=GvJk9wPt|2VX31T2F}6x3#(&IMqZND*a1muBh9?X zX_HSLo?$y$a;qFx^U1W|YAd%)Gaf|AEHqZ*{PW96FF*&nO-@c?c6t5=K_z@2f$8<^ zY}d|9NRviy7sF$61>@bV$B3*VeDg4DX3qScxVTL~5Go^T?}aG+th- z2`EduJx~ZcSssR;yX%oW&ze|$TF?;>HGHp~Eq?$w&SAD?d#s$$|4F@l*T7}X$7>}7 zRvPwxrPaLO5X-qYiQ7{P^4Ui2GDbq&DJ3Yu`)8zfMi1{>HEq`+uR1bJ4x!#n0D6_M8Zs_# z3mc%u30aK|avL-!XI&?{^%v4OXUr4OzaL*|-HV&M5GPx)SUqYMWw@Ex;%DHx^&FOD zncjYHD@AiYbGx1O(rsKW>Eg}cid)6bqA}!r!G{?x#)c?^k+q_uv%Xh3ha^A^{%wnpRPY({1LqK{NQy>!UjUc8f7x2` zgyLiGpsKlFO75ee2#drn3Glyna)PvUP}e(t6P z(8^W6g23+fzT5gZQQ^L-Yg#^P;QK8FTZAe)*|CKS6(I>8a2aoN+XEkYf2jAF!Zi3! zjS($tF@bu(ypeC>`IZtF;jz`F6A-Y7ZUQBuZxp&q4zHb9cc*!1`T3p9xL9`nWhNVr z!2lf=fCA>;1E&E|yfmrHqB#XnUCu28b*4#eZ{lLL(42#`ui?BO&uZj|d_Fh!Bw8g$ zn@2uezsJz@^XM(T{!CEw+EyG*eaF`FuTN%C zOZg)khBpDobCl(3ud$bhr>EdmuQ^l^Cic|y2m>LM+gsZGYKUAeJE5YUX9}j^JDoojv<}Cm&t+agmp?JE0%d#fo}m_cYogpjn5&egilTvDFz-Df}1i zB4)bXfn$dqb!cCa13DdCgMNehaa&${n5Mw&bxeKfNmHq%e{T_H@WB!H3QgFK2gNpB zP<;xkez-y-Lr(0^P^G!YH~WLut`0=mPXbVN64iv6Nd`s=eUQ;?V((+QU0&B4SF3*{Pm$AVrq;v&)c>VLy_UCe45VEsI@ZWM2TaB# zRU6XaLx0^H=0)Z!$rIu`3*s{Z!W7pU@6aHvX*vUuzME+!B5H}k_gFD)3=f;nI zi1|B!@iO%p;L{!JSEI~vyUByf_{HY=;RuAK##-h!06XFwxYi?xl}oWStJ*P{OcVe~ z_v(y8!+BaLQB`(D(XrL0ReKMn$R)8mU2@$q$Pq; zbZq-$IkP4V(`m}e<)cwnZLrjiA-X0@VY~Gi5-PKX20#Eag!JOw1br%7Rr}`(v@d!u zCo@&wE1SwM=zt~$K!eJ**9GAv!}Cogn9(d0X~BwPkU4gaWh?WVRcE3N?C%_R_D)Vw z(YmJTJ_0~fhItqHPqoIFGQYE2!~?aSRa{vjcDWhy5>oT zGOMFTWfL`aLx-!QL(9r?~D6y9Uhq=af8z!rqg#p zXk%gE-;=@G>MUv7p@P#ni@zP*$YQwA0Dlc21`%pV;p!_F@xI(^eA5&SZ{rU?^Wj}! z6Y%C^eMYilc_~MAwqV`h=I0;WA)MqJ^$IvyJ-O0)*RuLYjTL1TWd|(NbhIZ;nOop( z`4bc=fsxaeI@zc!vvYFFetFRKSMjef2_#oIzzPIxZ4oB0sxKOzX4Wltz#G@LD2Qr5 zm9o~xF;EU*_!O`}IigC{sU%1^$$B@>Fa_H0*>*1Amc^7tnKxcPpr8zZTme`6(0@J| zXfBE;0)lcuv%tqq05V8P2B^)Nhq~qdR|1KCfe>(GeuFaNc)T~zvma>o)FZv;sVD@D zynx%jpd8m<{zI zz44BQcmN85TNhy2plu`Nt$b;sKELSBpW)my@*ZnL{lFaD|7-8c-;zw*wh@(1yH+~o zQd6mwOU~P(B4CS|mX=v+F44&NRvMbQpcpDmU!|BhndzGgrsa}~;RGs*v>~aLX|A9$ zxrCyC3y6ZiciVh3@BH@t1LJY%FM8{e94DY4JQ} zYS0fcOC|N!{@iq*a@H$Qe9ONriBWJrhLhC?o5K2)!=~i)0hGh-mMd~RkqdIGCB(fU zy5*IvHssJ&gxudt>g(3w2{)axskJ_#h96qTc~<{c!`n^f zg+SOfdm8=UI!4%}d%RkXd}yWU1H66h)eDTsQr!qkcZE^zbI#F$k(dn7l7z}@YSv1+ zIcEYw{HJjfg()x7R@zQ&o;LdJ2vi6Fkl?OHM-Ga!%w}co(6=I5LZ>n{9pr~6!z|S$ zq_VfE7##n|{H(t$wPI-D`~L#((@V(MZ>p6Eb8k%4{lIGT;hZ9cg%~HhcbDCd%0RbM zs?uZG1wSL{Z0f+NzDiO?w9~XT^dWptKJ@M~0(@5*az*ZgabU465JN9eFY7vD8Wdz_ zlAIonnlivB;uDXov3sIgoKx2>G6a;@?v0qg;r`RnZ{4wMw2%}(e*c8k`R7sNT@>H} zfUU~mHR~8!4rJTHVlT=v3wz2kx&95Nz?@Tj8)s5E}t{|AFA=d_Y zOTqb{ATx>U``k~NJ2hYk3r#Gn1}|1Xj}jq!9%;{k(?9!WZt1z#{OATvapC-}#$LWi zi2R>~v0v6A<|?Eg)Ye#VyRyr7RJ$N4vFEFfmb1jHF(yZN^rc!ULDen>KWu(D9Z5!P ze(qg(G2HmSqyi2B&W`vo@N=3l?+dXbWn-`1LrY1^_mSilpKLLxQp}@s?=Tqw6Do5Pui*IhPZtaT|GAE&MF$;(4s9Bt5f+vbITElRv3( ze&@3GgY%ltiz;PZXq||TeA+sP9bc(#*G<2ck&zF3W?0$Bxit`EwvZb7jke;810>h3 zb}}!oS_xUbJ^$_PWrSlJ-;v4qq!@|L9uM#ALcMu|+|fni+AqPpu+CtjBrs#Y1jKVU zEc6L$d!2l-MgMi5&7?{Dfxj)qn;mIZudn7I6V$88%05A!PtCQTGSxXKMGh;qXa|fE zJBUmhM!}@e#A?s%bajm+=Ka1WxHZWaj;k#XT{T#;bH9c5zA8txVHEz(EeE*PP9eD9 z<2|evdxmVLj_n@`lp>6@ zy_ZTczm54_lGjPwPaq$dF1HdIks&Mp;%bge$QZnnp${}#&Z3)z95ei@b9;c=kJpY- z$G#RZbgyTi3&d4=3%+gXOSp|g^~^%K1id>re4gTka;7m@WA}bFo`GUbT8-n19VVdO}IkuW(H_iil_S}@$xy(Q*fCcNaD60 zxqsWK5lESLWnKgy^ci@da#k9^aW5)oLzbFxlUVBA&UM~79PF7=rW@Ot`>9(Gju3N{A4%EK0dPuz{=J_LUv|Pe^*x3eq_ExMNjB3?{$+xH^_Y z;e5pH)*~Lo@y=;b=P$Iqp9KR|j(>D-kaI4WeI&&HPFRtbZBMiQ^PwE`pF$Z7#(@UF zP2~&InXDTNx3`4)H2mD8yHl{Jk(|C(VA2vwY}3IRqo*qy9HvN7a!$$hlZqjmb6tZy zp1fLd^be5LmcI`_d3@@A`jLDS!b0qXVvP%y>+DfL86Ie=*TZ)PL??Lk^F};4=dwv; zPRBV>*)f&NE0vtjYHw@vs9l(Dk*g-}ARSciwv!f)E361d_9y<;9b7)PBw$3dh`AZi zAY4)BVh3t>;gR=s)nZW3PT_3bOLDK)eTZT^*m%P!HdC!FvK=Z=_iA>Bg!`SsC|P3u zz+oMr^PUcTebccFK>bqp475+?5RUC{Y7klp^p=Q;ZM+c8Zq6wBtH*5c=QHlp7wZS%6AszeebN>>_2^H7uuK@g%1{vF}DT>U{h`}c+u5ubXcFMH)fZ6-l z!y=qVN>jqgj)3T!mALcM;1!8}PDcMCU6<9?l#euNff${zE=b0d%;TcPFfw`y>zjLg#_WgnwatH|t}Y&WrR32m5W_AWNa`OqIc{ zW{_mX(Ck1psRCgMhJ*hXhcAG1ocb_kuY)%9rlYzq8h$K;X}=5m+8CYpJ4Yw6zLi%S zpu}dkAc_hVv>NfWy9eLsQ-6OzoBl{WAkRi|U;anmJ5dFwz(C9~-A(!Vfw z(E!S5ua;@}(q5GrIc6|PAOSPg{il$s$UBI}tk5xuP-VedGyZd}xqXvWvU_`{;Cf0> z5fN79T(#iq-q$RLb(of0ZA0lfepj^!a2-6 zv{v^7r2J*xmj&XVgZ>Wd=RqwGGe1`-Svll~bz(-y7*N1ooU5J*aY@&5ea5ss6n(a? z`N9l?w~=^1g2wLDVRD5ovqLc^Z#YRDFR+QYV4emH*fzOpzer3>Pudh??f``be>dD3 z)xB}1O6bZpnt=j(m92Fxq0dz89n>B05xx10QDL-YDz&e>h_u@9+RG)Pv4{2IYNiMy z8auH}j+fW*;q%Ymtbq+KI_r4gxGUeYJ>hq~vbe!N3%NntH+Dyh7I70!cu(qE_`Vp; z07NvH4Q2s#9;mKj;>umoviK|H+#CbgGq`D+QxI*$r6&D`yf%-M^{H;6gi4*j3?c9c z8$}NK?0I4%b?c`p2;SvL3*xY`0fe_KIZqPm`M%{DCrPUt{bS|zlhbHBNlUe7zcK}E z$L2zIl+z#Z!thJW!}{G&JAC@Pg`H(}GLM_m;uV}C9Yt(vF+F0Dy7{`k zY&v=ZZf?8^qSD>~2iP#{qQK632aMplZye6Q3X>dctS@JHSz2)zJaqXvFEZlr>9$oY z^&9^4pN`1EJcEw_wi@P{zJqQX470?WZTB*5Y7F!3#xJO^z|Gw@)bFoY5#daTP5OgI zcbKI$Ok(|9g_%#If*$3ga=U0_n%|#}eWwyeW~(19Te+!xF*(rd=LU(nM15;<7Z&oA zrqIw#r7}&_qgCdvS7+!|3?8w7JNRtHQ$~8Yyw(xC+n=- z7SQBo3+)tbg2NJn^=lukNOCkiEsgt~4tCrZ{aSnrHRMk@_?1^whFrEn3mT1NSC9B&c-(JrWu@FUhSNf+(>-_%kX#@LYnzq`^M#XX}(*!_LZCY za24(5Y$WH^=;GY^#0c{Y4{_!GPvm_bd#&6ypUpfwu%|+=UEe^Q+oe$7cXnyF@O67L3%SKO#rdayD^4^vH2hG{w%vp|_*jKf4 z=jb?40UP4S+Mi~(Uz(^cvgVB+r+Rt|;wnFRYcz(i=&Q14Ok=V-tTPw4%v&;ZrxI#w z6&rvLjj#yzBr5~N*7o09CkIE=>EWwo`ceL*@Y=504RB*xY#SY{)p3Gvn9zBL_FCN0 zl^axu8p~su8HpiDNi{%5ojAv1{0?t7*mflF9&Y_x4#)X(jyLl~c+s6*I1G7{zBI;tH*_ z94)o##4$cU4ohj~e#C^E><)3E`d;ftdwTQZpDmp)9)n5^+h%BE?)8LI2A`L!zjTBL zPYE&+#0&jDFc&4Tg}VC}E@4ZGyWbiK2dvn6Mpu!cQT_^6!RG!7)fE>V>?PNFm?vc5 z>A8gcW=5Xm2#LEW_;XgMQ$=Y-#lc|zs2}}2ny_4Kb%D@Vrtu6rOmUe!ph7;;L`XHi zXcDHc;OYbIk44?|A9-=Ml{Xap)^{jb5$Kl?v`CIT`bDXV*x{h+UARtzOd}#US>a%X zOdU`5^_P@lkQxB*B<&RQB?FgJOH2-~rMnXf_{5%~s&OlUM^i30FeOM{`XOXs)3_BU zEAyNr%bz8RJ=Cvw8y=)3p z`K|i!j$l~LqQ)kabHK}7WeyB$x*({t#cQWf98qh&X{R*Y--9)~g)?XCL>&z;v9#hY zTFY?DV&1fPE&*z}6Ki`Y5#(-eVYB;OzZjPSDnN%ArA8D>wODpQT4Jt}ah556JE+G_! z_P0uQ!qDhR94VdpAqajIOl4~>oTaQ8H5yXaTZUOb%cRAkWYV?KSNlTqgSM=Wgf)JP zz=?Q5f5zPEVO!NbOCbqEwP^Ff_O_`gdm67#U{Mp^_bKcq2IoO%zcJb(M5z`cjv1Ck z+!awNRhwjj6CQqu+xC#{UWo^3+h?6ymzq3r?3JV}<|u_9x=MWAm`1AqAnOsJ*@)^4 zr|`FkZlg{Cd!#Chmhn=_ZQe;~-DTUOv>)Tbmh0{z_42vWa|vNUO% z_5KA1xNHBgw0zjUH|s5xg$b4k z@Koa#-AFizrr6h2#$k*41tm7_jp$yL4X*DZcklq!u+>9E0WnhcOFPn7Vh^ao@~tno z@RwY)*+8&|Hpdq)`a=L*Teuw;_B@u;o!a!YaOO@bs-?*gqpm?nRkXl~mKFfF z+OVzE%RlC`M5-+KM_GXZ@9b;=2C(sq+R&Ko_RzZ%5P~kDieK3yzV4BN*{$E%KY;4k z)s?*vacHYN~u+?SoI`e@S2!9Co!cdvz;@N@{yj`0-9^8osR(V7PR-O&gM)x3owqs5oJpIwc zgY`#VzjI$V>YYDrIr8D;0JK<10@ycefw z;;oV(!gUR*xBg%xTl-#d>u(5}#jFrLKo}q0b{IuuZhuO7n++ zo@9)d#`(AT$mbW5g;c;&z>1_2Nk%;L?TIhfeK%PYp>5N<5wdihxw4-qvVsN6t@bol zDFgi~t`B&ZU3ek!#fXVE5Ao$7AwI+@amT_m2SclwQE{cLcv3kwhokq+!S%>Fe_*(Z z75)vhq@YqZqa~Hf$0S?T@nr_%mV%*aT${~4)6|(P@Bq_Q!VC4tZa`7?ra`4?oV+wSr2`TVSUmKS_>V@3%0*S#!+L=3f@oF=4k9U9xv0p1;Fx&}V;X2J~h zcz^}G3|;s8JyEFR*LB*fPUm+?f+ofnBQ5uK%NrwA+RV_~h<6-mw_wU?NGRI!zNTh% z&>ty6x8&gW75gdW)?p->&%?{*brS|k@b|(>&<^nyO55Pi_q*eK)=J*Uunw2cw--p%E!VXuDa? ztZ$HPKJ6$Sh7!UrpxVBLFSnpZOw$(ftvg!Nk1LVfL+FL(u zh1Abu(oCSmgqQ2IrE;Zz2f2DAD%T4XO6tU&)2IB}vV3{^xpz1MYFEPy_09RP2QvmA zIqw<(UaCnCs!mFX$+3sjnV*(O5)y`jW!*wzF-l^K`Bxgap+0Ej z@c^nf{Ic`6I5#9bcE7fwiiP8JZ9dr3FsD~SBiW_`8{UgFt*{$@qj#E)90JYra>Zs3 z$sCTuzOye2GdTO;4@;wgJK@!ij-|c--insluCR}{#q=D6Xz#nL6;`rkc*UzLTR%Y{ zN2YK;Zcz4YY=+|(0_?E=#~3U@I1fIyRiBF zIeWj=id+b|L;kSMs>NMfeB^(={IdrC;NYJy_$L+olL`OdOqgH0OpSa?FTRhwb<|%A Pe7HEdAEg|=c=LY&YVNkY diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 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 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 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 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 67ee153..0000000 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = spinifyapp - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinify.spinifyapp - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 dev.plugfox.spinify. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a3..0000000 --- a/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa..0000000 --- a/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 1ca7bf0..0000000 --- a/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Cocoa -import FlutterMacOS -import window_manager - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } - - override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { - super.order(place, relativeTo: otherWin) - hiddenWindowAtLaunch() - } -} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 5418c9f..0000000 --- a/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..696fcf6 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,3 @@ +import 'package:spinify/spinify.dart'; + +void main() => Spinify(); diff --git a/example/pubspec.yaml b/example/pubspec.yaml deleted file mode 100644 index fe83978..0000000 --- a/example/pubspec.yaml +++ /dev/null @@ -1,113 +0,0 @@ -name: spinifyapp - -description: Spinify App Example - -publish_to: 'none' - -version: 1.0.0+1 - -homepage: https://centrifugal.dev - -repository: https://github.com/PlugFox/spinify - -issue_tracker: https://github.com/PlugFox/spinify/issues - -funding: - - https://www.buymeacoffee.com/plugfox - - https://www.patreon.com/plugfox - - https://boosty.to/plugfox - -topics: - - spinify - - centrifugo - - centrifuge - - websocket - - cross-platform - -platforms: - android: - ios: - linux: - macos: - web: - windows: - - -environment: - sdk: '>=3.1.0-63.1.beta <4.0.0' - flutter: '>=3.10.1' - - -dependencies: - # Flutter SDK - flutter: - sdk: flutter - - # Localization - flutter_localizations: - sdk: flutter - intl: any - - # Utils - collection: any - async: any - meta: any - path: any - platform_info: ^4.0.2 - win32: ^5.0.6 - crypto: ^3.0.3 - convert: ^3.1.1 - - # Desktop - window_manager: ^0.3.5 - - # Logger - l: ^4.0.2 - - # Transport - spinify: - path: ../ - - # UI - cupertino_icons: ^1.0.2 - - -dev_dependencies: - # Unit & Widget tests for Flutter - flutter_test: - sdk: flutter - # Integration tests for Flutter - integration_test: - sdk: flutter - - #mockito: ^5.4.2 - - # Codegen - build_runner: ^2.4.6 - #flutter_launcher_icons: ^0.13.1 - #flutter_native_splash: ^2.3.1 - # build_verify: ^3.1.0 - pubspec_generator: '>=4.0.0 <5.0.0' - #flutter_gen_runner: ^5.3.1 - - # Linting - flutter_lints: ^3.0.0 - - -flutter: - generate: true - uses-material-design: true - - -flutter_intl: - enabled: true - class_name: GeneratedLocalization - main_locale: en - arb_dir: lib/src/common/localization - output_dir: lib/src/common/localization/generated - use_deferred_loading: false - - -#flutter_gen: -# output: lib/src/common/constant/ -# line_length: 120 \ No newline at end of file diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index ab73b3a..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1 +0,0 @@ -void main() {} diff --git a/example/web/favicon.png b/example/web/favicon.png deleted file mode 100644 index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png deleted file mode 100644 index b749bfef07473333cf1dd31e9eed89862a5d52aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 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 diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48dff1169879ba46840804b412fe02fefd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e525556d5d89141648c724331630325d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20998 zcmeFZ_gj-)&^4Nb2tlbLMU<{!p(#yjqEe+=0IA_oih%ScH9@5#MNp&}Y#;;(h=A0@ zh7{>lT2MkSQ344eAvrhici!td|HJuyvJm#Y_w1Q9Yu3!26dNlO-oxUDK_C#XnW^Co z5C{VN6#{~B0)K2j7}*1Xq(Nqemv23A-6&=ZpEijkVnSwVGqLv40?n0=p;k3-U5e5+ z+z3>aS`u9DS=!wg8ROu?X4TFoW6CFLL&{GzoVT)ldhLekLM|+j3tIxRd|*5=c{=s&*vfPdBr(Fyj(v@%eQj1Soy7m4^@VRl1~@-PV7y+c!xz$8436WBn$t{=}mEdK#k`aystimGgI{(IBx$!pAwFoE9Y`^t^;> zKAD)C(Dl^s%`?q5$P|fZf8Xymrtu^Pv(7D`rn>Z-w$Ahs!z9!94WNVxrJuXfHAaxg zC6s@|Z1$7R$(!#t%Jb{{s6(Y?NoQXDYq)!}X@jKPhe`{9KQ@sAU8y-5`xt?S9$jKH zoi}6m5PcG*^{kjvt+kwPpyQzVg4o)a>;LK`aaN2x4@itBD3Aq?yWTM20VRn1rrd+2 zKO=P0rMjEGq_UqpMa`~7B|p?xAN1SCoCp}QxAv8O`jLJ5CVh@umR%c%i^)6!o+~`F zaalSTQcl5iwOLC&H)efzd{8(88mo`GI(56T<(&p7>Qd^;R1hn1Y~jN~tApaL8>##U zd65bo8)79CplWxr#z4!6HvLz&N7_5AN#x;kLG?zQ(#p|lj<8VUlKY=Aw!ATqeL-VG z42gA!^cMNPj>(`ZMEbCrnkg*QTsn*u(nQPWI9pA{MQ=IsPTzd7q5E#7+z>Ch=fx$~ z;J|?(5jTo5UWGvsJa(Sx0?S#56+8SD!I^tftyeh_{5_31l6&Hywtn`bbqYDqGZXI( zCG7hBgvksX2ak8+)hB4jnxlO@A32C_RM&g&qDSb~3kM&)@A_j1*oTO@nicGUyv+%^ z=vB)4(q!ykzT==Z)3*3{atJ5}2PV*?Uw+HhN&+RvKvZL3p9E?gHjv{6zM!A|z|UHK z-r6jeLxbGn0D@q5aBzlco|nG2tr}N@m;CJX(4#Cn&p&sLKwzLFx1A5izu?X_X4x8r@K*d~7>t1~ zDW1Mv5O&WOxbzFC`DQ6yNJ(^u9vJdj$fl2dq`!Yba_0^vQHXV)vqv1gssZYzBct!j zHr9>ydtM8wIs}HI4=E}qAkv|BPWzh3^_yLH(|kdb?x56^BlDC)diWyPd*|f!`^12_U>TD^^94OCN0lVv~Sgvs94ecpE^}VY$w`qr_>Ue zTfH~;C<3H<0dS5Rkf_f@1x$Gms}gK#&k()IC0zb^QbR!YLoll)c$Agfi6MKI0dP_L z=Uou&u~~^2onea2%XZ@>`0x^L8CK6=I{ge;|HXMj)-@o~h&O{CuuwBX8pVqjJ*o}5 z#8&oF_p=uSo~8vn?R0!AMWvcbZmsrj{ZswRt(aEdbi~;HeVqIe)-6*1L%5u$Gbs}| zjFh?KL&U(rC2izSGtwP5FnsR@6$-1toz?RvLD^k~h9NfZgzHE7m!!7s6(;)RKo2z} zB$Ci@h({l?arO+vF;s35h=|WpefaOtKVx>l399}EsX@Oe3>>4MPy%h&^3N_`UTAHJ zI$u(|TYC~E4)|JwkWW3F!Tib=NzjHs5ii2uj0^m|Qlh-2VnB#+X~RZ|`SA*}}&8j9IDv?F;(Y^1=Z0?wWz;ikB zewU>MAXDi~O7a~?jx1x=&8GcR-fTp>{2Q`7#BE#N6D@FCp`?ht-<1|y(NArxE_WIu zP+GuG=Qq>SHWtS2M>34xwEw^uvo4|9)4s|Ac=ud?nHQ>ax@LvBqusFcjH0}{T3ZPQ zLO1l<@B_d-(IS682}5KA&qT1+{3jxKolW+1zL4inqBS-D>BohA!K5++41tM@ z@xe<-qz27}LnV#5lk&iC40M||JRmZ*A##K3+!j93eouU8@q-`W0r%7N`V$cR&JV;iX(@cS{#*5Q>~4BEDA)EikLSP@>Oo&Bt1Z~&0d5)COI%3$cLB_M?dK# z{yv2OqW!al-#AEs&QFd;WL5zCcp)JmCKJEdNsJlL9K@MnPegK23?G|O%v`@N{rIRa zi^7a}WBCD77@VQ-z_v{ZdRsWYrYgC$<^gRQwMCi6);%R~uIi31OMS}=gUTE(GKmCI z$zM>mytL{uNN+a&S38^ez(UT=iSw=l2f+a4)DyCA1Cs_N-r?Q@$3KTYosY!;pzQ0k zzh1G|kWCJjc(oZVBji@kN%)UBw(s{KaYGy=i{g3{)Z+&H8t2`^IuLLKWT6lL<-C(! zSF9K4xd-|VO;4}$s?Z7J_dYqD#Mt)WCDnsR{Kpjq275uUq6`v0y*!PHyS(}Zmv)_{>Vose9-$h8P0|y;YG)Bo}$(3Z%+Gs0RBmFiW!^5tBmDK-g zfe5%B*27ib+7|A*Fx5e)2%kIxh7xWoc3pZcXS2zik!63lAG1;sC1ja>BqH7D zODdi5lKW$$AFvxgC-l-)!c+9@YMC7a`w?G(P#MeEQ5xID#<}W$3bSmJ`8V*x2^3qz zVe<^^_8GHqYGF$nIQm0Xq2kAgYtm#UC1A(=&85w;rmg#v906 zT;RyMgbMpYOmS&S9c38^40oUp?!}#_84`aEVw;T;r%gTZkWeU;;FwM@0y0adt{-OK z(vGnPSlR=Nv2OUN!2=xazlnHPM9EWxXg2EKf0kI{iQb#FoP>xCB<)QY>OAM$Dcdbm zU6dU|%Mo(~avBYSjRc13@|s>axhrPl@Sr81{RSZUdz4(=|82XEbV*JAX6Lfbgqgz584lYgi0 z2-E{0XCVON$wHfvaLs;=dqhQJ&6aLn$D#0i(FkAVrXG9LGm3pSTf&f~RQb6|1_;W> z?n-;&hrq*~L=(;u#jS`*Yvh@3hU-33y_Kv1nxqrsf>pHVF&|OKkoC)4DWK%I!yq?P z=vXo8*_1iEWo8xCa{HJ4tzxOmqS0&$q+>LroMKI*V-rxhOc%3Y!)Y|N6p4PLE>Yek>Y(^KRECg8<|%g*nQib_Yc#A5q8Io z6Ig&V>k|~>B6KE%h4reAo*DfOH)_01tE0nWOxX0*YTJgyw7moaI^7gW*WBAeiLbD?FV9GSB zPv3`SX*^GRBM;zledO`!EbdBO_J@fEy)B{-XUTVQv}Qf~PSDpK9+@I`7G7|>Dgbbu z_7sX9%spVo$%qwRwgzq7!_N;#Td08m5HV#?^dF-EV1o)Q=Oa+rs2xH#g;ykLbwtCh znUnA^dW!XjspJ;otq$yV@I^s9Up(5k7rqhQd@OLMyyxVLj_+$#Vc*}Usevp^I(^vH zmDgHc0VMme|K&X?9&lkN{yq_(If)O`oUPW8X}1R5pSVBpfJe0t{sPA(F#`eONTh_) zxeLqHMfJX#?P(@6w4CqRE@Eiza; z;^5)Kk=^5)KDvd9Q<`=sJU8rjjxPmtWMTmzcH={o$U)j=QBuHarp?=}c??!`3d=H$nrJMyr3L-& zA#m?t(NqLM?I3mGgWA_C+0}BWy3-Gj7bR+d+U?n*mN$%5P`ugrB{PeV>jDUn;eVc- zzeMB1mI4?fVJatrNyq|+zn=!AiN~<}eoM#4uSx^K?Iw>P2*r=k`$<3kT00BE_1c(02MRz4(Hq`L^M&xt!pV2 zn+#U3@j~PUR>xIy+P>51iPayk-mqIK_5rlQMSe5&tDkKJk_$i(X&;K(11YGpEc-K= zq4Ln%^j>Zi_+Ae9eYEq_<`D+ddb8_aY!N;)(&EHFAk@Ekg&41ABmOXfWTo)Z&KotA zh*jgDGFYQ^y=m)<_LCWB+v48DTJw*5dwMm_YP0*_{@HANValf?kV-Ic3xsC}#x2h8 z`q5}d8IRmqWk%gR)s~M}(Qas5+`np^jW^oEd-pzERRPMXj$kS17g?H#4^trtKtq;C?;c ztd|%|WP2w2Nzg@)^V}!Gv++QF2!@FP9~DFVISRW6S?eP{H;;8EH;{>X_}NGj^0cg@ z!2@A>-CTcoN02^r6@c~^QUa={0xwK0v4i-tQ9wQq^=q*-{;zJ{Qe%7Qd!&X2>rV@4 z&wznCz*63_vw4>ZF8~%QCM?=vfzW0r_4O^>UA@otm_!N%mH)!ERy&b!n3*E*@?9d^ zu}s^By@FAhG(%?xgJMuMzuJw2&@$-oK>n z=UF}rt%vuaP9fzIFCYN-1&b#r^Cl6RDFIWsEsM|ROf`E?O(cy{BPO2Ie~kT+^kI^i zp>Kbc@C?}3vy-$ZFVX#-cx)Xj&G^ibX{pWggtr(%^?HeQL@Z( zM-430g<{>vT*)jK4aY9(a{lSy{8vxLbP~n1MXwM527ne#SHCC^F_2@o`>c>>KCq9c(4c$VSyMl*y3Nq1s+!DF| z^?d9PipQN(mw^j~{wJ^VOXDCaL$UtwwTpyv8IAwGOg<|NSghkAR1GSNLZ1JwdGJYm zP}t<=5=sNNUEjc=g(y)1n5)ynX(_$1-uGuDR*6Y^Wgg(LT)Jp><5X|}bt z_qMa&QP?l_n+iVS>v%s2Li_;AIeC=Ca^v1jX4*gvB$?H?2%ndnqOaK5-J%7a} zIF{qYa&NfVY}(fmS0OmXA70{znljBOiv5Yod!vFU{D~*3B3Ka{P8?^ zfhlF6o7aNT$qi8(w<}OPw5fqA7HUje*r*Oa(YV%*l0|9FP9KW@U&{VSW{&b0?@y)M zs%4k1Ax;TGYuZ9l;vP5@?3oQsp3)rjBeBvQQ>^B;z5pc=(yHhHtq6|0m(h4envn_j787fizY@V`o(!SSyE7vlMT zbo=Z1c=atz*G!kwzGB;*uPL$Ei|EbZLh8o+1BUMOpnU(uX&OG1MV@|!&HOOeU#t^x zr9=w2ow!SsTuJWT7%Wmt14U_M*3XiWBWHxqCVZI0_g0`}*^&yEG9RK9fHK8e+S^m? zfCNn$JTswUVbiC#>|=wS{t>-MI1aYPLtzO5y|LJ9nm>L6*wpr_m!)A2Fb1RceX&*|5|MwrvOk4+!0p99B9AgP*9D{Yt|x=X}O% zgIG$MrTB=n-!q%ROT|SzH#A$Xm;|ym)0>1KR}Yl0hr-KO&qMrV+0Ej3d@?FcgZ+B3 ztEk16g#2)@x=(ko8k7^Tq$*5pfZHC@O@}`SmzT1(V@x&NkZNM2F#Q-Go7-uf_zKC( zB(lHZ=3@dHaCOf6C!6i8rDL%~XM@rVTJbZL09?ht@r^Z_6x}}atLjvH^4Vk#Ibf(^LiBJFqorm?A=lE zzFmwvp4bT@Nv2V>YQT92X;t9<2s|Ru5#w?wCvlhcHLcsq0TaFLKy(?nzezJ>CECqj zggrI~Hd4LudM(m{L@ezfnpELsRFVFw>fx;CqZtie`$BXRn#Ns%AdoE$-Pf~{9A8rV zf7FbgpKmVzmvn-z(g+&+-ID=v`;6=)itq8oM*+Uz**SMm_{%eP_c0{<%1JGiZS19o z@Gj7$Se~0lsu}w!%;L%~mIAO;AY-2i`9A*ZfFs=X!LTd6nWOZ7BZH2M{l2*I>Xu)0 z`<=;ObglnXcVk!T>e$H?El}ra0WmPZ$YAN0#$?|1v26^(quQre8;k20*dpd4N{i=b zuN=y}_ew9SlE~R{2+Rh^7%PA1H5X(p8%0TpJ=cqa$65XL)$#ign-y!qij3;2>j}I; ziO@O|aYfn&up5F`YtjGw68rD3{OSGNYmBnl?zdwY$=RFsegTZ=kkzRQ`r7ZjQP!H( zp4>)&zf<*N!tI00xzm-ME_a{_I!TbDCr;8E;kCH4LlL-tqLxDuBn-+xgPk37S&S2^ z2QZumkIimwz!c@!r0)j3*(jPIs*V!iLTRl0Cpt_UVNUgGZzdvs0(-yUghJfKr7;=h zD~y?OJ-bWJg;VdZ^r@vlDoeGV&8^--!t1AsIMZ5S440HCVr%uk- z2wV>!W1WCvFB~p$P$$_}|H5>uBeAe>`N1FI8AxM|pq%oNs;ED8x+tb44E) zTj{^fbh@eLi%5AqT?;d>Es5D*Fi{Bpk)q$^iF!!U`r2hHAO_?#!aYmf>G+jHsES4W zgpTKY59d?hsb~F0WE&dUp6lPt;Pm zcbTUqRryw^%{ViNW%Z(o8}dd00H(H-MmQmOiTq{}_rnwOr*Ybo7*}3W-qBT!#s0Ie z-s<1rvvJx_W;ViUD`04%1pra*Yw0BcGe)fDKUK8aF#BwBwMPU;9`!6E(~!043?SZx z13K%z@$$#2%2ovVlgFIPp7Q6(vO)ud)=*%ZSucL2Dh~K4B|%q4KnSpj#n@(0B})!9 z8p*hY@5)NDn^&Pmo;|!>erSYg`LkO?0FB@PLqRvc>4IsUM5O&>rRv|IBRxi(RX(gJ ztQ2;??L~&Mv;aVr5Q@(?y^DGo%pO^~zijld41aA0KKsy_6FeHIn?fNHP-z>$OoWer zjZ5hFQTy*-f7KENRiCE$ZOp4|+Wah|2=n@|W=o}bFM}Y@0e62+_|#fND5cwa3;P{^pEzlJbF1Yq^}>=wy8^^^$I2M_MH(4Dw{F6hm+vrWV5!q;oX z;tTNhz5`-V={ew|bD$?qcF^WPR{L(E%~XG8eJx(DoGzt2G{l8r!QPJ>kpHeOvCv#w zr=SSwMDaUX^*~v%6K%O~i)<^6`{go>a3IdfZ8hFmz&;Y@P%ZygShQZ2DSHd`m5AR= zx$wWU06;GYwXOf(%MFyj{8rPFXD};JCe85Bdp4$YJ2$TzZ7Gr#+SwCvBI1o$QP0(c zy`P51FEBV2HTisM3bHqpmECT@H!Y2-bv2*SoSPoO?wLe{M#zDTy@ujAZ!Izzky~3k zRA1RQIIoC*Mej1PH!sUgtkR0VCNMX(_!b65mo66iM*KQ7xT8t2eev$v#&YdUXKwGm z7okYAqYF&bveHeu6M5p9xheRCTiU8PFeb1_Rht0VVSbm%|1cOVobc8mvqcw!RjrMRM#~=7xibH&Fa5Imc|lZ{eC|R__)OrFg4@X_ ze+kk*_sDNG5^ELmHnZ7Ue?)#6!O)#Nv*Dl2mr#2)w{#i-;}0*_h4A%HidnmclH#;Q zmQbq+P4DS%3}PpPm7K_K3d2s#k~x+PlTul7+kIKol0@`YN1NG=+&PYTS->AdzPv!> zQvzT=)9se*Jr1Yq+C{wbK82gAX`NkbXFZ)4==j4t51{|-v!!$H8@WKA={d>CWRW+g z*`L>9rRucS`vbXu0rzA1#AQ(W?6)}1+oJSF=80Kf_2r~Qm-EJ6bbB3k`80rCv(0d` zvCf3;L2ovYG_TES%6vSuoKfIHC6w;V31!oqHM8-I8AFzcd^+_86!EcCOX|Ta9k1!s z_Vh(EGIIsI3fb&dF$9V8v(sTBC%!#<&KIGF;R+;MyC0~}$gC}}= zR`DbUVc&Bx`lYykFZ4{R{xRaUQkWCGCQlEc;!mf=+nOk$RUg*7 z;kP7CVLEc$CA7@6VFpsp3_t~m)W0aPxjsA3e5U%SfY{tp5BV5jH-5n?YX7*+U+Zs%LGR>U- z!x4Y_|4{gx?ZPJobISy991O znrmrC3otC;#4^&Rg_iK}XH(XX+eUHN0@Oe06hJk}F?`$)KmH^eWz@@N%wEc)%>?Ft z#9QAroDeyfztQ5Qe{m*#R#T%-h*&XvSEn@N$hYRTCMXS|EPwzF3IIysD2waj`vQD{ zv_#^Pgr?s~I*NE=acf@dWVRNWTr(GN0wrL)Z2=`Dr>}&ZDNX|+^Anl{Di%v1Id$_p zK5_H5`RDjJx`BW7hc85|> zHMMsWJ4KTMRHGu+vy*kBEMjz*^K8VtU=bXJYdhdZ-?jTXa$&n)C?QQIZ7ln$qbGlr zS*TYE+ppOrI@AoPP=VI-OXm}FzgXRL)OPvR$a_=SsC<3Jb+>5makX|U!}3lx4tX&L z^C<{9TggZNoeX!P1jX_K5HkEVnQ#s2&c#umzV6s2U-Q;({l+j^?hi7JnQ7&&*oOy9 z(|0asVTWUCiCnjcOnB2pN0DpuTglKq;&SFOQ3pUdye*eT<2()7WKbXp1qq9=bhMWlF-7BHT|i3TEIT77AcjD(v=I207wi-=vyiw5mxgPdTVUC z&h^FEUrXwWs9en2C{ywZp;nvS(Mb$8sBEh-*_d-OEm%~p1b2EpcwUdf<~zmJmaSTO zSX&&GGCEz-M^)G$fBvLC2q@wM$;n4jp+mt0MJFLuJ%c`tSp8$xuP|G81GEd2ci$|M z4XmH{5$j?rqDWoL4vs!}W&!?!rtj=6WKJcE>)?NVske(p;|#>vL|M_$as=mi-n-()a*OU3Okmk0wC<9y7t^D(er-&jEEak2!NnDiOQ99Wx8{S8}=Ng!e0tzj*#T)+%7;aM$ z&H}|o|J1p{IK0Q7JggAwipvHvko6>Epmh4RFRUr}$*2K4dz85o7|3#Bec9SQ4Y*;> zXWjT~f+d)dp_J`sV*!w>B%)#GI_;USp7?0810&3S=WntGZ)+tzhZ+!|=XlQ&@G@~3 z-dw@I1>9n1{+!x^Hz|xC+P#Ab`E@=vY?3%Bc!Po~e&&&)Qp85!I|U<-fCXy*wMa&t zgDk!l;gk;$taOCV$&60z+}_$ykz=Ea*)wJQ3-M|p*EK(cvtIre0Pta~(95J7zoxBN zS(yE^3?>88AL0Wfuou$BM{lR1hkrRibz=+I9ccwd`ZC*{NNqL)3pCcw^ygMmrG^Yp zn5f}Xf>%gncC=Yq96;rnfp4FQL#{!Y*->e82rHgY4Zwy{`JH}b9*qr^VA{%~Z}jtp z_t$PlS6}5{NtTqXHN?uI8ut8rOaD#F1C^ls73S=b_yI#iZDOGz3#^L@YheGd>L;<( z)U=iYj;`{>VDNzIxcjbTk-X3keXR8Xbc`A$o5# zKGSk-7YcoBYuAFFSCjGi;7b<;n-*`USs)IX z=0q6WZ=L!)PkYtZE-6)azhXV|+?IVGTOmMCHjhkBjfy@k1>?yFO3u!)@cl{fFAXnRYsWk)kpT?X{_$J=|?g@Q}+kFw|%n!;Zo}|HE@j=SFMvT8v`6Y zNO;tXN^036nOB2%=KzxB?n~NQ1K8IO*UE{;Xy;N^ZNI#P+hRZOaHATz9(=)w=QwV# z`z3+P>9b?l-@$@P3<;w@O1BdKh+H;jo#_%rr!ute{|YX4g5}n?O7Mq^01S5;+lABE+7`&_?mR_z7k|Ja#8h{!~j)| zbBX;*fsbUak_!kXU%HfJ2J+G7;inu#uRjMb|8a){=^))y236LDZ$$q3LRlat1D)%7K0!q5hT5V1j3qHc7MG9 z_)Q=yQ>rs>3%l=vu$#VVd$&IgO}Za#?aN!xY>-<3PhzS&q!N<=1Q7VJBfHjug^4|) z*fW^;%3}P7X#W3d;tUs3;`O&>;NKZBMR8au6>7?QriJ@gBaorz-+`pUWOP73DJL=M z(33uT6Gz@Sv40F6bN|H=lpcO z^AJl}&=TIjdevuDQ!w0K*6oZ2JBOhb31q!XDArFyKpz!I$p4|;c}@^bX{>AXdt7Bm zaLTk?c%h@%xq02reu~;t@$bv`b3i(P=g}~ywgSFpM;}b$zAD+=I!7`V~}ARB(Wx0C(EAq@?GuxOL9X+ffbkn3+Op0*80TqmpAq~EXmv%cq36celXmRz z%0(!oMp&2?`W)ALA&#|fu)MFp{V~~zIIixOxY^YtO5^FSox8v$#d0*{qk0Z)pNTt0QVZ^$`4vImEB>;Lo2!7K05TpY-sl#sWBz_W-aDIV`Ksabi zvpa#93Svo!70W*Ydh)Qzm{0?CU`y;T^ITg-J9nfWeZ-sbw)G@W?$Eomf%Bg2frfh5 zRm1{|E0+(4zXy){$}uC3%Y-mSA2-^I>Tw|gQx|7TDli_hB>``)Q^aZ`LJC2V3U$SABP}T)%}9g2pF9dT}aC~!rFFgkl1J$ z`^z{Arn3On-m%}r}TGF8KQe*OjSJ=T|caa_E;v89A{t@$yT^(G9=N9F?^kT*#s3qhJq!IH5|AhnqFd z0B&^gm3w;YbMNUKU>naBAO@fbz zqw=n!@--}o5;k6DvTW9pw)IJVz;X}ncbPVrmH>4x);8cx;q3UyiML1PWp%bxSiS|^ zC5!kc4qw%NSOGQ*Kcd#&$30=lDvs#*4W4q0u8E02U)7d=!W7+NouEyuF1dyH$D@G& zaFaxo9Ex|ZXA5y{eZT*i*dP~INSMAi@mvEX@q5i<&o&#sM}Df?Og8n8Ku4vOux=T% zeuw~z1hR}ZNwTn8KsQHKLwe2>p^K`YWUJEdVEl|mO21Bov!D0D$qPoOv=vJJ`)|%_ z>l%`eexY7t{BlVKP!`a^U@nM?#9OC*t76My_E_<16vCz1x_#82qj2PkWiMWgF8bM9 z(1t4VdHcJ;B~;Q%x01k_gQ0>u2*OjuEWNOGX#4}+N?Gb5;+NQMqp}Puqw2HnkYuKA zzKFWGHc&K>gwVgI1Sc9OT1s6fq=>$gZU!!xsilA$fF`kLdGoX*^t}ao@+^WBpk>`8 z4v_~gK|c2rCq#DZ+H)$3v~Hoi=)=1D==e3P zpKrRQ+>O^cyTuWJ%2}__0Z9SM_z9rptd*;-9uC1tDw4+A!=+K%8~M&+Zk#13hY$Y$ zo-8$*8dD5@}XDi19RjK6T^J~DIXbF5w&l?JLHMrf0 zLv0{7*G!==o|B%$V!a=EtVHdMwXLtmO~vl}P6;S(R2Q>*kTJK~!}gloxj)m|_LYK{ zl(f1cB=EON&wVFwK?MGn^nWuh@f95SHatPs(jcwSY#Dnl1@_gkOJ5=f`%s$ZHljRH0 z+c%lrb=Gi&N&1>^L_}#m>=U=(oT^vTA&3!xXNyqi$pdW1BDJ#^{h|2tZc{t^vag3& zAD7*8C`chNF|27itjBUo^CCDyEpJLX3&u+(L;YeeMwnXEoyN(ytoEabcl$lSgx~Ltatn}b$@j_yyMrBb03)shJE*$;Mw=;mZd&8e>IzE+4WIoH zCSZE7WthNUL$|Y#m!Hn?x7V1CK}V`KwW2D$-7&ODy5Cj;!_tTOOo1Mm%(RUt)#$@3 zhurA)t<7qik%%1Et+N1?R#hdBB#LdQ7{%-C zn$(`5e0eFh(#c*hvF>WT*07fk$N_631?W>kfjySN8^XC9diiOd#s?4tybICF;wBjp zIPzilX3{j%4u7blhq)tnaOBZ_`h_JqHXuI7SuIlNTgBk9{HIS&3|SEPfrvcE<@}E` zKk$y*nzsqZ{J{uWW9;#n=de&&h>m#A#q)#zRonr(?mDOYU&h&aQWD;?Z(22wY?t$U3qo`?{+amA$^TkxL+Ex2dh`q7iR&TPd0Ymwzo#b? zP$#t=elB5?k$#uE$K>C$YZbYUX_JgnXA`oF_Ifz4H7LEOW~{Gww&3s=wH4+j8*TU| zSX%LtJWqhr-xGNSe{;(16kxnak6RnZ{0qZ^kJI5X*It_YuynSpi(^-}Lolr{)#z_~ zw!(J-8%7Ybo^c3(mED`Xz8xecP35a6M8HarxRn%+NJBE;dw>>Y2T&;jzRd4FSDO3T zt*y+zXCtZQ0bP0yf6HRpD|WmzP;DR^-g^}{z~0x~z4j8m zucTe%k&S9Nt-?Jb^gYW1w6!Y3AUZ0Jcq;pJ)Exz%7k+mUOm6%ApjjSmflfKwBo6`B zhNb@$NHTJ>guaj9S{@DX)!6)b-Shav=DNKWy(V00k(D!v?PAR0f0vDNq*#mYmUp6> z76KxbFDw5U{{qx{BRj(>?|C`82ICKbfLxoldov-M?4Xl+3;I4GzLHyPOzYw7{WQST zPNYcx5onA%MAO9??41Po*1zW(Y%Zzn06-lUp{s<3!_9vv9HBjT02On0Hf$}NP;wF) zP<`2p3}A^~1YbvOh{ePMx$!JGUPX-tbBzp3mDZMY;}h;sQ->!p97GA)9a|tF(Gh{1$xk7 zUw?ELkT({Xw!KIr);kTRb1b|UL`r2_`a+&UFVCdJ)1T#fdh;71EQl9790Br0m_`$x z9|ZANuchFci8GNZ{XbP=+uXSJRe(;V5laQz$u18#?X*9}x7cIEbnr%<=1cX3EIu7$ zhHW6pe5M(&qEtsqRa>?)*{O;OJT+YUhG5{km|YI7I@JL_3Hwao9aXneiSA~a* z|Lp@c-oMNyeAEuUz{F?kuou3x#C*gU?lon!RC1s37gW^0Frc`lqQWH&(J4NoZg3m8 z;Lin#8Q+cFPD7MCzj}#|ws7b@?D9Q4dVjS4dpco=4yX5SSH=A@U@yqPdp@?g?qeia zH=Tt_9)G=6C2QIPsi-QipnK(mc0xXIN;j$WLf@n8eYvMk;*H-Q4tK%(3$CN}NGgO8n}fD~+>?<3UzvsrMf*J~%i;VKQHbF%TPalFi=#sgj)(P#SM^0Q=Tr>4kJVw8X3iWsP|e8tj}NjlMdWp z@2+M4HQu~3!=bZpjh;;DIDk&X}=c8~kn)FWWH z2KL1w^rA5&1@@^X%MjZ7;u(kH=YhH2pJPFQe=hn>tZd5RC5cfGYis8s9PKaxi*}-s6*W zRA^PwR=y^5Z){!(4D9-KC;0~;b*ploznFOaU`bJ_7U?qAi#mTo!&rIECRL$_y@yI27x2?W+zqDBD5~KCVYKFZLK+>ABC(Kj zeAll)KMgIlAG`r^rS{loBrGLtzhHY8$)<_S<(Dpkr(Ym@@vnQ&rS@FC*>2@XCH}M+an74WcRDcoQ+a3@A z9tYhl5$z7bMdTvD2r&jztBuo37?*k~wcU9GK2-)MTFS-lux-mIRYUuGUCI~V$?s#< z?1qAWb(?ZLm(N>%S%y10COdaq_Tm5c^%ooIxpR=`3e4C|@O5wY+eLik&XVi5oT7oe zmxH)Jd*5eo@!7t`x8!K=-+zJ-Sz)B_V$)s1pW~CDU$=q^&ABvf6S|?TOMB-RIm@CoFg>mjIQE)?+A1_3s6zmFU_oW&BqyMz1mY*IcP_2knjq5 zqw~JK(cVsmzc7*EvTT2rvpeqhg)W=%TOZ^>f`rD4|7Z5fq*2D^lpCttIg#ictgqZ$P@ru6P#f$x#KfnfTZj~LG6U_d-kE~`;kU_X)`H5so@?C zWmb!7x|xk@0L~0JFall*@ltyiL^)@3m4MqC7(7H0sH!WidId1#f#6R{Q&A!XzO1IAcIx;$k66dumt6lpUw@nL2MvqJ5^kbOVZ<^2jt5-njy|2@`07}0w z;M%I1$FCoLy`8xp8Tk)bFr;7aJeQ9KK6p=O$U0-&JYYy8woV*>b+FB?xLX`=pirYM z5K$BA(u)+jR{?O2r$c_Qvl?M{=Ar{yQ!UVsVn4k@0!b?_lA;dVz9uaQUgBH8Oz(Sb zrEs;&Ey>_ex8&!N{PmQjp+-Hlh|OA&wvDai#GpU=^-B70V0*LF=^bi+Nhe_o|azZ%~ZZ1$}LTmWt4aoB1 zPgccm$EwYU+jrdBaQFxQfn5gd(gM`Y*Ro1n&Zi?j=(>T3kmf94vdhf?AuS8>$Va#P zGL5F+VHpxdsCUa}+RqavXCobI-@B;WJbMphpK2%6t=XvKWWE|ruvREgM+|V=i6;;O zx$g=7^`$XWn0fu!gF=Xe9cMB8Z_SelD>&o&{1XFS`|nInK3BXlaeD*rc;R-#osyIS zWv&>~^TLIyBB6oDX+#>3<_0+2C4u2zK^wmHXXDD9_)kmLYJ!0SzM|%G9{pi)`X$uf zW}|%%#LgyK7m(4{V&?x_0KEDq56tk|0YNY~B(Sr|>WVz-pO3A##}$JCT}5P7DY+@W z#gJv>pA5>$|E3WO2tV7G^SuymB?tY`ooKcN3!vaQMnBNk-WATF{-$#}FyzgtJ8M^; zUK6KWSG)}6**+rZ&?o@PK3??uN{Q)#+bDP9i1W&j)oaU5d0bIWJ_9T5ac!qc?x66Q z$KUSZ`nYY94qfN_dpTFr8OW~A?}LD;Yty-BA)-be5Z3S#t2Io%q+cAbnGj1t$|qFR z9o?8B7OA^KjCYL=-!p}w(dkC^G6Nd%_I=1))PC0w5}ZZGJxfK)jP4Fwa@b-SYBw?% zdz9B-<`*B2dOn(N;mcTm%Do)rIvfXRNFX&1h`?>Rzuj~Wx)$p13nrDlS8-jwq@e@n zNIj_|8or==8~1h*Ih?w*8K7rYkGlwlTWAwLKc5}~dfz3y`kM&^Q|@C%1VAp_$wnw6zG~W4O+^ z>i?NY?oXf^Puc~+fDM$VgRNBpOZj{2cMP~gCqWAX4 z7>%$ux8@a&_B(pt``KSt;r+sR-$N;jdpY>|pyvPiN)9ohd*>mVST3wMo)){`B(&eX z1?zZJ-4u9NZ|~j1rdZYq4R$?swf}<6(#ex%7r{kh%U@kT)&kWuAszS%oJts=*OcL9 zaZwK<5DZw%1IFHXgFplP6JiL^dk8+SgM$D?8X+gE4172hXh!WeqIO>}$I9?Nry$*S zQ#f)RuH{P7RwA3v9f<-w>{PSzom;>(i&^l{E0(&Xp4A-*q-@{W1oE3K;1zb{&n28dSC2$N+6auXe0}e4b z)KLJ?5c*>@9K#I^)W;uU_Z`enquTUxr>mNq z1{0_puF-M7j${rs!dxxo3EelGodF1TvjV;Zpo;s{5f1pyCuRp=HDZ?s#IA4f?h|-p zGd|Mq^4hDa@Bh!c4ZE?O&x&XZ_ptZGYK4$9F4~{%R!}G1leCBx`dtNUS|K zL-7J5s4W@%mhXg1!}a4PD%!t&Qn%f_oquRajn3@C*)`o&K9o7V6DwzVMEhjVdDJ1fjhr#@=lp#@4EBqi=CCQ>73>R(>QKPNM&_Jpe5G`n4wegeC`FYEPJ{|vwS>$-`fuRSp3927qOv|NC3T3G-0 zA{K`|+tQy1yqE$ShWt8ny&5~)%ITb@^+x$w0)f&om;P8B)@}=Wzy59BwUfZ1vqw87 za2lB8J(&*l#(V}Id8SyQ0C(2amzkz3EqG&Ed0Jq1)$|&>4_|NIe=5|n=3?siFV0fI z{As5DLW^gs|B-b4C;Hd(SM-S~GQhzb>HgF2|2Usww0nL^;x@1eaB)=+Clj+$fF@H( z-fqP??~QMT$KI-#m;QC*&6vkp&8699G3)Bq0*kFZXINw=b9OVaed(3(3kS|IZ)CM? zJdnW&%t8MveBuK21uiYj)_a{Fnw0OErMzMN?d$QoPwkhOwcP&p+t>P)4tHlYw-pPN z^oJ=uc$Sl>pv@fZH~ZqxSvdhF@F1s=oZawpr^-#l{IIOGG=T%QXjtwPhIg-F@k@uIlr?J->Ia zpEUQ*=4g|XYn4Gez&aHr*;t$u3oODPmc2Ku)2Og|xjc%w;q!Zz+zY)*3{7V8bK4;& zYV82FZ+8?v)`J|G1w4I0fWdKg|2b#iaazCv;|?(W-q}$o&Y}Q5d@BRk^jL7#{kbCK zSgkyu;=DV+or2)AxCBgq-nj5=@n^`%T#V+xBGEkW4lCqrE)LMv#f;AvD__cQ@Eg3`~x| zW+h9mofSXCq5|M)9|ez(#X?-sxB%Go8};sJ?2abp(Y!lyi>k)|{M*Z$c{e1-K4ky` MPgg&ebxsLQ025IeI{*Lx diff --git a/example/web/index.html b/example/web/index.html deleted file mode 100644 index 4fb3004..0000000 --- a/example/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - Spinify - - - - - - - - - - diff --git a/example/web/manifest.json b/example/web/manifest.json deleted file mode 100644 index 2716d59..0000000 --- a/example/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Spinify", - "short_name": "Spinify", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "Spinify App Example", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/example/windows/.gitignore b/example/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/example/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt deleted file mode 100644 index 0970965..0000000 --- a/example/windows/CMakeLists.txt +++ /dev/null @@ -1,102 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(spinifyapp LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "spinifyapp") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt deleted file mode 100644 index 930d207..0000000 --- a/example/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,104 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index d6b86fa..0000000 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - ScreenRetrieverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); - WindowManagerPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowManagerPlugin")); -} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake deleted file mode 100644 index bfa52f4..0000000 --- a/example/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - screen_retriever - window_manager -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c..0000000 --- a/example/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc deleted file mode 100644 index b13385c..0000000 --- a/example/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "dev.plugfox.spinify" "\0" - VALUE "FileDescription", "spinifyapp" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "spinifyapp" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 dev.plugfox.spinify. All rights reserved." "\0" - VALUE "OriginalFilename", "spinifyapp.exe" "\0" - VALUE "ProductName", "spinifyapp" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp deleted file mode 100644 index 0e4c5f5..0000000 --- a/example/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject &project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - // this->Show() - }); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652..0000000 --- a/example/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp deleted file mode 100644 index b9101f9..0000000 --- a/example/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // 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()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"spinifyapp", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h deleted file mode 100644 index 66a65d1..0000000 --- a/example/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf6370ebb9253ad831cc31de4a9c965f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest deleted file mode 100644 index a42ea76..0000000 --- a/example/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp deleted file mode 100644 index b2b0873..0000000 --- a/example/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h deleted file mode 100644 index 3879d54..0000000 --- a/example/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp deleted file mode 100644 index 145247b..0000000 --- a/example/windows/runner/win32_window.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: -/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = - L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { -public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar *GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t *GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - -private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar *instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; - -const wchar_t *WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { ++g_active_window_count; } - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring &title, const Point &origin, - const Size &size) { - Destroy(); - - const wchar_t *window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window *that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { return window_handle_; } - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = - RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, - &light_mode, &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h deleted file mode 100644 index e901dde..0000000 --- a/example/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/spinify.dart b/lib/spinify.dart index 7fdc9f0..d0da47c 100644 --- a/lib/spinify.dart +++ b/lib/spinify.dart @@ -2,25 +2,24 @@ library spinify; export 'package:fixnum/fixnum.dart'; -export 'src/model/channel_push.dart'; +export 'src/model/channel_event.dart'; export 'src/model/client_info.dart'; export 'src/model/command.dart'; export 'src/model/config.dart'; export 'src/model/exception.dart'; export 'src/model/history.dart'; export 'src/model/jwt.dart'; -export 'src/model/metric.dart'; +export 'src/model/metric.dart' show SpinifyMetrics, SpinifyMetrics$Channel; export 'src/model/presence_stats.dart'; -export 'src/model/pushes_stream.dart'; export 'src/model/reply.dart'; -export 'src/model/spinify_interface.dart'; export 'src/model/state.dart'; export 'src/model/states_stream.dart'; export 'src/model/stream_position.dart'; -export 'src/model/subscription.dart'; export 'src/model/subscription_config.dart'; export 'src/model/subscription_state.dart'; -export 'src/model/subscription_states_stream.dart'; +export 'src/model/subscription_states.dart'; export 'src/model/transport_interface.dart'; export 'src/spinify_impl.dart' show Spinify; +export 'src/spinify_interface.dart'; +export 'src/subscription_interface.dart'; export 'src/transport_fake.dart'; diff --git a/lib/src.old/client/config.dart b/lib/src.old/client/config.dart deleted file mode 100644 index 37b8f20..0000000 --- a/lib/src.old/client/config.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import '../model/pubspec.yaml.g.dart'; - -/// Token used for authentication -/// -/// {@category Client} -/// {@category Entity} -typedef SpinifyToken = String; - -/// Callback to get/refresh tokens -/// This callback is used for initial connection -/// and for refreshing expired tokens. -/// -/// If method returns null then connection will be established without token. -/// -/// {@category Client} -/// {@category Entity} -typedef SpinifyTokenCallback = FutureOr Function(); - -/// Callback to get initial connection payload data. -/// -/// If method returns null then no payload will be sent at connect time. -/// -/// {@category Client} -/// {@category Entity} -typedef SpinifyConnectionPayloadCallback = FutureOr?> Function(); - -/// {@template spinify_config} -/// Spinify client common options. -/// -/// There are several common options available when creating Client instance. -/// -/// - [connectionRetryInterval] - tweaks for reconnect backoff -/// - [client] - the user's client name and version -/// - [headers] - headers that are set when connecting the web socket -/// - [timeout] - maximum time to wait for the connection to be established -/// {@endtemplate} -/// {@category Client} -/// {@category Entity} -@immutable -final class SpinifyConfig { - /// {@macro spinify_config} - SpinifyConfig({ - this.getToken, - this.getPayload, - this.connectionRetryInterval = ( - min: const Duration(milliseconds: 500), - max: const Duration(seconds: 20), - ), - ({String name, String version})? client, - this.timeout = const Duration(seconds: 15), - this.serverPingDelay = const Duration(seconds: 8), - this.headers, - }) : client = client ?? - ( - name: Pubspec.name, - version: Pubspec.version.canonical, - ); - - /// Create a default config - /// - /// {@macro spinify_config} - factory SpinifyConfig.byDefault() = SpinifyConfig; - - /// Callback to get/refresh tokens - /// This callback is used for initial connection - /// and for refreshing expired tokens. - /// - /// If method returns null then connection will be established without token. - final SpinifyTokenCallback? getToken; - - /// Callback to get connection payload data. - /// The resulted data send with every connect request. - /// - /// If method returns null then no payload will be sent at connect time. - final SpinifyConnectionPayloadCallback? getPayload; - - /// The additional delay between expected server heartbeat pings. - /// - /// Centrifugo server periodically sends pings to clients and expects pong - /// from clients that works over bidirectional transports. - /// Sending ping and receiving pong allows to find broken connections faster. - /// Centrifugo sends pings on the Centrifugo client protocol level, - /// thus it's possible for clients to handle ping messages - /// on the client side to make sure connection is not broken. - /// - /// Centrifugo expects pong message - /// from bidirectional client SDK after sending ping to it. - /// By default, it waits no more than 8 seconds before closing a connection. - final Duration serverPingDelay; - - /// The [connectionRetryInterval] argument is specifying the - /// [backoff full jitter strategy](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) for reconnecting. - /// Tweaks for reconnect backoff algorithm (min delay, max delay) - /// If not specified, the reconnecting will be disabled. - final ({Duration min, Duration max}) connectionRetryInterval; - - /// The user's client name and version. - final ({String name, String version}) client; - - /// Headers that are set when connecting the web socket on dart:io platforms. - /// - /// Note that headers are ignored on the web platform. - final Map? headers; - - /// Maximum time to wait for the connection to be established. - /// If not specified, the timeout will be 15 seconds. - final Duration timeout; - - @override - String toString() => 'SpinifyConfig{}'; -} diff --git a/lib/src.old/client/disconnect_code.dart b/lib/src.old/client/disconnect_code.dart deleted file mode 100644 index c7c8839..0000000 --- a/lib/src.old/client/disconnect_code.dart +++ /dev/null @@ -1,38 +0,0 @@ -/// Disconnect codes. -/// -/// Server may send custom disconnect codes to a client. -/// Custom disconnect codes must be in range [3000, 4999]. -/// -/// Client automatically reconnects upon receiving code -/// in range 3000-3499, 4000-4499 (i.e. Client goes to connecting state). -/// Other codes result into going to disconnected state. -/// -/// Client implementation can use codes <3000 for client-side -/// specific disconnect reasons. -enum DisconnectCode { - /// Disconnect called - disconnectCalled(0, 'disconnect called'), - - /// Unauthorized - unauthorized(1, 'unauthorized'), - - /// Bad protocol - badProtocol(2, 'bad protocol'), - - /// Client message write error - messageSizeLimit(3, 'message size limit exceeded'), - - /// Timeout - timeout(4, 'timeout exceeded'), - - /// Unsubscribe error - unsubscribeError(5, 'unsubscribe error'); - - const DisconnectCode(this.code, this.reason); - - /// Disconnect code. - final int code; - - /// Disconnect reason. - final String reason; -} diff --git a/lib/src.old/client/observer.dart b/lib/src.old/client/observer.dart deleted file mode 100644 index 3968e32..0000000 --- a/lib/src.old/client/observer.dart +++ /dev/null @@ -1,41 +0,0 @@ -import '../model/event.dart'; -import '../model/exception.dart'; -import '../subscription/subscription.dart'; -import '../subscription/subscription_state.dart'; -import 'spinify_interface.dart'; -import 'state.dart'; - -/// An interface for observing the behavior of Spinify instances. -/// {@category Client} -/// {@subCategory Observer} -abstract class SpinifyObserver { - /// Called whenever a [ISpinify] is instantiated. - void onCreate(ISpinify client) {} - - /// Called whenever a [ISpinify] client changes its state - /// to [SpinifyState$Connecting]. - void onConnected(ISpinify client, SpinifyState$Connected state) {} - - /// Called whenever a [ISpinify] client receives a [SpinifyEvent]. - void onEvent(ISpinify client, SpinifyEvent event) {} - - /// Called whenever a [ISpinify] client changes its state - /// from [prev] to [next]. - void onStateChanged(ISpinify client, SpinifyState prev, SpinifyState next) {} - - /// Called whenever a [SpinifySubscription] changes its state - /// from [prev] to [next]. - /// Works both for client-side and server-side subscriptions. - void onSubscriptionChanged(SpinifySubscription subscription, - SpinifySubscriptionState prev, SpinifySubscriptionState next) {} - - /// Called whenever a [ISpinify] client changes its state - /// to [SpinifyState$Disconnected]. - void onDisconnected(ISpinify client, SpinifyState$Disconnected state) {} - - /// Called whenever an error is thrown in any Spinify client. - void onError(SpinifyException error, StackTrace stackTrace) {} - - /// Called whenever a [ISpinify] is closed. - void onClose(ISpinify client) {} -} diff --git a/lib/src.old/client/spinify.dart b/lib/src.old/client/spinify.dart deleted file mode 100644 index f443180..0000000 --- a/lib/src.old/client/spinify.dart +++ /dev/null @@ -1,822 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -import '../model/channel_presence.dart'; -import '../model/channel_push.dart'; -import '../model/connect.dart'; -import '../model/disconnect.dart'; -import '../model/event.dart'; -import '../model/exception.dart'; -import '../model/history.dart'; -import '../model/message.dart'; -import '../model/metrics.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/publication.dart'; -import '../model/pushes_stream.dart'; -import '../model/refresh.dart'; -import '../model/stream_position.dart'; -import '../model/subscribe.dart'; -import '../model/unsubscribe.dart'; -import '../subscription/client_subscription_manager.dart'; -import '../subscription/server_subscription_manager.dart'; -import '../subscription/subscription.dart'; -import '../subscription/subscription_config.dart'; -import '../transport/transport_interface.dart'; -import '../transport/ws_protobuf_transport.dart'; -import '../util/event_queue.dart'; -import '../util/logger.dart' as logger; -import 'config.dart'; -import 'disconnect_code.dart'; -import 'observer.dart'; -import 'spinify_interface.dart'; -import 'state.dart'; -import 'states_stream.dart'; - -/// {@template spinify} -/// Spinify client for Centrifuge. -/// -/// Centrifugo SDKs use WebSocket as the main data transport and send/receive -/// messages encoded according to our bidirectional protocol. -/// That protocol is built on top of the Protobuf schema -/// (both JSON and binary Protobuf formats are supported). -/// It provides asynchronous communication, sending RPC, -/// multiplexing subscriptions to channels, etc. -/// -/// Client SDK wraps the protocol and exposes a set of APIs to developers. -/// -/// Client connection has 4 states: -/// - [SpinifyState$Disconnected] -/// - [SpinifyState$Connecting] -/// - [SpinifyState$Connected] -/// - [SpinifyState$Closed] -/// -/// {@endtemplate} -/// {@category Client} -final class Spinify extends SpinifyBase - with - SpinifyErrorsMixin, - SpinifyStateMixin, - SpinifyEventReceiverMixin, - SpinifyConnectionMixin, - SpinifySendMixin, - SpinifyClientSubscriptionMixin, - SpinifyServerSubscriptionMixin, - SpinifyPublicationsMixin, - SpinifyPresenceMixin, - SpinifyHistoryMixin, - SpinifyRPCMixin, - SpinifyQueueMixin, - SpinifyMetricsMixin { - /// {@macro spinify} - Spinify([SpinifyConfig? config]) : super(config ?? SpinifyConfig.byDefault()); - - /// Create client and connect. - /// - /// {@macro spinify} - factory Spinify.connect(String url, [SpinifyConfig? config]) => - Spinify(config)..connect(url); - - /// The current [SpinifyObserver] instance. - static SpinifyObserver? observer; -} - -/// Base class for Spinify client. -abstract base class SpinifyBase implements ISpinify { - /// Create a new Spinify client. - SpinifyBase(SpinifyConfig config) : _config = config { - _transport = SpinifyWSPBTransport( - config: config, - ); - _initSpinify(); - } - - /// Internal transport responsible - /// for sending, receiving, encoding and decoding data from the server. - @nonVirtual - late final ISpinifyTransport _transport; - - /// Spinify config. - @nonVirtual - final SpinifyConfig _config; - - /// Manager responsible for client-side subscriptions. - late final ClientSubscriptionManager _clientSubscriptionManager = - ClientSubscriptionManager(_transport); - - /// Manager responsible for client-side subscriptions. - late final ServerSubscriptionManager _serverSubscriptionManager = - ServerSubscriptionManager(_transport); - - @override - ({ - Map client, - Map server, - }) get subscriptions => ( - client: _clientSubscriptionManager.subscriptions, - server: _serverSubscriptionManager.subscriptions - ); - - /// Init spinify client, override this method to add custom logic. - /// This method is called in constructor. - @protected - @mustCallSuper - void _initSpinify() { - logger.fine('Spinify client initialized'); - Spinify.observer?.onCreate(this); - } - - /// Called when connection established. - /// Right before [SpinifyState$Connected] state. - @protected - @mustCallSuper - void _onConnected(SpinifyState$Connected state) { - logger.fine('Connection established'); - Spinify.observer?.onConnected(this, state); - } - - /// Called when connection lost. - /// Right before [SpinifyState$Disconnected] state. - @protected - @mustCallSuper - void _onDisconnected(SpinifyState$Disconnected state) { - logger.fine('Connection lost'); - Spinify.observer?.onDisconnected(this, state); - } - - @override - @mustCallSuper - Future close() async { - await _transport.close(); - logger.fine('Spinify client closed'); - Spinify.observer?.onClose(this); - } - - @override - String toString() => 'Spinify{}'; -} - -/// Mixin responsible for event receiving and distribution by controllers -/// and streams to subscribers. -base mixin SpinifyEventReceiverMixin on SpinifyBase, SpinifyStateMixin { - @protected - @nonVirtual - final StreamController _pushController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _publicationsController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _messagesController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _joinController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _leaveController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _presenceController = - StreamController.broadcast(); - - @override - @nonVirtual - late final SpinifyPushesStream stream = SpinifyPushesStream( - pushes: _pushController.stream, - publications: _publicationsController.stream, - messages: _messagesController.stream, - presenceEvents: _presenceController.stream, - joinEvents: _joinController.stream, - leaveEvents: _leaveController.stream, - ); - - @override - void _initSpinify() { - _transport.events.addListener(_onEvent); - super._initSpinify(); - } - - /// Router for all events. - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _onEvent(SpinifyEvent event) { - Spinify.observer?.onEvent(this, event); - if (event is! SpinifyChannelPush) return; - // This is a push to a channel. - _clientSubscriptionManager.onPush(event); - _pushController.add(event); - switch (event) { - case SpinifyPublication publication: - logger.fine( - 'Publication event received for channel ${publication.channel}'); - _publicationsController.add(publication); - case SpinifyMessage message: - logger.fine('Message event received for channel ${message.channel}'); - _messagesController.add(message); - case SpinifyJoin join: - logger.fine('Join event received for channel ${join.channel} ' - 'and user ${join.info.user}'); - _presenceController.add(join); - _joinController.add(join); - case SpinifyLeave leave: - logger.fine('Leave event received for channel ${leave.channel} ' - 'and user ${leave.info.user}'); - _presenceController.add(leave); - _leaveController.add(leave); - case SpinifySubscribe subscribe: - _serverSubscriptionManager.subscribe(subscribe); - case SpinifyUnsubscribe unsubscribe: - _serverSubscriptionManager.unsubscribe(unsubscribe); - case SpinifyConnect _: - break; - case SpinifyDisconnect event: - final code = event.code; - final reconnect = - code < 3500 || code >= 5000 || (code >= 4000 && code < 4500); - if (reconnect) { - logger.fine('Disconnect transport by server push ' - 'and reconnect after backoff delay'); - _transport.disconnect(code, event.reason).ignore(); - } else { - logger - .fine('Disconnect interactive by server push, without reconnect'); - disconnect().ignore(); - } - break; - case SpinifyRefresh _: - logger.fine('Refresh connection token by server push'); - _refreshToken(); - break; - } - } - - @override - Future close() async { - await super.close(); - _transport.events.removeListener(_onEvent); - for (final controller in >[ - _pushController, - _publicationsController, - _messagesController, - _joinController, - _leaveController, - _presenceController, - ]) { - controller.close().ignore(); - } - } -} - -/// Mixin responsible for spinify states -base mixin SpinifyStateMixin on SpinifyBase, SpinifyErrorsMixin { - /// Refresh timer. - Timer? _refreshTimer; - - @override - @nonVirtual - SpinifyState get state => _state; - - @nonVirtual - @protected - late SpinifyState _state; - - @override - @nonVirtual - late final SpinifyStatesStream states = - SpinifyStatesStream(_statesController.stream); - - @override - void _initSpinify() { - _state = _transport.state; - _transport.states.addListener(_onStateChange); - super._initSpinify(); - } - - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _onStateChange(SpinifyState newState) { - final oldState = _state; - logger.info('State changed: ${oldState.type} -> ${newState.type}'); - _refreshTimer?.cancel(); - _refreshTimer = null; - switch (newState) { - case SpinifyState$Disconnected state: - _onDisconnected(state); - case SpinifyState$Connecting _: - break; - case SpinifyState$Connected state: - _onConnected(state); - if (state.expires == true) _setRefreshTimer(state.ttl); - case SpinifyState$Closed _: - break; - } - _statesController.add(_state = newState); - Spinify.observer?.onStateChanged(this, oldState, newState); - } - - @protected - @nonVirtual - final StreamController _statesController = - StreamController.broadcast(); - - /// Refresh connection token when ttl is expired. - void _setRefreshTimer(DateTime? ttl) { - _refreshTimer?.cancel(); - _refreshTimer = null; - if (ttl == null) return; - final now = DateTime.now(); - final duration = ttl.subtract(_config.timeout * 4).difference(now); - if (duration.isNegative) return; - _refreshTimer = Timer(duration, _refreshToken); - } - - /// Refresh token for subscription. - void _refreshToken() => Future(() async { - try { - _refreshTimer?.cancel(); - _refreshTimer = null; - final token = await _config.getToken?.call(); - if (token == null || !state.isConnected) return; - await _transport.sendRefresh(token); - } on Object catch (error, stackTrace) { - logger.warning( - error, - stackTrace, - 'Error while refreshing connection token', - ); - _emitError( - SpinifyRefreshException( - message: 'Error while refreshing connection token', - error: error, - ), - stackTrace, - ); - } - }).ignore(); - - @override - Future close() => super.close().whenComplete(() { - _transport.states.removeListener(_onStateChange); - _statesController.close().ignore(); - }); -} - -/// Mixin responsible for errors stream. -base mixin SpinifyErrorsMixin on SpinifyBase { - @protected - @nonVirtual - void _emitError(SpinifyException exception, StackTrace stackTrace) => - Spinify.observer?.onError(exception, stackTrace); -} - -/// Mixin responsible for connection. -base mixin SpinifyConnectionMixin - on SpinifyBase, SpinifyErrorsMixin, SpinifyStateMixin { - @override - Future connect(String url) async { - logger.fine('Interactively connecting to $url'); - try { - _refreshTimer?.cancel(); - _refreshTimer = null; - await _transport.connect(url, _serverSubscriptionManager); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyConnectionException( - message: 'Error while connecting to $url', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - FutureOr ready() async { - try { - switch (state) { - case SpinifyState$Disconnected _: - throw const SpinifyConnectionException( - message: 'Client is not connected', - ); - case SpinifyState$Closed _: - throw const SpinifyConnectionException( - message: 'Client is permanently closed', - ); - case SpinifyState$Connected _: - return; - case SpinifyState$Connecting _: - await states.connected.first.timeout(_config.timeout); - } - } on TimeoutException catch (error, stackTrace) { - _transport - .disconnect( - DisconnectCode.timeout.code, - DisconnectCode.timeout.reason, - ) - .ignore(); - final spinifyException = SpinifyConnectionException( - message: 'Timeout exception while waiting for connection', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyConnectionException( - message: 'Client is not connected', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - Future disconnect([ - int code = 0, - String reason = 'Disconnect called', - ]) async { - logger.fine('Interactively disconnecting'); - try { - await _transport.disconnect(code, reason); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyConnectionException( - message: 'Error while disconnecting', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - Future close() async { - logger.fine('Interactively closing'); - await super.close(); - } -} - -/// Mixin responsible for sending asynchronous messages. -base mixin SpinifySendMixin on SpinifyBase, SpinifyErrorsMixin { - @override - Future send(List data) async { - try { - await ready(); - await _transport.sendAsyncMessage(data); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySendException(error: error); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for client-side subscriptions. -base mixin SpinifyClientSubscriptionMixin on SpinifyBase, SpinifyErrorsMixin { - @override - SpinifyClientSubscription newSubscription( - String channel, [ - SpinifySubscriptionConfig? config, - ]) { - final sub = _clientSubscriptionManager[channel] ?? - _serverSubscriptionManager[channel]; - if (sub != null) { - throw SpinifySubscriptionException( - channel: channel, - message: 'Subscription already exists', - ); - } - return _clientSubscriptionManager.newSubscription(channel, config); - } - - @override - SpinifyClientSubscription? getSubscription(String channel) => - _clientSubscriptionManager[channel]; - - @override - Future removeSubscription( - SpinifyClientSubscription subscription, - ) async { - try { - await _clientSubscriptionManager.removeSubscription(subscription); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - channel: subscription.channel, - message: 'Error while unsubscribing', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - void _onConnected(SpinifyState$Connected state) { - super._onConnected(state); - _clientSubscriptionManager.subscribeAll(); - } - - @override - void _onDisconnected(SpinifyState$Disconnected state) { - super._onDisconnected(state); - _clientSubscriptionManager.unsubscribeAll(); - } - - @override - Future close() async { - await super.close(); - _clientSubscriptionManager.close(); - } -} - -/// Mixin responsible for server-side subscriptions. -base mixin SpinifyServerSubscriptionMixin on SpinifyBase { - @override - void _onConnected(SpinifyState$Connected state) { - super._onConnected(state); - _serverSubscriptionManager.setSubscribedAll(); - } - - @override - void _onDisconnected(SpinifyState$Disconnected state) { - super._onDisconnected(state); - _serverSubscriptionManager.setSubscribingAll(); - } - - @override - Future close() async { - await super.close(); - _serverSubscriptionManager.close(); - } -} - -/// Mixin responsible for publications. -base mixin SpinifyPublicationsMixin - on SpinifyBase, SpinifyErrorsMixin, SpinifyClientSubscriptionMixin { - @override - Future publish(String channel, List data) async { - try { - await ready(); - await _transport.publish(channel, data); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySendException( - message: 'Error while publishing to channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for presence. -base mixin SpinifyPresenceMixin on SpinifyBase, SpinifyErrorsMixin { - @override - Future presence(String channel) async { - try { - await ready(); - return await _transport.presence(channel); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyFetchException( - message: 'Error while fetching presence for channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - Future presenceStats(String channel) async { - try { - await ready(); - return await _transport.presenceStats(channel); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyFetchException( - message: 'Error while fetching presence for channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for history. -base mixin SpinifyHistoryMixin on SpinifyBase, SpinifyErrorsMixin { - @override - Future history( - String channel, { - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) async { - try { - await ready(); - return await _transport.history( - channel, - limit: limit, - since: since, - reverse: reverse, - ); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyFetchException( - message: 'Error while fetching history for channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for history. -base mixin SpinifyRPCMixin on SpinifyBase, SpinifyErrorsMixin { - @override - Future> rpc(String method, List data) async { - try { - await ready(); - return await _transport.rpc(method, data); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifyFetchException( - message: 'Error while remote procedure call for method $method', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Responsible for metrics. -base mixin SpinifyMetricsMixin on SpinifyBase, SpinifyStateMixin { - int _connectsTotal = 0, _connectsSuccessful = 0, _disconnects = 0; - DateTime? _lastDisconnectTime, _lastConnectTime; - ({int? code, String? reason})? _lastDisconnect; - String? _lastUrl; - late DateTime _initializedAt; - - @override - void _initSpinify() { - _initializedAt = DateTime.now().toUtc(); - super._initSpinify(); - } - - @override - Future connect(String url) async { - _lastUrl = url; - _connectsTotal++; - return super.connect(url); - } - - @override - void _onConnected(SpinifyState$Connected state) { - _lastConnectTime = DateTime.now().toUtc(); - _connectsSuccessful++; - super._onConnected(state); - } - - @override - void _onDisconnected(SpinifyState$Disconnected state) { - _lastDisconnectTime = DateTime.now().toUtc(); - _lastDisconnect = (code: state.closeCode, reason: state.closeReason); - _disconnects = 0; - super._onDisconnected(state); - } - - /// Get metrics of Spinify client. - @override - SpinifyMetrics get metrics { - final timestamp = DateTime.now().toUtc(); - return SpinifyMetrics( - timestamp: timestamp, - initializedAt: _initializedAt, - lastUrl: _lastUrl, - reconnects: (successful: _connectsSuccessful, total: _connectsTotal), - subscriptions: ( - client: _clientSubscriptionManager.count, - server: _serverSubscriptionManager.count, - ), - speed: _transport.speed, - state: state, - received: _transport.received, - transferred: _transport.transferred, - lastConnectTime: _lastConnectTime, - lastDisconnectTime: _lastDisconnectTime, - disconnects: _disconnects, - lastDisconnect: _lastDisconnect, - isRefreshActive: _refreshTimer?.isActive ?? false, - ); - } -} - -/// Mixin responsible for queue. -/// SHOULD BE LAST MIXIN. -base mixin SpinifyQueueMixin on SpinifyBase { - final SpinifyEventQueue _eventQueue = SpinifyEventQueue(); - - @override - Future connect(String url) => - _eventQueue.push('connect', () => super.connect(url)); - - @override - Future publish(String channel, List data) => - _eventQueue.push('publish', () => super.publish(channel, data)); - - /* @override - FutureOr ready() => _eventQueue.push('ready', super.ready); */ - - @override - Future presence(String channel) => - _eventQueue.push( - 'presence', - () => super.presence(channel), - ); - - @override - Future presenceStats(String channel) => - _eventQueue.push( - 'presenceStats', - () => super.presenceStats(channel), - ); - - @override - Future history( - String channel, { - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) => - _eventQueue.push( - 'history', - () => super.history( - channel, - limit: limit, - since: since, - reverse: reverse, - ), - ); - - @override - Future> rpc(String method, List data) => - _eventQueue.push>( - 'rpc', - () => super.rpc(method, data), - ); - - @override - Future disconnect([ - int code = 0, - String reason = 'Disconnect called', - ]) => - _eventQueue.push( - 'disconnect', () => super.disconnect(code, reason)); - - @override - Future close() => _eventQueue - .push('close', super.close) - .whenComplete(_eventQueue.close); -} diff --git a/lib/src.old/client/spinify_interface.dart b/lib/src.old/client/spinify_interface.dart deleted file mode 100644 index 5158298..0000000 --- a/lib/src.old/client/spinify_interface.dart +++ /dev/null @@ -1,146 +0,0 @@ -// ignore_for_file: one_member_abstracts - -import 'dart:async'; - -import '../model/history.dart'; -import '../model/metrics.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/pushes_stream.dart'; -import '../model/stream_position.dart'; -import '../subscription/subscription.dart'; -import '../subscription/subscription_config.dart'; -import 'state.dart'; -import 'states_stream.dart'; - -/// Spinify client interface. -abstract interface class ISpinify - implements - ISpinifyStateOwner, - ISpinifyAsyncMessageSender, - ISpinifyPublicationSender, - ISpinifyEventReceiver, - ISpinifySubscriptionsManager, - ISpinifyPresenceOwner, - ISpinifyHistoryOwner, - ISpinifyRemoteProcedureCall, - ISpinifyMetricsOwner { - /// Connect to the server. - /// [url] is a URL of endpoint. - Future connect(String url); - - /// Ready resolves when client successfully connected. - /// Throws exceptions if called not in connecting or connected state. - FutureOr ready(); - - /// Disconnect from the server. - Future disconnect([ - int code = 0, - String reason = 'Disconnect called', - ]); - - /// Client if not needed anymore. - /// Permanent close connection to the server and - /// free all allocated resources. - Future close(); -} - -/// Spinify client state owner interface. -abstract interface class ISpinifyStateOwner { - /// State of client. - SpinifyState get state; - - /// Stream of client states. - abstract final SpinifyStatesStream states; -} - -/// Spinify send publication interface. -abstract interface class ISpinifyPublicationSender { - /// Publish data to specific subscription channel - Future publish(String channel, List data); -} - -/// Spinify send asynchronous message interface. -abstract interface class ISpinifyAsyncMessageSender { - /// Send asynchronous message to a server. This method makes sense - /// only when using Centrifuge library for Go on a server side. In Centrifugo - /// asynchronous message handler does not exist. - Future send(List data); -} - -/// Spinify event receiver interface. -abstract interface class ISpinifyEventReceiver { - /// Stream of received pushes from Centrifugo server for a channel. - abstract final SpinifyPushesStream stream; -} - -/// Spinify client subscriptions manager interface. -abstract interface class ISpinifySubscriptionsManager { - /// Create new client-side subscription. - /// `newSubscription(channel, config)` allocates a new Subscription - /// in the registry or throws an exception if the Subscription - /// is already there. We will discuss common Subscription options below. - SpinifyClientSubscription newSubscription( - String channel, [ - SpinifySubscriptionConfig? config, - ]); - - /// Get subscription to the channel - /// from internal registry or null if not found. - /// - /// You need to call [SpinifyClientSubscription.subscribe] - /// to start receiving events - /// in the channel. - SpinifyClientSubscription? getSubscription(String channel); - - /// Remove the [SpinifySubscription] from internal registry - /// and unsubscribe from [SpinifyClientSubscription.channel]. - Future removeSubscription(SpinifyClientSubscription subscription); - - /// Get map wirth all registered client-side & server-side subscriptions. - /// Returns all registered subscriptions, - /// so you can iterate over all and do some action if required. - /// - /// For example: - /// ```dart - /// final subscription = spinify.subscriptions.client['chat']!; - /// await subscription.unsubscribe(); - /// ``` - ({ - Map client, - Map server, - }) get subscriptions; -} - -/// Spinify presence owner interface. -abstract interface class ISpinifyPresenceOwner { - /// Fetch presence information inside a channel. - Future presence(String channel); - - /// Fetch presence stats information inside a channel. - Future presenceStats(String channel); -} - -/// Spinify history owner interface. -abstract interface class ISpinifyHistoryOwner { - /// Fetch publication history inside a channel. - /// Only for channels where history is enabled. - Future history( - String channel, { - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }); -} - -/// Spinify remote procedure call interface. -abstract interface class ISpinifyRemoteProcedureCall { - /// Send arbitrary RPC and wait for response. - Future> rpc(String method, List data); -} - -/// Spinify metrics interface. -abstract interface class ISpinifyMetricsOwner { - /// Get metrics of Spinify client. - SpinifyMetrics get metrics; -} diff --git a/lib/src.old/client/state.dart b/lib/src.old/client/state.dart deleted file mode 100644 index 2dea951..0000000 --- a/lib/src.old/client/state.dart +++ /dev/null @@ -1,486 +0,0 @@ -import 'dart:convert'; - -import 'package:meta/meta.dart'; - -/// {@template state} -/// Spinify client connection states -/// -/// Client connection has 4 states: -/// -/// - disconnected -/// - connecting -/// - connected -/// - closed -/// -/// When a new Client is created it has a disconnected state. -/// To connect to a server connect() method must be called. -/// After calling connect Client moves to the connecting state. -/// If a Client can't connect to a server it attempts to create -/// a connection with an exponential backoff algorithm (with full jitter). -/// If a connection to a server is connected then the state becomes connected. -/// -/// If a connection is lost (due to a missing network for example, -/// or due to reconnect advice received from a server, -/// or due to some client-side closed that can't be recovered -/// without reconnecting) Client goes to the connecting state again. -/// In this state Client tries to reconnect -/// (again, with an exponential backoff algorithm). -/// -/// The Client's state can become disconnected. -/// This happens when Client's disconnect() method was called by a developer. -/// Also, this can happen due to server advice from a server, -/// or due to a terminal problem that happened on the client-side. -/// {@endtemplate} -/// {@category Client} -/// {@category Entity} -@immutable -sealed class SpinifyState extends _$SpinifyStateBase { - /// {@macro state} - const SpinifyState(super.timestamp); - - /// Disconnected state - /// {@macro state} - factory SpinifyState.disconnected({ - DateTime? timestamp, - int? closeCode, - String? closeReason, - }) = SpinifyState$Disconnected; - - /// Connecting - /// {@macro state} - factory SpinifyState.connecting({required String url, DateTime? timestamp}) = - SpinifyState$Connecting; - - /// Connected - /// {@macro state} - factory SpinifyState.connected({ - required String url, - DateTime? timestamp, - String? client, - String? version, - bool? expires, - DateTime? ttl, - Duration? pingInterval, - bool? sendPong, - String? session, - String? node, - List? data, - }) = SpinifyState$Connected; - - /// Permanently closed - /// {@macro state} - factory SpinifyState.closed({DateTime? timestamp}) = SpinifyState$Closed; - - /// Restore state from JSON - /// {@macro state} - factory SpinifyState.fromJson(Map json) => switch (( - json['type']?.toString().trim().toLowerCase(), - json['timestamp'] ?? DateTime.now().microsecondsSinceEpoch, - json['url'], - )) { - ('disconnected', int timestamp, _) => SpinifyState.disconnected( - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - closeCode: switch (json['closeCode']) { - int closeCode => closeCode, - _ => null, - }, - closeReason: switch (json['closeReason']) { - String closeReason => closeReason, - _ => null, - }, - ), - ('connecting', int timestamp, String url) => SpinifyState.connecting( - url: url, - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - ), - ('connected', int timestamp, String url) => SpinifyState.connected( - url: url, - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - client: json['client']?.toString(), - version: json['version']?.toString(), - expires: switch (json['expires']) { - bool expires => expires, - _ => null, - }, - ttl: switch (json['ttl']) { - int ttl => DateTime.fromMicrosecondsSinceEpoch(ttl), - _ => null, - }, - pingInterval: switch (json['pingInterval']) { - int pingInterval => Duration(seconds: pingInterval), - _ => null, - }, - sendPong: switch (json['sendPong']) { - bool sendPong => sendPong, - _ => null, - }, - session: json['session']?.toString(), - node: json['node']?.toString(), - data: switch (json['data']) { - String data when data.isNotEmpty => base64Decode(data), - _ => null, - }, - ), - ('closed', int timestamp, _) => SpinifyState.closed( - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - ), - _ => throw FormatException('Unknown state: $json'), - }; -} - -/// Disconnected -/// Client should handle disconnect advices from server. -/// In websocket case disconnect advice is sent in CLOSE Websocket frame. -/// Disconnect advice contains uint32 code and human-readable string reason. -/// -/// {@macro state} -/// {@category Client} -/// {@category Entity} -final class SpinifyState$Disconnected extends SpinifyState { - /// Disconnected - /// - /// {@macro state} - SpinifyState$Disconnected({ - DateTime? timestamp, - this.closeCode, - this.closeReason, - }) : super(timestamp ?? DateTime.now()); - - @override - String get type => 'disconnected'; - - @override - String? get url => null; - - /// The close code set when the WebSocket connection is closed. - /// If there is no close code available this property will be null. - final int? closeCode; - - /// The close reason set when the WebSocket connection is closed. - /// If there is no close reason available this property will be null. - final String? closeReason; - - @override - bool get isDisconnected => true; - - @override - bool get isConnecting => false; - - @override - bool get isConnected => false; - - @override - bool get isClosed => false; - - @override - R map({ - required SpinifyStateMatch disconnected, - required SpinifyStateMatch connecting, - required SpinifyStateMatch connected, - required SpinifyStateMatch closed, - }) => - disconnected(this); - - @override - Map toJson() => { - ...super.toJson(), - if (closeCode != null) 'closeCode': closeCode, - if (closeReason != null) 'closeReason': closeReason, - }; - - @override - int get hashCode => 0; - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SpinifyState$Disconnected && - other.timestamp.isAtSameMomentAs(timestamp)); - - @override - String toString() => r'SpinifyState$Disconnected{}'; -} - -/// Connecting -/// -/// {@macro state} -/// {@category Client} -/// {@category Entity} -final class SpinifyState$Connecting extends SpinifyState { - /// Connecting - /// - /// {@macro state} - SpinifyState$Connecting({required this.url, DateTime? timestamp}) - : super(timestamp ?? DateTime.now()); - - @override - String get type => 'connecting'; - - @override - final String url; - - @override - bool get isDisconnected => false; - - @override - bool get isConnecting => true; - - @override - bool get isConnected => false; - - @override - bool get isClosed => false; - - @override - R map({ - required SpinifyStateMatch disconnected, - required SpinifyStateMatch connecting, - required SpinifyStateMatch connected, - required SpinifyStateMatch closed, - }) => - connecting(this); - - @override - int get hashCode => 1; - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SpinifyState$Connecting && - other.timestamp.isAtSameMomentAs(timestamp)); - - @override - String toString() => r'SpinifyState$Connecting{}'; -} - -/// Connected -/// -/// {@macro state} -/// {@category Client} -/// {@category Entity} -final class SpinifyState$Connected extends SpinifyState { - /// Connected - /// - /// {@macro state} - SpinifyState$Connected({ - required this.url, - DateTime? timestamp, - this.client, - this.version, - this.ttl, - this.expires, - this.pingInterval, - this.sendPong, - this.session, - this.node, - this.data, - }) : super(timestamp ?? DateTime.now()); - - @override - String get type => 'connected'; - - @override - final String url; - - /// Unique client connection ID server issued to this connection - final String? client; - - /// Server version - final String? version; - - /// Whether a server will expire connection at some point - final bool? expires; - - /// Time when connection will be expired - final DateTime? ttl; - - /// Client must periodically (once in 25 secs, configurable) send - /// ping messages to server. If pong has not beed received in 5 secs - /// (configurable) then client must disconnect from server - /// and try to reconnect with backoff strategy. - final Duration? pingInterval; - - /// Whether to send asynchronous message when pong received. - final bool? sendPong; - - /// Session ID. - final String? session; - - /// Server node ID. - final String? node; - - /// Additional data returned from server on connect. - final List? data; - - @override - bool get isDisconnected => false; - - @override - bool get isConnecting => false; - - @override - bool get isConnected => true; - - @override - bool get isClosed => false; - - @override - R map({ - required SpinifyStateMatch disconnected, - required SpinifyStateMatch connecting, - required SpinifyStateMatch connected, - required SpinifyStateMatch closed, - }) => - connected(this); - - @override - Map toJson() => { - ...super.toJson(), - if (client != null) 'client': client, - if (version != null) 'version': version, - if (expires != null) 'expires': expires, - if (ttl != null) 'ttl': ttl?.microsecondsSinceEpoch, - if (pingInterval != null) 'pingInterval': pingInterval?.inSeconds, - if (sendPong != null) 'sendPong': sendPong, - if (session != null) 'session': session, - if (node != null) 'node': node, - if (data != null) 'data': base64Encode(data!), - }; - - @override - int get hashCode => 2; - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SpinifyState$Connected && - other.timestamp.isAtSameMomentAs(timestamp)); - - @override - String toString() => r'SpinifyState$Connected{}'; -} - -/// Permanently closed -/// -/// {@macro state} -/// {@category Client} -/// {@category Entity} -final class SpinifyState$Closed extends SpinifyState { - /// Permanently closed - /// - /// {@macro state} - SpinifyState$Closed({DateTime? timestamp}) - : super(timestamp ?? DateTime.now()); - - @override - String get type => 'closed'; - - @override - String? get url => null; - - @override - bool get isDisconnected => false; - - @override - bool get isConnecting => false; - - @override - bool get isConnected => true; - - @override - bool get isClosed => false; - - @override - R map({ - required SpinifyStateMatch disconnected, - required SpinifyStateMatch connecting, - required SpinifyStateMatch connected, - required SpinifyStateMatch closed, - }) => - closed(this); - - @override - int get hashCode => 3; - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SpinifyState$Closed && - other.timestamp.isAtSameMomentAs(timestamp)); - - @override - String toString() => r'SpinifyState$Closed{}'; -} - -/// Pattern matching for [SpinifyState]. -/// {@category Entity} -typedef SpinifyStateMatch = R Function(S state); - -@immutable -abstract base class _$SpinifyStateBase { - const _$SpinifyStateBase(this.timestamp); - - /// Represents the current state type. - abstract final String type; - - /// URL of endpoint. - abstract final String? url; - - /// Disconnected state - abstract final bool isDisconnected; - - /// Connecting state - abstract final bool isConnecting; - - /// Connected state - abstract final bool isConnected; - - /// Closed state - abstract final bool isClosed; - - /// Timestamp of state change. - final DateTime timestamp; - - /// Pattern matching for [SpinifyState]. - R map({ - required SpinifyStateMatch disconnected, - required SpinifyStateMatch connecting, - required SpinifyStateMatch connected, - required SpinifyStateMatch closed, - }); - - /// Pattern matching for [SpinifyState]. - R maybeMap({ - required R Function() orElse, - SpinifyStateMatch? disconnected, - SpinifyStateMatch? connecting, - SpinifyStateMatch? connected, - SpinifyStateMatch? closed, - }) => - map( - disconnected: disconnected ?? (_) => orElse(), - connecting: connecting ?? (_) => orElse(), - connected: connected ?? (_) => orElse(), - closed: closed ?? (_) => orElse(), - ); - - /// Pattern matching for [SpinifyState]. - R? mapOrNull({ - SpinifyStateMatch? disconnected, - SpinifyStateMatch? connecting, - SpinifyStateMatch? connected, - SpinifyStateMatch? closed, - }) => - map( - disconnected: disconnected ?? (_) => null, - connecting: connecting ?? (_) => null, - connected: connected ?? (_) => null, - closed: closed ?? (_) => null, - ); - - Map toJson() => { - 'type': type, - 'timestamp': timestamp.toUtc().toIso8601String(), - if (url != null) 'url': url, - }; -} diff --git a/lib/src.old/client/states_stream.dart b/lib/src.old/client/states_stream.dart deleted file mode 100644 index 5c2996c..0000000 --- a/lib/src.old/client/states_stream.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'state.dart'; - -/// Stream of Spinify's [SpinifyState] changes. -/// {@category Client} -/// {@category Entity} -final class SpinifyStatesStream extends StreamView { - /// Stream of Spinify's [SpinifyState] changes. - SpinifyStatesStream(super.stream); - - /// Connection has not yet been established, but the WebSocket is trying. - late final Stream disconnected = - whereType(); - - /// Disconnected state - late final Stream connecting = - whereType(); - - /// Connected - late final Stream connected = - whereType(); - - /// Permanently closed - late final Stream closed = - whereType(); - - /// Filtered stream of data of [SpinifyState]. - Stream whereType() => - transform(StreamTransformer.fromHandlers( - handleData: (data, sink) => switch (data) { - T valid => sink.add(valid), - _ => null, - }, - )).asBroadcastStream(); -} diff --git a/lib/src.old/model/channel_presence.dart b/lib/src.old/model/channel_presence.dart deleted file mode 100644 index cc2d5cb..0000000 --- a/lib/src.old/model/channel_presence.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:meta/meta.dart'; -import 'channel_push.dart'; -import 'client_info.dart'; - -/// {@template channel_presence} -/// Channel presence. -/// Join / Leave events. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -@immutable -sealed class SpinifyChannelPresence extends SpinifyChannelPush { - /// {@macro channel_presence} - const SpinifyChannelPresence({ - required super.timestamp, - required super.channel, - required this.info, - }); - - /// Client info - final SpinifyClientInfo info; - - /// Whether this is a join event - abstract final bool isJoin; - - /// Whether this is a leave event - abstract final bool isLeave; -} - -/// {@macro channel_presence} -/// {@category Event} -/// {@subCategory Push} -/// {@subCategory Presence} -final class SpinifyJoin extends SpinifyChannelPresence { - /// {@macro channel_presence} - const SpinifyJoin({ - required super.timestamp, - required super.channel, - required super.info, - }); - - @override - String get type => 'join'; - - @override - bool get isJoin => true; - - @override - bool get isLeave => false; - - @override - String toString() => 'SpinifyJoin{channel: $channel}'; -} - -/// {@macro channel_presence} -/// {@category Event} -/// {@subCategory Push} -/// {@subCategory Presence} -final class SpinifyLeave extends SpinifyChannelPresence { - /// {@macro channel_presence} - const SpinifyLeave({ - required super.timestamp, - required super.channel, - required super.info, - }); - - @override - String get type => 'leave'; - - @override - bool get isJoin => false; - - @override - bool get isLeave => true; - - @override - String toString() => 'SpinifyLeave{channel: $channel}'; -} diff --git a/lib/src.old/model/channel_push.dart b/lib/src.old/model/channel_push.dart deleted file mode 100644 index f8716d2..0000000 --- a/lib/src.old/model/channel_push.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:meta/meta.dart'; -import 'event.dart'; - -/// {@template spinify_channel_push} -/// Base class for all channel push events. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -@immutable -abstract base class SpinifyChannelPush extends SpinifyEvent { - /// {@macro spinify_channel_push} - const SpinifyChannelPush({ - required super.timestamp, - required this.channel, - }); - - /// Channel - final String channel; - - @override - @nonVirtual - bool get isPush => true; - - @override - String toString() => 'SpinifyChannelPush{channel: $channel}'; -} diff --git a/lib/src.old/model/client_info.dart b/lib/src.old/model/client_info.dart deleted file mode 100644 index d2e683c..0000000 --- a/lib/src.old/model/client_info.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template client_info} -/// Client information. -/// {@endtemplate} -/// {@category Entity} -@immutable -final class SpinifyClientInfo { - /// {@macro client_info} - const SpinifyClientInfo({ - required this.user, - required this.client, - required this.connectionInfo, - required this.channelInfo, - }); - - /// User - final String user; - - /// Client - final String client; - - /// Connection information - final List? connectionInfo; - - /// Channel information - final List? channelInfo; - - @override - int get hashCode => Object.hashAll([ - user, - client, - connectionInfo, - channelInfo, - ]); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SpinifyClientInfo && - user == other.client && - client == other.client && - connectionInfo == other.connectionInfo && - channelInfo == other.channelInfo; - - @override - String toString() => 'SpinifyClientInfo{' - 'user: $user, ' - 'client: $client, ' - 'connectionInfo: ${connectionInfo == null ? 'null' : 'bytes'}, ' - 'channelInfo: ${channelInfo == null ? 'null' : 'bytes'}' - '}'; -} diff --git a/lib/src.old/model/connect.dart b/lib/src.old/model/connect.dart deleted file mode 100644 index 7af4476..0000000 --- a/lib/src.old/model/connect.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'channel_push.dart'; - -/// {@template connect} -/// Connect push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifyConnect extends SpinifyChannelPush { - /// {@macro connect} - const SpinifyConnect({ - required super.timestamp, - required super.channel, - required this.client, - required this.version, - required this.data, - this.expires, - this.ttl, - this.pingInterval, - this.sendPong, - this.session, - this.node, - }); - - @override - String get type => 'connect'; - - /// Unique client connection ID server issued to this connection - final String client; - - /// Server version - final String version; - - /// Whether a server will expire connection at some point - final bool? expires; - - /// Time when connection will be expired - final DateTime? ttl; - - /// Client must periodically (once in 25 secs, configurable) send - /// ping messages to server. If pong has not beed received in 5 secs - /// (configurable) then client must disconnect from server - /// and try to reconnect with backoff strategy. - final Duration? pingInterval; - - /// Whether to send asynchronous message when pong received. - final bool? sendPong; - - /// Session ID. - final String? session; - - /// Server node ID. - final String? node; - - /// Payload of connected push. - final List data; - - @override - String toString() => 'SpinifyConnect{channel: $channel}'; -} diff --git a/lib/src.old/model/disconnect.dart b/lib/src.old/model/disconnect.dart deleted file mode 100644 index d29f24b..0000000 --- a/lib/src.old/model/disconnect.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'channel_push.dart'; - -/// {@template disconnect} -/// Disconnect push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifyDisconnect extends SpinifyChannelPush { - /// {@macro disconnect} - const SpinifyDisconnect({ - required super.timestamp, - required super.channel, - required this.code, - required this.reason, - required this.reconnect, - }); - - @override - String get type => 'disconnect'; - - /// Code of disconnect. - final int code; - - /// Reason of disconnect. - final String reason; - - /// Reconnect flag. - final bool reconnect; - - @override - String toString() => 'SpinifyDisconnect{channel: $channel}'; -} diff --git a/lib/src.old/model/event.dart b/lib/src.old/model/event.dart deleted file mode 100644 index 837e025..0000000 --- a/lib/src.old/model/event.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template spinify_event} -/// Base class for all channel events. -/// {@endtemplate} -/// {@category Event} -@immutable -abstract base class SpinifyEvent implements Comparable { - /// {@macro spinify_event} - const SpinifyEvent({ - required this.timestamp, - }); - - /// Event type. - abstract final String type; - - /// Timestamp of event - final DateTime timestamp; - - /// Whether this event is a push event. - bool get isPush; - - @override - int compareTo(SpinifyEvent other) => timestamp.compareTo(other.timestamp); - - @override - String toString() => 'SpinifyEvent{type: $type}'; -} diff --git a/lib/src.old/model/exception.dart b/lib/src.old/model/exception.dart deleted file mode 100644 index faba366..0000000 --- a/lib/src.old/model/exception.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template exception} -/// Spinify exception. -/// {@endtemplate} -/// {@category Exception} -@immutable -sealed class SpinifyException implements Exception { - /// {@macro exception} - const SpinifyException( - this.code, - this.message, [ - this.error, - ]); - - /// Error code. - final String code; - - /// Error message. - final String message; - - /// Source error of exception if exists. - final Object? error; - - @override - int get hashCode => code.hashCode; - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() => message; -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifyConnectionException extends SpinifyException { - /// {@macro exception} - const SpinifyConnectionException({String? message, Object? error}) - : super( - 'spinify_connection_exception', - message ?? 'Connection problem', - error, - ); -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifyReplyException extends SpinifyException { - /// {@macro exception} - const SpinifyReplyException({ - required this.replyCode, - required String replyMessage, - required this.temporary, - }) : super( - 'spinify_reply_exception', - replyMessage, - ); - - /// Reply code. - final int replyCode; - - /// Is reply error final. - final bool temporary; -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifyPingException extends SpinifyException { - /// {@macro exception} - const SpinifyPingException([Object? error]) - : super( - 'spinify_ping_exception', - 'Ping error', - error, - ); -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifySubscriptionException extends SpinifyException { - /// {@macro exception} - const SpinifySubscriptionException({ - required this.channel, - required String message, - Object? error, - }) : super( - 'spinify_subscription_exception', - message, - error, - ); - - /// Subscription channel. - final String channel; -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifySendException extends SpinifyException { - /// {@macro exception} - const SpinifySendException({ - String? message, - Object? error, - }) : super( - 'spinify_send_exception', - message ?? 'Failed to send message', - error, - ); -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifyFetchException extends SpinifyException { - /// {@macro exception} - const SpinifyFetchException({ - String? message, - Object? error, - }) : super( - 'spinify_fetch_exception', - message ?? 'Failed to fetch data', - error, - ); -} - -/// {@macro exception} -/// {@category Exception} -final class SpinifyRefreshException extends SpinifyException { - /// {@macro exception} - const SpinifyRefreshException({ - String? message, - Object? error, - }) : super( - 'spinify_refresh_exception', - message ?? 'Error while refreshing connection token', - error, - ); -} diff --git a/lib/src.old/model/history.dart b/lib/src.old/model/history.dart deleted file mode 100644 index 256e531..0000000 --- a/lib/src.old/model/history.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:meta/meta.dart'; -import 'publication.dart'; -import 'stream_position.dart'; - -/// {@template history} -/// History -/// {@endtemplate} -/// {@category Entity} -@immutable -final class SpinifyHistory { - /// {@macro history} - const SpinifyHistory({ - required this.publications, - required this.since, - }); - - /// Publications - final List publications; - - /// Offset and epoch of last publication in publications list - final SpinifyStreamPosition since; - - @override - String toString() => 'SpinifyHistory{}'; -} diff --git a/lib/src.old/model/jwt.dart b/lib/src.old/model/jwt.dart deleted file mode 100644 index 0787190..0000000 --- a/lib/src.old/model/jwt.dart +++ /dev/null @@ -1,409 +0,0 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; -import 'package:meta/meta.dart'; - -/// {@template jwt} -/// A JWT token consists of three parts: the header, -/// the payload, and the signature or encryption data. -/// The first two elements are JSON objects of a specific structure. -/// The third element is calculated based on the first two -/// and depends on the chosen algorithm -/// (in the case of using an unsigned JWT, it can be omitted). -/// Tokens can be re-encoded into a compact representation -/// (JWS/JWE Compact Serialization): -/// the header and payload are subjected to Base64-URL encoding, -/// after which the signature is added, -/// and all three elements are separated by periods ("."). -/// -/// https://centrifugal.dev/docs/server/authentication#connection-jwt-claims -/// {@endtemplate} -/// {@category Entity} -/// {@subCategory JWT} -@immutable -sealed class SpinifyJWT { - /// {@macro jwt} - /// - /// Creates JWT from [secret] (with HMAC-SHA256 algorithm) - const factory SpinifyJWT({ - required String sub, - int? exp, - int? iat, - String? jti, - String? aud, - String? iss, - Map? info, - String? b64info, - List? channels, - Map? subs, - Map? meta, - int? expireAt, - }) = _SpinifyJWTImpl; - - /// {@macro jwt} - /// - /// Parses JWT, if [secret] is provided - /// then checks signature by HMAC-SHA256 algorithm. - factory SpinifyJWT.decode(String jwt, [String? secret]) = - _SpinifyJWTImpl.decode; - - const SpinifyJWT._(); - - /// This is a standard JWT claim which must contain - /// an ID of the current application user (as string). - /// - /// If a user is not currently authenticated in an application, - /// but you want to let him connect anyway – you can use - /// an empty string as a user ID in sub claim. - /// This is called anonymous access. - abstract final String sub; - - /// This is a UNIX timestamp seconds when the token will expire. - /// This is a standard JWT claim - all JWT libraries - /// for different languages provide an API to set it. - /// - /// If exp claim is not provided then Centrifugo won't expire connection. - /// When provided special algorithm will find connections with exp in the past - /// and activate the connection refresh mechanism. - /// Refresh mechanism allows connection to survive and be prolonged. - /// In case of refresh failure, the client connection - /// will be eventually closed by Centrifugo - /// and won't be accepted until new valid and actual - /// credentials are provided in the connection token. - /// - /// You can use the connection expiration mechanism in - /// cases when you don't want users of your app - /// to be subscribed on channels after being banned/deactivated in the application. - /// Or to protect your users from token leakage - /// (providing a reasonably short time of expiration). - /// - /// Choose exp value wisely, you don't need small - /// values because the refresh mechanism - /// will hit your application often with refresh requests. - /// But setting this value too large can lead - /// to slow user connection deactivation. This is a trade-off. - /// - /// Read more about connection expiration below. - abstract final int? exp; - - /// This is a UNIX time when token was issued (seconds). - /// See [definition in RFC](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6). - /// This claim is optional - abstract final int? iat; - - /// This is a token unique ID. See [definition in RFC](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7). - /// This claim is optional. - abstract final String? jti; - - /// Audience. - /// [rfc7519 aud claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) - /// By default, Centrifugo does not check JWT audience. - /// - /// But you can force this check by setting token_audience string option: - /// ```json - /// { - /// "token_audience": "centrifugo" - /// } - /// ``` - /// - /// Setting token_audience will also affect subscription tokens - /// (used for channel token authorization). - /// If you need to separate connection token configuration - /// and subscription token configuration - /// check out separate subscription token config feature. - /// - /// This claim is optional. - abstract final String? aud; - - /// Issuer. - /// The "iss" (issuer) claim identifies the principal that issued the JWT. - /// [rfc7519 iss claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) - /// By default, Centrifugo does not check JWT issuer (rfc7519 iss claim). - /// But you can force this check by setting token_issuer string option: - /// ```json - /// { - /// "token_issuer": "my_app" - /// } - /// ``` - /// - /// Setting token_issuer will also affect subscription tokens - /// (used for channel token authorization). - /// If you need to separate connection token configuration - /// and subscription token configuration - /// check out separate subscription token config feature. - /// - /// This claim is optional. - abstract final String? iss; - - /// This claim is optional - this is additional information - /// about client connection - /// that can be provided for Centrifugo. - /// This information will be included in presence information, - /// join/leave events, and channel publication if it was published from a client-side. - abstract final Map? info; - - /// If you are using binary Protobuf protocol you may want info - /// to be custom bytes. Use this field in this case. - /// - /// This field contains a `base64` representation of your bytes. - /// After receiving Centrifugo will decode base64 back to bytes - /// and will embed the result into various places described above. - abstract final String? b64info; - - /// An optional array of strings with server-side channels - /// to subscribe a client to. - /// See more details about [server-side subscriptions](https://centrifugal.dev/docs/server/server_subs). - abstract final List? channels; - - /// Subscriptions - /// An optional map of channels with options. This is like a channels claim - /// but allows more control over server-side subscription since every channel - /// can be annotated with info, data, and so on using options. - /// The claim sub described above is a standart JWT claim to provide a user ID - /// (it's a shortcut from subject). - /// While claims have similar names they have - /// different purpose in a connection JWT. - /// - /// Example: - /// ```json - /// { - /// ... - /// "subs": { - /// "channel1": { - /// "data": {"welcome": "welcome to channel1"} - /// }, - /// "channel2": { - /// "data": {"welcome": "welcome to channel2"} - /// } - /// } - /// } - /// ``` - /// - /// Subscribe options: - /// - (optional) info - JSON object - Custom channel info - /// - (optional) b64info - string - Custom channel info in Base64 - /// - (optional) data - JSON object - Custom JSON data - /// - (optional) b64data - string - Same as `data` but in Base64 - /// - (optional) override - Override object - Override some channel options. - /// - /// Override object: - /// - (optional) presence - BoolValue - Override presence - /// - (optional) join_leave - BoolValue - Override join_leave - /// - (optional) position - BoolValue - Override position - /// - (optional) recover - BoolValue - Override recover - /// - /// BoolValue is an object like this: - /// ```json - /// { - /// "value": true - /// } - /// ``` - abstract final Map? subs; - - /// Meta is an additional JSON object (ex. `{"key": "value"}`) - /// that will be attached to a connection. - /// Unlike `info` it's never exposed to clients inside presence - /// and join/leave payloads - /// and only accessible on a backend side. It may be included - /// in proxy calls from Centrifugo - /// to the application backend (see `proxy_include_connection_meta` option). - /// Also, there is a `connections` API method in Centrifugo PRO that returns - /// this data in the connection description object. - abstract final Map? meta; - - /// By default, Centrifugo looks on `exp` claim - /// to configure connection expiration. - /// In most cases this is fine, but there could be situations - /// where you wish to decouple token expiration - /// check with connection expiration time. - /// As soon as the `expire_at` claim is provided (set) - /// in JWT Centrifugo relies on it for setting - /// connection expiration time - /// (JWT expiration still checked over `exp` though). - /// - /// `expire_at` is a UNIX timestamp seconds when the connection should expire. - /// - /// Set it to the future time for expiring connection at some point - /// Set it to 0 to disable connection expiration - /// (but still check token exp claim). - abstract final int? expireAt; - - /// Creates JWT from [secret] (with HMAC-SHA256 algorithm) - /// and current payload. - String encode(String secret); -} - -final class _SpinifyJWTImpl extends SpinifyJWT { - const _SpinifyJWTImpl({ - required this.sub, - this.exp, - this.iat, - this.jti, - this.aud, - this.iss, - this.info, - this.b64info, - this.channels, - this.subs, - this.meta, - this.expireAt, - }) : super._(); - - factory _SpinifyJWTImpl.decode(String jwt, [String? secret]) { - // Разделение токена на составляющие части - var parts = jwt.split('.'); - if (parts.length != 3) { - throw const FormatException( - 'Invalid token format, expected 3 parts separated by "."'); - } - final [encodedHeader, encodedPayload, encodedSignature] = parts; - - if (secret != null) { - // Вычисление подписи - final key = utf8.encode(secret); // Your 256 bit secret key - final bytes = utf8.encode('$encodedHeader.$encodedPayload'); - var hmacSha256 = Hmac(sha256, key); // HMAC-SHA256 - var digest = hmacSha256.convert(bytes); - - // Кодирование подписи - var computedSignature = base64Url.encode(digest.bytes); - - // Сравнение подписи в токене с вычисленной подписью - if (computedSignature != encodedSignature) { - throw const FormatException('Invalid token signature'); - } - } - - Map payload; - try { - payload = const Base64Decoder() - .fuse(const Utf8Decoder()) - .fuse>( - const JsonDecoder().cast>()) - .convert(encodedPayload); - } on Object catch (_, stackTrace) { - Error.throwWithStackTrace( - const FormatException('Can\'t decode token payload'), stackTrace); - } - try { - return _SpinifyJWTImpl( - sub: payload['sub'] as String, - exp: payload['exp'] as int?, - iat: payload['iat'] as int?, - jti: payload['jti'] as String?, - aud: payload['aud'] as String?, - iss: payload['iss'] as String?, - info: payload['info'] as Map?, - b64info: payload['b64info'] as String?, - channels: (payload['channels'] as Iterable?) - ?.whereType() - .toList(), - subs: payload['subs'] as Map?, - meta: payload['meta'] as Map?, - expireAt: payload['expire_at'] as int?, - ); - } on Object catch (_, stackTrace) { - Error.throwWithStackTrace( - const FormatException('Invalid token payload data'), stackTrace); - } - } - - static final Converter, String> _$encoder = - const JsonEncoder() - .cast, String>() - .fuse>(const Utf8Encoder()) - .fuse(const Base64Encoder.urlSafe()) - .fuse(const _UnpaddedBase64Converter()); - - static final String _$headerHmacSha256 = _$encoder.convert({ - 'alg': 'HS256', - 'typ': 'JWT', - }); - - @override - final String sub; - - @override - final int? exp; - - @override - final int? iat; - - @override - final String? jti; - - @override - final String? aud; - - @override - final String? iss; - - @override - final Map? info; - - @override - final String? b64info; - - @override - final List? channels; - - @override - final Map? subs; - - @override - final Map? meta; - - @override - final int? expireAt; - - @override - String encode(String secret) { - // Encode header and payload - final encodedHeader = _$headerHmacSha256; - final encodedPayload = _$encoder.convert({ - 'sub': sub, - if (exp != null) 'exp': exp, - if (iat != null) 'iat': iat, - if (jti != null) 'jti': jti, - if (aud != null) 'aud': aud, - if (iss != null) 'iss': iss, - if (info != null) 'info': info, - if (b64info != null) 'b64info': b64info, - if (channels != null) 'channels': channels, - if (subs != null) 'subs': subs, - if (meta != null) 'meta': meta, - if (expireAt != null) 'expire_at': expireAt, - }); - - // Payload signature - final key = utf8.encode(secret); // Your 256 bit secret key - final bytes = utf8.encode('$encodedHeader.$encodedPayload'); - - final hmacSha256 = Hmac(sha256, key); // HMAC-SHA256 - final digest = hmacSha256.convert(bytes); - - // Encode signature - final encodedSignature = const Base64Encoder.urlSafe() - .fuse(const _UnpaddedBase64Converter()) - .convert(digest.bytes); - - // Return JWT - return '$encodedHeader.$encodedPayload.$encodedSignature'; - } - - @override - String toString() => 'SpinifyJWT{sub: $sub}'; -} - -/// A converter that converts Base64-encoded strings -/// to unpadded Base64-encoded strings. -class _UnpaddedBase64Converter extends Converter { - const _UnpaddedBase64Converter(); - - @override - String convert(String input) { - final padding = input.indexOf('=', input.length - 2); - if (padding != -1) return input.substring(0, padding); - return input; - } -} diff --git a/lib/src.old/model/message.dart b/lib/src.old/model/message.dart deleted file mode 100644 index 7066365..0000000 --- a/lib/src.old/model/message.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'channel_push.dart'; - -/// {@template message} -/// Message push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifyMessage extends SpinifyChannelPush { - /// {@macro message} - const SpinifyMessage({ - required super.timestamp, - required super.channel, - required this.data, - }); - - @override - String get type => 'message'; - - /// Payload of message. - final List data; - - @override - String toString() => 'SpinifyMessage{channel: $channel}'; -} diff --git a/lib/src.old/model/metrics.dart b/lib/src.old/model/metrics.dart deleted file mode 100644 index 5250e36..0000000 --- a/lib/src.old/model/metrics.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:meta/meta.dart'; -import '../client/state.dart'; - -/// Subscription count -/// - total -/// - unsubscribed -/// - subscribing -/// - subscribed -/// -/// {@category Metrics} -/// {@category Entity} -typedef SpinifySubscriptionCount = ({ - int total, - int unsubscribed, - int subscribing, - int subscribed -}); - -/// {@template metrics} -/// Metrics of Spinify client. -/// {@endtemplate} -/// -/// {@category Metrics} -/// {@category Entity} -@immutable -final class SpinifyMetrics implements Comparable { - /// {@macro metrics} - const SpinifyMetrics({ - required this.timestamp, - required this.initializedAt, - required this.state, - required this.transferred, - required this.received, - required this.reconnects, - required this.subscriptions, - required this.speed, - required this.lastUrl, - required this.lastConnectTime, - required this.lastDisconnectTime, - required this.disconnects, - required this.lastDisconnect, - required this.isRefreshActive, - }); - - /// Timestamp of the metrics. - final DateTime timestamp; - - /// The time when the client was initialized. - final DateTime initializedAt; - - /// The current state of the client. - final SpinifyState state; - - /// The total number of messages & size of bytes sent. - final ({BigInt count, BigInt size}) transferred; - - /// The total number of messages & size of bytes received. - final ({BigInt count, BigInt size}) received; - - /// The total number of times the connection has been re-established. - final ({int successful, int total}) reconnects; - - /// The number of subscriptions. - final ({ - SpinifySubscriptionCount client, - SpinifySubscriptionCount server - }) subscriptions; - - /// The speed of the request/response in milliseconds. - /// - min - minimum speed - /// - avg - average speed - /// - max - maximum speed - final ({int min, int avg, int max}) speed; - - /// The last URL used to connect. - final String? lastUrl; - - /// The time of the last connect. - final DateTime? lastConnectTime; - - /// The time of the last disconnect. - final DateTime? lastDisconnectTime; - - /// The total number of times the connection has been disconnected. - final int disconnects; - - /// The last disconnect reason. - final ({int? code, String? reason})? lastDisconnect; - - /// Is refresh active. - final bool isRefreshActive; - - @override - int compareTo(SpinifyMetrics other) => timestamp.compareTo(other.timestamp); - - /// Convert metrics to JSON. - Map toJson() => { - 'timestamp': timestamp.toIso8601String(), - 'initializedAt': initializedAt.toIso8601String(), - 'lastConnectTime': lastConnectTime?.toIso8601String(), - 'lastDisconnectTime': lastDisconnectTime?.toIso8601String(), - 'state': state.toJson(), - 'lastUrl': lastUrl, - 'reconnects': { - 'successful': reconnects.successful, - 'total': reconnects.total, - }, - 'subscriptions': >{ - 'client': { - 'total': subscriptions.client.total, - 'unsubscribed': subscriptions.client.unsubscribed, - 'subscribing': subscriptions.client.subscribing, - 'subscribed': subscriptions.client.subscribed, - }, - 'server': { - 'total': subscriptions.server.total, - 'unsubscribed': subscriptions.server.unsubscribed, - 'subscribing': subscriptions.server.subscribing, - 'subscribed': subscriptions.server.subscribed, - }, - }, - 'speed': { - 'min': speed.min, - 'avg': speed.avg, - 'max': speed.max, - }, - 'transferred': { - 'count': transferred.count, - 'size': transferred.size, - }, - 'received': { - 'count': received.count, - 'size': received.size, - }, - 'isRefreshActive': isRefreshActive, - 'disconnects': disconnects, - 'lastDisconnect': switch (lastDisconnect) { - (:int? code, :String? reason) => { - 'code': code, - 'reason': reason, - }, - _ => null, - }, - }; - - @override - String toString() => 'SpinifyMetrics{}'; -} diff --git a/lib/src.old/model/presence.dart b/lib/src.old/model/presence.dart deleted file mode 100644 index 65fd7e2..0000000 --- a/lib/src.old/model/presence.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:meta/meta.dart'; -import 'client_info.dart'; - -/// {@template presence} -/// Presence -/// {@endtemplate} -/// {@category Entity} -@immutable -final class SpinifyPresence { - /// {@macro presence} - const SpinifyPresence({ - required this.channel, - required this.clients, - }); - - /// Channel - final String channel; - - /// Publications - final Map clients; - - @override - String toString() => 'SpinifyPresence{channel: $channel}'; -} diff --git a/lib/src.old/model/presence_stats.dart b/lib/src.old/model/presence_stats.dart deleted file mode 100644 index eb3bc04..0000000 --- a/lib/src.old/model/presence_stats.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template presence_stats} -/// Presence stats -/// {@endtemplate} -/// {@category Entity} -@immutable -final class SpinifyPresenceStats { - /// {@macro presence_stats} - const SpinifyPresenceStats({ - required this.channel, - required this.clients, - required this.users, - }); - - /// Channel - final String channel; - - /// Clients count - final int clients; - - /// Users count - final int users; - - @override - String toString() => 'SpinifyPresenceStats{channel: $channel}'; -} diff --git a/lib/src.old/model/publication.dart b/lib/src.old/model/publication.dart deleted file mode 100644 index 2c9e27f..0000000 --- a/lib/src.old/model/publication.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:fixnum/fixnum.dart' as fixnum; -import 'package:meta/meta.dart'; -import 'channel_push.dart'; -import 'client_info.dart'; - -/// {@template publication} -/// Publication context -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -@immutable -final class SpinifyPublication extends SpinifyChannelPush { - /// {@macro publication} - const SpinifyPublication({ - required super.timestamp, - required super.channel, - required this.data, - this.offset, - this.info, - this.tags, - }); - - @override - String get type => 'publication'; - - /// Publication payload - final List data; - - /// Optional offset inside history stream, this is an incremental number - final fixnum.Int64? offset; - - /// Optional information about client connection who published this - /// (only exists if publication comes from client-side publish() API). - final SpinifyClientInfo? info; - - /// Optional tags, this is a map with string keys and string values - final Map? tags; - - @override - String toString() => 'SpinifyPublication{channel: $channel}'; -} diff --git a/lib/src.old/model/pubspec.yaml.g.dart b/lib/src.old/model/pubspec.yaml.g.dart deleted file mode 100644 index 5fdca7f..0000000 --- a/lib/src.old/model/pubspec.yaml.g.dart +++ /dev/null @@ -1,497 +0,0 @@ -// ignore_for_file: lines_longer_than_80_chars, unnecessary_raw_strings -// ignore_for_file: use_raw_strings, avoid_classes_with_only_static_members -// ignore_for_file: avoid_escaping_inner_quotes, prefer_single_quotes - -/// GENERATED CODE - DO NOT MODIFY BY HAND - -library pubspec; - -// ***************************************************************************** -// * pubspec_generator * -// ***************************************************************************** - -/* - - MIT License - - Copyright (c) 2023 Plague Fox - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -/// Given a version number MAJOR.MINOR.PATCH, increment the: -/// -/// 1. MAJOR version when you make incompatible API changes -/// 2. MINOR version when you add functionality in a backward compatible manner -/// 3. PATCH version when you make backward compatible bug fixes -/// -/// Additional labels for pre-release and build metadata are available -/// as extensions to the MAJOR.MINOR.PATCH format. -typedef PubspecVersion = ({ - String representation, - String canonical, - int major, - int minor, - int patch, - List preRelease, - List build -}); - -/// # The pubspec file -/// -/// Code generated pubspec.yaml.g.dart from pubspec.yaml -/// This class is generated from pubspec.yaml, do not edit directly. -/// -/// Every pub package needs some metadata so it can specify its dependencies. -/// Pub packages that are shared with others also need to provide some other -/// information so users can discover them. All of this metadata goes -/// in the package’s pubspec: -/// a file named pubspec.yaml that’s written in the YAML language. -/// -/// Read more: -/// - https://pub.dev/packages/pubspec_generator -/// - https://dart.dev/tools/pub/pubspec -sealed class Pubspec { - /// Version - /// - /// Current app [version] - /// - /// Every package has a version. - /// A version number is required to host your package on the pub.dev site, - /// but can be omitted for local-only packages. - /// If you omit it, your package is implicitly versioned 0.0.0. - /// - /// Versioning is necessary for reusing code while letting it evolve quickly. - /// A version number is three numbers separated by dots, like 0.2.43. - /// It can also optionally have a build ( +1, +2, +hotfix.oopsie) - /// or prerelease (-dev.4, -alpha.12, -beta.7, -rc.5) suffix. - /// - /// Each time you publish your package, you publish it at a specific version. - /// Once that’s been done, consider it hermetically sealed: - /// you can’t touch it anymore. To make more changes, - /// you’ll need a new version. - /// - /// When you select a version, - /// follow [semantic versioning](https://semver.org/). - static const PubspecVersion version = ( - /// Non-canonical string representation of the version as provided - /// in the pubspec.yaml file. - representation: r'0.0.1-pre.6', - - /// Returns a 'canonicalized' representation - /// of the application version. - /// This represents the version string in accordance with - /// Semantic Versioning (SemVer) standards. - canonical: r'0.0.1-pre.6', - - /// MAJOR version when you make incompatible API changes. - /// The major version number: 1 in "1.2.3". - major: 0, - - /// MINOR version when you add functionality - /// in a backward compatible manner. - /// The minor version number: 2 in "1.2.3". - minor: 0, - - /// PATCH version when you make backward compatible bug fixes. - /// The patch version number: 3 in "1.2.3". - patch: 1, - - /// The pre-release identifier: "foo" in "1.2.3-foo". - preRelease: [r'pre', r'6'], - - /// The build identifier: "foo" in "1.2.3+foo". - build: [], - ); - - /// Build date and time (UTC) - static final DateTime timestamp = DateTime.utc( - 2023, - 8, - 4, - 8, - 56, - 57, - 323, - 753, - ); - - /// Name - /// - /// Current app [name] - /// - /// Every package needs a name. - /// It’s how other packages refer to yours, and how it appears to the world, - /// should you publish it. - /// - /// The name should be all lowercase, with underscores to separate words, - /// just_like_this. Use only basic Latin letters and Arabic digits: - /// [a-z0-9_]. Also, make sure the name is a valid Dart identifier—that - /// it doesn’t start with digits - /// and isn’t a [reserved word](https://dart.dev/language/keywords). - /// - /// Try to pick a name that is clear, terse, and not already in use. - /// A quick search of packages on the [pub.dev site](https://pub.dev/packages) - /// to make sure that nothing else is using your name is recommended. - static const String name = r'spinify'; - - /// Description - /// - /// Current app [description] - /// - /// This is optional for your own personal packages, - /// but if you intend to publish your package you must provide a description, - /// which should be in English. - /// The description should be relatively short, from 60 to 180 characters - /// and tell a casual reader what they might want to know about your package. - /// - /// Think of the description as the sales pitch for your package. - /// Users see it when they [browse for packages](https://pub.dev/packages). - /// The description is plain text: no markdown or HTML. - static const String description = - r'Dart client to communicate with Centrifuge and Centrifugo from Flutter and VM over WebSockets'; - - /// Homepage - /// - /// Current app [homepage] - /// - /// This should be a URL pointing to the website for your package. - /// For [hosted packages](https://dart.dev/tools/pub/dependencies#hosted-packages), - /// this URL is linked from the package’s page. - /// While providing a homepage is optional, - /// please provide it or repository (or both). - /// It helps users understand where your package is coming from. - static const String homepage = r'https://centrifugal.dev'; - - /// Repository - /// - /// Current app [repository] - /// - /// Repository - /// The optional repository field should contain the URL for your package’s - /// source code repository—for example, - /// https://github.com//. - /// If you publish your package to the pub.dev site, - /// then your package’s page displays the repository URL. - /// While providing a repository is optional, - /// please provide it or homepage (or both). - /// It helps users understand where your package is coming from. - static const String repository = r'https://github.com/PlugFox/spinify'; - - /// Issue tracker - /// - /// Current app [issueTracker] - /// - /// The optional issue_tracker field should contain a URL for the package’s - /// issue tracker, where existing bugs can be viewed and new bugs can be filed. - /// The pub.dev site attempts to display a link - /// to each package’s issue tracker, using the value of this field. - /// If issue_tracker is missing but repository is present and points to GitHub, - /// then the pub.dev site uses the default issue tracker - /// (https://github.com///issues). - static const String issueTracker = - r'https://github.com/PlugFox/spinify/issues'; - - /// Documentation - /// - /// Current app [documentation] - /// - /// Some packages have a site that hosts documentation, - /// separate from the main homepage and from the Pub-generated API reference. - /// If your package has additional documentation, add a documentation: - /// field with that URL; pub shows a link to this documentation - /// on your package’s page. - static const String documentation = r''; - - /// Publish_to - /// - /// Current app [publishTo] - /// - /// The default uses the [pub.dev](https://pub.dev/) site. - /// Specify none to prevent a package from being published. - /// This setting can be used to specify a custom pub package server to publish. - /// - /// ```yaml - /// publish_to: none - /// ``` - static const String publishTo = r'https://pub.dev/'; - - /// Funding - /// - /// Current app [funding] - /// - /// Package authors can use the funding property to specify - /// a list of URLs that provide information on how users - /// can help fund the development of the package. For example: - /// - /// ```yaml - /// funding: - /// - https://www.buymeacoffee.com/example_user - /// - https://www.patreon.com/some-account - /// ``` - /// - /// If published to [pub.dev](https://pub.dev/) the links are displayed on the package page. - /// This aims to help users fund the development of their dependencies. - static const List funding = [ - r'https://www.buymeacoffee.com/plugfox', - r'https://www.patreon.com/plugfox', - r'https://boosty.to/plugfox', - ]; - - /// False_secrets - /// - /// Current app [falseSecrets] - /// - /// When you try to publish a package, - /// pub conducts a search for potential leaks of secret credentials, - /// API keys, or cryptographic keys. - /// If pub detects a potential leak in a file that would be published, - /// then pub warns you and refuses to publish the package. - /// - /// Leak detection isn’t perfect. To avoid false positives, - /// you can tell pub not to search for leaks in certain files, - /// by creating an allowlist using gitignore - /// patterns under false_secrets in the pubspec. - /// - /// For example, the following entry causes pub not to look - /// for leaks in the file lib/src/hardcoded_api_key.dart - /// and in all .pem files in the test/localhost_certificates/ directory: - /// - /// ```yaml - /// false_secrets: - /// - /lib/src/hardcoded_api_key.dart - /// - /test/localhost_certificates/*.pem - /// ``` - /// - /// Starting a gitignore pattern with slash (/) ensures - /// that the pattern is considered relative to the package’s root directory. - static const List falseSecrets = []; - - /// Screenshots - /// - /// Current app [screenshots] - /// - /// Packages can showcase their widgets or other visual elements - /// using screenshots displayed on their pub.dev page. - /// To specify screenshots for the package to display, - /// use the screenshots field. - /// - /// A package can list up to 10 screenshots under the screenshots field. - /// Don’t include logos or other branding imagery in this section. - /// Each screenshot includes one description and one path. - /// The description explains what the screenshot depicts - /// in no more than 160 characters. For example: - /// - /// ```yaml - /// screenshots: - /// - description: 'This screenshot shows the transformation of a number of bytes - /// to a human-readable expression.' - /// path: path/to/image/in/package/500x500.webp - /// - description: 'This screenshot shows a stack trace returning a human-readable - /// representation.' - /// path: path/to/image/in/package.png - /// ``` - /// - /// Pub.dev limits screenshots to the following specifications: - /// - /// - File size: max 4 MB per image. - /// - File types: png, jpg, gif, or webp. - /// - Static and animated images are both allowed. - /// - /// Keep screenshot files small. Each download of the package - /// includes all screenshot files. - /// - /// Pub.dev generates the package’s thumbnail image from the first screenshot. - /// If this screenshot uses animation, pub.dev uses its first frame. - static const List screenshots = []; - - /// Topics - /// - /// Current app [topics] - /// - /// Package authors can use the topics field to categorize their package. Topics can be used to assist discoverability during search with filters on pub.dev. Pub.dev displays the topics on the package page as well as in the search results. - /// - /// The field consists of a list of names. For example: - /// - /// ```yaml - /// topics: - /// - network - /// - http - /// ``` - /// - /// Pub.dev requires topics to follow these specifications: - /// - /// - Tag each package with at most 5 topics. - /// - Write the topic name following these requirements: - /// 1) Use between 2 and 32 characters. - /// 2) Use only lowercase alphanumeric characters or hyphens (a-z, 0-9, -). - /// 3) Don’t use two consecutive hyphens (--). - /// 4) Start the name with lowercase alphabet characters (a-z). - /// 5) End with alphanumeric characters (a-z or 0-9). - /// - /// When choosing topics, consider if existing topics are relevant. - /// Tagging with existing topics helps users discover your package. - static const List topics = [ - r'spinify', - r'centrifugo', - r'centrifuge', - r'websocket', - r'cross-platform', - ]; - - /// Environment - static const Map environment = { - 'sdk': '>=3.0.0 <4.0.0', - }; - - /// Platforms - /// - /// Current app [platforms] - /// - /// When you [publish a package](https://dart.dev/tools/pub/publishing), - /// pub.dev automatically detects the platforms that the package supports. - /// If this platform-support list is incorrect, - /// use platforms to explicitly declare which platforms your package supports. - /// - /// For example, the following platforms entry causes - /// pub.dev to list the package as supporting - /// Android, iOS, Linux, macOS, Web, and Windows: - /// - /// ```yaml - /// # This package supports all platforms listed below. - /// platforms: - /// android: - /// ios: - /// linux: - /// macos: - /// web: - /// windows: - /// ``` - /// - /// Here is an example of declaring that the package supports only Linux and macOS (and not, for example, Windows): - /// - /// ```yaml - /// # This package supports only Linux and macOS. - /// platforms: - /// linux: - /// macos: - /// ``` - static const Map platforms = { - 'android': r'', - 'ios': r'', - 'linux': r'', - 'macos': r'', - 'web': r'', - 'windows': r'', - }; - - /// Dependencies - /// - /// Current app [dependencies] - /// - /// [Dependencies](https://dart.dev/tools/pub/glossary#dependency) - /// are the pubspec’s `raison d’être`. - /// In this section you list each package that - /// your package needs in order to work. - /// - /// Dependencies fall into one of two types. - /// Regular dependencies are listed under dependencies: - /// these are packages that anyone using your package will also need. - /// Dependencies that are only needed in - /// the development phase of the package itself - /// are listed under dev_dependencies. - /// - /// During the development process, - /// you might need to temporarily override a dependency. - /// You can do so using dependency_overrides. - /// - /// For more information, - /// see [Package dependencies](https://dart.dev/tools/pub/dependencies). - static const Map dependencies = { - 'meta': r'^1.9.1', - 'ws': r'^1.0.0-pre.6', - 'protobuf': r'^3.0.0', - 'crypto': r'^3.0.3', - 'fixnum': r'^1.1.0', - 'stack_trace': r'^1.11.0', - }; - - /// Developer dependencies - static const Map devDependencies = { - 'build_runner': r'^2.4.6', - 'pubspec_generator': r'^4.0.0', - 'lints': r'^2.0.1', - 'test': r'^1.24.2', - }; - - /// Dependency overrides - static const Map dependencyOverrides = {}; - - /// Executables - /// - /// Current app [executables] - /// - /// A package may expose one or more of its scripts as executables - /// that can be run directly from the command line. - /// To make a script publicly available, - /// list it under the executables field. - /// Entries are listed as key/value pairs: - /// - /// ```yaml - /// : - /// ``` - /// - /// For example, the following pubspec entry lists two scripts: - /// - /// ```yaml - /// executables: - /// slidy: main - /// fvm: - /// ``` - /// - /// Once the package is activated using pub global activate, - /// typing `slidy` executes `bin/main.dart`. - /// Typing `fvm` executes `bin/fvm.dart`. - /// If you don’t specify the value, it is inferred from the key. - /// - /// For more information, see pub global. - static const Map executables = {}; - - /// Source data from pubspec.yaml - static const Map source = { - 'name': name, - 'description': description, - 'repository': repository, - 'issue_tracker': issueTracker, - 'homepage': homepage, - 'documentation': documentation, - 'publish_to': publishTo, - 'version': version, - 'funding': funding, - 'false_secrets': falseSecrets, - 'screenshots': screenshots, - 'topics': topics, - 'platforms': platforms, - 'environment': environment, - 'dependencies': dependencies, - 'dev_dependencies': devDependencies, - 'dependency_overrides': dependencyOverrides, - }; -} diff --git a/lib/src.old/model/pushes_stream.dart b/lib/src.old/model/pushes_stream.dart deleted file mode 100644 index acdaf95..0000000 --- a/lib/src.old/model/pushes_stream.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import 'channel_presence.dart'; -import 'channel_push.dart'; -import 'event.dart'; -import 'message.dart'; -import 'publication.dart'; - -/// Stream of received pushes from Centrifugo server for a channel. -/// {@category Event} -/// {@category Client} -/// {@category Subscription} -/// {@subCategory Push} -/// {@subCategory Channel} -@immutable -final class SpinifyPushesStream extends StreamView { - /// Stream of received events. - const SpinifyPushesStream({ - required Stream pushes, - required this.publications, - required this.messages, - required this.presenceEvents, - required this.joinEvents, - required this.leaveEvents, - }) : super(pushes); - - /// Publications stream. - final Stream publications; - - /// Messages stream. - final Stream messages; - - /// Stream of presence (join & leave) events. - final Stream presenceEvents; - - /// Join events - final Stream joinEvents; - - /// Leave events - final Stream leaveEvents; - - /// Filtered stream of data of [SpinifyEvent]. - Stream whereType() => - transform(StreamTransformer.fromHandlers( - handleData: (data, sink) => switch (data) { - T valid => sink.add(valid), - _ => null, - }, - )).asBroadcastStream(); - - @override - String toString() => 'SpinifyPushesStream{}'; -} diff --git a/lib/src.old/model/refresh.dart b/lib/src.old/model/refresh.dart deleted file mode 100644 index fb635d5..0000000 --- a/lib/src.old/model/refresh.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'channel_push.dart'; - -/// {@template refresh} -/// Refresh push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifyRefresh extends SpinifyChannelPush { - /// {@macro refresh} - const SpinifyRefresh({ - required super.timestamp, - required super.channel, - required this.expires, - this.ttl, - }); - - @override - String get type => 'refresh'; - - /// Whether a server will expire connection at some point - final bool expires; - - /// Time when connection will be expired - final DateTime? ttl; - - @override - String toString() => 'SpinifyRefresh{channel: $channel}'; -} diff --git a/lib/src.old/model/refresh_result.dart b/lib/src.old/model/refresh_result.dart deleted file mode 100644 index 37ef0b0..0000000 --- a/lib/src.old/model/refresh_result.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:meta/meta.dart'; - -/// Result of connection refresh -final class SpinifyRefreshResult { - /// Result of connection refresh - const SpinifyRefreshResult({ - required this.expires, - this.client, - this.version, - this.ttl, - }); - - /// Unique client connection ID server issued to this connection - final String? client; - - /// Server version - final String? version; - - /// Whether a server will expire connection at some point - final bool expires; - - /// Time when connection will be expired - final DateTime? ttl; -} - -/// Result of subscription refresh -@immutable -final class SpinifySubRefreshResult { - /// Result of connection refresh - const SpinifySubRefreshResult({ - required this.expires, - this.ttl, - }); - - /// Whether a server will expire subscription at some point - final bool expires; - - /// Time when subscription will be expired - final DateTime? ttl; -} diff --git a/lib/src.old/model/stream_position.dart b/lib/src.old/model/stream_position.dart deleted file mode 100644 index c9047ab..0000000 --- a/lib/src.old/model/stream_position.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:fixnum/fixnum.dart' as fixnum; - -/// Stream position. -/// {@category Entity} -typedef SpinifyStreamPosition = ({fixnum.Int64 offset, String epoch}); diff --git a/lib/src.old/model/subscribe.dart b/lib/src.old/model/subscribe.dart deleted file mode 100644 index b855747..0000000 --- a/lib/src.old/model/subscribe.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'channel_push.dart'; -import 'stream_position.dart'; - -/// {@template subscribe} -/// Subscribe push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifySubscribe extends SpinifyChannelPush { - /// {@macro subscribe} - const SpinifySubscribe({ - required super.timestamp, - required super.channel, - required this.positioned, - required this.recoverable, - required this.data, - required this.streamPosition, - }); - - @override - String get type => 'subscribe'; - - /// Whether subscription is positioned. - final bool positioned; - - /// Whether subscription is recoverable. - final bool recoverable; - - /// Data attached to subscription. - final List data; - - /// Stream position. - final SpinifyStreamPosition? streamPosition; - - @override - String toString() => 'SpinifySubscribe{channel: $channel}'; -} diff --git a/lib/src.old/model/unsubscribe.dart b/lib/src.old/model/unsubscribe.dart deleted file mode 100644 index c312cb0..0000000 --- a/lib/src.old/model/unsubscribe.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'channel_push.dart'; - -/// {@template unsubscribe} -/// Unsubscribe push from Centrifugo server. -/// {@endtemplate} -/// {@category Event} -/// {@subCategory Push} -final class SpinifyUnsubscribe extends SpinifyChannelPush { - /// {@macro unsubscribe} - const SpinifyUnsubscribe({ - required super.timestamp, - required super.channel, - required this.code, - required this.reason, - }); - - @override - String get type => 'unsubscribe'; - - /// Code of unsubscribe. - final int code; - - /// Reason of unsubscribe. - final String reason; - - @override - String toString() => 'SpinifyUnsubscribe{channel: $channel}'; -} diff --git a/lib/src.old/subscription/client_subscription_impl.dart b/lib/src.old/subscription/client_subscription_impl.dart deleted file mode 100644 index 90d9abe..0000000 --- a/lib/src.old/subscription/client_subscription_impl.dart +++ /dev/null @@ -1,582 +0,0 @@ -import 'dart:async'; - -import 'package:fixnum/fixnum.dart' as fixnum; -import 'package:meta/meta.dart'; - -import '../client/disconnect_code.dart'; -import '../client/spinify.dart'; -import '../model/channel_presence.dart'; -import '../model/channel_push.dart'; -import '../model/connect.dart'; -import '../model/disconnect.dart'; -import '../model/event.dart'; -import '../model/exception.dart'; -import '../model/history.dart'; -import '../model/message.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/publication.dart'; -import '../model/pushes_stream.dart'; -import '../model/refresh.dart'; -import '../model/stream_position.dart'; -import '../model/subscribe.dart'; -import '../model/unsubscribe.dart'; -import '../transport/transport_interface.dart'; -import '../util/event_queue.dart'; -import '../util/logger.dart' as logger; -import 'subscription.dart'; -import 'subscription_config.dart'; -import 'subscription_state.dart'; -import 'subscription_states_stream.dart'; - -/// Client-side subscription implementation. -final class SpinifyClientSubscriptionImpl extends SpinifyClientSubscriptionBase - with - SpinifyClientSubscriptionEventReceiverMixin, - SpinifyClientSubscriptionErrorsMixin, - SpinifyClientSubscriptionSubscribeMixin, - SpinifyClientSubscriptionPublishingMixin, - SpinifyClientSubscriptionHistoryMixin, - SpinifyClientSubscriptionPresenceMixin, - SpinifyClientSubscriptionQueueMixin { - /// Client-side subscription implementation. - SpinifyClientSubscriptionImpl({ - required super.channel, - required super.transportWeakRef, - SpinifySubscriptionConfig? config, - }) : super(config: config ?? const SpinifySubscriptionConfig.byDefault()); -} - -/// Base class for client-side subscription. -abstract base class SpinifyClientSubscriptionBase - extends SpinifyClientSubscription { - /// Client-side subscription implementation. - SpinifyClientSubscriptionBase({ - required this.channel, - required WeakReference transportWeakRef, - required SpinifySubscriptionConfig config, - }) : _config = config { - _transportWeakRef = transportWeakRef; - _initSubscription(); - } - - @override - final String channel; - - /// Offset of last received publication. - late fixnum.Int64 _offset; - - @override - SpinifyStreamPosition? get since => switch (state.since?.epoch) { - String epoch => (epoch: epoch, offset: _offset), - _ => state.since, - }; - - /// Weak reference to transport. - @nonVirtual - late final WeakReference _transportWeakRef; - - /// Internal transport responsible - /// for sending, receiving, encoding and decoding data from the server. - ISpinifyTransport get _transport => _transportWeakRef.target!; - - /// Subscription config. - final SpinifySubscriptionConfig _config; - - /// Init subscription. - @protected - @mustCallSuper - void _initSubscription() { - _state = SpinifySubscriptionState.unsubscribed( - since: _config.since, code: 0, reason: 'initial state'); - _offset = _config.since?.offset ?? fixnum.Int64.ZERO; - } - - /// Subscription has 3 states: - /// - `unsubscribed` - /// - `subscribing` - /// - `subscribed` - @override - SpinifySubscriptionState get state => _state; - late SpinifySubscriptionState _state; - - /// Stream of subscription states. - @override - late final SpinifySubscriptionStateStream states = - SpinifySubscriptionStateStream(_stateController.stream); - - /// States controller. - final StreamController _stateController = - StreamController.broadcast(); - - /// Set new state. - void _setState(SpinifySubscriptionState state) { - if (_state == state) return; - final previousState = _state; - _stateController.add(_state = state); - Spinify.observer?.onSubscriptionChanged(this, previousState, state); - } - - /// Notify about new publication. - @nonVirtual - void _handlePublication(SpinifyPublication publication) { - final offset = publication.offset; - if (offset != null && offset > _offset) _offset = offset; - } - - /// Close subscription. - @mustCallSuper - Future close([int code = 0, String reason = 'closed']) async { - if (!_state.isUnsubscribed) { - _setState(SpinifySubscriptionState.unsubscribed( - code: code, - reason: reason, - recoverable: false, - since: since, - )); - } - _stateController.close().ignore(); - } - - @override - String toString() => 'SpinifyClientSubscription{channel: $channel}'; -} - -/// Mixin responsible for event receiving and distribution by controllers -/// and streams to subscribers. -base mixin SpinifyClientSubscriptionEventReceiverMixin - on SpinifyClientSubscriptionBase { - @protected - @nonVirtual - final StreamController _pushController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _publicationsController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _messagesController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _joinController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _leaveController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _presenceController = - StreamController.broadcast(); - - @override - @nonVirtual - late final SpinifyPushesStream stream = SpinifyPushesStream( - pushes: _pushController.stream, - publications: _publicationsController.stream, - messages: _messagesController.stream, - presenceEvents: _presenceController.stream, - joinEvents: _joinController.stream, - leaveEvents: _leaveController.stream, - ); - - @override - void _initSubscription() { - super._initSubscription(); - } - - /// Handle push event from server for the specific channel. - /// Called from `SpinifyClientSubscriptionsManager.onPush` - @nonVirtual - void onPush(SpinifyChannelPush push) { - // This is a push to a channel. - _pushController.add(push); - switch (push) { - case SpinifyPublication publication: - _handlePublication(publication); - _publicationsController.add(publication); - case SpinifyMessage message: - _messagesController.add(message); - case SpinifyJoin join: - _presenceController.add(join); - _joinController.add(join); - case SpinifyLeave leave: - _presenceController.add(leave); - _leaveController.add(leave); - case SpinifySubscribe _: - break; // For server side subscriptions. - case SpinifyUnsubscribe _: - break; // For server side subscriptions. - case SpinifyConnect _: - break; - case SpinifyDisconnect _: - break; - case SpinifyRefresh _: - break; - } - } - - @override - Future close([int code = 0, String reason = 'closed']) async { - await super.close(code, reason); - for (final controller in >[ - _pushController, - _publicationsController, - _messagesController, - _joinController, - _leaveController, - _presenceController, - ]) { - controller.close().ignore(); - } - } -} - -/// Mixin responsible for errors stream. -base mixin SpinifyClientSubscriptionErrorsMixin - on SpinifyClientSubscriptionBase { - @protected - @nonVirtual - void _emitError(SpinifyException exception, StackTrace stackTrace) => - Spinify.observer?.onError(exception, stackTrace); -} - -/// Mixin responsible for subscribing. -base mixin SpinifyClientSubscriptionSubscribeMixin - on SpinifyClientSubscriptionBase, SpinifyClientSubscriptionErrorsMixin { - /// Refresh timer. - Timer? _refreshTimer; - - /// Start subscribing to a channel - @override - Future subscribe() async { - logger.fine('Subscribing to $channel'); - try { - if (state.isSubscribed) { - return; - } else if (state.isSubscribing) { - return await ready(); - } - _refreshTimer?.cancel(); - _refreshTimer = null; - _setState(SpinifySubscriptionState$Subscribing( - since: since, - recoverable: state.recoverable, - )); - final subscribed = await _transport.subscribe( - channel, - _config, - switch (state.since) { - null => null, - (epoch: String epoch, offset: fixnum.Int64 _) => ( - epoch: epoch, - offset: _offset, - ), - }, - ); - final offset = subscribed.since?.offset; - if (offset != null && offset > _offset) _offset = offset; - _setState(SpinifySubscriptionState$Subscribed( - since: subscribed.since ?? since, - recoverable: subscribed.recoverable, - ttl: subscribed.ttl, - )); - if (subscribed.publications.isNotEmpty) { - subscribed.publications.forEach(_handlePublication); - } - if (subscribed.expires) _setRefreshTimer(subscribed.ttl); - } on SpinifyException catch (error, stackTrace) { - unsubscribe(0, 'error while subscribing').ignore(); - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - unsubscribe(0, 'error while subscribing').ignore(); - final spinifyException = SpinifySubscriptionException( - message: 'Error while subscribing', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - /// Await for subscription to be ready. - @override - FutureOr ready() async { - try { - switch (state) { - case SpinifySubscriptionState$Unsubscribed _: - throw SpinifySubscriptionException( - message: 'Subscription is not subscribed', - channel: channel, - ); - case SpinifySubscriptionState$Subscribed _: - return; - case SpinifySubscriptionState$Subscribing _: - await states.subscribed.first.timeout(_config.timeout); - } - } on TimeoutException catch (error, stackTrace) { - _transport - .disconnect( - DisconnectCode.timeout.code, - DisconnectCode.timeout.reason, - ) - .ignore(); - final spinifyException = SpinifySubscriptionException( - message: 'Timeout exception while waiting for subscribing to $channel', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Subscription is not subscribed', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - /// Unsubscribe from a channel - @override - Future unsubscribe( - [int code = 0, String reason = 'unsubscribe called']) async { - _refreshTimer?.cancel(); - _refreshTimer = null; - if (state.isUnsubscribed) return; - _setState(SpinifySubscriptionState.unsubscribed( - code: code, - reason: reason, - since: since, - recoverable: state.recoverable, - )); - if (!_transport.state.isConnected) return; - try { - await _transport.unsubscribe(channel, _config); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while unsubscribing', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - _transport - .disconnect( - DisconnectCode.unsubscribeError.code, - DisconnectCode.unsubscribeError.reason, - ) - .ignore(); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - /// Refresh subscription when ttl is expired. - void _setRefreshTimer(DateTime? ttl) { - _refreshTimer?.cancel(); - _refreshTimer = null; - if (ttl == null) return; - final now = DateTime.now(); - final duration = ttl.subtract(_config.timeout * 4).difference(now); - if (duration.isNegative) return; - _refreshTimer = Timer(duration, _refreshToken); - } - - /// Refresh token for subscription. - void _refreshToken() => Future(() async { - logger.fine('Refreshing subscription token for $channel'); - try { - _refreshTimer?.cancel(); - _refreshTimer = null; - final token = await _config.getToken?.call(); - if (token == null || !state.isSubscribed) return; - final result = await _transport.sendSubRefresh(channel, token); - if (result.expires) _setRefreshTimer(result.ttl); - } on Object catch (error, stackTrace) { - logger.warning( - error, - stackTrace, - 'Error while refreshing subscription token', - ); - _emitError( - SpinifyRefreshException( - message: 'Error while refreshing subscription token', - error: error, - ), - stackTrace, - ); - } - }).ignore(); - - @override - Future close([int code = 0, String reason = 'closed']) async { - logger.fine('Closing subscription to $channel'); - _refreshTimer?.cancel(); - _refreshTimer = null; - try { - if (!state.isUnsubscribed) await unsubscribe(code, reason); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while unsubscribing from channel $channel', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - } - await super.close(code, reason); - } -} - -/// Mixin responsible for publishing. -base mixin SpinifyClientSubscriptionPublishingMixin - on SpinifyClientSubscriptionBase, SpinifyClientSubscriptionErrorsMixin { - @override - Future publish(List data) async { - try { - await ready(); - await _transport.publish(channel, data); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySendException( - message: 'Error while publishing to channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for history. -base mixin SpinifyClientSubscriptionHistoryMixin - on SpinifyClientSubscriptionBase, SpinifyClientSubscriptionErrorsMixin { - @override - Future history({ - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) async { - await ready(); - try { - return await _transport.history( - channel, - limit: limit, - since: since, - reverse: reverse, - ); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for presence. -base mixin SpinifyClientSubscriptionPresenceMixin - on SpinifyClientSubscriptionBase, SpinifyClientSubscriptionErrorsMixin { - @override - Future presence() async { - await ready(); - try { - return await _transport.presence(channel); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - Future presenceStats() async { - await ready(); - try { - return await _transport.presenceStats(channel); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for queue. -/// SHOULD BE LAST MIXIN. -base mixin SpinifyClientSubscriptionQueueMixin - on SpinifyClientSubscriptionBase { - final SpinifyEventQueue _eventQueue = SpinifyEventQueue(); - - @override - Future subscribe() => _eventQueue.push( - 'subscribe', - super.subscribe, - ); - - @override - Future unsubscribe([ - int code = 0, - String reason = 'unsubscribe called', - ]) => - _eventQueue.push( - 'unsubscribe', - () => super.unsubscribe(code, reason), - ); - - @override - Future publish(List data) => _eventQueue.push( - 'publish', - () => super.publish(data), - ); - - @override - Future history({ - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) => - _eventQueue.push( - 'history', - () => super.history( - limit: limit, - since: since, - reverse: reverse, - ), - ); - - @override - Future presence() => - _eventQueue.push('presence', super.presence); - - @override - Future presenceStats() => _eventQueue - .push('presenceStats', super.presenceStats); - - @override - Future close([int code = 0, String reason = 'closed']) => _eventQueue - .push('close', () => super.close(code, reason)) - .whenComplete(_eventQueue.close); -} diff --git a/lib/src.old/subscription/client_subscription_manager.dart b/lib/src.old/subscription/client_subscription_manager.dart deleted file mode 100644 index 5c222ad..0000000 --- a/lib/src.old/subscription/client_subscription_manager.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:collection'; - -import '../model/channel_push.dart'; -import '../model/exception.dart'; -import '../transport/transport_interface.dart'; -import 'client_subscription_impl.dart'; -import 'subscription.dart'; -import 'subscription_config.dart'; -import 'subscription_state.dart'; - -/// Responsible for managing client-side subscriptions. -final class ClientSubscriptionManager { - /// Responsible for managing client-side subscriptions. - ClientSubscriptionManager(ISpinifyTransport transport) - : _transportWeakRef = WeakReference(transport); - - /// Spinify client weak reference. - final WeakReference _transportWeakRef; - - /// Subscriptions count. - ({int total, int unsubscribed, int subscribing, int subscribed}) get count { - var total = 0, unsubscribed = 0, subscribing = 0, subscribed = 0; - for (final entry in _channelSubscriptions.values) { - total++; - switch (entry.state) { - case SpinifySubscriptionState$Unsubscribed _: - unsubscribed++; - case SpinifySubscriptionState$Subscribing _: - subscribing++; - case SpinifySubscriptionState$Subscribed _: - subscribed++; - } - } - return ( - total: total, - unsubscribed: unsubscribed, - subscribing: subscribing, - subscribed: subscribed, - ); - } - - /// Subscriptions registry (channel -> subscription). - /// Channel : SpinifyClientSubscription - final Map _channelSubscriptions = - {}; - - /// Create new client-side subscription. - /// `newSubscription(channel, config)` allocates a new Subscription - /// in the registry or throws an exception if the Subscription - /// is already there. We will discuss common Subscription options below. - SpinifyClientSubscription newSubscription( - String channel, - SpinifySubscriptionConfig? config, - ) { - if (_channelSubscriptions.containsKey(channel)) { - throw SpinifySubscriptionException( - channel: channel, - message: 'Subscription to a channel "$channel" already exists ' - 'in client\'s internal registry', - ); - } - return _channelSubscriptions[channel] = SpinifyClientSubscriptionImpl( - channel: channel, - config: config ?? const SpinifySubscriptionConfig.byDefault(), - transportWeakRef: _transportWeakRef, - ); - } - - /// Get map wirth all registered client-side subscriptions. - /// Returns all registered subscriptions, - /// so you can iterate over all and do some action if required - /// (for example, you want to unsubscribe/remove all subscriptions). - Map get subscriptions => - UnmodifiableMapView({ - for (final entry in _channelSubscriptions.entries) - entry.key: entry.value, - }); - - /// Remove the [SpinifyClientSubscription] from internal registry - /// and unsubscribe from [SpinifyClientSubscription.channel]. - Future removeSubscription( - SpinifyClientSubscription subscription, - ) async { - final subFromRegistry = _channelSubscriptions[subscription.channel]; - try { - await subFromRegistry?.unsubscribe(); - if (!identical(subFromRegistry, subscription)) { - // If not the same subscription instance - unsubscribe it too. - await subscription.unsubscribe(); - } - } on SpinifyException { - rethrow; - } on Object catch (error, stackTrace) { - Error.throwWithStackTrace( - SpinifySubscriptionException( - channel: subscription.channel, - message: 'Error while unsubscribing', - error: error, - ), - stackTrace, - ); - } finally { - _channelSubscriptions.remove(subscription.channel)?.close().ignore(); - } - } - - /// Establish all subscriptions for the specific client. - void subscribeAll() { - for (final entry in _channelSubscriptions.values) { - entry.subscribe().ignore(); - } - } - - /// Disconnect all subscriptions for the specific client - /// from internal registry. - void unsubscribeAll([ - int code = 0, - String reason = 'connection closed', - ]) { - for (final entry in _channelSubscriptions.values) { - entry.unsubscribe(code, reason).ignore(); - } - } - - /// Remove all subscriptions for the specific client from internal registry. - void close([ - int code = 0, - String reason = 'client closed', - ]) { - for (final entry in _channelSubscriptions.values) { - entry.unsubscribe(code, reason).whenComplete(entry.close).ignore(); - } - _channelSubscriptions.clear(); - } - - /// Handle push event from server for the specific channel. - void onPush(SpinifyChannelPush push) => - _channelSubscriptions[push.channel]?.onPush(push); - - /// Get subscription to the channel - /// from internal registry or null if not found. - /// - /// You need to call [SpinifyClientSubscription.subscribe] - /// to start receiving events - SpinifyClientSubscription? operator [](String channel) => - _channelSubscriptions[channel]; -} diff --git a/lib/src.old/subscription/server_subscription_impl.dart b/lib/src.old/subscription/server_subscription_impl.dart deleted file mode 100644 index 0297568..0000000 --- a/lib/src.old/subscription/server_subscription_impl.dart +++ /dev/null @@ -1,450 +0,0 @@ -import 'dart:async'; - -import 'package:fixnum/fixnum.dart' as fixnum; -import 'package:meta/meta.dart'; - -import '../client/spinify.dart'; -import '../model/channel_presence.dart'; -import '../model/channel_push.dart'; -import '../model/connect.dart'; -import '../model/disconnect.dart'; -import '../model/event.dart'; -import '../model/exception.dart'; -import '../model/history.dart'; -import '../model/message.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/publication.dart'; -import '../model/pushes_stream.dart'; -import '../model/refresh.dart'; -import '../model/stream_position.dart'; -import '../model/subscribe.dart'; -import '../model/unsubscribe.dart'; -import '../transport/transport_interface.dart'; -import '../util/event_queue.dart'; -import '../util/logger.dart' as logger; -import 'subscription.dart'; -import 'subscription_state.dart'; -import 'subscription_states_stream.dart'; - -/// Server-side subscription implementation. -final class SpinifyServerSubscriptionImpl extends SpinifyServerSubscriptionBase - with - SpinifyServerSubscriptionEventReceiverMixin, - SpinifyServerSubscriptionErrorsMixin, - SpinifyServerSubscriptionReadyMixin, - SpinifyServerSubscriptionPublishingMixin, - SpinifyServerSubscriptionHistoryMixin, - SpinifyServerSubscriptionPresenceMixin, - SpinifyServerSubscriptionQueueMixin { - /// Server-side subscription implementation. - SpinifyServerSubscriptionImpl({ - required super.channel, - required super.transportWeakRef, - }); -} - -/// Base class for server-side subscription. -abstract base class SpinifyServerSubscriptionBase - extends SpinifyServerSubscription { - /// Base class for server-side subscription. - SpinifyServerSubscriptionBase({ - required this.channel, - required WeakReference transportWeakRef, - }) { - _transportWeakRef = transportWeakRef; - _initSubscription(); - } - - @override - final String channel; - - @override - SpinifyStreamPosition? get since => switch (state.since?.epoch) { - String epoch => (epoch: epoch, offset: _offset), - _ => state.since, - }; - - /// Offset of last received publication. - fixnum.Int64 _offset = fixnum.Int64.ZERO; - - /// Weak reference to transport. - @nonVirtual - late final WeakReference _transportWeakRef; - - /// Internal transport responsible - /// for sending, receiving, encoding and decoding data from the server. - ISpinifyTransport get _transport => _transportWeakRef.target!; - - /// Init subscription. - @protected - @mustCallSuper - void _initSubscription() { - _state = - SpinifySubscriptionState.unsubscribed(code: 0, reason: 'initial state'); - } - - /// Subscription has 3 states: - /// - `unsubscribed` - /// - `subscribing` - /// - `subscribed` - @override - SpinifySubscriptionState get state => _state; - late SpinifySubscriptionState _state; - - /// Stream of subscription states. - @override - late final SpinifySubscriptionStateStream states = - SpinifySubscriptionStateStream(_stateController.stream); - - /// States controller. - final StreamController _stateController = - StreamController.broadcast(); - - /// Set new state. - void _setState(SpinifySubscriptionState state) { - if (_state == state) return; - final previousState = _state; - _stateController.add(_state = state); - Spinify.observer?.onSubscriptionChanged(this, previousState, state); - } - - /// Notify about new publication. - @nonVirtual - void _handlePublication(SpinifyPublication publication) { - final offset = publication.offset; - if (offset != null && offset > _offset) _offset = offset; - } - - /// Close subscription. - @mustCallSuper - Future close([int code = 0, String reason = 'closed']) async { - if (!_state.isUnsubscribed) - _setState(SpinifySubscriptionState.unsubscribed( - code: 0, - reason: 'closed', - recoverable: false, - since: since, - )); - _stateController.close().ignore(); - } - - @override - String toString() => 'SpinifyServerSubscription{channel: $channel}'; -} - -/// Mixin responsible for event receiving and distribution by controllers -/// and streams to subscribers. -base mixin SpinifyServerSubscriptionEventReceiverMixin - on SpinifyServerSubscriptionBase { - @protected - @nonVirtual - final StreamController _pushController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _publicationsController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _messagesController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _joinController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _leaveController = - StreamController.broadcast(); - - @protected - @nonVirtual - final StreamController _presenceController = - StreamController.broadcast(); - - @override - @nonVirtual - late final SpinifyPushesStream stream = SpinifyPushesStream( - pushes: _pushController.stream, - publications: _publicationsController.stream, - messages: _messagesController.stream, - presenceEvents: _presenceController.stream, - joinEvents: _joinController.stream, - leaveEvents: _leaveController.stream, - ); - - @override - void _initSubscription() { - super._initSubscription(); - } - - /// Handle push event from server for the specific channel. - /// Called from `SpinifyClientSubscriptionsManager.onPush` - @nonVirtual - void onPush(SpinifyChannelPush push) { - // This is a push to a channel. - _pushController.add(push); - switch (push) { - case SpinifyPublication publication: - _handlePublication(publication); - _publicationsController.add(publication); - case SpinifyMessage message: - _messagesController.add(message); - case SpinifyJoin join: - _presenceController.add(join); - _joinController.add(join); - case SpinifyLeave leave: - _presenceController.add(leave); - _leaveController.add(leave); - case SpinifySubscribe sub: - final offset = sub.streamPosition?.offset; - if (offset != null && offset > _offset) _offset = offset; - _setState(SpinifySubscriptionState.subscribed( - since: sub.streamPosition ?? since, - recoverable: sub.recoverable, - )); - case SpinifyUnsubscribe unsub: - _setState(SpinifySubscriptionState.unsubscribed( - code: unsub.code, - reason: unsub.reason, - recoverable: state.recoverable, - since: since, - )); - case SpinifyConnect _: - break; - case SpinifyDisconnect _: - break; - case SpinifyRefresh _: - break; - } - } - - @override - Future close([int code = 0, String reason = 'closed']) async { - await super.close(code, reason); - for (final controller in >[ - _pushController, - _publicationsController, - _messagesController, - _joinController, - _leaveController, - _presenceController, - ]) { - controller.close().ignore(); - } - } -} - -/// Mixin responsible for errors stream. -base mixin SpinifyServerSubscriptionErrorsMixin - on SpinifyServerSubscriptionBase { - @protected - @nonVirtual - void _emitError(SpinifyException exception, StackTrace stackTrace) => - Spinify.observer?.onError(exception, stackTrace); -} - -/// Mixin responsible for ready method. -base mixin SpinifyServerSubscriptionReadyMixin - on SpinifyServerSubscriptionBase, SpinifyServerSubscriptionErrorsMixin { - /// Await for subscription to be ready. - @override - FutureOr ready() async { - try { - switch (state) { - case SpinifySubscriptionState$Unsubscribed _: - throw SpinifySubscriptionException( - message: 'Subscription is not subscribed', - channel: channel, - ); - case SpinifySubscriptionState$Subscribed _: - return; - case SpinifySubscriptionState$Subscribing _: - await states.subscribed.first; - } - } on SpinifyException catch (error, stackTrace) { - _emitError(error, stackTrace); - rethrow; - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Subscription is not subscribed', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - /// Mark subscription as ready. - void setSubscribed() { - if (!state.isSubscribed) - _setState(SpinifySubscriptionState.subscribed( - since: since, - recoverable: state.recoverable, - )); - } - - /// Mark subscription as subscribing. - void setSubscribing() { - if (!state.isSubscribing) - _setState(SpinifySubscriptionState.subscribing( - since: since, - recoverable: state.recoverable, - )); - } - - /// Mark subscription as unsubscribed. - void setUnsubscribed(int code, String reason) { - if (!state.isUnsubscribed) - _setState(SpinifySubscriptionState.unsubscribed( - code: code, - reason: reason, - recoverable: state.recoverable, - since: since, - )); - } - - @override - Future close([int code = 0, String reason = 'closed']) async { - logger.fine('Closing subscription to $channel'); - if (!state.isUnsubscribed) setUnsubscribed(code, reason); - await super.close(code, reason); - } -} - -/// Mixin responsible for publishing. -base mixin SpinifyServerSubscriptionPublishingMixin - on SpinifyServerSubscriptionBase, SpinifyServerSubscriptionErrorsMixin { - @override - Future publish(List data) async { - try { - await ready(); - await _transport.publish(channel, data); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySendException( - message: 'Error while publishing to channel $channel', - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for history. -base mixin SpinifyServerSubscriptionHistoryMixin - on SpinifyServerSubscriptionBase, SpinifyServerSubscriptionErrorsMixin { - @override - Future history({ - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) async { - await ready(); - try { - return await _transport.history( - channel, - limit: limit, - since: since, - reverse: reverse, - ); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for presence. -base mixin SpinifyServerSubscriptionPresenceMixin - on SpinifyServerSubscriptionBase, SpinifyServerSubscriptionErrorsMixin { - @override - Future presence() async { - await ready(); - try { - return await _transport.presence(channel); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } - - @override - Future presenceStats() async { - await ready(); - try { - return await _transport.presenceStats(channel); - } on Object catch (error, stackTrace) { - final spinifyException = SpinifySubscriptionException( - message: 'Error while fetching history', - channel: channel, - error: error, - ); - _emitError(spinifyException, stackTrace); - Error.throwWithStackTrace(spinifyException, stackTrace); - } - } -} - -/// Mixin responsible for queue. -/// SHOULD BE LAST MIXIN. -base mixin SpinifyServerSubscriptionQueueMixin - on SpinifyServerSubscriptionBase { - final SpinifyEventQueue _eventQueue = SpinifyEventQueue(); - - @override - FutureOr ready() => _eventQueue.push( - 'ready', - super.ready, - ); - - @override - Future publish(List data) => _eventQueue.push( - 'publish', - () => super.publish(data), - ); - - @override - Future history({ - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) => - _eventQueue.push( - 'history', - () => super.history( - limit: limit, - since: since, - reverse: reverse, - ), - ); - - @override - Future presence() => - _eventQueue.push('presence', super.presence); - - @override - Future presenceStats() => _eventQueue - .push('presenceStats', super.presenceStats); - - @override - Future close([int code = 0, String reason = 'closed']) => _eventQueue - .push('close', () => super.close(code, reason)) - .whenComplete(_eventQueue.close); -} diff --git a/lib/src.old/subscription/server_subscription_manager.dart b/lib/src.old/subscription/server_subscription_manager.dart deleted file mode 100644 index 258b74d..0000000 --- a/lib/src.old/subscription/server_subscription_manager.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'dart:collection'; - -import '../model/channel_push.dart'; -import '../model/subscribe.dart'; -import '../model/unsubscribe.dart'; -import '../transport/transport_interface.dart'; -import 'server_subscription_impl.dart'; -import 'subscription.dart'; -import 'subscription_state.dart'; - -/// Responsible for managing client-side subscriptions. -final class ServerSubscriptionManager { - /// Create a new instance of [ServerSubscriptionManager]. - ServerSubscriptionManager(ISpinifyTransport transport) - : _transportWeakRef = WeakReference(transport); - - /// Spinify client weak reference. - final WeakReference _transportWeakRef; - - /// Subscriptions count. - ({int total, int unsubscribed, int subscribing, int subscribed}) get count { - var total = 0, unsubscribed = 0, subscribing = 0, subscribed = 0; - for (final entry in _channelSubscriptions.values) { - total++; - switch (entry.state) { - case SpinifySubscriptionState$Unsubscribed _: - unsubscribed++; - case SpinifySubscriptionState$Subscribing _: - subscribing++; - case SpinifySubscriptionState$Subscribed _: - subscribed++; - } - } - return ( - total: total, - unsubscribed: unsubscribed, - subscribing: subscribing, - subscribed: subscribed, - ); - } - - /// Subscriptions registry (channel -> subscription). - /// Channel : SpinifyClientSubscription - final Map _channelSubscriptions = - {}; - - /// Get map wirth all registered client-side subscriptions. - /// Returns all registered subscriptions, - /// so you can iterate over all and do some action if required - /// (for example, you want to unsubscribe/remove all subscriptions). - Map get subscriptions => - UnmodifiableMapView({ - for (final entry in _channelSubscriptions.entries) - entry.key: entry.value, - }); - - /// Called on [SpinifySubscribe] push from server. - void subscribe(SpinifySubscribe subscribe) {} - - /// Called on [SpinifyUnsubscribe] push from server. - void unsubscribe(SpinifyUnsubscribe subscribe) {} - - /// Called when client finished connection handshake with server. - /// Add non existing subscriptions to registry and mark all connected. - /// Remove subscriptions which are not in [subs] argument. - void upsert(List subs) { - final currentChannels = _channelSubscriptions.keys.toSet(); - // Remove subscriptions which are not in subs argument. - for (final channel in currentChannels) { - if (subs.any((e) => e.channel == channel)) continue; - _channelSubscriptions.remove(channel)?.close(); - } - // Add non existing subscriptions to registry and mark all connected. - for (final sub in subs) { - (_channelSubscriptions[sub.channel] ??= SpinifyServerSubscriptionImpl( - channel: sub.channel, - transportWeakRef: _transportWeakRef, - )) - .onPush(sub); - } - } - - /// Called when subscribed to a server-side channel upon Client moving to - /// connected state or during connection lifetime if server sends Subscribe - /// push message. - void setSubscribedAll() { - for (final entry in _channelSubscriptions.values) { - if (entry.state.isSubscribed) continue; - } - } - - /// Called when existing connection lost (Client reconnects) or Client - /// explicitly disconnected. Client continue keeping server-side subscription - /// registry with stream position information where applicable. - void setSubscribingAll() { - for (final entry in _channelSubscriptions.values) { - if (entry.state.isSubscribing) continue; - entry.setSubscribing(); - } - } - - /// Called when server sent unsubscribe push or server-side subscription - /// previously existed in SDK registry disappeared upon Client reconnect. - void setUnsubscribedAll([int code = 0, String reason = 'unsubscribed']) { - for (final entry in _channelSubscriptions.values) { - if (entry.state.isUnsubscribed) continue; - entry.setUnsubscribed(code, reason); - } - } - - /// Close all subscriptions. - void close([ - int code = 0, - String reason = 'client closed', - ]) { - for (final entry in _channelSubscriptions.values) { - entry.close(code, reason).ignore(); - } - _channelSubscriptions.clear(); - } - - /// Handle push event from server for the specific channel. - void onPush(SpinifyChannelPush push) => - _channelSubscriptions[push.channel]?.onPush(push); - - /// Get subscription to the channel - /// from internal registry or null if not found. - /// - /// You need to call [SpinifyClientSubscription.subscribe] - /// to start receiving events - SpinifyServerSubscription? operator [](String channel) => - _channelSubscriptions[channel]; -} diff --git a/lib/src.old/subscription/subcibed_on_channel.dart b/lib/src.old/subscription/subcibed_on_channel.dart deleted file mode 100644 index a86b15b..0000000 --- a/lib/src.old/subscription/subcibed_on_channel.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:meta/meta.dart'; -import '../model/publication.dart'; -import '../model/stream_position.dart'; - -/// Subscribed on channel message. -/// {@category Subscription} -/// {@category Entity} -/// {@subCategory Channel} -@immutable -final class SubcibedOnChannel { - /// Subscribed on channel message. - const SubcibedOnChannel({ - required this.channel, - required this.expires, - required this.ttl, - required this.recoverable, - required this.since, - required this.publications, - required this.recovered, - required this.positioned, - required this.wasRecovering, - required this.data, - }); - - /// Channel name. - final String channel; - - /// Whether channel is expired. - final bool expires; - - /// Time to live in seconds. - final DateTime? ttl; - - /// Whether channel is recoverable. - final bool recoverable; - - /// Stream position. - final SpinifyStreamPosition? since; - - /// List of publications since last stream position. - final List publications; - - /// Whether channel is recovered after stream failure. - final bool recovered; - - /// Whether channel is positioned at last stream position. - final bool positioned; - - /// Whether channel is recovering after stream failure. - final bool wasRecovering; - - /// Raw data. - final List? data; - - @override - String toString() => 'SubcibedOnChannel{channel: $channel}'; -} diff --git a/lib/src.old/subscription/subscription.dart b/lib/src.old/subscription/subscription.dart deleted file mode 100644 index f268ae4..0000000 --- a/lib/src.old/subscription/subscription.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'dart:async'; - -import '../model/history.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/pushes_stream.dart'; -import '../model/stream_position.dart'; -import 'subscription_state.dart'; -import 'subscription_states_stream.dart'; - -/// {@template subscription} -/// Spinify subscription interface. -/// -/// Client allows subscribing on channels. -/// This can be done by creating Subscription object. -/// -/// ```dart -/// final subscription = client.newSubscription('chat'); -/// await subscription.subscribe(); -/// ``` -/// When anewSubscription method is called Client allocates a new -/// Subscription instance and saves it in the internal subscription registry. -/// Having a registry of allocated subscriptions allows SDK to manage -/// resubscribes upon reconnecting to a server. -/// Centrifugo connectors do not allow creating two subscriptions to the -/// same channel – in this case, newSubscription can throw an exception. -/// -/// Subscription has 3 states: -/// - [SpinifySubscriptionState$Unsubscribed] -/// - [SpinifySubscriptionState$Subscribing] -/// - [SpinifySubscriptionState$Subscribed] -/// -/// When a new Subscription is created it has an unsubscribed state. -/// -/// - For client-side subscriptions see [SpinifyClientSubscription]. -/// - For server-side subscriptions see [SpinifyServerSubscription]. -/// {@endtemplate} -/// {@category Subscription} -sealed class SpinifySubscription { - /// Channel name. - abstract final String channel; - - /// Current subscription state. - abstract final SpinifySubscriptionState state; - - /// Offset of last successfully received message. - abstract final SpinifyStreamPosition? since; - - /// Stream of subscription states. - abstract final SpinifySubscriptionStateStream states; - - /// Stream of received pushes from Centrifugo server for a channel. - abstract final SpinifyPushesStream stream; - - /// Await for subscription to be ready. - /// Ready resolves when subscription successfully subscribed. - /// Throws exceptions if called not in subscribing or subscribed state. - FutureOr ready(); - - /// Publish data to current Subscription channel - Future publish(List data); - - /// Fetch publication history inside a channel. - /// Only for channels where history is enabled. - Future history({ - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }); - - /// Fetch presence information inside a channel. - Future presence(); - - /// Fetch presence stats information inside a channel. - Future presenceStats(); -} - -/// {@template client_subscription} -/// # Centrifuge client-side subscription representation. -/// -/// Client allows subscribing on channels. -/// This can be done by creating Subscription object. -/// -/// When a newSubscription method is called Client allocates a new Subscription -/// instance and saves it in the internal subscription registry. -/// Having a registry of allocated subscriptions allows SDK to manage -/// resubscribes upon reconnecting to a server. -/// -/// Centrifugo connectors do not allow creating two subscriptions -/// to the same channel – in this case, newSubscription can throw an exception. -/// -/// ## Subscription has 3 states: -/// -/// - `unsubscribed` -/// - `subscribing` -/// - `subscribed` -/// -/// When a new Subscription is created it has an `unsubscribed` state. -/// -/// ## Subscription common options -/// -/// There are several common options available when -/// creating Subscription instance: -/// -/// - option to set subscription token and callback to get subscription token -/// upon expiration (see below more details) -/// - option to set subscription data -/// (attached to every subscribe/resubscribe request) -/// - options to tweak resubscribe backoff algorithm -/// - option to start Subscription since known -/// Stream Position (i.e. attempt recovery on first subscribe) -/// - option to ask server to make subscription positioned -/// (if not forced by a server) -/// - option to ask server to make subscription recoverable -/// (if not forced by a server) -/// - option to ask server to push Join/Leave messages -/// (if not forced by a server) -/// -/// ## Subscription methods -/// -/// - subscribe() – start subscribing to a channel -/// - unsubscribe() - unsubscribe from a channel -/// - publish(data) - publish data to Subscription channel -/// - history(options) - request Subscription channel history -/// - presence() - request Subscription channel online presence information -/// - presenceStats() - request Subscription channel online presence stats -/// information (number of client connections and unique users in a channel). -/// -/// {@endtemplate} -/// {@category Subscription} -/// {@subCategory Client-side} -abstract class SpinifyClientSubscription extends SpinifySubscription { - /// Start subscribing to a channel - Future subscribe(); - - /// Unsubscribe from a channel - Future unsubscribe([ - int code = 0, - String reason = 'unsubscribe called', - ]); -} - -/// {@template server_subscription} -/// Centrifuge server-side subscription representation. -/// -/// We encourage using client-side subscriptions where possible -/// as they provide a better control and isolation from connection. -/// But in some cases you may want to use server-side subscriptions -/// (i.e. subscriptions created by server upon connection establishment). -/// -/// Technically, client SDK keeps server-side subscriptions -/// in internal registry, similar to client-side subscriptions -/// but without possibility to control them. -/// {@endtemplate} -/// {@category Subscription} -/// {@subCategory Server-side} -abstract class SpinifyServerSubscription extends SpinifySubscription {} diff --git a/lib/src.old/subscription/subscription_config.dart b/lib/src.old/subscription/subscription_config.dart deleted file mode 100644 index 306a503..0000000 --- a/lib/src.old/subscription/subscription_config.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:async'; - -import 'package:fixnum/fixnum.dart' as fixnum; -import 'package:meta/meta.dart'; - -/// Token used for subscription. -/// {@category Subscription} -/// {@category Entity} -typedef SpinifySubscriptionToken = String; - -/// Callback to get token for subscription. -/// If method returns null then subscription will be established without token. -/// {@category Subscription} -/// {@category Entity} -typedef SpinifySubscriptionTokenCallback = FutureOr - Function(); - -/// Callback to set subscription payload data. -/// -/// If method returns null then no payload will be sent at subscribe time. - -/// {@category Subscription} -/// {@category Entity} -typedef SpinifySubscribePayloadCallback = FutureOr?> Function(); - -/// {@template subscription_config} -/// Subscription common options -/// -/// There are several common options available when -/// creating Subscription instance: -/// -/// - option to set subscription token and callback to get subscription token -/// upon expiration (see below more details) -/// - option to set subscription data -/// (attached to every subscribe/resubscribe request) -/// - options to tweak resubscribe backoff algorithm -/// - option to start Subscription since known -/// Stream Position (i.e. attempt recovery on first subscribe) -/// - option to ask server to make subscription positioned -/// (if not forced by a server) -/// - option to ask server to make subscription recoverable -/// (if not forced by a server) -/// - option to ask server to push Join/Leave messages -/// (if not forced by a server) -/// {@endtemplate} -/// {@category Subscription} -/// {@category Entity} -@immutable -class SpinifySubscriptionConfig { - /// {@macro subscription_config} - const SpinifySubscriptionConfig({ - this.getToken, - this.getPayload, - this.resubscribeInterval = ( - min: const Duration(milliseconds: 500), - max: const Duration(seconds: 20), - ), - this.since, - this.positioned = false, - this.recoverable = false, - this.joinLeave = false, - this.timeout = const Duration(seconds: 15), - }); - - /// Create a default config - /// - /// {@macro subscription_config} - @literal - const factory SpinifySubscriptionConfig.byDefault() = - SpinifySubscriptionConfig; - - /// Callback to get token for subscription - /// and get updated token upon expiration. - final SpinifySubscriptionTokenCallback? getToken; - - /// Data to send with subscription request. - /// Subscription `data` is attached to every subscribe/resubscribe request. - final SpinifySubscribePayloadCallback? getPayload; - - /// Resubscribe backoff algorithm - final ({Duration min, Duration max}) resubscribeInterval; - - /// Start Subscription [since] known Stream Position - /// (i.e. attempt recovery on first subscribe) - final ({fixnum.Int64 offset, String epoch})? since; - - /// Ask server to make subscription [positioned] (if not forced by a server) - final bool positioned; - - /// Ask server to make subscription [recoverable] (if not forced by a server) - final bool recoverable; - - /// Ask server to push Join/Leave messages (if not forced by a server) - final bool joinLeave; - - /// Maximum time to wait for the subscription to be established. - /// If not specified, the timeout will be 15 seconds. - final Duration timeout; - - @override - String toString() => 'SpinifySubscriptionConfig{}'; -} diff --git a/lib/src.old/subscription/subscription_state.dart b/lib/src.old/subscription/subscription_state.dart deleted file mode 100644 index 31a1f66..0000000 --- a/lib/src.old/subscription/subscription_state.dart +++ /dev/null @@ -1,317 +0,0 @@ -import 'package:fixnum/fixnum.dart' as fixnum; -import 'package:meta/meta.dart'; - -/// {@template subscription_state} -/// Subscription has 3 states: -/// -/// - `unsubscribed` -/// - `subscribing` -/// - `subscribed` -/// -/// When a new Subscription is created it has an `unsubscribed` state. -/// {@endtemplate} -/// {@category Subscription} -/// {@category Entity} -@immutable -sealed class SpinifySubscriptionState extends _$SpinifySubscriptionStateBase { - /// {@macro subscription_state} - const SpinifySubscriptionState( - {required super.timestamp, - required super.since, - required super.recoverable}); - - /// Unsubscribed - /// {@macro subscription_state} - factory SpinifySubscriptionState.unsubscribed({ - required int code, - required String reason, - DateTime? timestamp, - ({fixnum.Int64 offset, String epoch})? since, - bool recoverable, - }) = SpinifySubscriptionState$Unsubscribed; - - /// Subscribing - /// {@macro subscription_state} - factory SpinifySubscriptionState.subscribing({ - DateTime? timestamp, - ({fixnum.Int64 offset, String epoch})? since, - bool recoverable, - }) = SpinifySubscriptionState$Subscribing; - - /// Subscribed - /// {@macro subscription_state} - factory SpinifySubscriptionState.subscribed({ - DateTime? timestamp, - ({fixnum.Int64 offset, String epoch})? since, - bool recoverable, - DateTime? ttl, - }) = SpinifySubscriptionState$Subscribed; -} - -/// Unsubscribed state -/// -/// {@macro subscription_state} -/// {@category Subscription} -/// {@category Entity} -final class SpinifySubscriptionState$Unsubscribed - extends SpinifySubscriptionState { - /// {@macro subscription_state} - SpinifySubscriptionState$Unsubscribed({ - required this.code, - required this.reason, - DateTime? timestamp, - super.since, - super.recoverable = false, - }) : super(timestamp: timestamp ?? DateTime.now()); - - @override - String get type => 'unsubscribed'; - - /// Unsubscribe code. - final int code; - - /// Unsubscribe reason. - final String reason; - - @override - bool get isUnsubscribed => true; - - @override - bool get isSubscribing => false; - - @override - bool get isSubscribed => false; - - @override - R map({ - required SpinifySubscriptionStateMatch - unsubscribed, - required SpinifySubscriptionStateMatch - subscribing, - required SpinifySubscriptionStateMatch - subscribed, - }) => - unsubscribed(this); - - @override - Map toJson() => { - ...super.toJson(), - 'code': code, - 'reason': reason, - }; - - @override - int get hashCode => Object.hash(0, timestamp, since); - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() => r'SpinifySubscriptionState$Unsubscribed{}'; -} - -/// Subscribing state -/// -/// {@macro subscription_state} -/// {@category Subscription} -/// {@category Entity} -final class SpinifySubscriptionState$Subscribing - extends SpinifySubscriptionState { - /// {@macro subscription_state} - SpinifySubscriptionState$Subscribing({ - DateTime? timestamp, - super.since, - super.recoverable = false, - }) : super(timestamp: timestamp ?? DateTime.now()); - - @override - String get type => 'subscribing'; - - @override - bool get isUnsubscribed => false; - - @override - bool get isSubscribing => true; - - @override - bool get isSubscribed => false; - - @override - R map({ - required SpinifySubscriptionStateMatch - unsubscribed, - required SpinifySubscriptionStateMatch - subscribing, - required SpinifySubscriptionStateMatch - subscribed, - }) => - subscribing(this); - - @override - int get hashCode => Object.hash(1, timestamp, since); - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() => r'SpinifySubscriptionState$Subscribing{}'; -} - -/// Subscribed state -/// -/// {@macro subscription_state} -/// {@category Subscription} -/// {@category Entity} -final class SpinifySubscriptionState$Subscribed - extends SpinifySubscriptionState { - /// {@macro subscription_state} - SpinifySubscriptionState$Subscribed({ - DateTime? timestamp, - super.since, - super.recoverable = false, - this.ttl, - }) : super(timestamp: timestamp ?? DateTime.now()); - - @override - String get type => 'subscribed'; - - /// Time to live in seconds. - final DateTime? ttl; - - @override - bool get isUnsubscribed => false; - - @override - bool get isSubscribing => false; - - @override - bool get isSubscribed => true; - - @override - R map({ - required SpinifySubscriptionStateMatch - unsubscribed, - required SpinifySubscriptionStateMatch - subscribing, - required SpinifySubscriptionStateMatch - subscribed, - }) => - subscribed(this); - - @override - Map toJson() => { - ...super.toJson(), - if (ttl != null) 'ttl': ttl?.toUtc().toIso8601String(), - }; - - @override - int get hashCode => Object.hash(2, timestamp, since, recoverable, ttl); - - @override - bool operator ==(Object other) => identical(this, other); - - @override - String toString() => r'SpinifySubscriptionState$Subscribed{}'; -} - -/// Pattern matching for [SpinifySubscriptionState]. -/// {@category Entity} -typedef SpinifySubscriptionStateMatch = R - Function(S state); - -@immutable -abstract base class _$SpinifySubscriptionStateBase { - const _$SpinifySubscriptionStateBase({ - required this.timestamp, - required this.since, - required this.recoverable, - }); - - /// Represents the current state type. - abstract final String type; - - /// Timestamp of state change. - final DateTime timestamp; - - /// Stream Position - final ({fixnum.Int64 offset, String epoch})? since; - - /// Whether channel is recoverable. - final bool recoverable; - - /// Whether channel is unsubscribed. - abstract final bool isUnsubscribed; - - /// Whether channel is subscribing. - abstract final bool isSubscribing; - - /// Whether channel is subscribed. - abstract final bool isSubscribed; - - /// Pattern matching for [SpinifySubscriptionState]. - R map({ - required SpinifySubscriptionStateMatch - unsubscribed, - required SpinifySubscriptionStateMatch - subscribing, - required SpinifySubscriptionStateMatch - subscribed, - }); - - /// Pattern matching for [SpinifySubscriptionState]. - R maybeMap({ - required R Function() orElse, - SpinifySubscriptionStateMatch? - unsubscribed, - SpinifySubscriptionStateMatch? - subscribing, - SpinifySubscriptionStateMatch? - subscribed, - }) => - map( - unsubscribed: unsubscribed ?? (_) => orElse(), - subscribing: subscribing ?? (_) => orElse(), - subscribed: subscribed ?? (_) => orElse(), - ); - - /// Pattern matching for [SpinifySubscriptionState]. - R? mapOrNull({ - SpinifySubscriptionStateMatch? - unsubscribed, - SpinifySubscriptionStateMatch? - subscribing, - SpinifySubscriptionStateMatch? - subscribed, - }) => - map( - unsubscribed: unsubscribed ?? (_) => null, - subscribing: subscribing ?? (_) => null, - subscribed: subscribed ?? (_) => null, - ); - - Map toJson() => { - 'type': type, - 'timestamp': timestamp.toUtc().toIso8601String(), - if (since != null) - 'since': switch (since) { - (:fixnum.Int64 offset, :String epoch) => { - 'offset': offset, - 'epoch': epoch, - }, - _ => null, - }, - 'recoverable': recoverable, - }; -} diff --git a/lib/src.old/subscription/subscription_states_stream.dart b/lib/src.old/subscription/subscription_states_stream.dart deleted file mode 100644 index 428c57e..0000000 --- a/lib/src.old/subscription/subscription_states_stream.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import 'subscription_state.dart'; - -/// Stream of Spinify's [SpinifySubscriptionState] changes. -/// {@category Subscription} -/// {@category Entity} -@immutable -final class SpinifySubscriptionStateStream - extends StreamView { - /// Stream of Spinify's [SpinifySubscriptionState] changes. - SpinifySubscriptionStateStream(super.stream); - - /// Unsubscribed - late final Stream unsubscribed = - whereType(); - - /// Subscribing - late final Stream subscribing = - whereType(); - - /// Subscribed - late final Stream subscribed = - whereType(); - - /// Filtered stream of data of [SpinifySubscriptionState]. - Stream whereType() => - transform(StreamTransformer.fromHandlers( - handleData: (data, sink) => switch (data) { - T valid => sink.add(valid), - _ => null, - }, - )).asBroadcastStream(); - - @override - String toString() => 'SpinifySubscriptionStateStream{}'; -} diff --git a/lib/src.old/subscription/unsubscribe_code.dart b/lib/src.old/subscription/unsubscribe_code.dart deleted file mode 100644 index 96795fe..0000000 --- a/lib/src.old/subscription/unsubscribe_code.dart +++ /dev/null @@ -1,19 +0,0 @@ -/// Unsubscribe codes. -enum UnsubscribeCode { - /// Disconnect called - unsubscribeCalled(0, 'unsubscribe called'), - - /// Unauthorized - unauthorized(1, 'unauthorized'), - - /// Client closed - clientClosed(2, 'client closed'); - - const UnsubscribeCode(this.code, this.reason); - - /// Unsubscribe code. - final int code; - - /// Unsubscribe reason. - final String reason; -} diff --git a/lib/src.old/transport/protobuf/client.pb.dart b/lib/src.old/transport/protobuf/client.pb.dart deleted file mode 100644 index acfb55c..0000000 --- a/lib/src.old/transport/protobuf/client.pb.dart +++ /dev/null @@ -1,3505 +0,0 @@ -// -// Generated code. Do not modify. -// source: client.proto -// -// @dart = 2.12 - -// ignore_for_file: annotate_overrides, camel_case_types -// ignore_for_file: constant_identifier_names, library_prefixes -// ignore_for_file: non_constant_identifier_names, prefer_final_fields -// ignore_for_file: unnecessary_import, unnecessary_this, unused_import - -import 'dart:core' as $core; - -import 'package:fixnum/fixnum.dart' as $fixnum; -import 'package:protobuf/protobuf.dart' as $pb; - -class Error extends $pb.GeneratedMessage { - factory Error() => create(); - Error._() : super(); - factory Error.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Error.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Error', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) - ..aOS(2, _omitFieldNames ? '' : 'message') - ..aOB(3, _omitFieldNames ? '' : 'temporary') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Error clone() => Error()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Error copyWith(void Function(Error) updates) => - super.copyWith((message) => updates(message as Error)) as Error; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Error create() => Error._(); - Error createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Error getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Error? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get code => $_getIZ(0); - @$pb.TagNumber(1) - set code($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasCode() => $_has(0); - @$pb.TagNumber(1) - void clearCode() => clearField(1); - - @$pb.TagNumber(2) - $core.String get message => $_getSZ(1); - @$pb.TagNumber(2) - set message($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasMessage() => $_has(1); - @$pb.TagNumber(2) - void clearMessage() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get temporary => $_getBF(2); - @$pb.TagNumber(3) - set temporary($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasTemporary() => $_has(2); - @$pb.TagNumber(3) - void clearTemporary() => clearField(3); -} - -class EmulationRequest extends $pb.GeneratedMessage { - factory EmulationRequest() => create(); - EmulationRequest._() : super(); - factory EmulationRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory EmulationRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'EmulationRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'node') - ..aOS(2, _omitFieldNames ? '' : 'session') - ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - EmulationRequest clone() => EmulationRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - EmulationRequest copyWith(void Function(EmulationRequest) updates) => - super.copyWith((message) => updates(message as EmulationRequest)) - as EmulationRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static EmulationRequest create() => EmulationRequest._(); - EmulationRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static EmulationRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static EmulationRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get node => $_getSZ(0); - @$pb.TagNumber(1) - set node($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasNode() => $_has(0); - @$pb.TagNumber(1) - void clearNode() => clearField(1); - - @$pb.TagNumber(2) - $core.String get session => $_getSZ(1); - @$pb.TagNumber(2) - set session($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasSession() => $_has(1); - @$pb.TagNumber(2) - void clearSession() => clearField(2); - - @$pb.TagNumber(3) - $core.List<$core.int> get data => $_getN(2); - @$pb.TagNumber(3) - set data($core.List<$core.int> v) { - $_setBytes(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasData() => $_has(2); - @$pb.TagNumber(3) - void clearData() => clearField(3); -} - -class Command extends $pb.GeneratedMessage { - factory Command() => create(); - Command._() : super(); - factory Command.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Command.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Command', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OU3) - ..aOM(4, _omitFieldNames ? '' : 'connect', - subBuilder: ConnectRequest.create) - ..aOM(5, _omitFieldNames ? '' : 'subscribe', - subBuilder: SubscribeRequest.create) - ..aOM(6, _omitFieldNames ? '' : 'unsubscribe', - subBuilder: UnsubscribeRequest.create) - ..aOM(7, _omitFieldNames ? '' : 'publish', - subBuilder: PublishRequest.create) - ..aOM(8, _omitFieldNames ? '' : 'presence', - subBuilder: PresenceRequest.create) - ..aOM(9, _omitFieldNames ? '' : 'presenceStats', - subBuilder: PresenceStatsRequest.create) - ..aOM(10, _omitFieldNames ? '' : 'history', - subBuilder: HistoryRequest.create) - ..aOM(11, _omitFieldNames ? '' : 'ping', - subBuilder: PingRequest.create) - ..aOM(12, _omitFieldNames ? '' : 'send', - subBuilder: SendRequest.create) - ..aOM(13, _omitFieldNames ? '' : 'rpc', - subBuilder: RPCRequest.create) - ..aOM(14, _omitFieldNames ? '' : 'refresh', - subBuilder: RefreshRequest.create) - ..aOM(15, _omitFieldNames ? '' : 'subRefresh', - subBuilder: SubRefreshRequest.create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Command clone() => Command()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Command copyWith(void Function(Command) updates) => - super.copyWith((message) => updates(message as Command)) as Command; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Command create() => Command._(); - Command createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Command getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Command? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get id => $_getIZ(0); - @$pb.TagNumber(1) - set id($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasId() => $_has(0); - @$pb.TagNumber(1) - void clearId() => clearField(1); - - @$pb.TagNumber(4) - ConnectRequest get connect => $_getN(1); - @$pb.TagNumber(4) - set connect(ConnectRequest v) { - setField(4, v); - } - - @$pb.TagNumber(4) - $core.bool hasConnect() => $_has(1); - @$pb.TagNumber(4) - void clearConnect() => clearField(4); - @$pb.TagNumber(4) - ConnectRequest ensureConnect() => $_ensure(1); - - @$pb.TagNumber(5) - SubscribeRequest get subscribe => $_getN(2); - @$pb.TagNumber(5) - set subscribe(SubscribeRequest v) { - setField(5, v); - } - - @$pb.TagNumber(5) - $core.bool hasSubscribe() => $_has(2); - @$pb.TagNumber(5) - void clearSubscribe() => clearField(5); - @$pb.TagNumber(5) - SubscribeRequest ensureSubscribe() => $_ensure(2); - - @$pb.TagNumber(6) - UnsubscribeRequest get unsubscribe => $_getN(3); - @$pb.TagNumber(6) - set unsubscribe(UnsubscribeRequest v) { - setField(6, v); - } - - @$pb.TagNumber(6) - $core.bool hasUnsubscribe() => $_has(3); - @$pb.TagNumber(6) - void clearUnsubscribe() => clearField(6); - @$pb.TagNumber(6) - UnsubscribeRequest ensureUnsubscribe() => $_ensure(3); - - @$pb.TagNumber(7) - PublishRequest get publish => $_getN(4); - @$pb.TagNumber(7) - set publish(PublishRequest v) { - setField(7, v); - } - - @$pb.TagNumber(7) - $core.bool hasPublish() => $_has(4); - @$pb.TagNumber(7) - void clearPublish() => clearField(7); - @$pb.TagNumber(7) - PublishRequest ensurePublish() => $_ensure(4); - - @$pb.TagNumber(8) - PresenceRequest get presence => $_getN(5); - @$pb.TagNumber(8) - set presence(PresenceRequest v) { - setField(8, v); - } - - @$pb.TagNumber(8) - $core.bool hasPresence() => $_has(5); - @$pb.TagNumber(8) - void clearPresence() => clearField(8); - @$pb.TagNumber(8) - PresenceRequest ensurePresence() => $_ensure(5); - - @$pb.TagNumber(9) - PresenceStatsRequest get presenceStats => $_getN(6); - @$pb.TagNumber(9) - set presenceStats(PresenceStatsRequest v) { - setField(9, v); - } - - @$pb.TagNumber(9) - $core.bool hasPresenceStats() => $_has(6); - @$pb.TagNumber(9) - void clearPresenceStats() => clearField(9); - @$pb.TagNumber(9) - PresenceStatsRequest ensurePresenceStats() => $_ensure(6); - - @$pb.TagNumber(10) - HistoryRequest get history => $_getN(7); - @$pb.TagNumber(10) - set history(HistoryRequest v) { - setField(10, v); - } - - @$pb.TagNumber(10) - $core.bool hasHistory() => $_has(7); - @$pb.TagNumber(10) - void clearHistory() => clearField(10); - @$pb.TagNumber(10) - HistoryRequest ensureHistory() => $_ensure(7); - - @$pb.TagNumber(11) - PingRequest get ping => $_getN(8); - @$pb.TagNumber(11) - set ping(PingRequest v) { - setField(11, v); - } - - @$pb.TagNumber(11) - $core.bool hasPing() => $_has(8); - @$pb.TagNumber(11) - void clearPing() => clearField(11); - @$pb.TagNumber(11) - PingRequest ensurePing() => $_ensure(8); - - @$pb.TagNumber(12) - SendRequest get send => $_getN(9); - @$pb.TagNumber(12) - set send(SendRequest v) { - setField(12, v); - } - - @$pb.TagNumber(12) - $core.bool hasSend() => $_has(9); - @$pb.TagNumber(12) - void clearSend() => clearField(12); - @$pb.TagNumber(12) - SendRequest ensureSend() => $_ensure(9); - - @$pb.TagNumber(13) - RPCRequest get rpc => $_getN(10); - @$pb.TagNumber(13) - set rpc(RPCRequest v) { - setField(13, v); - } - - @$pb.TagNumber(13) - $core.bool hasRpc() => $_has(10); - @$pb.TagNumber(13) - void clearRpc() => clearField(13); - @$pb.TagNumber(13) - RPCRequest ensureRpc() => $_ensure(10); - - @$pb.TagNumber(14) - RefreshRequest get refresh => $_getN(11); - @$pb.TagNumber(14) - set refresh(RefreshRequest v) { - setField(14, v); - } - - @$pb.TagNumber(14) - $core.bool hasRefresh() => $_has(11); - @$pb.TagNumber(14) - void clearRefresh() => clearField(14); - @$pb.TagNumber(14) - RefreshRequest ensureRefresh() => $_ensure(11); - - @$pb.TagNumber(15) - SubRefreshRequest get subRefresh => $_getN(12); - @$pb.TagNumber(15) - set subRefresh(SubRefreshRequest v) { - setField(15, v); - } - - @$pb.TagNumber(15) - $core.bool hasSubRefresh() => $_has(12); - @$pb.TagNumber(15) - void clearSubRefresh() => clearField(15); - @$pb.TagNumber(15) - SubRefreshRequest ensureSubRefresh() => $_ensure(12); -} - -class Reply extends $pb.GeneratedMessage { - factory Reply() => create(); - Reply._() : super(); - factory Reply.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Reply.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Reply', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OU3) - ..aOM(2, _omitFieldNames ? '' : 'error', subBuilder: Error.create) - ..aOM(4, _omitFieldNames ? '' : 'push', subBuilder: Push.create) - ..aOM(5, _omitFieldNames ? '' : 'connect', - subBuilder: ConnectResult.create) - ..aOM(6, _omitFieldNames ? '' : 'subscribe', - subBuilder: SubscribeResult.create) - ..aOM(7, _omitFieldNames ? '' : 'unsubscribe', - subBuilder: UnsubscribeResult.create) - ..aOM(8, _omitFieldNames ? '' : 'publish', - subBuilder: PublishResult.create) - ..aOM(9, _omitFieldNames ? '' : 'presence', - subBuilder: PresenceResult.create) - ..aOM(10, _omitFieldNames ? '' : 'presenceStats', - subBuilder: PresenceStatsResult.create) - ..aOM(11, _omitFieldNames ? '' : 'history', - subBuilder: HistoryResult.create) - ..aOM(12, _omitFieldNames ? '' : 'ping', - subBuilder: PingResult.create) - ..aOM(13, _omitFieldNames ? '' : 'rpc', - subBuilder: RPCResult.create) - ..aOM(14, _omitFieldNames ? '' : 'refresh', - subBuilder: RefreshResult.create) - ..aOM(15, _omitFieldNames ? '' : 'subRefresh', - subBuilder: SubRefreshResult.create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Reply clone() => Reply()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Reply copyWith(void Function(Reply) updates) => - super.copyWith((message) => updates(message as Reply)) as Reply; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Reply create() => Reply._(); - Reply createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Reply getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Reply? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get id => $_getIZ(0); - @$pb.TagNumber(1) - set id($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasId() => $_has(0); - @$pb.TagNumber(1) - void clearId() => clearField(1); - - @$pb.TagNumber(2) - Error get error => $_getN(1); - @$pb.TagNumber(2) - set error(Error v) { - setField(2, v); - } - - @$pb.TagNumber(2) - $core.bool hasError() => $_has(1); - @$pb.TagNumber(2) - void clearError() => clearField(2); - @$pb.TagNumber(2) - Error ensureError() => $_ensure(1); - - @$pb.TagNumber(4) - Push get push => $_getN(2); - @$pb.TagNumber(4) - set push(Push v) { - setField(4, v); - } - - @$pb.TagNumber(4) - $core.bool hasPush() => $_has(2); - @$pb.TagNumber(4) - void clearPush() => clearField(4); - @$pb.TagNumber(4) - Push ensurePush() => $_ensure(2); - - @$pb.TagNumber(5) - ConnectResult get connect => $_getN(3); - @$pb.TagNumber(5) - set connect(ConnectResult v) { - setField(5, v); - } - - @$pb.TagNumber(5) - $core.bool hasConnect() => $_has(3); - @$pb.TagNumber(5) - void clearConnect() => clearField(5); - @$pb.TagNumber(5) - ConnectResult ensureConnect() => $_ensure(3); - - @$pb.TagNumber(6) - SubscribeResult get subscribe => $_getN(4); - @$pb.TagNumber(6) - set subscribe(SubscribeResult v) { - setField(6, v); - } - - @$pb.TagNumber(6) - $core.bool hasSubscribe() => $_has(4); - @$pb.TagNumber(6) - void clearSubscribe() => clearField(6); - @$pb.TagNumber(6) - SubscribeResult ensureSubscribe() => $_ensure(4); - - @$pb.TagNumber(7) - UnsubscribeResult get unsubscribe => $_getN(5); - @$pb.TagNumber(7) - set unsubscribe(UnsubscribeResult v) { - setField(7, v); - } - - @$pb.TagNumber(7) - $core.bool hasUnsubscribe() => $_has(5); - @$pb.TagNumber(7) - void clearUnsubscribe() => clearField(7); - @$pb.TagNumber(7) - UnsubscribeResult ensureUnsubscribe() => $_ensure(5); - - @$pb.TagNumber(8) - PublishResult get publish => $_getN(6); - @$pb.TagNumber(8) - set publish(PublishResult v) { - setField(8, v); - } - - @$pb.TagNumber(8) - $core.bool hasPublish() => $_has(6); - @$pb.TagNumber(8) - void clearPublish() => clearField(8); - @$pb.TagNumber(8) - PublishResult ensurePublish() => $_ensure(6); - - @$pb.TagNumber(9) - PresenceResult get presence => $_getN(7); - @$pb.TagNumber(9) - set presence(PresenceResult v) { - setField(9, v); - } - - @$pb.TagNumber(9) - $core.bool hasPresence() => $_has(7); - @$pb.TagNumber(9) - void clearPresence() => clearField(9); - @$pb.TagNumber(9) - PresenceResult ensurePresence() => $_ensure(7); - - @$pb.TagNumber(10) - PresenceStatsResult get presenceStats => $_getN(8); - @$pb.TagNumber(10) - set presenceStats(PresenceStatsResult v) { - setField(10, v); - } - - @$pb.TagNumber(10) - $core.bool hasPresenceStats() => $_has(8); - @$pb.TagNumber(10) - void clearPresenceStats() => clearField(10); - @$pb.TagNumber(10) - PresenceStatsResult ensurePresenceStats() => $_ensure(8); - - @$pb.TagNumber(11) - HistoryResult get history => $_getN(9); - @$pb.TagNumber(11) - set history(HistoryResult v) { - setField(11, v); - } - - @$pb.TagNumber(11) - $core.bool hasHistory() => $_has(9); - @$pb.TagNumber(11) - void clearHistory() => clearField(11); - @$pb.TagNumber(11) - HistoryResult ensureHistory() => $_ensure(9); - - @$pb.TagNumber(12) - PingResult get ping => $_getN(10); - @$pb.TagNumber(12) - set ping(PingResult v) { - setField(12, v); - } - - @$pb.TagNumber(12) - $core.bool hasPing() => $_has(10); - @$pb.TagNumber(12) - void clearPing() => clearField(12); - @$pb.TagNumber(12) - PingResult ensurePing() => $_ensure(10); - - @$pb.TagNumber(13) - RPCResult get rpc => $_getN(11); - @$pb.TagNumber(13) - set rpc(RPCResult v) { - setField(13, v); - } - - @$pb.TagNumber(13) - $core.bool hasRpc() => $_has(11); - @$pb.TagNumber(13) - void clearRpc() => clearField(13); - @$pb.TagNumber(13) - RPCResult ensureRpc() => $_ensure(11); - - @$pb.TagNumber(14) - RefreshResult get refresh => $_getN(12); - @$pb.TagNumber(14) - set refresh(RefreshResult v) { - setField(14, v); - } - - @$pb.TagNumber(14) - $core.bool hasRefresh() => $_has(12); - @$pb.TagNumber(14) - void clearRefresh() => clearField(14); - @$pb.TagNumber(14) - RefreshResult ensureRefresh() => $_ensure(12); - - @$pb.TagNumber(15) - SubRefreshResult get subRefresh => $_getN(13); - @$pb.TagNumber(15) - set subRefresh(SubRefreshResult v) { - setField(15, v); - } - - @$pb.TagNumber(15) - $core.bool hasSubRefresh() => $_has(13); - @$pb.TagNumber(15) - void clearSubRefresh() => clearField(15); - @$pb.TagNumber(15) - SubRefreshResult ensureSubRefresh() => $_ensure(13); -} - -class Push extends $pb.GeneratedMessage { - factory Push() => create(); - Push._() : super(); - factory Push.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Push.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Push', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(2, _omitFieldNames ? '' : 'channel') - ..aOM(4, _omitFieldNames ? '' : 'pub', - subBuilder: Publication.create) - ..aOM(5, _omitFieldNames ? '' : 'join', subBuilder: Join.create) - ..aOM(6, _omitFieldNames ? '' : 'leave', subBuilder: Leave.create) - ..aOM(7, _omitFieldNames ? '' : 'unsubscribe', - subBuilder: Unsubscribe.create) - ..aOM(8, _omitFieldNames ? '' : 'message', - subBuilder: Message.create) - ..aOM(9, _omitFieldNames ? '' : 'subscribe', - subBuilder: Subscribe.create) - ..aOM(10, _omitFieldNames ? '' : 'connect', - subBuilder: Connect.create) - ..aOM(11, _omitFieldNames ? '' : 'disconnect', - subBuilder: Disconnect.create) - ..aOM(12, _omitFieldNames ? '' : 'refresh', - subBuilder: Refresh.create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Push clone() => Push()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Push copyWith(void Function(Push) updates) => - super.copyWith((message) => updates(message as Push)) as Push; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Push create() => Push._(); - Push createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Push getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Push? _defaultInstance; - - @$pb.TagNumber(2) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(2) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(2) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(2) - void clearChannel() => clearField(2); - - @$pb.TagNumber(4) - Publication get pub => $_getN(1); - @$pb.TagNumber(4) - set pub(Publication v) { - setField(4, v); - } - - @$pb.TagNumber(4) - $core.bool hasPub() => $_has(1); - @$pb.TagNumber(4) - void clearPub() => clearField(4); - @$pb.TagNumber(4) - Publication ensurePub() => $_ensure(1); - - @$pb.TagNumber(5) - Join get join => $_getN(2); - @$pb.TagNumber(5) - set join(Join v) { - setField(5, v); - } - - @$pb.TagNumber(5) - $core.bool hasJoin() => $_has(2); - @$pb.TagNumber(5) - void clearJoin() => clearField(5); - @$pb.TagNumber(5) - Join ensureJoin() => $_ensure(2); - - @$pb.TagNumber(6) - Leave get leave => $_getN(3); - @$pb.TagNumber(6) - set leave(Leave v) { - setField(6, v); - } - - @$pb.TagNumber(6) - $core.bool hasLeave() => $_has(3); - @$pb.TagNumber(6) - void clearLeave() => clearField(6); - @$pb.TagNumber(6) - Leave ensureLeave() => $_ensure(3); - - @$pb.TagNumber(7) - Unsubscribe get unsubscribe => $_getN(4); - @$pb.TagNumber(7) - set unsubscribe(Unsubscribe v) { - setField(7, v); - } - - @$pb.TagNumber(7) - $core.bool hasUnsubscribe() => $_has(4); - @$pb.TagNumber(7) - void clearUnsubscribe() => clearField(7); - @$pb.TagNumber(7) - Unsubscribe ensureUnsubscribe() => $_ensure(4); - - @$pb.TagNumber(8) - Message get message => $_getN(5); - @$pb.TagNumber(8) - set message(Message v) { - setField(8, v); - } - - @$pb.TagNumber(8) - $core.bool hasMessage() => $_has(5); - @$pb.TagNumber(8) - void clearMessage() => clearField(8); - @$pb.TagNumber(8) - Message ensureMessage() => $_ensure(5); - - @$pb.TagNumber(9) - Subscribe get subscribe => $_getN(6); - @$pb.TagNumber(9) - set subscribe(Subscribe v) { - setField(9, v); - } - - @$pb.TagNumber(9) - $core.bool hasSubscribe() => $_has(6); - @$pb.TagNumber(9) - void clearSubscribe() => clearField(9); - @$pb.TagNumber(9) - Subscribe ensureSubscribe() => $_ensure(6); - - @$pb.TagNumber(10) - Connect get connect => $_getN(7); - @$pb.TagNumber(10) - set connect(Connect v) { - setField(10, v); - } - - @$pb.TagNumber(10) - $core.bool hasConnect() => $_has(7); - @$pb.TagNumber(10) - void clearConnect() => clearField(10); - @$pb.TagNumber(10) - Connect ensureConnect() => $_ensure(7); - - @$pb.TagNumber(11) - Disconnect get disconnect => $_getN(8); - @$pb.TagNumber(11) - set disconnect(Disconnect v) { - setField(11, v); - } - - @$pb.TagNumber(11) - $core.bool hasDisconnect() => $_has(8); - @$pb.TagNumber(11) - void clearDisconnect() => clearField(11); - @$pb.TagNumber(11) - Disconnect ensureDisconnect() => $_ensure(8); - - @$pb.TagNumber(12) - Refresh get refresh => $_getN(9); - @$pb.TagNumber(12) - set refresh(Refresh v) { - setField(12, v); - } - - @$pb.TagNumber(12) - $core.bool hasRefresh() => $_has(9); - @$pb.TagNumber(12) - void clearRefresh() => clearField(12); - @$pb.TagNumber(12) - Refresh ensureRefresh() => $_ensure(9); -} - -class ClientInfo extends $pb.GeneratedMessage { - factory ClientInfo() => create(); - ClientInfo._() : super(); - factory ClientInfo.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ClientInfo.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ClientInfo', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'user') - ..aOS(2, _omitFieldNames ? '' : 'client') - ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'connInfo', $pb.PbFieldType.OY) - ..a<$core.List<$core.int>>( - 4, _omitFieldNames ? '' : 'chanInfo', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - ClientInfo clone() => ClientInfo()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ClientInfo copyWith(void Function(ClientInfo) updates) => - super.copyWith((message) => updates(message as ClientInfo)) as ClientInfo; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ClientInfo create() => ClientInfo._(); - ClientInfo createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static ClientInfo getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ClientInfo? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get user => $_getSZ(0); - @$pb.TagNumber(1) - set user($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasUser() => $_has(0); - @$pb.TagNumber(1) - void clearUser() => clearField(1); - - @$pb.TagNumber(2) - $core.String get client => $_getSZ(1); - @$pb.TagNumber(2) - set client($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasClient() => $_has(1); - @$pb.TagNumber(2) - void clearClient() => clearField(2); - - @$pb.TagNumber(3) - $core.List<$core.int> get connInfo => $_getN(2); - @$pb.TagNumber(3) - set connInfo($core.List<$core.int> v) { - $_setBytes(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasConnInfo() => $_has(2); - @$pb.TagNumber(3) - void clearConnInfo() => clearField(3); - - @$pb.TagNumber(4) - $core.List<$core.int> get chanInfo => $_getN(3); - @$pb.TagNumber(4) - set chanInfo($core.List<$core.int> v) { - $_setBytes(3, v); - } - - @$pb.TagNumber(4) - $core.bool hasChanInfo() => $_has(3); - @$pb.TagNumber(4) - void clearChanInfo() => clearField(4); -} - -class Publication extends $pb.GeneratedMessage { - factory Publication() => create(); - Publication._() : super(); - factory Publication.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Publication.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Publication', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 4, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..aOM(5, _omitFieldNames ? '' : 'info', - subBuilder: ClientInfo.create) - ..a<$fixnum.Int64>(6, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..m<$core.String, $core.String>(7, _omitFieldNames ? '' : 'tags', - entryClassName: 'Publication.TagsEntry', - keyFieldType: $pb.PbFieldType.OS, - valueFieldType: $pb.PbFieldType.OS, - packageName: const $pb.PackageName('centrifugal.centrifuge.protocol')) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Publication clone() => Publication()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Publication copyWith(void Function(Publication) updates) => - super.copyWith((message) => updates(message as Publication)) - as Publication; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Publication create() => Publication._(); - Publication createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Publication getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Publication? _defaultInstance; - - @$pb.TagNumber(4) - $core.List<$core.int> get data => $_getN(0); - @$pb.TagNumber(4) - set data($core.List<$core.int> v) { - $_setBytes(0, v); - } - - @$pb.TagNumber(4) - $core.bool hasData() => $_has(0); - @$pb.TagNumber(4) - void clearData() => clearField(4); - - @$pb.TagNumber(5) - ClientInfo get info => $_getN(1); - @$pb.TagNumber(5) - set info(ClientInfo v) { - setField(5, v); - } - - @$pb.TagNumber(5) - $core.bool hasInfo() => $_has(1); - @$pb.TagNumber(5) - void clearInfo() => clearField(5); - @$pb.TagNumber(5) - ClientInfo ensureInfo() => $_ensure(1); - - @$pb.TagNumber(6) - $fixnum.Int64 get offset => $_getI64(2); - @$pb.TagNumber(6) - set offset($fixnum.Int64 v) { - $_setInt64(2, v); - } - - @$pb.TagNumber(6) - $core.bool hasOffset() => $_has(2); - @$pb.TagNumber(6) - void clearOffset() => clearField(6); - - @$pb.TagNumber(7) - $core.Map<$core.String, $core.String> get tags => $_getMap(3); -} - -class Join extends $pb.GeneratedMessage { - factory Join() => create(); - Join._() : super(); - factory Join.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Join.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Join', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'info', - subBuilder: ClientInfo.create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Join clone() => Join()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Join copyWith(void Function(Join) updates) => - super.copyWith((message) => updates(message as Join)) as Join; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Join create() => Join._(); - Join createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Join getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Join? _defaultInstance; - - @$pb.TagNumber(1) - ClientInfo get info => $_getN(0); - @$pb.TagNumber(1) - set info(ClientInfo v) { - setField(1, v); - } - - @$pb.TagNumber(1) - $core.bool hasInfo() => $_has(0); - @$pb.TagNumber(1) - void clearInfo() => clearField(1); - @$pb.TagNumber(1) - ClientInfo ensureInfo() => $_ensure(0); -} - -class Leave extends $pb.GeneratedMessage { - factory Leave() => create(); - Leave._() : super(); - factory Leave.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Leave.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Leave', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'info', - subBuilder: ClientInfo.create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Leave clone() => Leave()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Leave copyWith(void Function(Leave) updates) => - super.copyWith((message) => updates(message as Leave)) as Leave; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Leave create() => Leave._(); - Leave createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Leave getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Leave? _defaultInstance; - - @$pb.TagNumber(1) - ClientInfo get info => $_getN(0); - @$pb.TagNumber(1) - set info(ClientInfo v) { - setField(1, v); - } - - @$pb.TagNumber(1) - $core.bool hasInfo() => $_has(0); - @$pb.TagNumber(1) - void clearInfo() => clearField(1); - @$pb.TagNumber(1) - ClientInfo ensureInfo() => $_ensure(0); -} - -class Unsubscribe extends $pb.GeneratedMessage { - factory Unsubscribe() => create(); - Unsubscribe._() : super(); - factory Unsubscribe.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Unsubscribe.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Unsubscribe', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(2, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) - ..aOS(3, _omitFieldNames ? '' : 'reason') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Unsubscribe clone() => Unsubscribe()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Unsubscribe copyWith(void Function(Unsubscribe) updates) => - super.copyWith((message) => updates(message as Unsubscribe)) - as Unsubscribe; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Unsubscribe create() => Unsubscribe._(); - Unsubscribe createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Unsubscribe getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Unsubscribe? _defaultInstance; - - @$pb.TagNumber(2) - $core.int get code => $_getIZ(0); - @$pb.TagNumber(2) - set code($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(2) - $core.bool hasCode() => $_has(0); - @$pb.TagNumber(2) - void clearCode() => clearField(2); - - @$pb.TagNumber(3) - $core.String get reason => $_getSZ(1); - @$pb.TagNumber(3) - set reason($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(3) - $core.bool hasReason() => $_has(1); - @$pb.TagNumber(3) - void clearReason() => clearField(3); -} - -class Subscribe extends $pb.GeneratedMessage { - factory Subscribe() => create(); - Subscribe._() : super(); - factory Subscribe.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Subscribe.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Subscribe', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOB(1, _omitFieldNames ? '' : 'recoverable') - ..aOS(4, _omitFieldNames ? '' : 'epoch') - ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..aOB(6, _omitFieldNames ? '' : 'positioned') - ..a<$core.List<$core.int>>( - 7, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Subscribe clone() => Subscribe()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Subscribe copyWith(void Function(Subscribe) updates) => - super.copyWith((message) => updates(message as Subscribe)) as Subscribe; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Subscribe create() => Subscribe._(); - Subscribe createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Subscribe getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Subscribe? _defaultInstance; - - @$pb.TagNumber(1) - $core.bool get recoverable => $_getBF(0); - @$pb.TagNumber(1) - set recoverable($core.bool v) { - $_setBool(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasRecoverable() => $_has(0); - @$pb.TagNumber(1) - void clearRecoverable() => clearField(1); - - @$pb.TagNumber(4) - $core.String get epoch => $_getSZ(1); - @$pb.TagNumber(4) - set epoch($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(4) - $core.bool hasEpoch() => $_has(1); - @$pb.TagNumber(4) - void clearEpoch() => clearField(4); - - @$pb.TagNumber(5) - $fixnum.Int64 get offset => $_getI64(2); - @$pb.TagNumber(5) - set offset($fixnum.Int64 v) { - $_setInt64(2, v); - } - - @$pb.TagNumber(5) - $core.bool hasOffset() => $_has(2); - @$pb.TagNumber(5) - void clearOffset() => clearField(5); - - @$pb.TagNumber(6) - $core.bool get positioned => $_getBF(3); - @$pb.TagNumber(6) - set positioned($core.bool v) { - $_setBool(3, v); - } - - @$pb.TagNumber(6) - $core.bool hasPositioned() => $_has(3); - @$pb.TagNumber(6) - void clearPositioned() => clearField(6); - - @$pb.TagNumber(7) - $core.List<$core.int> get data => $_getN(4); - @$pb.TagNumber(7) - set data($core.List<$core.int> v) { - $_setBytes(4, v); - } - - @$pb.TagNumber(7) - $core.bool hasData() => $_has(4); - @$pb.TagNumber(7) - void clearData() => clearField(7); -} - -class Message extends $pb.GeneratedMessage { - factory Message() => create(); - Message._() : super(); - factory Message.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Message.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Message', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Message clone() => Message()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Message copyWith(void Function(Message) updates) => - super.copyWith((message) => updates(message as Message)) as Message; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Message create() => Message._(); - Message createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Message getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Message? _defaultInstance; - - @$pb.TagNumber(1) - $core.List<$core.int> get data => $_getN(0); - @$pb.TagNumber(1) - set data($core.List<$core.int> v) { - $_setBytes(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasData() => $_has(0); - @$pb.TagNumber(1) - void clearData() => clearField(1); -} - -class Connect extends $pb.GeneratedMessage { - factory Connect() => create(); - Connect._() : super(); - factory Connect.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Connect.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Connect', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'client') - ..aOS(2, _omitFieldNames ? '' : 'version') - ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..m<$core.String, SubscribeResult>(4, _omitFieldNames ? '' : 'subs', - entryClassName: 'Connect.SubsEntry', - keyFieldType: $pb.PbFieldType.OS, - valueFieldType: $pb.PbFieldType.OM, - valueCreator: SubscribeResult.create, - valueDefaultOrMaker: SubscribeResult.getDefault, - packageName: const $pb.PackageName('centrifugal.centrifuge.protocol')) - ..aOB(5, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(6, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..a<$core.int>(7, _omitFieldNames ? '' : 'ping', $pb.PbFieldType.OU3) - ..aOB(8, _omitFieldNames ? '' : 'pong') - ..aOS(9, _omitFieldNames ? '' : 'session') - ..aOS(10, _omitFieldNames ? '' : 'node') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Connect clone() => Connect()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Connect copyWith(void Function(Connect) updates) => - super.copyWith((message) => updates(message as Connect)) as Connect; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Connect create() => Connect._(); - Connect createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Connect getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Connect? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get client => $_getSZ(0); - @$pb.TagNumber(1) - set client($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasClient() => $_has(0); - @$pb.TagNumber(1) - void clearClient() => clearField(1); - - @$pb.TagNumber(2) - $core.String get version => $_getSZ(1); - @$pb.TagNumber(2) - set version($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasVersion() => $_has(1); - @$pb.TagNumber(2) - void clearVersion() => clearField(2); - - @$pb.TagNumber(3) - $core.List<$core.int> get data => $_getN(2); - @$pb.TagNumber(3) - set data($core.List<$core.int> v) { - $_setBytes(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasData() => $_has(2); - @$pb.TagNumber(3) - void clearData() => clearField(3); - - @$pb.TagNumber(4) - $core.Map<$core.String, SubscribeResult> get subs => $_getMap(3); - - @$pb.TagNumber(5) - $core.bool get expires => $_getBF(4); - @$pb.TagNumber(5) - set expires($core.bool v) { - $_setBool(4, v); - } - - @$pb.TagNumber(5) - $core.bool hasExpires() => $_has(4); - @$pb.TagNumber(5) - void clearExpires() => clearField(5); - - @$pb.TagNumber(6) - $core.int get ttl => $_getIZ(5); - @$pb.TagNumber(6) - set ttl($core.int v) { - $_setUnsignedInt32(5, v); - } - - @$pb.TagNumber(6) - $core.bool hasTtl() => $_has(5); - @$pb.TagNumber(6) - void clearTtl() => clearField(6); - - @$pb.TagNumber(7) - $core.int get ping => $_getIZ(6); - @$pb.TagNumber(7) - set ping($core.int v) { - $_setUnsignedInt32(6, v); - } - - @$pb.TagNumber(7) - $core.bool hasPing() => $_has(6); - @$pb.TagNumber(7) - void clearPing() => clearField(7); - - @$pb.TagNumber(8) - $core.bool get pong => $_getBF(7); - @$pb.TagNumber(8) - set pong($core.bool v) { - $_setBool(7, v); - } - - @$pb.TagNumber(8) - $core.bool hasPong() => $_has(7); - @$pb.TagNumber(8) - void clearPong() => clearField(8); - - @$pb.TagNumber(9) - $core.String get session => $_getSZ(8); - @$pb.TagNumber(9) - set session($core.String v) { - $_setString(8, v); - } - - @$pb.TagNumber(9) - $core.bool hasSession() => $_has(8); - @$pb.TagNumber(9) - void clearSession() => clearField(9); - - @$pb.TagNumber(10) - $core.String get node => $_getSZ(9); - @$pb.TagNumber(10) - set node($core.String v) { - $_setString(9, v); - } - - @$pb.TagNumber(10) - $core.bool hasNode() => $_has(9); - @$pb.TagNumber(10) - void clearNode() => clearField(10); -} - -class Disconnect extends $pb.GeneratedMessage { - factory Disconnect() => create(); - Disconnect._() : super(); - factory Disconnect.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Disconnect.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Disconnect', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) - ..aOS(2, _omitFieldNames ? '' : 'reason') - ..aOB(3, _omitFieldNames ? '' : 'reconnect') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Disconnect clone() => Disconnect()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Disconnect copyWith(void Function(Disconnect) updates) => - super.copyWith((message) => updates(message as Disconnect)) as Disconnect; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Disconnect create() => Disconnect._(); - Disconnect createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Disconnect getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Disconnect? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get code => $_getIZ(0); - @$pb.TagNumber(1) - set code($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasCode() => $_has(0); - @$pb.TagNumber(1) - void clearCode() => clearField(1); - - @$pb.TagNumber(2) - $core.String get reason => $_getSZ(1); - @$pb.TagNumber(2) - set reason($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasReason() => $_has(1); - @$pb.TagNumber(2) - void clearReason() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get reconnect => $_getBF(2); - @$pb.TagNumber(3) - set reconnect($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasReconnect() => $_has(2); - @$pb.TagNumber(3) - void clearReconnect() => clearField(3); -} - -class Refresh extends $pb.GeneratedMessage { - factory Refresh() => create(); - Refresh._() : super(); - factory Refresh.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Refresh.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Refresh', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOB(1, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(2, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Refresh clone() => Refresh()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Refresh copyWith(void Function(Refresh) updates) => - super.copyWith((message) => updates(message as Refresh)) as Refresh; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Refresh create() => Refresh._(); - Refresh createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Refresh getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Refresh? _defaultInstance; - - @$pb.TagNumber(1) - $core.bool get expires => $_getBF(0); - @$pb.TagNumber(1) - set expires($core.bool v) { - $_setBool(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasExpires() => $_has(0); - @$pb.TagNumber(1) - void clearExpires() => clearField(1); - - @$pb.TagNumber(2) - $core.int get ttl => $_getIZ(1); - @$pb.TagNumber(2) - set ttl($core.int v) { - $_setUnsignedInt32(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasTtl() => $_has(1); - @$pb.TagNumber(2) - void clearTtl() => clearField(2); -} - -class ConnectRequest extends $pb.GeneratedMessage { - factory ConnectRequest() => create(); - ConnectRequest._() : super(); - factory ConnectRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ConnectRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ConnectRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'token') - ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..m<$core.String, SubscribeRequest>(3, _omitFieldNames ? '' : 'subs', - entryClassName: 'ConnectRequest.SubsEntry', - keyFieldType: $pb.PbFieldType.OS, - valueFieldType: $pb.PbFieldType.OM, - valueCreator: SubscribeRequest.create, - valueDefaultOrMaker: SubscribeRequest.getDefault, - packageName: const $pb.PackageName('centrifugal.centrifuge.protocol')) - ..aOS(4, _omitFieldNames ? '' : 'name') - ..aOS(5, _omitFieldNames ? '' : 'version') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - ConnectRequest clone() => ConnectRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ConnectRequest copyWith(void Function(ConnectRequest) updates) => - super.copyWith((message) => updates(message as ConnectRequest)) - as ConnectRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ConnectRequest create() => ConnectRequest._(); - ConnectRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static ConnectRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ConnectRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get token => $_getSZ(0); - @$pb.TagNumber(1) - set token($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasToken() => $_has(0); - @$pb.TagNumber(1) - void clearToken() => clearField(1); - - @$pb.TagNumber(2) - $core.List<$core.int> get data => $_getN(1); - @$pb.TagNumber(2) - set data($core.List<$core.int> v) { - $_setBytes(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasData() => $_has(1); - @$pb.TagNumber(2) - void clearData() => clearField(2); - - @$pb.TagNumber(3) - $core.Map<$core.String, SubscribeRequest> get subs => $_getMap(2); - - @$pb.TagNumber(4) - $core.String get name => $_getSZ(3); - @$pb.TagNumber(4) - set name($core.String v) { - $_setString(3, v); - } - - @$pb.TagNumber(4) - $core.bool hasName() => $_has(3); - @$pb.TagNumber(4) - void clearName() => clearField(4); - - @$pb.TagNumber(5) - $core.String get version => $_getSZ(4); - @$pb.TagNumber(5) - set version($core.String v) { - $_setString(4, v); - } - - @$pb.TagNumber(5) - $core.bool hasVersion() => $_has(4); - @$pb.TagNumber(5) - void clearVersion() => clearField(5); -} - -class ConnectResult extends $pb.GeneratedMessage { - factory ConnectResult() => create(); - ConnectResult._() : super(); - factory ConnectResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ConnectResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ConnectResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'client') - ..aOS(2, _omitFieldNames ? '' : 'version') - ..aOB(3, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(4, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..a<$core.List<$core.int>>( - 5, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..m<$core.String, SubscribeResult>(6, _omitFieldNames ? '' : 'subs', - entryClassName: 'ConnectResult.SubsEntry', - keyFieldType: $pb.PbFieldType.OS, - valueFieldType: $pb.PbFieldType.OM, - valueCreator: SubscribeResult.create, - valueDefaultOrMaker: SubscribeResult.getDefault, - packageName: const $pb.PackageName('centrifugal.centrifuge.protocol')) - ..a<$core.int>(7, _omitFieldNames ? '' : 'ping', $pb.PbFieldType.OU3) - ..aOB(8, _omitFieldNames ? '' : 'pong') - ..aOS(9, _omitFieldNames ? '' : 'session') - ..aOS(10, _omitFieldNames ? '' : 'node') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - ConnectResult clone() => ConnectResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ConnectResult copyWith(void Function(ConnectResult) updates) => - super.copyWith((message) => updates(message as ConnectResult)) - as ConnectResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ConnectResult create() => ConnectResult._(); - ConnectResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static ConnectResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ConnectResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get client => $_getSZ(0); - @$pb.TagNumber(1) - set client($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasClient() => $_has(0); - @$pb.TagNumber(1) - void clearClient() => clearField(1); - - @$pb.TagNumber(2) - $core.String get version => $_getSZ(1); - @$pb.TagNumber(2) - set version($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasVersion() => $_has(1); - @$pb.TagNumber(2) - void clearVersion() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get expires => $_getBF(2); - @$pb.TagNumber(3) - set expires($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasExpires() => $_has(2); - @$pb.TagNumber(3) - void clearExpires() => clearField(3); - - @$pb.TagNumber(4) - $core.int get ttl => $_getIZ(3); - @$pb.TagNumber(4) - set ttl($core.int v) { - $_setUnsignedInt32(3, v); - } - - @$pb.TagNumber(4) - $core.bool hasTtl() => $_has(3); - @$pb.TagNumber(4) - void clearTtl() => clearField(4); - - @$pb.TagNumber(5) - $core.List<$core.int> get data => $_getN(4); - @$pb.TagNumber(5) - set data($core.List<$core.int> v) { - $_setBytes(4, v); - } - - @$pb.TagNumber(5) - $core.bool hasData() => $_has(4); - @$pb.TagNumber(5) - void clearData() => clearField(5); - - @$pb.TagNumber(6) - $core.Map<$core.String, SubscribeResult> get subs => $_getMap(5); - - @$pb.TagNumber(7) - $core.int get ping => $_getIZ(6); - @$pb.TagNumber(7) - set ping($core.int v) { - $_setUnsignedInt32(6, v); - } - - @$pb.TagNumber(7) - $core.bool hasPing() => $_has(6); - @$pb.TagNumber(7) - void clearPing() => clearField(7); - - @$pb.TagNumber(8) - $core.bool get pong => $_getBF(7); - @$pb.TagNumber(8) - set pong($core.bool v) { - $_setBool(7, v); - } - - @$pb.TagNumber(8) - $core.bool hasPong() => $_has(7); - @$pb.TagNumber(8) - void clearPong() => clearField(8); - - @$pb.TagNumber(9) - $core.String get session => $_getSZ(8); - @$pb.TagNumber(9) - set session($core.String v) { - $_setString(8, v); - } - - @$pb.TagNumber(9) - $core.bool hasSession() => $_has(8); - @$pb.TagNumber(9) - void clearSession() => clearField(9); - - @$pb.TagNumber(10) - $core.String get node => $_getSZ(9); - @$pb.TagNumber(10) - set node($core.String v) { - $_setString(9, v); - } - - @$pb.TagNumber(10) - $core.bool hasNode() => $_has(9); - @$pb.TagNumber(10) - void clearNode() => clearField(10); -} - -class RefreshRequest extends $pb.GeneratedMessage { - factory RefreshRequest() => create(); - RefreshRequest._() : super(); - factory RefreshRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory RefreshRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'RefreshRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'token') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - RefreshRequest clone() => RefreshRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - RefreshRequest copyWith(void Function(RefreshRequest) updates) => - super.copyWith((message) => updates(message as RefreshRequest)) - as RefreshRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RefreshRequest create() => RefreshRequest._(); - RefreshRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RefreshRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static RefreshRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get token => $_getSZ(0); - @$pb.TagNumber(1) - set token($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasToken() => $_has(0); - @$pb.TagNumber(1) - void clearToken() => clearField(1); -} - -class RefreshResult extends $pb.GeneratedMessage { - factory RefreshResult() => create(); - RefreshResult._() : super(); - factory RefreshResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory RefreshResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'RefreshResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'client') - ..aOS(2, _omitFieldNames ? '' : 'version') - ..aOB(3, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(4, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - RefreshResult clone() => RefreshResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - RefreshResult copyWith(void Function(RefreshResult) updates) => - super.copyWith((message) => updates(message as RefreshResult)) - as RefreshResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RefreshResult create() => RefreshResult._(); - RefreshResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RefreshResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static RefreshResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get client => $_getSZ(0); - @$pb.TagNumber(1) - set client($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasClient() => $_has(0); - @$pb.TagNumber(1) - void clearClient() => clearField(1); - - @$pb.TagNumber(2) - $core.String get version => $_getSZ(1); - @$pb.TagNumber(2) - set version($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasVersion() => $_has(1); - @$pb.TagNumber(2) - void clearVersion() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get expires => $_getBF(2); - @$pb.TagNumber(3) - set expires($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasExpires() => $_has(2); - @$pb.TagNumber(3) - void clearExpires() => clearField(3); - - @$pb.TagNumber(4) - $core.int get ttl => $_getIZ(3); - @$pb.TagNumber(4) - set ttl($core.int v) { - $_setUnsignedInt32(3, v); - } - - @$pb.TagNumber(4) - $core.bool hasTtl() => $_has(3); - @$pb.TagNumber(4) - void clearTtl() => clearField(4); -} - -class SubscribeRequest extends $pb.GeneratedMessage { - factory SubscribeRequest() => create(); - SubscribeRequest._() : super(); - factory SubscribeRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SubscribeRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SubscribeRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..aOS(2, _omitFieldNames ? '' : 'token') - ..aOB(3, _omitFieldNames ? '' : 'recover') - ..aOS(6, _omitFieldNames ? '' : 'epoch') - ..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$core.List<$core.int>>( - 8, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..aOB(9, _omitFieldNames ? '' : 'positioned') - ..aOB(10, _omitFieldNames ? '' : 'recoverable') - ..aOB(11, _omitFieldNames ? '' : 'joinLeave') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - SubscribeRequest clone() => SubscribeRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SubscribeRequest copyWith(void Function(SubscribeRequest) updates) => - super.copyWith((message) => updates(message as SubscribeRequest)) - as SubscribeRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static SubscribeRequest create() => SubscribeRequest._(); - SubscribeRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SubscribeRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static SubscribeRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); - - @$pb.TagNumber(2) - $core.String get token => $_getSZ(1); - @$pb.TagNumber(2) - set token($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasToken() => $_has(1); - @$pb.TagNumber(2) - void clearToken() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get recover => $_getBF(2); - @$pb.TagNumber(3) - set recover($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasRecover() => $_has(2); - @$pb.TagNumber(3) - void clearRecover() => clearField(3); - - @$pb.TagNumber(6) - $core.String get epoch => $_getSZ(3); - @$pb.TagNumber(6) - set epoch($core.String v) { - $_setString(3, v); - } - - @$pb.TagNumber(6) - $core.bool hasEpoch() => $_has(3); - @$pb.TagNumber(6) - void clearEpoch() => clearField(6); - - @$pb.TagNumber(7) - $fixnum.Int64 get offset => $_getI64(4); - @$pb.TagNumber(7) - set offset($fixnum.Int64 v) { - $_setInt64(4, v); - } - - @$pb.TagNumber(7) - $core.bool hasOffset() => $_has(4); - @$pb.TagNumber(7) - void clearOffset() => clearField(7); - - @$pb.TagNumber(8) - $core.List<$core.int> get data => $_getN(5); - @$pb.TagNumber(8) - set data($core.List<$core.int> v) { - $_setBytes(5, v); - } - - @$pb.TagNumber(8) - $core.bool hasData() => $_has(5); - @$pb.TagNumber(8) - void clearData() => clearField(8); - - @$pb.TagNumber(9) - $core.bool get positioned => $_getBF(6); - @$pb.TagNumber(9) - set positioned($core.bool v) { - $_setBool(6, v); - } - - @$pb.TagNumber(9) - $core.bool hasPositioned() => $_has(6); - @$pb.TagNumber(9) - void clearPositioned() => clearField(9); - - @$pb.TagNumber(10) - $core.bool get recoverable => $_getBF(7); - @$pb.TagNumber(10) - set recoverable($core.bool v) { - $_setBool(7, v); - } - - @$pb.TagNumber(10) - $core.bool hasRecoverable() => $_has(7); - @$pb.TagNumber(10) - void clearRecoverable() => clearField(10); - - @$pb.TagNumber(11) - $core.bool get joinLeave => $_getBF(8); - @$pb.TagNumber(11) - set joinLeave($core.bool v) { - $_setBool(8, v); - } - - @$pb.TagNumber(11) - $core.bool hasJoinLeave() => $_has(8); - @$pb.TagNumber(11) - void clearJoinLeave() => clearField(11); -} - -class SubscribeResult extends $pb.GeneratedMessage { - factory SubscribeResult() => create(); - SubscribeResult._() : super(); - factory SubscribeResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SubscribeResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SubscribeResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOB(1, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(2, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..aOB(3, _omitFieldNames ? '' : 'recoverable') - ..aOS(6, _omitFieldNames ? '' : 'epoch') - ..pc( - 7, _omitFieldNames ? '' : 'publications', $pb.PbFieldType.PM, - subBuilder: Publication.create) - ..aOB(8, _omitFieldNames ? '' : 'recovered') - ..a<$fixnum.Int64>(9, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..aOB(10, _omitFieldNames ? '' : 'positioned') - ..a<$core.List<$core.int>>( - 11, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..aOB(12, _omitFieldNames ? '' : 'wasRecovering') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - SubscribeResult clone() => SubscribeResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SubscribeResult copyWith(void Function(SubscribeResult) updates) => - super.copyWith((message) => updates(message as SubscribeResult)) - as SubscribeResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static SubscribeResult create() => SubscribeResult._(); - SubscribeResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SubscribeResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static SubscribeResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.bool get expires => $_getBF(0); - @$pb.TagNumber(1) - set expires($core.bool v) { - $_setBool(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasExpires() => $_has(0); - @$pb.TagNumber(1) - void clearExpires() => clearField(1); - - @$pb.TagNumber(2) - $core.int get ttl => $_getIZ(1); - @$pb.TagNumber(2) - set ttl($core.int v) { - $_setUnsignedInt32(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasTtl() => $_has(1); - @$pb.TagNumber(2) - void clearTtl() => clearField(2); - - @$pb.TagNumber(3) - $core.bool get recoverable => $_getBF(2); - @$pb.TagNumber(3) - set recoverable($core.bool v) { - $_setBool(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasRecoverable() => $_has(2); - @$pb.TagNumber(3) - void clearRecoverable() => clearField(3); - - @$pb.TagNumber(6) - $core.String get epoch => $_getSZ(3); - @$pb.TagNumber(6) - set epoch($core.String v) { - $_setString(3, v); - } - - @$pb.TagNumber(6) - $core.bool hasEpoch() => $_has(3); - @$pb.TagNumber(6) - void clearEpoch() => clearField(6); - - @$pb.TagNumber(7) - $core.List get publications => $_getList(4); - - @$pb.TagNumber(8) - $core.bool get recovered => $_getBF(5); - @$pb.TagNumber(8) - set recovered($core.bool v) { - $_setBool(5, v); - } - - @$pb.TagNumber(8) - $core.bool hasRecovered() => $_has(5); - @$pb.TagNumber(8) - void clearRecovered() => clearField(8); - - @$pb.TagNumber(9) - $fixnum.Int64 get offset => $_getI64(6); - @$pb.TagNumber(9) - set offset($fixnum.Int64 v) { - $_setInt64(6, v); - } - - @$pb.TagNumber(9) - $core.bool hasOffset() => $_has(6); - @$pb.TagNumber(9) - void clearOffset() => clearField(9); - - @$pb.TagNumber(10) - $core.bool get positioned => $_getBF(7); - @$pb.TagNumber(10) - set positioned($core.bool v) { - $_setBool(7, v); - } - - @$pb.TagNumber(10) - $core.bool hasPositioned() => $_has(7); - @$pb.TagNumber(10) - void clearPositioned() => clearField(10); - - @$pb.TagNumber(11) - $core.List<$core.int> get data => $_getN(8); - @$pb.TagNumber(11) - set data($core.List<$core.int> v) { - $_setBytes(8, v); - } - - @$pb.TagNumber(11) - $core.bool hasData() => $_has(8); - @$pb.TagNumber(11) - void clearData() => clearField(11); - - @$pb.TagNumber(12) - $core.bool get wasRecovering => $_getBF(9); - @$pb.TagNumber(12) - set wasRecovering($core.bool v) { - $_setBool(9, v); - } - - @$pb.TagNumber(12) - $core.bool hasWasRecovering() => $_has(9); - @$pb.TagNumber(12) - void clearWasRecovering() => clearField(12); -} - -class SubRefreshRequest extends $pb.GeneratedMessage { - factory SubRefreshRequest() => create(); - SubRefreshRequest._() : super(); - factory SubRefreshRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SubRefreshRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SubRefreshRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..aOS(2, _omitFieldNames ? '' : 'token') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - SubRefreshRequest clone() => SubRefreshRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SubRefreshRequest copyWith(void Function(SubRefreshRequest) updates) => - super.copyWith((message) => updates(message as SubRefreshRequest)) - as SubRefreshRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static SubRefreshRequest create() => SubRefreshRequest._(); - SubRefreshRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SubRefreshRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static SubRefreshRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); - - @$pb.TagNumber(2) - $core.String get token => $_getSZ(1); - @$pb.TagNumber(2) - set token($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasToken() => $_has(1); - @$pb.TagNumber(2) - void clearToken() => clearField(2); -} - -class SubRefreshResult extends $pb.GeneratedMessage { - factory SubRefreshResult() => create(); - SubRefreshResult._() : super(); - factory SubRefreshResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SubRefreshResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SubRefreshResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOB(1, _omitFieldNames ? '' : 'expires') - ..a<$core.int>(2, _omitFieldNames ? '' : 'ttl', $pb.PbFieldType.OU3) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - SubRefreshResult clone() => SubRefreshResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SubRefreshResult copyWith(void Function(SubRefreshResult) updates) => - super.copyWith((message) => updates(message as SubRefreshResult)) - as SubRefreshResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static SubRefreshResult create() => SubRefreshResult._(); - SubRefreshResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SubRefreshResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static SubRefreshResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.bool get expires => $_getBF(0); - @$pb.TagNumber(1) - set expires($core.bool v) { - $_setBool(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasExpires() => $_has(0); - @$pb.TagNumber(1) - void clearExpires() => clearField(1); - - @$pb.TagNumber(2) - $core.int get ttl => $_getIZ(1); - @$pb.TagNumber(2) - set ttl($core.int v) { - $_setUnsignedInt32(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasTtl() => $_has(1); - @$pb.TagNumber(2) - void clearTtl() => clearField(2); -} - -class UnsubscribeRequest extends $pb.GeneratedMessage { - factory UnsubscribeRequest() => create(); - UnsubscribeRequest._() : super(); - factory UnsubscribeRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory UnsubscribeRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'UnsubscribeRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - UnsubscribeRequest clone() => UnsubscribeRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - UnsubscribeRequest copyWith(void Function(UnsubscribeRequest) updates) => - super.copyWith((message) => updates(message as UnsubscribeRequest)) - as UnsubscribeRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static UnsubscribeRequest create() => UnsubscribeRequest._(); - UnsubscribeRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static UnsubscribeRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static UnsubscribeRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); -} - -class UnsubscribeResult extends $pb.GeneratedMessage { - factory UnsubscribeResult() => create(); - UnsubscribeResult._() : super(); - factory UnsubscribeResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory UnsubscribeResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'UnsubscribeResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - UnsubscribeResult clone() => UnsubscribeResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - UnsubscribeResult copyWith(void Function(UnsubscribeResult) updates) => - super.copyWith((message) => updates(message as UnsubscribeResult)) - as UnsubscribeResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static UnsubscribeResult create() => UnsubscribeResult._(); - UnsubscribeResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static UnsubscribeResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static UnsubscribeResult? _defaultInstance; -} - -class PublishRequest extends $pb.GeneratedMessage { - factory PublishRequest() => create(); - PublishRequest._() : super(); - factory PublishRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PublishRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PublishRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PublishRequest clone() => PublishRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PublishRequest copyWith(void Function(PublishRequest) updates) => - super.copyWith((message) => updates(message as PublishRequest)) - as PublishRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PublishRequest create() => PublishRequest._(); - PublishRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PublishRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PublishRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); - - @$pb.TagNumber(2) - $core.List<$core.int> get data => $_getN(1); - @$pb.TagNumber(2) - set data($core.List<$core.int> v) { - $_setBytes(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasData() => $_has(1); - @$pb.TagNumber(2) - void clearData() => clearField(2); -} - -class PublishResult extends $pb.GeneratedMessage { - factory PublishResult() => create(); - PublishResult._() : super(); - factory PublishResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PublishResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PublishResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PublishResult clone() => PublishResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PublishResult copyWith(void Function(PublishResult) updates) => - super.copyWith((message) => updates(message as PublishResult)) - as PublishResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PublishResult create() => PublishResult._(); - PublishResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PublishResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PublishResult? _defaultInstance; -} - -class PresenceRequest extends $pb.GeneratedMessage { - factory PresenceRequest() => create(); - PresenceRequest._() : super(); - factory PresenceRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PresenceRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PresenceRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PresenceRequest clone() => PresenceRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PresenceRequest copyWith(void Function(PresenceRequest) updates) => - super.copyWith((message) => updates(message as PresenceRequest)) - as PresenceRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PresenceRequest create() => PresenceRequest._(); - PresenceRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PresenceRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PresenceRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); -} - -class PresenceResult extends $pb.GeneratedMessage { - factory PresenceResult() => create(); - PresenceResult._() : super(); - factory PresenceResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PresenceResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PresenceResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..m<$core.String, ClientInfo>(1, _omitFieldNames ? '' : 'presence', - entryClassName: 'PresenceResult.PresenceEntry', - keyFieldType: $pb.PbFieldType.OS, - valueFieldType: $pb.PbFieldType.OM, - valueCreator: ClientInfo.create, - valueDefaultOrMaker: ClientInfo.getDefault, - packageName: const $pb.PackageName('centrifugal.centrifuge.protocol')) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PresenceResult clone() => PresenceResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PresenceResult copyWith(void Function(PresenceResult) updates) => - super.copyWith((message) => updates(message as PresenceResult)) - as PresenceResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PresenceResult create() => PresenceResult._(); - PresenceResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PresenceResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PresenceResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.Map<$core.String, ClientInfo> get presence => $_getMap(0); -} - -class PresenceStatsRequest extends $pb.GeneratedMessage { - factory PresenceStatsRequest() => create(); - PresenceStatsRequest._() : super(); - factory PresenceStatsRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PresenceStatsRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PresenceStatsRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PresenceStatsRequest clone() => - PresenceStatsRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PresenceStatsRequest copyWith(void Function(PresenceStatsRequest) updates) => - super.copyWith((message) => updates(message as PresenceStatsRequest)) - as PresenceStatsRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PresenceStatsRequest create() => PresenceStatsRequest._(); - PresenceStatsRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PresenceStatsRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PresenceStatsRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); -} - -class PresenceStatsResult extends $pb.GeneratedMessage { - factory PresenceStatsResult() => create(); - PresenceStatsResult._() : super(); - factory PresenceStatsResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PresenceStatsResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PresenceStatsResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'numClients', $pb.PbFieldType.OU3) - ..a<$core.int>(2, _omitFieldNames ? '' : 'numUsers', $pb.PbFieldType.OU3) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PresenceStatsResult clone() => PresenceStatsResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PresenceStatsResult copyWith(void Function(PresenceStatsResult) updates) => - super.copyWith((message) => updates(message as PresenceStatsResult)) - as PresenceStatsResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PresenceStatsResult create() => PresenceStatsResult._(); - PresenceStatsResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PresenceStatsResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PresenceStatsResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get numClients => $_getIZ(0); - @$pb.TagNumber(1) - set numClients($core.int v) { - $_setUnsignedInt32(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasNumClients() => $_has(0); - @$pb.TagNumber(1) - void clearNumClients() => clearField(1); - - @$pb.TagNumber(2) - $core.int get numUsers => $_getIZ(1); - @$pb.TagNumber(2) - set numUsers($core.int v) { - $_setUnsignedInt32(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasNumUsers() => $_has(1); - @$pb.TagNumber(2) - void clearNumUsers() => clearField(2); -} - -class StreamPosition extends $pb.GeneratedMessage { - factory StreamPosition() => create(); - StreamPosition._() : super(); - factory StreamPosition.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory StreamPosition.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'StreamPosition', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..aOS(2, _omitFieldNames ? '' : 'epoch') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - StreamPosition clone() => StreamPosition()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - StreamPosition copyWith(void Function(StreamPosition) updates) => - super.copyWith((message) => updates(message as StreamPosition)) - as StreamPosition; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static StreamPosition create() => StreamPosition._(); - StreamPosition createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static StreamPosition getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static StreamPosition? _defaultInstance; - - @$pb.TagNumber(1) - $fixnum.Int64 get offset => $_getI64(0); - @$pb.TagNumber(1) - set offset($fixnum.Int64 v) { - $_setInt64(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasOffset() => $_has(0); - @$pb.TagNumber(1) - void clearOffset() => clearField(1); - - @$pb.TagNumber(2) - $core.String get epoch => $_getSZ(1); - @$pb.TagNumber(2) - set epoch($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasEpoch() => $_has(1); - @$pb.TagNumber(2) - void clearEpoch() => clearField(2); -} - -class HistoryRequest extends $pb.GeneratedMessage { - factory HistoryRequest() => create(); - HistoryRequest._() : super(); - factory HistoryRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory HistoryRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'HistoryRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'channel') - ..a<$core.int>(7, _omitFieldNames ? '' : 'limit', $pb.PbFieldType.O3) - ..aOM(8, _omitFieldNames ? '' : 'since', - subBuilder: StreamPosition.create) - ..aOB(9, _omitFieldNames ? '' : 'reverse') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - HistoryRequest clone() => HistoryRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - HistoryRequest copyWith(void Function(HistoryRequest) updates) => - super.copyWith((message) => updates(message as HistoryRequest)) - as HistoryRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static HistoryRequest create() => HistoryRequest._(); - HistoryRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static HistoryRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static HistoryRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get channel => $_getSZ(0); - @$pb.TagNumber(1) - set channel($core.String v) { - $_setString(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasChannel() => $_has(0); - @$pb.TagNumber(1) - void clearChannel() => clearField(1); - - @$pb.TagNumber(7) - $core.int get limit => $_getIZ(1); - @$pb.TagNumber(7) - set limit($core.int v) { - $_setSignedInt32(1, v); - } - - @$pb.TagNumber(7) - $core.bool hasLimit() => $_has(1); - @$pb.TagNumber(7) - void clearLimit() => clearField(7); - - @$pb.TagNumber(8) - StreamPosition get since => $_getN(2); - @$pb.TagNumber(8) - set since(StreamPosition v) { - setField(8, v); - } - - @$pb.TagNumber(8) - $core.bool hasSince() => $_has(2); - @$pb.TagNumber(8) - void clearSince() => clearField(8); - @$pb.TagNumber(8) - StreamPosition ensureSince() => $_ensure(2); - - @$pb.TagNumber(9) - $core.bool get reverse => $_getBF(3); - @$pb.TagNumber(9) - set reverse($core.bool v) { - $_setBool(3, v); - } - - @$pb.TagNumber(9) - $core.bool hasReverse() => $_has(3); - @$pb.TagNumber(9) - void clearReverse() => clearField(9); -} - -class HistoryResult extends $pb.GeneratedMessage { - factory HistoryResult() => create(); - HistoryResult._() : super(); - factory HistoryResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory HistoryResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'HistoryResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..pc( - 1, _omitFieldNames ? '' : 'publications', $pb.PbFieldType.PM, - subBuilder: Publication.create) - ..aOS(2, _omitFieldNames ? '' : 'epoch') - ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - HistoryResult clone() => HistoryResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - HistoryResult copyWith(void Function(HistoryResult) updates) => - super.copyWith((message) => updates(message as HistoryResult)) - as HistoryResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static HistoryResult create() => HistoryResult._(); - HistoryResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); - @$core.pragma('dart2js:noInline') - static HistoryResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static HistoryResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.List get publications => $_getList(0); - - @$pb.TagNumber(2) - $core.String get epoch => $_getSZ(1); - @$pb.TagNumber(2) - set epoch($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasEpoch() => $_has(1); - @$pb.TagNumber(2) - void clearEpoch() => clearField(2); - - @$pb.TagNumber(3) - $fixnum.Int64 get offset => $_getI64(2); - @$pb.TagNumber(3) - set offset($fixnum.Int64 v) { - $_setInt64(2, v); - } - - @$pb.TagNumber(3) - $core.bool hasOffset() => $_has(2); - @$pb.TagNumber(3) - void clearOffset() => clearField(3); -} - -class PingRequest extends $pb.GeneratedMessage { - factory PingRequest() => create(); - PingRequest._() : super(); - factory PingRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PingRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PingRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PingRequest clone() => PingRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PingRequest copyWith(void Function(PingRequest) updates) => - super.copyWith((message) => updates(message as PingRequest)) - as PingRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PingRequest create() => PingRequest._(); - PingRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PingRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PingRequest? _defaultInstance; -} - -class PingResult extends $pb.GeneratedMessage { - factory PingResult() => create(); - PingResult._() : super(); - factory PingResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory PingResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'PingResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - PingResult clone() => PingResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - PingResult copyWith(void Function(PingResult) updates) => - super.copyWith((message) => updates(message as PingResult)) as PingResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static PingResult create() => PingResult._(); - PingResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static PingResult getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static PingResult? _defaultInstance; -} - -class RPCRequest extends $pb.GeneratedMessage { - factory RPCRequest() => create(); - RPCRequest._() : super(); - factory RPCRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory RPCRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'RPCRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..aOS(2, _omitFieldNames ? '' : 'method') - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - RPCRequest clone() => RPCRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - RPCRequest copyWith(void Function(RPCRequest) updates) => - super.copyWith((message) => updates(message as RPCRequest)) as RPCRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RPCRequest create() => RPCRequest._(); - RPCRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RPCRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static RPCRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.List<$core.int> get data => $_getN(0); - @$pb.TagNumber(1) - set data($core.List<$core.int> v) { - $_setBytes(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasData() => $_has(0); - @$pb.TagNumber(1) - void clearData() => clearField(1); - - @$pb.TagNumber(2) - $core.String get method => $_getSZ(1); - @$pb.TagNumber(2) - set method($core.String v) { - $_setString(1, v); - } - - @$pb.TagNumber(2) - $core.bool hasMethod() => $_has(1); - @$pb.TagNumber(2) - void clearMethod() => clearField(2); -} - -class RPCResult extends $pb.GeneratedMessage { - factory RPCResult() => create(); - RPCResult._() : super(); - factory RPCResult.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory RPCResult.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'RPCResult', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - RPCResult clone() => RPCResult()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - RPCResult copyWith(void Function(RPCResult) updates) => - super.copyWith((message) => updates(message as RPCResult)) as RPCResult; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RPCResult create() => RPCResult._(); - RPCResult createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RPCResult getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static RPCResult? _defaultInstance; - - @$pb.TagNumber(1) - $core.List<$core.int> get data => $_getN(0); - @$pb.TagNumber(1) - set data($core.List<$core.int> v) { - $_setBytes(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasData() => $_has(0); - @$pb.TagNumber(1) - void clearData() => clearField(1); -} - -class SendRequest extends $pb.GeneratedMessage { - factory SendRequest() => create(); - SendRequest._() : super(); - factory SendRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SendRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SendRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'centrifugal.centrifuge.protocol'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - SendRequest clone() => SendRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SendRequest copyWith(void Function(SendRequest) updates) => - super.copyWith((message) => updates(message as SendRequest)) - as SendRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static SendRequest create() => SendRequest._(); - SendRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SendRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static SendRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.List<$core.int> get data => $_getN(0); - @$pb.TagNumber(1) - set data($core.List<$core.int> v) { - $_setBytes(0, v); - } - - @$pb.TagNumber(1) - $core.bool hasData() => $_has(0); - @$pb.TagNumber(1) - void clearData() => clearField(1); -} - -const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); -const _omitMessageNames = - $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src.old/transport/protobuf/client.pbenum.dart b/lib/src.old/transport/protobuf/client.pbenum.dart deleted file mode 100644 index 84b2da2..0000000 --- a/lib/src.old/transport/protobuf/client.pbenum.dart +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated code. Do not modify. -// source: client.proto -// -// @dart = 2.12 - -// ignore_for_file: annotate_overrides, camel_case_types -// ignore_for_file: constant_identifier_names, library_prefixes -// ignore_for_file: non_constant_identifier_names, prefer_final_fields -// ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/lib/src.old/transport/protobuf/client.pbjson.dart b/lib/src.old/transport/protobuf/client.pbjson.dart deleted file mode 100644 index 877e34d..0000000 --- a/lib/src.old/transport/protobuf/client.pbjson.dart +++ /dev/null @@ -1,1116 +0,0 @@ -// -// Generated code. Do not modify. -// source: client.proto -// -// @dart = 2.12 - -// ignore_for_file: annotate_overrides, camel_case_types -// ignore_for_file: constant_identifier_names, library_prefixes -// ignore_for_file: non_constant_identifier_names, prefer_final_fields -// ignore_for_file: unnecessary_import, unnecessary_this, unused_import - -import 'dart:convert' as $convert; -import 'dart:core' as $core; -import 'dart:typed_data' as $typed_data; - -@$core.Deprecated('Use errorDescriptor instead') -const Error$json = { - '1': 'Error', - '2': [ - {'1': 'code', '3': 1, '4': 1, '5': 13, '10': 'code'}, - {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, - {'1': 'temporary', '3': 3, '4': 1, '5': 8, '10': 'temporary'}, - ], -}; - -/// Descriptor for `Error`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List errorDescriptor = $convert.base64Decode( - 'CgVFcnJvchISCgRjb2RlGAEgASgNUgRjb2RlEhgKB21lc3NhZ2UYAiABKAlSB21lc3NhZ2USHA' - 'oJdGVtcG9yYXJ5GAMgASgIUgl0ZW1wb3Jhcnk='); - -@$core.Deprecated('Use emulationRequestDescriptor instead') -const EmulationRequest$json = { - '1': 'EmulationRequest', - '2': [ - {'1': 'node', '3': 1, '4': 1, '5': 9, '10': 'node'}, - {'1': 'session', '3': 2, '4': 1, '5': 9, '10': 'session'}, - {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, - ], -}; - -/// Descriptor for `EmulationRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List emulationRequestDescriptor = $convert.base64Decode( - 'ChBFbXVsYXRpb25SZXF1ZXN0EhIKBG5vZGUYASABKAlSBG5vZGUSGAoHc2Vzc2lvbhgCIAEoCV' - 'IHc2Vzc2lvbhISCgRkYXRhGAMgASgMUgRkYXRh'); - -@$core.Deprecated('Use commandDescriptor instead') -const Command$json = { - '1': 'Command', - '2': [ - {'1': 'id', '3': 1, '4': 1, '5': 13, '10': 'id'}, - { - '1': 'connect', - '3': 4, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ConnectRequest', - '10': 'connect' - }, - { - '1': 'subscribe', - '3': 5, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubscribeRequest', - '10': 'subscribe' - }, - { - '1': 'unsubscribe', - '3': 6, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.UnsubscribeRequest', - '10': 'unsubscribe' - }, - { - '1': 'publish', - '3': 7, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PublishRequest', - '10': 'publish' - }, - { - '1': 'presence', - '3': 8, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PresenceRequest', - '10': 'presence' - }, - { - '1': 'presence_stats', - '3': 9, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PresenceStatsRequest', - '10': 'presenceStats' - }, - { - '1': 'history', - '3': 10, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.HistoryRequest', - '10': 'history' - }, - { - '1': 'ping', - '3': 11, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PingRequest', - '10': 'ping' - }, - { - '1': 'send', - '3': 12, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SendRequest', - '10': 'send' - }, - { - '1': 'rpc', - '3': 13, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.RPCRequest', - '10': 'rpc' - }, - { - '1': 'refresh', - '3': 14, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.RefreshRequest', - '10': 'refresh' - }, - { - '1': 'sub_refresh', - '3': 15, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubRefreshRequest', - '10': 'subRefresh' - }, - ], - '9': [ - {'1': 2, '2': 3}, - {'1': 3, '2': 4}, - ], -}; - -/// Descriptor for `Command`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List commandDescriptor = $convert.base64Decode( - 'CgdDb21tYW5kEg4KAmlkGAEgASgNUgJpZBJJCgdjb25uZWN0GAQgASgLMi8uY2VudHJpZnVnYW' - 'wuY2VudHJpZnVnZS5wcm90b2NvbC5Db25uZWN0UmVxdWVzdFIHY29ubmVjdBJPCglzdWJzY3Jp' - 'YmUYBSABKAsyMS5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLlN1YnNjcmliZVJlcX' - 'Vlc3RSCXN1YnNjcmliZRJVCgt1bnN1YnNjcmliZRgGIAEoCzIzLmNlbnRyaWZ1Z2FsLmNlbnRy' - 'aWZ1Z2UucHJvdG9jb2wuVW5zdWJzY3JpYmVSZXF1ZXN0Ugt1bnN1YnNjcmliZRJJCgdwdWJsaX' - 'NoGAcgASgLMi8uY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5QdWJsaXNoUmVxdWVz' - 'dFIHcHVibGlzaBJMCghwcmVzZW5jZRgIIAEoCzIwLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucH' - 'JvdG9jb2wuUHJlc2VuY2VSZXF1ZXN0UghwcmVzZW5jZRJcCg5wcmVzZW5jZV9zdGF0cxgJIAEo' - 'CzI1LmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuUHJlc2VuY2VTdGF0c1JlcXVlc3' - 'RSDXByZXNlbmNlU3RhdHMSSQoHaGlzdG9yeRgKIAEoCzIvLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1' - 'Z2UucHJvdG9jb2wuSGlzdG9yeVJlcXVlc3RSB2hpc3RvcnkSQAoEcGluZxgLIAEoCzIsLmNlbn' - 'RyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuUGluZ1JlcXVlc3RSBHBpbmcSQAoEc2VuZBgM' - 'IAEoCzIsLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuU2VuZFJlcXVlc3RSBHNlbm' - 'QSPQoDcnBjGA0gASgLMisuY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5SUENSZXF1' - 'ZXN0UgNycGMSSQoHcmVmcmVzaBgOIAEoCzIvLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG' - '9jb2wuUmVmcmVzaFJlcXVlc3RSB3JlZnJlc2gSUwoLc3ViX3JlZnJlc2gYDyABKAsyMi5jZW50' - 'cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLlN1YlJlZnJlc2hSZXF1ZXN0UgpzdWJSZWZyZX' - 'NoSgQIAhADSgQIAxAE'); - -@$core.Deprecated('Use replyDescriptor instead') -const Reply$json = { - '1': 'Reply', - '2': [ - {'1': 'id', '3': 1, '4': 1, '5': 13, '10': 'id'}, - { - '1': 'error', - '3': 2, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Error', - '10': 'error' - }, - { - '1': 'push', - '3': 4, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Push', - '10': 'push' - }, - { - '1': 'connect', - '3': 5, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ConnectResult', - '10': 'connect' - }, - { - '1': 'subscribe', - '3': 6, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubscribeResult', - '10': 'subscribe' - }, - { - '1': 'unsubscribe', - '3': 7, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.UnsubscribeResult', - '10': 'unsubscribe' - }, - { - '1': 'publish', - '3': 8, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PublishResult', - '10': 'publish' - }, - { - '1': 'presence', - '3': 9, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PresenceResult', - '10': 'presence' - }, - { - '1': 'presence_stats', - '3': 10, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PresenceStatsResult', - '10': 'presenceStats' - }, - { - '1': 'history', - '3': 11, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.HistoryResult', - '10': 'history' - }, - { - '1': 'ping', - '3': 12, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PingResult', - '10': 'ping' - }, - { - '1': 'rpc', - '3': 13, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.RPCResult', - '10': 'rpc' - }, - { - '1': 'refresh', - '3': 14, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.RefreshResult', - '10': 'refresh' - }, - { - '1': 'sub_refresh', - '3': 15, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubRefreshResult', - '10': 'subRefresh' - }, - ], - '9': [ - {'1': 3, '2': 4}, - ], -}; - -/// Descriptor for `Reply`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List replyDescriptor = $convert.base64Decode( - 'CgVSZXBseRIOCgJpZBgBIAEoDVICaWQSPAoFZXJyb3IYAiABKAsyJi5jZW50cmlmdWdhbC5jZW' - '50cmlmdWdlLnByb3RvY29sLkVycm9yUgVlcnJvchI5CgRwdXNoGAQgASgLMiUuY2VudHJpZnVn' - 'YWwuY2VudHJpZnVnZS5wcm90b2NvbC5QdXNoUgRwdXNoEkgKB2Nvbm5lY3QYBSABKAsyLi5jZW' - '50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLkNvbm5lY3RSZXN1bHRSB2Nvbm5lY3QSTgoJ' - 'c3Vic2NyaWJlGAYgASgLMjAuY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5TdWJzY3' - 'JpYmVSZXN1bHRSCXN1YnNjcmliZRJUCgt1bnN1YnNjcmliZRgHIAEoCzIyLmNlbnRyaWZ1Z2Fs' - 'LmNlbnRyaWZ1Z2UucHJvdG9jb2wuVW5zdWJzY3JpYmVSZXN1bHRSC3Vuc3Vic2NyaWJlEkgKB3' - 'B1Ymxpc2gYCCABKAsyLi5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLlB1Ymxpc2hS' - 'ZXN1bHRSB3B1Ymxpc2gSSwoIcHJlc2VuY2UYCSABKAsyLy5jZW50cmlmdWdhbC5jZW50cmlmdW' - 'dlLnByb3RvY29sLlByZXNlbmNlUmVzdWx0UghwcmVzZW5jZRJbCg5wcmVzZW5jZV9zdGF0cxgK' - 'IAEoCzI0LmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuUHJlc2VuY2VTdGF0c1Jlc3' - 'VsdFINcHJlc2VuY2VTdGF0cxJICgdoaXN0b3J5GAsgASgLMi4uY2VudHJpZnVnYWwuY2VudHJp' - 'ZnVnZS5wcm90b2NvbC5IaXN0b3J5UmVzdWx0UgdoaXN0b3J5Ej8KBHBpbmcYDCABKAsyKy5jZW' - '50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLlBpbmdSZXN1bHRSBHBpbmcSPAoDcnBjGA0g' - 'ASgLMiouY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5SUENSZXN1bHRSA3JwYxJICg' - 'dyZWZyZXNoGA4gASgLMi4uY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5SZWZyZXNo' - 'UmVzdWx0UgdyZWZyZXNoElIKC3N1Yl9yZWZyZXNoGA8gASgLMjEuY2VudHJpZnVnYWwuY2VudH' - 'JpZnVnZS5wcm90b2NvbC5TdWJSZWZyZXNoUmVzdWx0UgpzdWJSZWZyZXNoSgQIAxAE'); - -@$core.Deprecated('Use pushDescriptor instead') -const Push$json = { - '1': 'Push', - '2': [ - {'1': 'channel', '3': 2, '4': 1, '5': 9, '10': 'channel'}, - { - '1': 'pub', - '3': 4, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Publication', - '10': 'pub' - }, - { - '1': 'join', - '3': 5, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Join', - '10': 'join' - }, - { - '1': 'leave', - '3': 6, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Leave', - '10': 'leave' - }, - { - '1': 'unsubscribe', - '3': 7, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Unsubscribe', - '10': 'unsubscribe' - }, - { - '1': 'message', - '3': 8, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Message', - '10': 'message' - }, - { - '1': 'subscribe', - '3': 9, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Subscribe', - '10': 'subscribe' - }, - { - '1': 'connect', - '3': 10, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Connect', - '10': 'connect' - }, - { - '1': 'disconnect', - '3': 11, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Disconnect', - '10': 'disconnect' - }, - { - '1': 'refresh', - '3': 12, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Refresh', - '10': 'refresh' - }, - ], - '9': [ - {'1': 1, '2': 2}, - {'1': 3, '2': 4}, - ], -}; - -/// Descriptor for `Push`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List pushDescriptor = $convert.base64Decode( - 'CgRQdXNoEhgKB2NoYW5uZWwYAiABKAlSB2NoYW5uZWwSPgoDcHViGAQgASgLMiwuY2VudHJpZn' - 'VnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5QdWJsaWNhdGlvblIDcHViEjkKBGpvaW4YBSABKAsy' - 'JS5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLkpvaW5SBGpvaW4SPAoFbGVhdmUYBi' - 'ABKAsyJi5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLkxlYXZlUgVsZWF2ZRJOCgt1' - 'bnN1YnNjcmliZRgHIAEoCzIsLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuVW5zdW' - 'JzY3JpYmVSC3Vuc3Vic2NyaWJlEkIKB21lc3NhZ2UYCCABKAsyKC5jZW50cmlmdWdhbC5jZW50' - 'cmlmdWdlLnByb3RvY29sLk1lc3NhZ2VSB21lc3NhZ2USSAoJc3Vic2NyaWJlGAkgASgLMiouY2' - 'VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5TdWJzY3JpYmVSCXN1YnNjcmliZRJCCgdj' - 'b25uZWN0GAogASgLMiguY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5Db25uZWN0Ug' - 'djb25uZWN0EksKCmRpc2Nvbm5lY3QYCyABKAsyKy5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnBy' - 'b3RvY29sLkRpc2Nvbm5lY3RSCmRpc2Nvbm5lY3QSQgoHcmVmcmVzaBgMIAEoCzIoLmNlbnRyaW' - 'Z1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuUmVmcmVzaFIHcmVmcmVzaEoECAEQAkoECAMQBA=='); - -@$core.Deprecated('Use clientInfoDescriptor instead') -const ClientInfo$json = { - '1': 'ClientInfo', - '2': [ - {'1': 'user', '3': 1, '4': 1, '5': 9, '10': 'user'}, - {'1': 'client', '3': 2, '4': 1, '5': 9, '10': 'client'}, - {'1': 'conn_info', '3': 3, '4': 1, '5': 12, '10': 'connInfo'}, - {'1': 'chan_info', '3': 4, '4': 1, '5': 12, '10': 'chanInfo'}, - ], -}; - -/// Descriptor for `ClientInfo`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List clientInfoDescriptor = $convert.base64Decode( - 'CgpDbGllbnRJbmZvEhIKBHVzZXIYASABKAlSBHVzZXISFgoGY2xpZW50GAIgASgJUgZjbGllbn' - 'QSGwoJY29ubl9pbmZvGAMgASgMUghjb25uSW5mbxIbCgljaGFuX2luZm8YBCABKAxSCGNoYW5J' - 'bmZv'); - -@$core.Deprecated('Use publicationDescriptor instead') -const Publication$json = { - '1': 'Publication', - '2': [ - {'1': 'data', '3': 4, '4': 1, '5': 12, '10': 'data'}, - { - '1': 'info', - '3': 5, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ClientInfo', - '10': 'info' - }, - {'1': 'offset', '3': 6, '4': 1, '5': 4, '10': 'offset'}, - { - '1': 'tags', - '3': 7, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Publication.TagsEntry', - '10': 'tags' - }, - ], - '3': [Publication_TagsEntry$json], - '9': [ - {'1': 1, '2': 2}, - {'1': 2, '2': 3}, - {'1': 3, '2': 4}, - ], -}; - -@$core.Deprecated('Use publicationDescriptor instead') -const Publication_TagsEntry$json = { - '1': 'TagsEntry', - '2': [ - {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, - {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, - ], - '7': {'7': true}, -}; - -/// Descriptor for `Publication`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List publicationDescriptor = $convert.base64Decode( - 'CgtQdWJsaWNhdGlvbhISCgRkYXRhGAQgASgMUgRkYXRhEj8KBGluZm8YBSABKAsyKy5jZW50cm' - 'lmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLkNsaWVudEluZm9SBGluZm8SFgoGb2Zmc2V0GAYg' - 'ASgEUgZvZmZzZXQSSgoEdGFncxgHIAMoCzI2LmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG' - '9jb2wuUHVibGljYXRpb24uVGFnc0VudHJ5UgR0YWdzGjcKCVRhZ3NFbnRyeRIQCgNrZXkYASAB' - 'KAlSA2tleRIUCgV2YWx1ZRgCIAEoCVIFdmFsdWU6AjgBSgQIARACSgQIAhADSgQIAxAE'); - -@$core.Deprecated('Use joinDescriptor instead') -const Join$json = { - '1': 'Join', - '2': [ - { - '1': 'info', - '3': 1, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ClientInfo', - '10': 'info' - }, - ], -}; - -/// Descriptor for `Join`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List joinDescriptor = $convert.base64Decode( - 'CgRKb2luEj8KBGluZm8YASABKAsyKy5jZW50cmlmdWdhbC5jZW50cmlmdWdlLnByb3RvY29sLk' - 'NsaWVudEluZm9SBGluZm8='); - -@$core.Deprecated('Use leaveDescriptor instead') -const Leave$json = { - '1': 'Leave', - '2': [ - { - '1': 'info', - '3': 1, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ClientInfo', - '10': 'info' - }, - ], -}; - -/// Descriptor for `Leave`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List leaveDescriptor = $convert.base64Decode( - 'CgVMZWF2ZRI/CgRpbmZvGAEgASgLMisuY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC' - '5DbGllbnRJbmZvUgRpbmZv'); - -@$core.Deprecated('Use unsubscribeDescriptor instead') -const Unsubscribe$json = { - '1': 'Unsubscribe', - '2': [ - {'1': 'code', '3': 2, '4': 1, '5': 13, '10': 'code'}, - {'1': 'reason', '3': 3, '4': 1, '5': 9, '10': 'reason'}, - ], - '9': [ - {'1': 1, '2': 2}, - ], -}; - -/// Descriptor for `Unsubscribe`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List unsubscribeDescriptor = $convert.base64Decode( - 'CgtVbnN1YnNjcmliZRISCgRjb2RlGAIgASgNUgRjb2RlEhYKBnJlYXNvbhgDIAEoCVIGcmVhc2' - '9uSgQIARAC'); - -@$core.Deprecated('Use subscribeDescriptor instead') -const Subscribe$json = { - '1': 'Subscribe', - '2': [ - {'1': 'recoverable', '3': 1, '4': 1, '5': 8, '10': 'recoverable'}, - {'1': 'epoch', '3': 4, '4': 1, '5': 9, '10': 'epoch'}, - {'1': 'offset', '3': 5, '4': 1, '5': 4, '10': 'offset'}, - {'1': 'positioned', '3': 6, '4': 1, '5': 8, '10': 'positioned'}, - {'1': 'data', '3': 7, '4': 1, '5': 12, '10': 'data'}, - ], - '9': [ - {'1': 2, '2': 3}, - {'1': 3, '2': 4}, - ], -}; - -/// Descriptor for `Subscribe`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List subscribeDescriptor = $convert.base64Decode( - 'CglTdWJzY3JpYmUSIAoLcmVjb3ZlcmFibGUYASABKAhSC3JlY292ZXJhYmxlEhQKBWVwb2NoGA' - 'QgASgJUgVlcG9jaBIWCgZvZmZzZXQYBSABKARSBm9mZnNldBIeCgpwb3NpdGlvbmVkGAYgASgI' - 'Ugpwb3NpdGlvbmVkEhIKBGRhdGEYByABKAxSBGRhdGFKBAgCEANKBAgDEAQ='); - -@$core.Deprecated('Use messageDescriptor instead') -const Message$json = { - '1': 'Message', - '2': [ - {'1': 'data', '3': 1, '4': 1, '5': 12, '10': 'data'}, - ], -}; - -/// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List messageDescriptor = - $convert.base64Decode('CgdNZXNzYWdlEhIKBGRhdGEYASABKAxSBGRhdGE='); - -@$core.Deprecated('Use connectDescriptor instead') -const Connect$json = { - '1': 'Connect', - '2': [ - {'1': 'client', '3': 1, '4': 1, '5': 9, '10': 'client'}, - {'1': 'version', '3': 2, '4': 1, '5': 9, '10': 'version'}, - {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, - { - '1': 'subs', - '3': 4, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Connect.SubsEntry', - '10': 'subs' - }, - {'1': 'expires', '3': 5, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 6, '4': 1, '5': 13, '10': 'ttl'}, - {'1': 'ping', '3': 7, '4': 1, '5': 13, '10': 'ping'}, - {'1': 'pong', '3': 8, '4': 1, '5': 8, '10': 'pong'}, - {'1': 'session', '3': 9, '4': 1, '5': 9, '10': 'session'}, - {'1': 'node', '3': 10, '4': 1, '5': 9, '10': 'node'}, - ], - '3': [Connect_SubsEntry$json], -}; - -@$core.Deprecated('Use connectDescriptor instead') -const Connect_SubsEntry$json = { - '1': 'SubsEntry', - '2': [ - {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, - { - '1': 'value', - '3': 2, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubscribeResult', - '10': 'value' - }, - ], - '7': {'7': true}, -}; - -/// Descriptor for `Connect`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List connectDescriptor = $convert.base64Decode( - 'CgdDb25uZWN0EhYKBmNsaWVudBgBIAEoCVIGY2xpZW50EhgKB3ZlcnNpb24YAiABKAlSB3Zlcn' - 'Npb24SEgoEZGF0YRgDIAEoDFIEZGF0YRJGCgRzdWJzGAQgAygLMjIuY2VudHJpZnVnYWwuY2Vu' - 'dHJpZnVnZS5wcm90b2NvbC5Db25uZWN0LlN1YnNFbnRyeVIEc3VicxIYCgdleHBpcmVzGAUgAS' - 'gIUgdleHBpcmVzEhAKA3R0bBgGIAEoDVIDdHRsEhIKBHBpbmcYByABKA1SBHBpbmcSEgoEcG9u' - 'ZxgIIAEoCFIEcG9uZxIYCgdzZXNzaW9uGAkgASgJUgdzZXNzaW9uEhIKBG5vZGUYCiABKAlSBG' - '5vZGUaaQoJU3Vic0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5EkYKBXZhbHVlGAIgASgLMjAuY2Vu' - 'dHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5TdWJzY3JpYmVSZXN1bHRSBXZhbHVlOgI4AQ' - '=='); - -@$core.Deprecated('Use disconnectDescriptor instead') -const Disconnect$json = { - '1': 'Disconnect', - '2': [ - {'1': 'code', '3': 1, '4': 1, '5': 13, '10': 'code'}, - {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, - {'1': 'reconnect', '3': 3, '4': 1, '5': 8, '10': 'reconnect'}, - ], -}; - -/// Descriptor for `Disconnect`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List disconnectDescriptor = $convert.base64Decode( - 'CgpEaXNjb25uZWN0EhIKBGNvZGUYASABKA1SBGNvZGUSFgoGcmVhc29uGAIgASgJUgZyZWFzb2' - '4SHAoJcmVjb25uZWN0GAMgASgIUglyZWNvbm5lY3Q='); - -@$core.Deprecated('Use refreshDescriptor instead') -const Refresh$json = { - '1': 'Refresh', - '2': [ - {'1': 'expires', '3': 1, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 2, '4': 1, '5': 13, '10': 'ttl'}, - ], -}; - -/// Descriptor for `Refresh`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List refreshDescriptor = $convert.base64Decode( - 'CgdSZWZyZXNoEhgKB2V4cGlyZXMYASABKAhSB2V4cGlyZXMSEAoDdHRsGAIgASgNUgN0dGw='); - -@$core.Deprecated('Use connectRequestDescriptor instead') -const ConnectRequest$json = { - '1': 'ConnectRequest', - '2': [ - {'1': 'token', '3': 1, '4': 1, '5': 9, '10': 'token'}, - {'1': 'data', '3': 2, '4': 1, '5': 12, '10': 'data'}, - { - '1': 'subs', - '3': 3, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ConnectRequest.SubsEntry', - '10': 'subs' - }, - {'1': 'name', '3': 4, '4': 1, '5': 9, '10': 'name'}, - {'1': 'version', '3': 5, '4': 1, '5': 9, '10': 'version'}, - ], - '3': [ConnectRequest_SubsEntry$json], -}; - -@$core.Deprecated('Use connectRequestDescriptor instead') -const ConnectRequest_SubsEntry$json = { - '1': 'SubsEntry', - '2': [ - {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, - { - '1': 'value', - '3': 2, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubscribeRequest', - '10': 'value' - }, - ], - '7': {'7': true}, -}; - -/// Descriptor for `ConnectRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List connectRequestDescriptor = $convert.base64Decode( - 'Cg5Db25uZWN0UmVxdWVzdBIUCgV0b2tlbhgBIAEoCVIFdG9rZW4SEgoEZGF0YRgCIAEoDFIEZG' - 'F0YRJNCgRzdWJzGAMgAygLMjkuY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5Db25u' - 'ZWN0UmVxdWVzdC5TdWJzRW50cnlSBHN1YnMSEgoEbmFtZRgEIAEoCVIEbmFtZRIYCgd2ZXJzaW' - '9uGAUgASgJUgd2ZXJzaW9uGmoKCVN1YnNFbnRyeRIQCgNrZXkYASABKAlSA2tleRJHCgV2YWx1' - 'ZRgCIAEoCzIxLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuU3Vic2NyaWJlUmVxdW' - 'VzdFIFdmFsdWU6AjgB'); - -@$core.Deprecated('Use connectResultDescriptor instead') -const ConnectResult$json = { - '1': 'ConnectResult', - '2': [ - {'1': 'client', '3': 1, '4': 1, '5': 9, '10': 'client'}, - {'1': 'version', '3': 2, '4': 1, '5': 9, '10': 'version'}, - {'1': 'expires', '3': 3, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 4, '4': 1, '5': 13, '10': 'ttl'}, - {'1': 'data', '3': 5, '4': 1, '5': 12, '10': 'data'}, - { - '1': 'subs', - '3': 6, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ConnectResult.SubsEntry', - '10': 'subs' - }, - {'1': 'ping', '3': 7, '4': 1, '5': 13, '10': 'ping'}, - {'1': 'pong', '3': 8, '4': 1, '5': 8, '10': 'pong'}, - {'1': 'session', '3': 9, '4': 1, '5': 9, '10': 'session'}, - {'1': 'node', '3': 10, '4': 1, '5': 9, '10': 'node'}, - ], - '3': [ConnectResult_SubsEntry$json], -}; - -@$core.Deprecated('Use connectResultDescriptor instead') -const ConnectResult_SubsEntry$json = { - '1': 'SubsEntry', - '2': [ - {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, - { - '1': 'value', - '3': 2, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.SubscribeResult', - '10': 'value' - }, - ], - '7': {'7': true}, -}; - -/// Descriptor for `ConnectResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List connectResultDescriptor = $convert.base64Decode( - 'Cg1Db25uZWN0UmVzdWx0EhYKBmNsaWVudBgBIAEoCVIGY2xpZW50EhgKB3ZlcnNpb24YAiABKA' - 'lSB3ZlcnNpb24SGAoHZXhwaXJlcxgDIAEoCFIHZXhwaXJlcxIQCgN0dGwYBCABKA1SA3R0bBIS' - 'CgRkYXRhGAUgASgMUgRkYXRhEkwKBHN1YnMYBiADKAsyOC5jZW50cmlmdWdhbC5jZW50cmlmdW' - 'dlLnByb3RvY29sLkNvbm5lY3RSZXN1bHQuU3Vic0VudHJ5UgRzdWJzEhIKBHBpbmcYByABKA1S' - 'BHBpbmcSEgoEcG9uZxgIIAEoCFIEcG9uZxIYCgdzZXNzaW9uGAkgASgJUgdzZXNzaW9uEhIKBG' - '5vZGUYCiABKAlSBG5vZGUaaQoJU3Vic0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5EkYKBXZhbHVl' - 'GAIgASgLMjAuY2VudHJpZnVnYWwuY2VudHJpZnVnZS5wcm90b2NvbC5TdWJzY3JpYmVSZXN1bH' - 'RSBXZhbHVlOgI4AQ=='); - -@$core.Deprecated('Use refreshRequestDescriptor instead') -const RefreshRequest$json = { - '1': 'RefreshRequest', - '2': [ - {'1': 'token', '3': 1, '4': 1, '5': 9, '10': 'token'}, - ], -}; - -/// Descriptor for `RefreshRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List refreshRequestDescriptor = $convert - .base64Decode('Cg5SZWZyZXNoUmVxdWVzdBIUCgV0b2tlbhgBIAEoCVIFdG9rZW4='); - -@$core.Deprecated('Use refreshResultDescriptor instead') -const RefreshResult$json = { - '1': 'RefreshResult', - '2': [ - {'1': 'client', '3': 1, '4': 1, '5': 9, '10': 'client'}, - {'1': 'version', '3': 2, '4': 1, '5': 9, '10': 'version'}, - {'1': 'expires', '3': 3, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 4, '4': 1, '5': 13, '10': 'ttl'}, - ], -}; - -/// Descriptor for `RefreshResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List refreshResultDescriptor = $convert.base64Decode( - 'Cg1SZWZyZXNoUmVzdWx0EhYKBmNsaWVudBgBIAEoCVIGY2xpZW50EhgKB3ZlcnNpb24YAiABKA' - 'lSB3ZlcnNpb24SGAoHZXhwaXJlcxgDIAEoCFIHZXhwaXJlcxIQCgN0dGwYBCABKA1SA3R0bA=='); - -@$core.Deprecated('Use subscribeRequestDescriptor instead') -const SubscribeRequest$json = { - '1': 'SubscribeRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - {'1': 'token', '3': 2, '4': 1, '5': 9, '10': 'token'}, - {'1': 'recover', '3': 3, '4': 1, '5': 8, '10': 'recover'}, - {'1': 'epoch', '3': 6, '4': 1, '5': 9, '10': 'epoch'}, - {'1': 'offset', '3': 7, '4': 1, '5': 4, '10': 'offset'}, - {'1': 'data', '3': 8, '4': 1, '5': 12, '10': 'data'}, - {'1': 'positioned', '3': 9, '4': 1, '5': 8, '10': 'positioned'}, - {'1': 'recoverable', '3': 10, '4': 1, '5': 8, '10': 'recoverable'}, - {'1': 'join_leave', '3': 11, '4': 1, '5': 8, '10': 'joinLeave'}, - ], - '9': [ - {'1': 4, '2': 5}, - {'1': 5, '2': 6}, - ], -}; - -/// Descriptor for `SubscribeRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List subscribeRequestDescriptor = $convert.base64Decode( - 'ChBTdWJzY3JpYmVSZXF1ZXN0EhgKB2NoYW5uZWwYASABKAlSB2NoYW5uZWwSFAoFdG9rZW4YAi' - 'ABKAlSBXRva2VuEhgKB3JlY292ZXIYAyABKAhSB3JlY292ZXISFAoFZXBvY2gYBiABKAlSBWVw' - 'b2NoEhYKBm9mZnNldBgHIAEoBFIGb2Zmc2V0EhIKBGRhdGEYCCABKAxSBGRhdGESHgoKcG9zaX' - 'Rpb25lZBgJIAEoCFIKcG9zaXRpb25lZBIgCgtyZWNvdmVyYWJsZRgKIAEoCFILcmVjb3ZlcmFi' - 'bGUSHQoKam9pbl9sZWF2ZRgLIAEoCFIJam9pbkxlYXZlSgQIBBAFSgQIBRAG'); - -@$core.Deprecated('Use subscribeResultDescriptor instead') -const SubscribeResult$json = { - '1': 'SubscribeResult', - '2': [ - {'1': 'expires', '3': 1, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 2, '4': 1, '5': 13, '10': 'ttl'}, - {'1': 'recoverable', '3': 3, '4': 1, '5': 8, '10': 'recoverable'}, - {'1': 'epoch', '3': 6, '4': 1, '5': 9, '10': 'epoch'}, - { - '1': 'publications', - '3': 7, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Publication', - '10': 'publications' - }, - {'1': 'recovered', '3': 8, '4': 1, '5': 8, '10': 'recovered'}, - {'1': 'offset', '3': 9, '4': 1, '5': 4, '10': 'offset'}, - {'1': 'positioned', '3': 10, '4': 1, '5': 8, '10': 'positioned'}, - {'1': 'data', '3': 11, '4': 1, '5': 12, '10': 'data'}, - {'1': 'was_recovering', '3': 12, '4': 1, '5': 8, '10': 'wasRecovering'}, - ], - '9': [ - {'1': 4, '2': 5}, - {'1': 5, '2': 6}, - ], -}; - -/// Descriptor for `SubscribeResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List subscribeResultDescriptor = $convert.base64Decode( - 'Cg9TdWJzY3JpYmVSZXN1bHQSGAoHZXhwaXJlcxgBIAEoCFIHZXhwaXJlcxIQCgN0dGwYAiABKA' - '1SA3R0bBIgCgtyZWNvdmVyYWJsZRgDIAEoCFILcmVjb3ZlcmFibGUSFAoFZXBvY2gYBiABKAlS' - 'BWVwb2NoElAKDHB1YmxpY2F0aW9ucxgHIAMoCzIsLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucH' - 'JvdG9jb2wuUHVibGljYXRpb25SDHB1YmxpY2F0aW9ucxIcCglyZWNvdmVyZWQYCCABKAhSCXJl' - 'Y292ZXJlZBIWCgZvZmZzZXQYCSABKARSBm9mZnNldBIeCgpwb3NpdGlvbmVkGAogASgIUgpwb3' - 'NpdGlvbmVkEhIKBGRhdGEYCyABKAxSBGRhdGESJQoOd2FzX3JlY292ZXJpbmcYDCABKAhSDXdh' - 'c1JlY292ZXJpbmdKBAgEEAVKBAgFEAY='); - -@$core.Deprecated('Use subRefreshRequestDescriptor instead') -const SubRefreshRequest$json = { - '1': 'SubRefreshRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - {'1': 'token', '3': 2, '4': 1, '5': 9, '10': 'token'}, - ], -}; - -/// Descriptor for `SubRefreshRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List subRefreshRequestDescriptor = $convert.base64Decode( - 'ChFTdWJSZWZyZXNoUmVxdWVzdBIYCgdjaGFubmVsGAEgASgJUgdjaGFubmVsEhQKBXRva2VuGA' - 'IgASgJUgV0b2tlbg=='); - -@$core.Deprecated('Use subRefreshResultDescriptor instead') -const SubRefreshResult$json = { - '1': 'SubRefreshResult', - '2': [ - {'1': 'expires', '3': 1, '4': 1, '5': 8, '10': 'expires'}, - {'1': 'ttl', '3': 2, '4': 1, '5': 13, '10': 'ttl'}, - ], -}; - -/// Descriptor for `SubRefreshResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List subRefreshResultDescriptor = $convert.base64Decode( - 'ChBTdWJSZWZyZXNoUmVzdWx0EhgKB2V4cGlyZXMYASABKAhSB2V4cGlyZXMSEAoDdHRsGAIgAS' - 'gNUgN0dGw='); - -@$core.Deprecated('Use unsubscribeRequestDescriptor instead') -const UnsubscribeRequest$json = { - '1': 'UnsubscribeRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - ], -}; - -/// Descriptor for `UnsubscribeRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List unsubscribeRequestDescriptor = - $convert.base64Decode( - 'ChJVbnN1YnNjcmliZVJlcXVlc3QSGAoHY2hhbm5lbBgBIAEoCVIHY2hhbm5lbA=='); - -@$core.Deprecated('Use unsubscribeResultDescriptor instead') -const UnsubscribeResult$json = { - '1': 'UnsubscribeResult', -}; - -/// Descriptor for `UnsubscribeResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List unsubscribeResultDescriptor = - $convert.base64Decode('ChFVbnN1YnNjcmliZVJlc3VsdA=='); - -@$core.Deprecated('Use publishRequestDescriptor instead') -const PublishRequest$json = { - '1': 'PublishRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - {'1': 'data', '3': 2, '4': 1, '5': 12, '10': 'data'}, - ], -}; - -/// Descriptor for `PublishRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List publishRequestDescriptor = $convert.base64Decode( - 'Cg5QdWJsaXNoUmVxdWVzdBIYCgdjaGFubmVsGAEgASgJUgdjaGFubmVsEhIKBGRhdGEYAiABKA' - 'xSBGRhdGE='); - -@$core.Deprecated('Use publishResultDescriptor instead') -const PublishResult$json = { - '1': 'PublishResult', -}; - -/// Descriptor for `PublishResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List publishResultDescriptor = - $convert.base64Decode('Cg1QdWJsaXNoUmVzdWx0'); - -@$core.Deprecated('Use presenceRequestDescriptor instead') -const PresenceRequest$json = { - '1': 'PresenceRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - ], -}; - -/// Descriptor for `PresenceRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List presenceRequestDescriptor = $convert.base64Decode( - 'Cg9QcmVzZW5jZVJlcXVlc3QSGAoHY2hhbm5lbBgBIAEoCVIHY2hhbm5lbA=='); - -@$core.Deprecated('Use presenceResultDescriptor instead') -const PresenceResult$json = { - '1': 'PresenceResult', - '2': [ - { - '1': 'presence', - '3': 1, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.PresenceResult.PresenceEntry', - '10': 'presence' - }, - ], - '3': [PresenceResult_PresenceEntry$json], -}; - -@$core.Deprecated('Use presenceResultDescriptor instead') -const PresenceResult_PresenceEntry$json = { - '1': 'PresenceEntry', - '2': [ - {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, - { - '1': 'value', - '3': 2, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.ClientInfo', - '10': 'value' - }, - ], - '7': {'7': true}, -}; - -/// Descriptor for `PresenceResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List presenceResultDescriptor = $convert.base64Decode( - 'Cg5QcmVzZW5jZVJlc3VsdBJZCghwcmVzZW5jZRgBIAMoCzI9LmNlbnRyaWZ1Z2FsLmNlbnRyaW' - 'Z1Z2UucHJvdG9jb2wuUHJlc2VuY2VSZXN1bHQuUHJlc2VuY2VFbnRyeVIIcHJlc2VuY2UaaAoN' - 'UHJlc2VuY2VFbnRyeRIQCgNrZXkYASABKAlSA2tleRJBCgV2YWx1ZRgCIAEoCzIrLmNlbnRyaW' - 'Z1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9jb2wuQ2xpZW50SW5mb1IFdmFsdWU6AjgB'); - -@$core.Deprecated('Use presenceStatsRequestDescriptor instead') -const PresenceStatsRequest$json = { - '1': 'PresenceStatsRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - ], -}; - -/// Descriptor for `PresenceStatsRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List presenceStatsRequestDescriptor = - $convert.base64Decode( - 'ChRQcmVzZW5jZVN0YXRzUmVxdWVzdBIYCgdjaGFubmVsGAEgASgJUgdjaGFubmVs'); - -@$core.Deprecated('Use presenceStatsResultDescriptor instead') -const PresenceStatsResult$json = { - '1': 'PresenceStatsResult', - '2': [ - {'1': 'num_clients', '3': 1, '4': 1, '5': 13, '10': 'numClients'}, - {'1': 'num_users', '3': 2, '4': 1, '5': 13, '10': 'numUsers'}, - ], -}; - -/// Descriptor for `PresenceStatsResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List presenceStatsResultDescriptor = $convert.base64Decode( - 'ChNQcmVzZW5jZVN0YXRzUmVzdWx0Eh8KC251bV9jbGllbnRzGAEgASgNUgpudW1DbGllbnRzEh' - 'sKCW51bV91c2VycxgCIAEoDVIIbnVtVXNlcnM='); - -@$core.Deprecated('Use streamPositionDescriptor instead') -const StreamPosition$json = { - '1': 'StreamPosition', - '2': [ - {'1': 'offset', '3': 1, '4': 1, '5': 4, '10': 'offset'}, - {'1': 'epoch', '3': 2, '4': 1, '5': 9, '10': 'epoch'}, - ], -}; - -/// Descriptor for `StreamPosition`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List streamPositionDescriptor = $convert.base64Decode( - 'Cg5TdHJlYW1Qb3NpdGlvbhIWCgZvZmZzZXQYASABKARSBm9mZnNldBIUCgVlcG9jaBgCIAEoCV' - 'IFZXBvY2g='); - -@$core.Deprecated('Use historyRequestDescriptor instead') -const HistoryRequest$json = { - '1': 'HistoryRequest', - '2': [ - {'1': 'channel', '3': 1, '4': 1, '5': 9, '10': 'channel'}, - {'1': 'limit', '3': 7, '4': 1, '5': 5, '10': 'limit'}, - { - '1': 'since', - '3': 8, - '4': 1, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.StreamPosition', - '10': 'since' - }, - {'1': 'reverse', '3': 9, '4': 1, '5': 8, '10': 'reverse'}, - ], - '9': [ - {'1': 2, '2': 3}, - {'1': 3, '2': 4}, - {'1': 4, '2': 5}, - {'1': 5, '2': 6}, - {'1': 6, '2': 7}, - ], -}; - -/// Descriptor for `HistoryRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List historyRequestDescriptor = $convert.base64Decode( - 'Cg5IaXN0b3J5UmVxdWVzdBIYCgdjaGFubmVsGAEgASgJUgdjaGFubmVsEhQKBWxpbWl0GAcgAS' - 'gFUgVsaW1pdBJFCgVzaW5jZRgIIAEoCzIvLmNlbnRyaWZ1Z2FsLmNlbnRyaWZ1Z2UucHJvdG9j' - 'b2wuU3RyZWFtUG9zaXRpb25SBXNpbmNlEhgKB3JldmVyc2UYCSABKAhSB3JldmVyc2VKBAgCEA' - 'NKBAgDEARKBAgEEAVKBAgFEAZKBAgGEAc='); - -@$core.Deprecated('Use historyResultDescriptor instead') -const HistoryResult$json = { - '1': 'HistoryResult', - '2': [ - { - '1': 'publications', - '3': 1, - '4': 3, - '5': 11, - '6': '.centrifugal.centrifuge.protocol.Publication', - '10': 'publications' - }, - {'1': 'epoch', '3': 2, '4': 1, '5': 9, '10': 'epoch'}, - {'1': 'offset', '3': 3, '4': 1, '5': 4, '10': 'offset'}, - ], -}; - -/// Descriptor for `HistoryResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List historyResultDescriptor = $convert.base64Decode( - 'Cg1IaXN0b3J5UmVzdWx0ElAKDHB1YmxpY2F0aW9ucxgBIAMoCzIsLmNlbnRyaWZ1Z2FsLmNlbn' - 'RyaWZ1Z2UucHJvdG9jb2wuUHVibGljYXRpb25SDHB1YmxpY2F0aW9ucxIUCgVlcG9jaBgCIAEo' - 'CVIFZXBvY2gSFgoGb2Zmc2V0GAMgASgEUgZvZmZzZXQ='); - -@$core.Deprecated('Use pingRequestDescriptor instead') -const PingRequest$json = { - '1': 'PingRequest', -}; - -/// Descriptor for `PingRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List pingRequestDescriptor = - $convert.base64Decode('CgtQaW5nUmVxdWVzdA=='); - -@$core.Deprecated('Use pingResultDescriptor instead') -const PingResult$json = { - '1': 'PingResult', -}; - -/// Descriptor for `PingResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List pingResultDescriptor = - $convert.base64Decode('CgpQaW5nUmVzdWx0'); - -@$core.Deprecated('Use rPCRequestDescriptor instead') -const RPCRequest$json = { - '1': 'RPCRequest', - '2': [ - {'1': 'data', '3': 1, '4': 1, '5': 12, '10': 'data'}, - {'1': 'method', '3': 2, '4': 1, '5': 9, '10': 'method'}, - ], -}; - -/// Descriptor for `RPCRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List rPCRequestDescriptor = $convert.base64Decode( - 'CgpSUENSZXF1ZXN0EhIKBGRhdGEYASABKAxSBGRhdGESFgoGbWV0aG9kGAIgASgJUgZtZXRob2' - 'Q='); - -@$core.Deprecated('Use rPCResultDescriptor instead') -const RPCResult$json = { - '1': 'RPCResult', - '2': [ - {'1': 'data', '3': 1, '4': 1, '5': 12, '10': 'data'}, - ], -}; - -/// Descriptor for `RPCResult`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List rPCResultDescriptor = - $convert.base64Decode('CglSUENSZXN1bHQSEgoEZGF0YRgBIAEoDFIEZGF0YQ=='); - -@$core.Deprecated('Use sendRequestDescriptor instead') -const SendRequest$json = { - '1': 'SendRequest', - '2': [ - {'1': 'data', '3': 1, '4': 1, '5': 12, '10': 'data'}, - ], -}; - -/// Descriptor for `SendRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List sendRequestDescriptor = - $convert.base64Decode('CgtTZW5kUmVxdWVzdBISCgRkYXRhGAEgASgMUgRkYXRh'); diff --git a/lib/src.old/transport/protobuf/client.pbserver.dart b/lib/src.old/transport/protobuf/client.pbserver.dart deleted file mode 100644 index 6dbd1e8..0000000 --- a/lib/src.old/transport/protobuf/client.pbserver.dart +++ /dev/null @@ -1,13 +0,0 @@ -// -// Generated code. Do not modify. -// source: client.proto -// -// @dart = 2.12 - -// ignore_for_file: annotate_overrides, camel_case_types -// ignore_for_file: constant_identifier_names -// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names, prefer_final_fields -// ignore_for_file: unnecessary_import, unnecessary_this, unused_import - -export 'client.pb.dart'; diff --git a/lib/src.old/transport/protobuf/client.proto b/lib/src.old/transport/protobuf/client.proto deleted file mode 100644 index 938803a..0000000 --- a/lib/src.old/transport/protobuf/client.proto +++ /dev/null @@ -1,273 +0,0 @@ -syntax = "proto3"; - -package centrifugal.centrifuge.protocol; - -option go_package = "./;protocol"; - -message Error { - uint32 code = 1; - string message = 2; - bool temporary = 3; -} - -message EmulationRequest { - string node = 1; - string session = 2; - bytes data = 3; -} - -// Command sent from a client to a server. -// ProtocolVersion2 uses id and one of the possible request messages. -message Command { - // Id of command to let client match replies to commands. - uint32 id = 1; - - reserved 2, 3; - - // ProtocolVersion2 client can send one of the following requests. Server will - // only take the first non-null request out of these and may return an error - // if client passed more than one request. We are not using oneof here due to - // JSON interoperability concerns. - ConnectRequest connect = 4; - SubscribeRequest subscribe = 5; - UnsubscribeRequest unsubscribe = 6; - PublishRequest publish = 7; - PresenceRequest presence = 8; - PresenceStatsRequest presence_stats = 9; - HistoryRequest history = 10; - PingRequest ping = 11; - SendRequest send = 12; - RPCRequest rpc = 13; - RefreshRequest refresh = 14; - SubRefreshRequest sub_refresh = 15; -} - -// Reply sent from a server to a client. -// ProtocolVersion2 uses id and one of the possible concrete result messages. -message Reply { - // Id will only be set to a value > 0 for replies to commands. For pushes - // it will have zero value. - uint32 id = 1; - // Error can only be set in replies to commands. For pushes it will have zero - // value. - Error error = 2; - - reserved 3; - - // ProtocolVersion2 server can send one of the following fields. We are not - // using oneof here due to JSON interoperability concerns. - Push push = 4; - ConnectResult connect = 5; - SubscribeResult subscribe = 6; - UnsubscribeResult unsubscribe = 7; - PublishResult publish = 8; - PresenceResult presence = 9; - PresenceStatsResult presence_stats = 10; - HistoryResult history = 11; - PingResult ping = 12; - RPCResult rpc = 13; - RefreshResult refresh = 14; - SubRefreshResult sub_refresh = 15; -} - -// Push can be sent to a client as part of Reply in case of bidirectional -// transport or without additional wrapping in case of unidirectional -// transports. ProtocolVersion2 uses channel and one of the possible concrete -// push messages. -message Push { - reserved 1, 3; - string channel = 2; - - // ProtocolVersion2 server can push one of the following fields to the client. - // We are not using oneof here due to JSON interoperability concerns. - Publication pub = 4; - Join join = 5; - Leave leave = 6; - Unsubscribe unsubscribe = 7; - Message message = 8; - Subscribe subscribe = 9; - Connect connect = 10; - Disconnect disconnect = 11; - Refresh refresh = 12; -} - -message ClientInfo { - string user = 1; - string client = 2; - bytes conn_info = 3; - bytes chan_info = 4; -} - -message Publication { - reserved 1, 2, 3; - bytes data = 4; - ClientInfo info = 5; - uint64 offset = 6; - map tags = 7; -} - -message Join { ClientInfo info = 1; } - -message Leave { ClientInfo info = 1; } - -message Unsubscribe { - reserved 1; - uint32 code = 2; - string reason = 3; -} - -message Subscribe { - bool recoverable = 1; - reserved 2, 3; - string epoch = 4; - uint64 offset = 5; - bool positioned = 6; - bytes data = 7; -} - -message Message { bytes data = 1; } - -message Connect { - string client = 1; - string version = 2; - bytes data = 3; - map subs = 4; - bool expires = 5; - uint32 ttl = 6; - uint32 ping = 7; - bool pong = 8; - string session = 9; - string node = 10; -} - -message Disconnect { - uint32 code = 1; - string reason = 2; - bool reconnect = 3; -} - -message Refresh { - bool expires = 1; - uint32 ttl = 2; -} - -message ConnectRequest { - string token = 1; - bytes data = 2; - map subs = 3; - string name = 4; - string version = 5; -} - -message ConnectResult { - string client = 1; - string version = 2; - bool expires = 3; - uint32 ttl = 4; - bytes data = 5; - map subs = 6; - uint32 ping = 7; - bool pong = 8; - string session = 9; - string node = 10; -} - -message RefreshRequest { string token = 1; } - -message RefreshResult { - string client = 1; - string version = 2; - bool expires = 3; - uint32 ttl = 4; -} - -message SubscribeRequest { - string channel = 1; - string token = 2; - bool recover = 3; - reserved 4, 5; - string epoch = 6; - uint64 offset = 7; - bytes data = 8; - bool positioned = 9; - bool recoverable = 10; - bool join_leave = 11; -} - -message SubscribeResult { - bool expires = 1; - uint32 ttl = 2; - bool recoverable = 3; - reserved 4, 5; - string epoch = 6; - repeated Publication publications = 7; - bool recovered = 8; - uint64 offset = 9; - bool positioned = 10; - bytes data = 11; - bool was_recovering = 12; -} - -message SubRefreshRequest { - string channel = 1; - string token = 2; -} - -message SubRefreshResult { - bool expires = 1; - uint32 ttl = 2; -} - -message UnsubscribeRequest { string channel = 1; } - -message UnsubscribeResult {} - -message PublishRequest { - string channel = 1; - bytes data = 2; -} - -message PublishResult {} - -message PresenceRequest { string channel = 1; } - -message PresenceResult { map presence = 1; } - -message PresenceStatsRequest { string channel = 1; } - -message PresenceStatsResult { - uint32 num_clients = 1; - uint32 num_users = 2; -} - -message StreamPosition { - uint64 offset = 1; - string epoch = 2; -} - -message HistoryRequest { - string channel = 1; - reserved 2, 3, 4, 5, 6; - int32 limit = 7; - StreamPosition since = 8; - bool reverse = 9; -} - -message HistoryResult { - repeated Publication publications = 1; - string epoch = 2; - uint64 offset = 3; -} - -message PingRequest {} - -message PingResult {} - -message RPCRequest { - bytes data = 1; - string method = 2; -} - -message RPCResult { bytes data = 1; } - -message SendRequest { bytes data = 1; } \ No newline at end of file diff --git a/lib/src.old/transport/transport_interface.dart b/lib/src.old/transport/transport_interface.dart deleted file mode 100644 index cf8ae41..0000000 --- a/lib/src.old/transport/transport_interface.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:async'; - -import '../client/state.dart'; -import '../model/event.dart'; -import '../model/history.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/refresh_result.dart'; -import '../model/stream_position.dart'; -import '../subscription/server_subscription_manager.dart'; -import '../subscription/subcibed_on_channel.dart'; -import '../subscription/subscription_config.dart'; -import '../util/notifier.dart'; - -/// Class responsible for sending and receiving data from the server. -abstract interface class ISpinifyTransport { - /// Current state - SpinifyState get state; - - /// State observable. - abstract final SpinifyListenable states; - - /// Spinify events. - abstract final SpinifyListenable events; - - /// Received bytes count & size. - ({BigInt count, BigInt size}) get received; - - /// Transferred bytes count & size. - ({BigInt count, BigInt size}) get transferred; - - /// Message response timeout in milliseconds. - ({int min, int avg, int max}) get speed; - - /// Connect to the server. - /// [url] is a URL of endpoint. - /// [subs] is a list of server-side subscriptions to subscribe on connect. - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ); - - /// Send asynchronous message to a server. This method makes sense - /// only when using Centrifuge library for Go on a server side. In Centrifuge - /// asynchronous message handler does not exist. - Future sendAsyncMessage(List data); - - /// Subscribe on channel with optional [since] position. - Future subscribe( - String channel, - SpinifySubscriptionConfig config, - SpinifyStreamPosition? since, - ); - - /// Unsubscribe from channel. - Future unsubscribe( - String channel, - SpinifySubscriptionConfig config, - ); - - /// Publish data to channel. - Future publish(String channel, List data); - - /// Fetch publication history inside a channel. - /// Only for channels where history is enabled. - Future history( - String channel, { - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }); - - /// Fetch presence information inside a channel. - Future presence(String channel); - - /// Fetch presence stats information inside a channel. - Future presenceStats(String channel); - - /// Disconnect from the server. - /// e.g. code: 0, reason: 'disconnect called' - Future disconnect(int code, String reason); - - /// Send refresh token command to server. - Future sendRefresh(String token); - - /// Send subscription channel refresh token command to server. - Future sendSubRefresh( - String channel, - String token, - ); - - /// Send arbitrary RPC and wait for response. - Future> rpc(String method, List data); - - /// Permanent close connection to the server and - /// free all allocated resources. - Future close(); -} diff --git a/lib/src.old/transport/transport_protobuf_codec.dart b/lib/src.old/transport/transport_protobuf_codec.dart deleted file mode 100644 index 303885d..0000000 --- a/lib/src.old/transport/transport_protobuf_codec.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:convert'; - -import 'package:protobuf/protobuf.dart' as pb; - -import '../util/logger.dart' as logger; -import 'protobuf/client.pb.dart' as pb; - -/// Codec for encoding and decoding protobuf messages. -final class TransportProtobufCodec extends Codec> { - /// Create a new instance of [TransportProtobufCodec]. - const TransportProtobufCodec(); - - @override - Converter, Iterable> get decoder => - const TransportProtobufDecoder(); - - @override - Converter> get encoder => - const TransportProtobufEncoder(); -} - -/// Encoder for protobuf messages. -final class TransportProtobufEncoder extends Converter> { - /// Create a new instance of [TransportProtobufEncoder]. - const TransportProtobufEncoder(); - - @override - List convert(pb.GeneratedMessage input) { - /* final buffer = pb.CodedBufferWriter(); - input.writeToCodedBufferWriter(buffer); - return buffer.toBuffer(); */ - final commandData = input.writeToBuffer(); - final length = commandData.lengthInBytes; - final writer = pb.CodedBufferWriter() - ..writeInt32NoTag(length); //..writeRawBytes(commandData); - return writer.toBuffer() + commandData; - } -} - -/// Decoder for protobuf messages. -final class TransportProtobufDecoder - extends Converter, Iterable> { - /// Create a new instance of [TransportProtobufDecoder]. - const TransportProtobufDecoder(); - - @override - Iterable convert(List input) sync* { - final reader = pb.CodedBufferReader(input); - while (!reader.isAtEnd()) { - try { - final reply = pb.Reply(); - reader.readMessage(reply, pb.ExtensionRegistry.EMPTY); - yield reply; - } on Object catch (error, stackTrace) { - logger.warning( - error, - stackTrace, - 'Failed to decode reply: $error', - ); - } - } - } -} diff --git a/lib/src.old/transport/ws_protobuf_transport.dart b/lib/src.old/transport/ws_protobuf_transport.dart deleted file mode 100644 index 1246d76..0000000 --- a/lib/src.old/transport/ws_protobuf_transport.dart +++ /dev/null @@ -1,998 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; - -import 'package:meta/meta.dart'; -import 'package:protobuf/protobuf.dart' as pb; -import 'package:ws/ws.dart'; - -import '../client/config.dart'; -import '../client/disconnect_code.dart'; -import '../client/state.dart'; -import '../model/channel_presence.dart'; -import '../model/client_info.dart'; -import '../model/connect.dart'; -import '../model/disconnect.dart'; -import '../model/event.dart'; -import '../model/exception.dart'; -import '../model/history.dart'; -import '../model/message.dart'; -import '../model/presence.dart'; -import '../model/presence_stats.dart'; -import '../model/publication.dart'; -import '../model/refresh.dart'; -import '../model/refresh_result.dart'; -import '../model/stream_position.dart'; -import '../model/subscribe.dart'; -import '../model/unsubscribe.dart'; -import '../subscription/server_subscription_manager.dart'; -import '../subscription/subcibed_on_channel.dart'; -import '../subscription/subscription.dart'; -import '../subscription/subscription_config.dart'; -import '../subscription/subscription_state.dart'; -import '../util/logger.dart' as logger; -import '../util/notifier.dart'; -import '../util/speed_meter.dart'; -import 'protobuf/client.pb.dart' as pb; -import 'transport_interface.dart'; -import 'transport_protobuf_codec.dart'; - -/// Base class for Spinify transport with Protobuf & WebSocket protocol. -abstract base class SpinifyWSPBTransportBase implements ISpinifyTransport { - /// Create a new instance of [SpinifyWSPBTransportBase]. - SpinifyWSPBTransportBase({ - required SpinifyConfig config, - }) : _config = config, - _webSocket = WebSocketClient( - WebSocketOptions.selector( - js: () => WebSocketOptions.js( - connectionRetryInterval: null, - protocols: _$protocolsSpinifyProtobuf, - timeout: config.timeout, - useBlobForBinary: false, - ), - vm: () => WebSocketOptions.vm( - connectionRetryInterval: null, - protocols: _$protocolsSpinifyProtobuf, - timeout: config.timeout, - headers: config.headers, - ), - ), - ) { - _initTransport(); - } - - /// Protocols for websocket. - static const List _$protocolsSpinifyProtobuf = [ - 'centrifuge-protobuf' - ]; - - /// Spinify config. - final SpinifyConfig _config; - - @override - final SpinifyChangeNotifier events = - SpinifyChangeNotifier(); - - /// Init transport, override this method to add custom logic. - @protected - @mustCallSuper - void _initTransport() {} - - /// Websocket client. - @nonVirtual - final WebSocketClient _webSocket; - - @override - @mustCallSuper - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ) async {} - - @override - @mustCallSuper - Future disconnect(int code, String reason) async { - if (_webSocket.state.readyState.isDisconnecting || - _webSocket.state.readyState.isClosed) { - // Already disconnected - do nothing. - return; - } - await _webSocket.disconnect(code, reason); - } - - @override - @mustCallSuper - Future close() async { - await disconnect(DisconnectCode.disconnectCalled.code, 'Client closed'); - await _webSocket.close(); - events.close(); - } -} - -/// Class responsible for sending and receiving data from the server -/// through the Protobuf & WebSocket protocol. -// ignore: lines_longer_than_80_chars -final class SpinifyWSPBTransport = SpinifyWSPBTransportBase - with - SpinifyWSPBReplyMixin, - SpinifyWSPBStateHandlerMixin, - SpinifyWSPBSenderMixin, - SpinifyWSPBConnectionMixin, - SpinifyWSPBPingPongMixin, - SpinifyWSPBSubscription, - SpinifyWSPBHandlerMixin; - -/// Stored completer for responses. -typedef _ReplyCompleter = ({ - void Function(pb.Reply reply) complete, - void Function(Object error, StackTrace stackTrace) fail, -}); - -/// Mixin responsible for holding reply completers. -base mixin SpinifyWSPBReplyMixin on SpinifyWSPBTransportBase { - /// Completers for messages by id. - /// Contains timer for timeout and completer for response. - final Map _replyCompleters = {}; - - /// Observe reply future by command id. - Future _awaitReply(int commandId, [Duration? timeout]) { - final completer = Completer.sync(); - final timeoutTimer = timeout != null && timeout > Duration.zero - ? Timer( - _config.timeout, - () => _replyCompleters.remove(commandId)?.fail( - TimeoutException('Timeout for command #$commandId'), - StackTrace.current, - ), - ) - : null; - // Add completer to Hash Table for future response. - // Completer will be completed by [_handleWebSocketMessage]. - _replyCompleters[commandId] = ( - complete: (reply) { - timeoutTimer?.cancel(); - _replyCompleters.remove(commandId); - if (completer.isCompleted) return; - completer.complete(reply); - }, - fail: (error, stackTrace) { - logger.warning( - error, - StackTrace.current, - 'Error while processing reply', - ); - timeoutTimer?.cancel(); - _replyCompleters.remove(commandId); - if (completer.isCompleted) return; - completer.completeError(error, stackTrace); - }, - ); - return completer.future; - } - - /// Complete reply by id. - void _completeReply(pb.Reply reply) => - _replyCompleters.remove(reply.id)?.complete(reply); - - /// Fail all replies. - void _failAllReplies(Object error, StackTrace stackTrace) { - for (final completer in _replyCompleters.values) { - completer.fail(error, stackTrace); - } - _replyCompleters.clear(); - } -} - -/// Mixin responsible for sending data through websocket with protobuf. -base mixin SpinifyWSPBSenderMixin - on SpinifyWSPBTransportBase, SpinifyWSPBReplyMixin { - /// Encoder protobuf commands to bytes. - static const Converter> _commandEncoder = - TransportProtobufEncoder(); - - /// Speed meter of the connection. - final SpinifySpeedMeter _speedMeter = SpinifySpeedMeter(15); - - @override - ({int min, int avg, int max}) get speed => _speedMeter.speed; - - /// Counter for messages. - int _messageId = 1; - - @nonVirtual - @protected - Future _sendMessage( - Req request, - Rep result, - ) async { - final command = _createCommand(request, false); - // Send command and wait for response. - final future = _awaitReply(command.id); - final stopwatch = Stopwatch()..start(); - pb.Reply reply; - try { - await _sendCommand(command); - reply = await future; - _speedMeter.add(stopwatch.elapsedMilliseconds); - } finally { - stopwatch.stop(); - } - if (reply.hasError()) { - throw SpinifyReplyException( - replyCode: reply.error.code, - replyMessage: reply.error.message, - temporary: reply.error.temporary, - ); - } - if (reply.hasConnect()) { - return result..mergeFromMessage(reply.connect); - } else if (reply.hasSubscribe()) { - return result..mergeFromMessage(reply.subscribe); - } else if (reply.hasPublish()) { - return result..mergeFromMessage(reply.publish); - } else if (reply.hasPing()) { - return result..mergeFromMessage(reply.publish); - } else if (reply.hasUnsubscribe()) { - return result..mergeFromMessage(reply.unsubscribe); - } else if (reply.hasPresence()) { - return result..mergeFromMessage(reply.presence); - } else if (reply.hasPresenceStats()) { - return result..mergeFromMessage(reply.presenceStats); - } else if (reply.hasHistory()) { - return result..mergeFromMessage(reply.history); - } else if (reply.hasRpc()) { - return result..mergeFromMessage(reply.rpc); - } else if (reply.hasRefresh()) { - return result..mergeFromMessage(reply.refresh); - } else if (reply.hasSubRefresh()) { - return result..mergeFromMessage(reply.subRefresh); - } else { - throw ArgumentError('Unknown reply type: $reply}'); - } - } - - @override - Future sendAsyncMessage(List data) => - _sendAsyncMessage(pb.Message()..data = data); - - @nonVirtual - @protected - Future _sendAsyncMessage( - Req request, - ) async { - final command = _createCommand(request, true); - return _sendCommand(command); - } - - pb.Command _createCommand( - pb.GeneratedMessage request, - bool isAsync, - ) { - late final cmd = pb.Command(); - switch (request) { - case pb.ConnectRequest request: - cmd.connect = request; - case pb.PublishRequest request: - cmd.publish = request; - case pb.PingRequest request: - cmd.ping = request; - case pb.SubscribeRequest request: - cmd.subscribe = request; - case pb.UnsubscribeRequest request: - cmd.unsubscribe = request; - case pb.HistoryRequest request: - cmd.history = request; - case pb.PresenceRequest request: - cmd.presence = request; - case pb.PresenceStatsRequest request: - cmd.presenceStats = request; - case pb.RPCRequest request: - cmd.rpc = request; - case pb.RefreshRequest request: - cmd.refresh = request; - case pb.SubRefreshRequest request: - cmd.subRefresh = request; - case pb.SendRequest request: - cmd.send = request; - case pb.Command _: - // Already a command, do nothing and return it. - // e.g. used for pong async message. - return request; - default: - throw ArgumentError('unknown request type'); - } - if (!isAsync) cmd.id = _messageId++; - return cmd; - } - - BigInt _transferredCount = BigInt.zero; - BigInt _transferredSize = BigInt.zero; - @override - ({BigInt count, BigInt size}) get transferred => - (count: _transferredCount, size: _transferredSize); - - Future _sendCommand(pb.Command command) async { - if (!_webSocket.state.readyState.isOpen) throw StateError('Not connected'); - final data = _commandEncoder.convert(command); - await _webSocket.add(data); - _transferredCount += BigInt.one; - _transferredSize += BigInt.from(data.length); - } -} - -/// Mixin responsible for connection. -base mixin SpinifyWSPBConnectionMixin - on - SpinifyWSPBTransportBase, - SpinifyWSPBSenderMixin, - SpinifyWSPBStateHandlerMixin { - @override - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ) async { - try { - await super.connect(url, serverSubscriptionManager); - await _webSocket.connect(url); - final request = pb.ConnectRequest(); - final token = await _config.getToken?.call(); - assert(token == null || token.length > 5, 'Spinify JWT is too short'); - if (token != null) request.token = token; - final payload = await _config.getPayload?.call(); - if (payload != null) request.data = payload; - request - ..name = _config.client.name - ..version = _config.client.version; - // Add server-side subscriptions to connect request. - { - final subs = serverSubscriptionManager.subscriptions.values; - for (final SpinifyServerSubscription( - channel: String channel, - state: SpinifySubscriptionState(:recoverable, :since), - ) in subs) { - if (since == null) continue; - final subRequest = pb.SubscribeRequest() - ..recover = recoverable - ..offset = since.offset - ..epoch = since.epoch; - request.subs.putIfAbsent(channel, () => subRequest); - } - } - final pb.ConnectResult result; - try { - result = await _sendMessage(request, pb.ConnectResult()); - } on Object catch (error, stackTrace) { - Error.throwWithStackTrace( - SpinifyConnectionException( - message: 'Error while making connect request', - error: error, - ), - stackTrace, - ); - } - if (!_webSocket.state.readyState.isOpen) { - throw StateError('Connection closed during connection process'); - } - final now = DateTime.now(); - final expires = result.hasExpires() && result.expires && result.hasTtl(); - - // Update server-side subscriptions. - { - final subs = result.subs.entries.map((e) { - final channel = e.key; - final sub = e.value; - final positioned = sub.hasPositioned() && sub.positioned; - final recoverable = sub.hasRecoverable() && sub.recoverable; - return SpinifySubscribe( - timestamp: now, - channel: channel, - positioned: positioned, - recoverable: recoverable, - data: sub.hasData() ? sub.data : const [], - streamPosition: - (positioned || recoverable) && sub.hasOffset() && sub.hasEpoch() - ? (offset: sub.offset, epoch: sub.epoch) - : null, - ); - }).toList(growable: false); - serverSubscriptionManager.upsert(subs); - } - - _setState(SpinifyState$Connected( - url: url, - timestamp: now, - client: result.hasClient() ? result.client : null, - version: result.hasVersion() ? result.version : null, - expires: expires, - ttl: expires ? now.add(Duration(seconds: result.ttl)) : null, - node: result.hasNode() ? result.node : null, - pingInterval: result.hasPing() ? Duration(seconds: result.ping) : null, - sendPong: result.hasPong() ? result.pong : null, - session: result.hasSession() ? result.session : null, - data: result.hasData() ? result.data : null, - )); - } on Object { - disconnect(DisconnectCode.unauthorized.code, 'Connection failed') - .ignore(); - rethrow; - } - } - - @override - Future sendRefresh(String token) { - if (!_state.isConnected) throw StateError('Not connected'); - return _sendMessage(pb.RefreshRequest()..token = token, pb.RefreshResult()) - .then( - (result) { - final state = _state; - if (state is SpinifyState$Connected) { - final now = DateTime.now(); - final expires = - result.hasExpires() && result.expires && result.hasTtl(); - final ttl = expires ? now.add(Duration(seconds: result.ttl)) : null; - _setState(SpinifyState$Connected( - url: state.url, - timestamp: now, - client: result.hasClient() ? result.client : null, - version: result.hasVersion() ? result.version : null, - expires: expires, - ttl: ttl, - node: state.node, - pingInterval: state.pingInterval, - sendPong: state.sendPong, - session: state.session, - data: state.data, - )); - return SpinifyRefreshResult( - expires: expires, - ttl: ttl, - ); - } else { - throw StateError('Not connected'); - } - }, - ); - } -} - -/// Handler for websocket states. -base mixin SpinifyWSPBStateHandlerMixin - on SpinifyWSPBTransportBase, SpinifyWSPBReplyMixin { - // Subscribe to websocket state after first connection. - /// Subscription to websocket state. - StreamSubscription? _webSocketClosedStateSubscription; - - @override - @nonVirtual - SpinifyState get state => _state; - - @protected - @nonVirtual - SpinifyState _state = SpinifyState$Disconnected( - timestamp: DateTime.now(), - closeCode: null, - closeReason: 'Not connected yet', - ); - - @override - @nonVirtual - final SpinifyChangeNotifier states = SpinifyChangeNotifier(); - - @override - void _initTransport() { - super._initTransport(); - } - - /// Change state of spinify client. - @protected - @nonVirtual - void _setState(SpinifyState state) { - if (state == _state) return; - states.notify(_state = state); - } - - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _handleWebSocketClosedStates(WebSocketClientState$Closed state) { - _setState( - SpinifyState$Disconnected( - timestamp: DateTime.now(), - closeCode: state.closeCode, - closeReason: state.closeReason, - ), - ); - _failAllReplies( - const SpinifyReplyException( - replyCode: 3000, - replyMessage: 'Connection closed', - temporary: true, - ), - StackTrace.current, - ); - } - - @override - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ) { - // Change state to connecting before connection. - _setState(SpinifyState$Connecting(url: url)); - // Subscribe to websocket state after initialization. - _webSocketClosedStateSubscription ??= _webSocket.stateChanges.closed.listen( - _handleWebSocketClosedStates, - cancelOnError: false, - ); - return super.connect(url, serverSubscriptionManager); - } - - @override - Future close() async { - _webSocketClosedStateSubscription?.cancel().ignore(); - _setState(SpinifyState$Closed()); - await super.close(); - states.close(); - } -} - -/// Handler for websocket messages and decode protobuf. -base mixin SpinifyWSPBHandlerMixin - on - SpinifyWSPBTransportBase, - SpinifyWSPBSenderMixin, - SpinifyWSPBPingPongMixin { - /// Encoder protobuf commands to bytes. - static const Converter, Iterable> _replyDecoder = - TransportProtobufDecoder(); - - /// Subscription to websocket messages/data. - StreamSubscription>? _webSocketMessageSubscription; - - BigInt _receivedCount = BigInt.zero; - BigInt _receivedSize = BigInt.zero; - @override - ({BigInt count, BigInt size}) get received => - (count: _receivedCount, size: _receivedSize); - - @override - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ) { - // Subscribe to websocket messages after first connection. - _webSocketMessageSubscription ??= _webSocket.stream.bytes.listen( - _handleWebSocketMessage, - cancelOnError: false, - ); - return super.connect(url, serverSubscriptionManager); - } - - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _handleWebSocketMessage(List response) { - _receivedCount += BigInt.one; - _receivedSize += BigInt.from(response.length); - final replies = _replyDecoder.convert(response); - for (final reply in replies) { - if (reply.hasId() && reply.id > 0) { - logger.fine('Reply for command #${reply.id} received'); - _completeReply(reply); - } else if (reply.hasPush()) { - logger.info('Push message from server received'); - _onPush(reply.push); - } else { - logger.fine('Ping message from server received'); - _onPing(); // Every empty message from server is a ping. - } - } - } - - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _onPing() { - _restartPingTimer(); - if (state case SpinifyState$Connected(:bool? sendPong)) { - if (sendPong != true) return; - _sendAsyncMessage(pb.PingRequest()).ignore(); - logger.fine('Pong message sent'); - } - } - - /// Push can be sent to a client as part of Reply in case of bidirectional - /// transport or without additional wrapping in case of unidirectional - /// transports. ProtocolVersion2 uses channel and one of the possible concrete - /// push messages. - @protected - @nonVirtual - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _onPush(pb.Push push) { - final now = DateTime.now(); - final channel = push.hasChannel() ? push.channel : ''; - if (push.hasPub()) { - events.notify($publicationDecode(push.channel)(push.pub)); - } else if (push.hasMessage()) { - events.notify( - SpinifyMessage( - timestamp: now, - channel: channel, - data: push.message.hasData() ? push.message.data : const [], - ), - ); - } else if (push.hasJoin()) { - events.notify( - SpinifyJoin( - timestamp: now, - channel: channel, - info: $decodeClientInfo(push.join.info), - ), - ); - } else if (push.hasLeave()) { - events.notify( - SpinifyLeave( - timestamp: now, - channel: channel, - info: $decodeClientInfo(push.join.info), - ), - ); - } else if (push.hasSubscribe()) { - final positioned = - push.subscribe.hasPositioned() && push.subscribe.positioned; - final recoverable = - push.subscribe.hasRecoverable() && push.subscribe.recoverable; - events.notify( - SpinifySubscribe( - timestamp: now, - channel: channel, - positioned: positioned, - recoverable: recoverable, - data: push.subscribe.hasData() ? push.subscribe.data : const [], - streamPosition: (positioned || recoverable) && - push.subscribe.hasOffset() && - push.subscribe.hasEpoch() - ? (offset: push.subscribe.offset, epoch: push.subscribe.epoch) - : null, - ), - ); - } else if (push.hasUnsubscribe()) { - events.notify( - SpinifyUnsubscribe( - timestamp: now, - channel: channel, - code: push.unsubscribe.hasCode() ? push.unsubscribe.code : 0, - reason: push.unsubscribe.hasReason() ? push.unsubscribe.reason : 'OK', - ), - ); - } else if (push.hasConnect()) { - final connect = push.connect; - final expires = - connect.hasExpires() && connect.expires && connect.hasTtl(); - events.notify( - SpinifyConnect( - timestamp: now, - channel: channel, - data: push.message.hasData() ? push.message.data : const [], - client: connect.hasClient() ? connect.client : '', - version: connect.hasVersion() ? connect.version : '', - ttl: expires ? now.add(Duration(seconds: connect.ttl)) : null, - expires: expires, - node: connect.hasNode() ? connect.node : null, - pingInterval: - connect.hasPing() ? Duration(seconds: connect.ping) : null, - sendPong: connect.hasPong() ? connect.pong : null, - session: connect.hasSession() ? connect.session : null, - ), - ); - } else if (push.hasDisconnect()) { - events.notify( - SpinifyDisconnect( - timestamp: now, - channel: channel, - code: push.disconnect.hasCode() ? push.disconnect.code : 0, - reason: push.disconnect.hasReason() - ? push.disconnect.reason - : 'disconnect from server', - reconnect: - push.disconnect.hasReconnect() && push.disconnect.reconnect, - ), - ); - } else if (push.hasRefresh()) { - events.notify(SpinifyRefresh( - timestamp: now, - channel: channel, - expires: push.refresh.hasExpires() && push.refresh.expires, - ttl: push.refresh.hasTtl() - ? now.add(Duration(seconds: push.refresh.ttl)) - : null, - )); - } - } - - @override - Future close() async { - await super.close(); - _webSocketMessageSubscription?.cancel().ignore(); - } -} - -/// Mixin responsible for spinify subscriptions. -base mixin SpinifyWSPBSubscription - on SpinifyWSPBTransportBase, SpinifyWSPBSenderMixin { - @override - Future subscribe( - String channel, - SpinifySubscriptionConfig config, - SpinifyStreamPosition? since, - ) async { - if (!state.isConnected) { - throw SpinifySubscriptionException( - channel: channel, - message: 'Spinify client is not connected', - ); - } - final request = pb.SubscribeRequest() - ..channel = channel - ..positioned = config.positioned - ..recoverable = config.recoverable - ..joinLeave = config.joinLeave; - final token = await config.getToken?.call(); - assert( - token == null || token.length > 5, - 'Spinify Subscription JWT is too short', - ); - if (token != null && token.isNotEmpty) request.token = token; - final data = await config.getPayload?.call(); - if (data != null) request.data = data; - if (since != null) { - request - ..recover = true - ..offset = since.offset - ..epoch = since.epoch; - } else { - request.recover = false; - } - final pb.SubscribeResult result; - try { - result = await _sendMessage(request, pb.SubscribeResult()) - .timeout(config.timeout); - } on TimeoutException { - disconnect( - DisconnectCode.timeout.code, - 'Timeout while subscribing to channel $channel', - ).ignore(); - rethrow; - } on Object catch (error, stackTrace) { - Error.throwWithStackTrace( - SpinifySubscriptionException( - channel: channel, - message: 'Error while making subscribe request', - error: error, - ), - stackTrace, - ); - } - final now = DateTime.now(); - final publicationDecoder = $publicationDecode(channel); - final publications = result.publications.isEmpty - ? _emptyPublicationsList - : UnmodifiableListView( - result.publications - .map(publicationDecoder) - .toList(growable: false), - ); - final recoverable = result.hasRecoverable() && result.recoverable; - final expires = result.hasExpires() && result.expires && result.hasTtl(); - return SubcibedOnChannel( - channel: channel, - expires: expires, - ttl: expires ? now.add(Duration(seconds: result.ttl)) : null, - recoverable: recoverable, - since: recoverable && result.hasOffset() && result.hasEpoch() - ? (offset: result.offset, epoch: result.epoch) - : null, - publications: publications, - recovered: result.hasRecovered() && result.recovered, - positioned: result.hasPositioned() && result.positioned, - wasRecovering: result.hasWasRecovering() && result.wasRecovering, - data: result.hasData() ? result.data : null, - ); - } - - @override - Future unsubscribe( - String channel, - SpinifySubscriptionConfig config, - ) async { - if (_webSocket.state.readyState.isDisconnecting || - _webSocket.state.readyState.isClosed) { - // Disconnected - do nothing. - return; - } - final request = pb.UnsubscribeRequest()..channel = channel; - await _sendMessage(request, pb.UnsubscribeResult()).timeout(config.timeout); - } - - @override - Future publish(String channel, List data) => _sendMessage( - pb.PublishRequest() - ..channel = channel - ..data = data, - pb.PublishResult()); - - @override - Future history( - String channel, { - int? limit, - SpinifyStreamPosition? since, - bool? reverse, - }) async { - final request = pb.HistoryRequest()..channel = channel; - if (limit != null) request.limit = limit; - if (reverse != null) request.reverse = reverse; - if (since != null) { - request.since = pb.StreamPosition() - ..offset = since.offset - ..epoch = since.epoch; - } - final result = await _sendMessage(request, pb.HistoryResult()); - final publicationDecoder = $publicationDecode(channel); - return SpinifyHistory( - publications: result.publications.isEmpty - ? _emptyPublicationsList - : UnmodifiableListView( - result.publications - .map(publicationDecoder) - .toList(growable: false), - ), - since: (epoch: result.epoch, offset: result.offset), - ); - } - - @override - Future presence(String channel) => - _sendMessage(pb.PresenceRequest()..channel = channel, pb.PresenceResult()) - .then( - (r) => SpinifyPresence( - channel: channel, - clients: UnmodifiableMapView( - { - for (final e in r.presence.entries) - e.key: SpinifyClientInfo( - user: e.value.user, - client: e.value.client, - channelInfo: e.value.hasChanInfo() ? e.value.chanInfo : null, - connectionInfo: - e.value.hasConnInfo() ? e.value.connInfo : null, - ) - }, - ), - ), - ); - - @override - Future presenceStats(String channel) => _sendMessage( - pb.PresenceStatsRequest()..channel = channel, - pb.PresenceStatsResult()) - .then( - (r) => SpinifyPresenceStats( - channel: channel, - clients: r.hasNumClients() ? r.numClients : 0, - users: r.hasNumUsers() ? r.numUsers : 0, - ), - ); - - @override - Future sendSubRefresh( - String channel, - String token, - ) => - _sendMessage( - pb.SubRefreshRequest() - ..channel = channel - ..token = token, - pb.SubRefreshResult()) - .then( - (r) { - final expires = r.hasExpires() && r.expires && r.hasTtl(); - return SpinifySubRefreshResult( - expires: expires, - ttl: expires ? DateTime.now().add(Duration(seconds: r.ttl)) : null, - ); - }, - ); - - @override - Future> rpc(String method, List data) => _sendMessage( - pb.RPCRequest() - ..method = method - ..data = data, - pb.RPCResult()) - .then>((r) => r.hasData() ? r.data : const []); -} - -/// To maintain connection alive and detect broken connections -/// server periodically sends empty commands to clients -/// and expects empty replies from them. -/// -/// When client does not receive ping from a server for some -/// time it can consider connection broken and try to reconnect. -/// Usually a server sends pings every 25 seconds. -base mixin SpinifyWSPBPingPongMixin on SpinifyWSPBTransportBase { - @protected - @nonVirtual - Timer? _pingTimer; - - @override - Future connect( - String url, - ServerSubscriptionManager serverSubscriptionManager, - ) async { - _tearDownPingTimer(); - await super.connect(url, serverSubscriptionManager); - _restartPingTimer(); - } - - /// Start or restart keepalive timer, - /// you should restart it after each received ping message. - /// Or connection will be closed by timeout. - @protected - @nonVirtual - void _restartPingTimer() { - _tearDownPingTimer(); - if (state case SpinifyState$Connected(:Duration pingInterval)) { - _pingTimer = Timer( - pingInterval + _config.serverPingDelay, - () => disconnect( - DisconnectCode.badProtocol.code, - 'No ping from server', - ), - ); - } - } - - /// Stop keepalive timer. - @protected - @nonVirtual - void _tearDownPingTimer() => _pingTimer?.cancel(); - - @override - Future close() async { - _tearDownPingTimer(); - return super.close(); - } -} - -final List _emptyPublicationsList = - List.empty(growable: false); - -/// Decode protobuf messages to Spinify models. -SpinifyPublication Function(pb.Publication publication) $publicationDecode( - String channel, -) { - final timestamp = DateTime.now(); - return (publication) => SpinifyPublication( - timestamp: timestamp, - channel: channel, - offset: publication.hasOffset() ? publication.offset : null, - data: publication.data, - tags: publication.tags, - info: - publication.hasInfo() ? $decodeClientInfo(publication.info) : null, - ); -} - -/// Decode protobuf client info to Spinify client info. -SpinifyClientInfo $decodeClientInfo(pb.ClientInfo info) => SpinifyClientInfo( - client: info.client, - user: info.user, - channelInfo: info.hasChanInfo() ? info.chanInfo : null, - connectionInfo: info.hasConnInfo() ? info.connInfo : null, - ); diff --git a/lib/src.old/util/event_queue.dart b/lib/src.old/util/event_queue.dart deleted file mode 100644 index 85c4ef4..0000000 --- a/lib/src.old/util/event_queue.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'logger.dart'; - -/// Event Queue -final class SpinifyEventQueue { - /// Create a new instance of [SpinifyEventQueue]. - SpinifyEventQueue(); - - final DoubleLinkedQueue> _queue = - DoubleLinkedQueue>(); - Future? _processing; - bool _isClosed = false; - - /// Push it at the end of the queue. - Future push(String id, FutureOr Function() fn) { - final task = SpinifyTask(id, fn); - _queue.add(task); - _exec(); - return task.future; - } - - /// Mark the queue as closed. - /// The queue will be processed until it's empty. - /// But all new and current events will be rejected with [WSClientClosed]. - FutureOr close() async { - _isClosed = true; - await _processing; - } - - /// Execute the queue. - void _exec() => _processing ??= Future.doWhile(() async { - final event = _queue.first; - try { - if (_isClosed) { - event.reject( - StateError('Event Queue already closed'), - StackTrace.current, - ); - } else { - await event(); - } - } on Object catch (error, stackTrace) { - warning( - error, - stackTrace, - 'Error while processing event "${event.id}"', - ); - Future.sync(() => event.reject(error, stackTrace)).ignore(); - } - if (_queue.isEmpty) { - _processing = null; - return false; - } else { - _queue.removeFirst(); - final isEmpty = _queue.isEmpty; - if (isEmpty) _processing = null; - return !isEmpty; - } - }); -} - -/// Task for the [SpinifyEventQueue]. -class SpinifyTask { - /// Create a new instance of [SpinifyTask]. - SpinifyTask(this.id, FutureOr Function() fn) - : _fn = fn, - _completer = Completer(); - - final Completer _completer; - - /// Unique identifier for the task. - final String id; - - final FutureOr Function() _fn; - - /// Future of the task. - Future get future => _completer.future; - - /// Execute the task. - FutureOr call() async { - final result = await _fn(); - if (!_completer.isCompleted) { - _completer.complete(result); - } - return result; - } - - /// Reject the task with [error] and [stackTrace]. - void reject(Object error, [StackTrace? stackTrace]) { - if (_completer.isCompleted) return; // coverage:ignore-line - _completer.completeError(error, stackTrace); - } -} diff --git a/lib/src.old/util/logger.dart b/lib/src.old/util/logger.dart deleted file mode 100644 index a163a6e..0000000 --- a/lib/src.old/util/logger.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:async'; -import 'dart:developer' as developer; - -import 'package:meta/meta.dart'; - -/// Constants used to debug the Spinify client. -/// --dart-define=dev.plugfox.spinify.debug=true -bool get $enableLogging => - const bool.fromEnvironment( - 'dev.plugfox.spinify.log', - defaultValue: false, - ) || - Zone.current[#dev.plugfox.spinify.log] == true; - -/// Tracing information -@internal -final void Function(Object? message) fine = _logAll('FINE', 500); - -/// Static configuration messages -@internal -final void Function(Object? message) config = _logAll('CONF', 700); - -/// Iformational messages -@internal -final void Function(Object? message) info = _logAll('INFO', 800); - -/// Potential problems -@internal -final void Function(Object exception, [StackTrace? stackTrace, String? reason]) - warning = _logAll('WARN', 900); - -/// Serious failures -@internal -final void Function(Object error, [StackTrace stackTrace, String? reason]) - severe = _logAll('ERR!', 1000); - -void Function( - Object? message, [ - StackTrace? stackTrace, - String? reason, -]) _logAll(String prefix, int level) => (message, [stackTrace, reason]) { - // coverage:ignore-start - if (!$enableLogging) return; - developer.log( - reason ?? message?.toString() ?? '', - level: level, - name: 'spinify', - error: message is Exception || message is Error ? message : null, - stackTrace: stackTrace, - ); - // coverage:ignore-end - }; diff --git a/lib/src.old/util/notifier.dart b/lib/src.old/util/notifier.dart deleted file mode 100644 index f2d73fb..0000000 --- a/lib/src.old/util/notifier.dart +++ /dev/null @@ -1,34 +0,0 @@ -/// Notify about value changes. -typedef ValueChanged = void Function(T value); - -/// Notify about value changes. -abstract interface class SpinifyListenable { - /// Add listener. - void addListener(ValueChanged listener); - - /// Remove listener. - void removeListener(ValueChanged listener); -} - -/// Notify about value changes. -final class SpinifyChangeNotifier implements SpinifyListenable { - /// Notify about value changes. - SpinifyChangeNotifier(); - - /// Notify about value changes. - void notify(T value) { - for (var i = 0; i < _listeners.length; i++) _listeners[i](value); - } - - /// Listeners. - final List> _listeners = >[]; - - @override - void addListener(ValueChanged listener) => _listeners.add(listener); - - @override - void removeListener(ValueChanged listener) => _listeners.remove(listener); - - /// Close notifier. - void close() => _listeners.clear(); -} diff --git a/lib/src.old/util/speed_meter.dart b/lib/src.old/util/speed_meter.dart deleted file mode 100644 index 905efb5..0000000 --- a/lib/src.old/util/speed_meter.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:math' as math; - -/// Speed meter -class SpinifySpeedMeter { - /// Speed meter - SpinifySpeedMeter(this.size) : _speeds = List.filled(size, 0); - - /// Size of the speed meter - final int size; - final List _speeds; - int _pointer = 0; - int _count = 0; - - /// Add new speed in ms - void add(num speed) { - _speeds[_pointer] = speed.toInt(); - _pointer = (_pointer + 1) % size; - if (_count < size) _count++; - } - - /// Get speed in ms - ({int min, int avg, int max}) get speed { - if (_count == 0) return (min: 0, avg: 0, max: 0); - var sum = _speeds.first, min = sum, max = sum; - for (final value in _speeds) { - min = math.min(min, value); - max = math.max(max, value); - sum += value; - } - return (min: min, avg: sum ~/ _count, max: max); - } -} diff --git a/lib/src/model/annotations.dart b/lib/src/model/annotations.dart new file mode 100644 index 0000000..e92ccfe --- /dev/null +++ b/lib/src/model/annotations.dart @@ -0,0 +1,28 @@ +import 'package:meta/meta.dart'; + +/// Method used manually by the user. +@internal +const SpinifyAnnotation interactive = SpinifyAnnotation('interactive'); + +/// Method used by the side effect. +@internal +const SpinifyAnnotation sideEffect = SpinifyAnnotation('sideEffect'); + +// TODO(plugfox): add more annotations + +/// Annotation for Spinify library. +@internal +@immutable +final class SpinifyAnnotation { + @literal + const SpinifyAnnotation( + this.name, { + this.meta = const {}, + }); + + /// Annotation name. + final String name; + + /// Annotation metadata. + final Map meta; +} diff --git a/lib/src/model/channel_push.dart b/lib/src/model/channel_event.dart similarity index 51% rename from lib/src/model/channel_push.dart rename to lib/src/model/channel_event.dart index 2d34c8e..eb07773 100644 --- a/lib/src/model/channel_push.dart +++ b/lib/src/model/channel_event.dart @@ -27,6 +27,98 @@ sealed class SpinifyChannelEvent implements Comparable { /// Push type. abstract final String type; + /// Map this event to a value of type [T]. + T map({ + required T Function(SpinifyPublication event) publication, + required T Function(SpinifyPresence event) presence, + required T Function(SpinifyUnsubscribe event) unsubscribe, + required T Function(SpinifyMessage event) message, + required T Function(SpinifySubscribe event) subscribe, + required T Function(SpinifyConnect event) connect, + required T Function(SpinifyDisconnect event) disconnect, + required T Function(SpinifyRefresh event) refresh, + }) => + switch (this) { + SpinifyPublication event => publication(event), + SpinifyPresence event => presence(event), + SpinifyUnsubscribe event => unsubscribe(event), + SpinifyMessage event => message(event), + SpinifySubscribe event => subscribe(event), + SpinifyConnect event => connect(event), + SpinifyDisconnect event => disconnect(event), + SpinifyRefresh event => refresh(event), + }; + + /// Map this event to a value of type [T]. + T maybeMap({ + required T Function(SpinifyChannelEvent event) orElse, + T Function(SpinifyPublication event)? publication, + T Function(SpinifyPresence event)? presence, + T Function(SpinifyUnsubscribe event)? unsubscribe, + T Function(SpinifyMessage event)? message, + T Function(SpinifySubscribe event)? subscribe, + T Function(SpinifyConnect event)? connect, + T Function(SpinifyDisconnect event)? disconnect, + T Function(SpinifyRefresh event)? refresh, + }) => + map( + publication: (event) => publication?.call(event) ?? orElse(event), + presence: (event) => presence?.call(event) ?? orElse(event), + unsubscribe: (event) => unsubscribe?.call(event) ?? orElse(event), + message: (event) => message?.call(event) ?? orElse(event), + subscribe: (event) => subscribe?.call(event) ?? orElse(event), + connect: (event) => connect?.call(event) ?? orElse(event), + disconnect: (event) => disconnect?.call(event) ?? orElse(event), + refresh: (event) => refresh?.call(event) ?? orElse(event), + ); + + /// Map this event to a value of type [T] or return `null`. + T? mapOrNull({ + T Function(SpinifyPublication event)? publication, + T Function(SpinifyPresence event)? presence, + T Function(SpinifyUnsubscribe event)? unsubscribe, + T Function(SpinifyMessage event)? message, + T Function(SpinifySubscribe event)? subscribe, + T Function(SpinifyConnect event)? connect, + T Function(SpinifyDisconnect event)? disconnect, + T Function(SpinifyRefresh event)? refresh, + }) => + maybeMap( + orElse: (event) => null, + publication: publication, + presence: presence, + unsubscribe: unsubscribe, + message: message, + subscribe: subscribe, + connect: connect, + disconnect: disconnect, + refresh: refresh, + ); + + /// Whether this is a publication event + abstract final bool isPublication; + + /// Whether this is a presence event + abstract final bool isPresence; + + /// Whether this is an unsubscribe event + abstract final bool isUnsubscribe; + + /// Whether this is a message event + abstract final bool isMessage; + + /// Whether this is a subscribe event + abstract final bool isSubscribe; + + /// Whether this is a connect event + abstract final bool isConnect; + + /// Whether this is a disconnect event + abstract final bool isDisconnect; + + /// Whether this is a refresh event + abstract final bool isRefresh; + @override int compareTo(SpinifyChannelEvent other) => timestamp.compareTo(other.timestamp); @@ -67,6 +159,43 @@ final class SpinifyPublication extends SpinifyChannelEvent { /// Optional tags, this is a map with string keys and string values final Map? tags; + + /// Copy this publication with a new channel. + SpinifyPublication copyWith({required String channel}) => + channel == this.channel + ? this + : SpinifyPublication( + timestamp: timestamp, + channel: channel, + data: data, + offset: offset, + info: info, + tags: tags, + ); + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => true; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } /// {@template channel_presence} @@ -112,6 +241,30 @@ sealed class SpinifyPresence extends SpinifyChannelEvent { /// Publications //abstract final Map clients; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => true; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } /// Join event @@ -185,6 +338,30 @@ final class SpinifyUnsubscribe extends SpinifyChannelEvent { /// Reason of unsubscribe. final String reason; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => true; } /// {@template message} @@ -206,6 +383,30 @@ final class SpinifyMessage extends SpinifyChannelEvent { /// Payload of message. final List data; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => true; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } /// {@template subscribe} @@ -238,7 +439,31 @@ final class SpinifySubscribe extends SpinifyChannelEvent { final bool positioned; /// Data attached to subscription. - final List data; + final List? data; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => true; + + @override + bool get isUnsubscribe => false; } /// {@template connect} @@ -294,7 +519,31 @@ final class SpinifyConnect extends SpinifyChannelEvent { final String? node; /// Payload of connected push. - final List data; + final List? data; + + @override + bool get isConnect => true; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } /// {@template disconnect} @@ -317,6 +566,24 @@ final class SpinifyDisconnect extends SpinifyChannelEvent { String get type => 'Disconnect'; /// Code of disconnect. + /// Codes have some rules which should be followed by a client + /// connector implementation. + /// These rules described below. + /// + /// Codes in range 0-2999 should not be used by a Centrifuge library user. + /// Those are reserved for the client-side and transport specific needs. + /// Codes in range >=5000 should not be used also. + /// Those are reserved by Centrifuge. + /// + /// Client should reconnect upon receiving code in range + /// 3000-3499, 4000-4499, >=5000. + /// For codes <3000 reconnect behavior can be adjusted for specific transport. + /// + /// Codes in range 3500-3999 and 4500-4999 are application terminal codes, + /// no automatic reconnect should be made by a client implementation. + /// + /// Library users supposed to use codes in range 4000-4999 for creating custom + /// disconnects. final int code; /// Reason of disconnect. @@ -324,6 +591,30 @@ final class SpinifyDisconnect extends SpinifyChannelEvent { /// Reconnect flag. final bool reconnect; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => true; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => false; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } /// {@template refresh} @@ -349,4 +640,28 @@ final class SpinifyRefresh extends SpinifyChannelEvent { /// Time when connection will be expired final DateTime? ttl; + + @override + bool get isConnect => false; + + @override + bool get isDisconnect => false; + + @override + bool get isMessage => false; + + @override + bool get isPresence => false; + + @override + bool get isPublication => false; + + @override + bool get isRefresh => true; + + @override + bool get isSubscribe => false; + + @override + bool get isUnsubscribe => false; } diff --git a/lib/src/model/channel_events.dart b/lib/src/model/channel_events.dart new file mode 100644 index 0000000..e0c6450 --- /dev/null +++ b/lib/src/model/channel_events.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'channel_event.dart'; + +/// Stream of received pushes from Centrifugo server for a channels. +/// {@category Event} +/// {@category Client} +/// {@category Subscription} +/// {@subCategory Push} +/// {@subCategory Channel} +extension type SpinifyChannelEvents(Stream _) + implements Stream { + /// Stream of publication events. + SpinifyChannelEvents publication({String? channel}) => + filter(channel: channel); + + /// Stream of presence events. + SpinifyChannelEvents presence({String? channel}) => + filter(channel: channel); + + /// Stream of unsubscribe events. + SpinifyChannelEvents unsubscribe({String? channel}) => + filter(channel: channel); + + /// Stream of message events. + SpinifyChannelEvents message({String? channel}) => + filter(channel: channel); + + /// Stream of subscribe events. + SpinifyChannelEvents subscribe({String? channel}) => + filter(channel: channel); + + /// Stream of connect events. + SpinifyChannelEvents connect({String? channel}) => + filter(channel: channel); + + /// Stream of disconnect events. + SpinifyChannelEvents disconnect({String? channel}) => + filter(channel: channel); + + /// Stream of refresh events. + SpinifyChannelEvents refresh({String? channel}) => + filter(channel: channel); + + /// Filtered stream of [SpinifyChannelEvent]. + SpinifyChannelEvents filter( + {String? channel}) => + SpinifyChannelEvents(transform(StreamTransformer.fromHandlers( + handleData: (data, sink) => switch (data) { + S valid when channel == null || valid.channel == channel => + sink.add(valid), + _ => null, + }, + ))); +} diff --git a/lib/src/model/command.dart b/lib/src/model/command.dart index ee5885b..eb7b122 100644 --- a/lib/src/model/command.dart +++ b/lib/src/model/command.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:fixnum/fixnum.dart'; import 'package:meta/meta.dart'; @@ -121,7 +119,7 @@ final class SpinifySubscribeRequest extends SpinifyCommand { /// Subscription data /// (attached to every subscribe/resubscribe request) - final Uint8List? data; + final List? data; /// Option to ask server to make subscription positioned /// (if not forced by a server) @@ -169,7 +167,7 @@ final class SpinifyPublishRequest extends SpinifyCommand { final String channel; /// Data to publish. - final Uint8List data; + final List data; } /// {@macro command} diff --git a/lib/src/model/config.dart b/lib/src/model/config.dart index 6982aee..02e07ac 100644 --- a/lib/src/model/config.dart +++ b/lib/src/model/config.dart @@ -19,7 +19,7 @@ typedef SpinifyToken = String; /// /// {@category Client} /// {@category Entity} -typedef SpinifyTokenCallback = FutureOr Function(); +typedef SpinifyTokenCallback = Future Function(); /// Callback to get initial connection payload data. /// @@ -27,7 +27,7 @@ typedef SpinifyTokenCallback = FutureOr Function(); /// /// {@category Client} /// {@category Entity} -typedef SpinifyConnectionPayloadCallback = FutureOr?> Function(); +typedef SpinifyConnectionPayloadCallback = Future?> Function(); /// Log level for logger extension type const SpinifyLogLevel._(int level) implements int { @@ -121,6 +121,9 @@ extension type const SpinifyLogLevel._(int level) implements int { error: error, critical: critical, ); + + /// If log level is warning or higher + bool get isError => level > 3; } /// Logger function to use for logging. @@ -144,6 +147,89 @@ typedef SpinifyLogger = void Function( Map context, ); +/// {@template spinify_log_buffer} +/// Circular buffer for storing log entries. +/// {@endtemplate} +final class SpinifyLogBuffer { + /// {@macro spinify_log_buffer} + SpinifyLogBuffer({ + this.size = 1000, + }) : _logs = List.filled(size.clamp(0, 100000), null, growable: false); + + /// The maximum number of log entries to keep in the buffer. + final int size; + + /// The number of log entries currently in the buffer. + int get length => _length; + + /// Whether the buffer is empty. + bool get isEmpty => _logs.first == null; + + /// Whether the buffer is full. + bool get isFull => _logs.last != null; + + int _index = 0; + int _length = 0; + + final List< + ({ + SpinifyLogLevel level, + String event, + String message, + Map context + })?> _logs; + + /// Get all log entries from the buffer. + List< + ({ + SpinifyLogLevel level, + String event, + String message, + Map context + })> get logs { + if (_logs.last == null) { + return _logs.sublist(0, _index).cast(); + } else if (_index == 0) { + return _logs.cast(); + } else { + return [ + ..._logs.sublist(_index), + ..._logs.sublist(0, _index), + ].cast(); + } + } + + /// Add a log entry to the buffer. + void add({ + required SpinifyLogLevel level, + required String event, + required String message, + required Map context, + }) { + _logs[_index] = ( + level: level, + event: event, + message: message, + context: context, + ); + if (_length < size) { + _length++; + _index++; + } else { + _index = (_index + 1) % size; + } + } + + /// Clear all log entries from the buffer. + void clear() { + for (var i = 0; i < _length; i++) { + _logs[i] = null; + } + _length = 0; + _index = 0; + } +} + /// {@template spinify_config} /// Spinify client common options. /// diff --git a/lib/src/model/history.dart b/lib/src/model/history.dart index b9e46ff..791ce90 100644 --- a/lib/src/model/history.dart +++ b/lib/src/model/history.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'channel_push.dart'; +import 'channel_event.dart'; import 'stream_position.dart'; /// {@template history} diff --git a/lib/src/model/metric.dart b/lib/src/model/metric.dart index 6054141..874b789 100644 --- a/lib/src/model/metric.dart +++ b/lib/src/model/metric.dart @@ -1,6 +1,8 @@ +import 'package:fixnum/fixnum.dart' as fixnum; import 'package:meta/meta.dart'; import 'state.dart'; +import 'subscription_state.dart'; /* /// Subscription count @@ -41,16 +43,16 @@ sealed class SpinifyMetrics implements Comparable { abstract final SpinifyState state; /// The total number of bytes sent. - abstract final BigInt bytesSent; + abstract final fixnum.Int64 bytesSent; /// The total number of bytes received. - abstract final BigInt bytesReceived; + abstract final fixnum.Int64 bytesReceived; /// The total number of messages sent. - abstract final BigInt messagesSent; + abstract final fixnum.Int64 messagesSent; /// The total number of messages received. - abstract final BigInt messagesReceived; + abstract final fixnum.Int64 messagesReceived; /* /// The number of subscriptions. @@ -67,7 +69,7 @@ sealed class SpinifyMetrics implements Comparable { /// Is refresh active. final bool isRefreshActive; - */ + */ /// The total number of successful connections. abstract final int connects; @@ -93,8 +95,38 @@ sealed class SpinifyMetrics implements Comparable { /// The time of the last disconnect. abstract final DateTime? lastDisconnectAt; + /// The last received ping at. + abstract final DateTime? lastPingAt; + + /// Pings count. + abstract final fixnum.Int64 receivedPings; + + /// Metrics of all channels. + abstract final Map channels; + /// Convert metrics to JSON. - Map toJson() => {}; + Map toJson() => { + 'timestamp': timestamp.toUtc().toIso8601String(), + 'initializedAt': initializedAt.toUtc().toIso8601String(), + 'commandId': commandId, + 'state': state, + 'bytesSent': bytesSent.toString(), + 'bytesReceived': bytesReceived.toString(), + 'messagesSent': messagesSent.toString(), + 'messagesReceived': messagesReceived.toString(), + 'connects': connects, + 'lastConnectAt': lastConnectAt?.toUtc().toIso8601String(), + 'reconnectUrl': reconnectUrl, + 'reconnectAttempts': reconnectAttempts, + 'nextReconnectAt': nextReconnectAt?.toUtc().toIso8601String(), + 'disconnects': disconnects, + 'lastDisconnectAt': lastDisconnectAt?.toUtc().toIso8601String(), + 'lastPingAt': lastPingAt?.toUtc().toIso8601String(), + 'receivedPings': receivedPings.toString(), + 'channels': { + for (final entry in channels.entries) entry.key: entry.value.toJson(), + }, + }; @override int compareTo(SpinifyMetrics other) => timestamp.compareTo(other.timestamp); @@ -103,6 +135,64 @@ sealed class SpinifyMetrics implements Comparable { String toString() => 'SpinifyMetrics{}'; } +/// {@template metrics_channel} +/// Metrics of Spinify channel. +/// {@endtemplate} +/// +/// {@category Metrics} +sealed class SpinifyMetrics$Channel { + /// {@macro metrics_channel} + const SpinifyMetrics$Channel(); + + /// The current state of the channel. + abstract final SpinifySubscriptionState state; + + /// The total number of publications sent. + abstract final fixnum.Int64 publicationsSent; + + /// The total number of publications received. + abstract final fixnum.Int64 publicationsReceived; + + /// The total number of successful subscriptions. + abstract final int subscribes; + + /// The time of the last subscribe. + abstract final DateTime? lastSubscribeAt; + + /// Number of reconnect attempts. + /// If null, then client is not connected yet or interractively resubscribed. + abstract final int? resubscribeAttempts; + + /// Next reconnect time in case of connection lost. + abstract final DateTime? nextResubscribeAt; + + /// The total number of times the connection has been unsubscribed. + abstract final int unsubscribes; + + /// The time of the last unsubscribe. + abstract final DateTime? lastUnsubscribeAt; + + /// The time of the next token refresh. + abstract final DateTime? ttl; + + /// Convert channel metrics to JSON. + Map toJson() => { + 'state': state, + 'publicationsSent': publicationsSent.toString(), + 'publicationsReceived': publicationsReceived.toString(), + 'subscribes': subscribes, + 'lastSubscribeAt': lastSubscribeAt?.toUtc().toIso8601String(), + 'resubscribeAttempts': resubscribeAttempts, + 'nextResubscribeAt': nextResubscribeAt?.toUtc().toIso8601String(), + 'unsubscribes': unsubscribes, + 'lastUnsubscribeAt': lastUnsubscribeAt?.toUtc().toIso8601String(), + 'ttl': ttl?.toUtc().toIso8601String(), + }; + + @override + String toString() => r'SpinifyMetrics$Channel{}'; +} + /// {@macro metrics} @immutable final class SpinifyMetrics$Immutable extends SpinifyMetrics { @@ -123,6 +213,9 @@ final class SpinifyMetrics$Immutable extends SpinifyMetrics { required this.bytesSent, required this.messagesReceived, required this.messagesSent, + required this.lastPingAt, + required this.receivedPings, + required this.channels, }); @override @@ -159,16 +252,72 @@ final class SpinifyMetrics$Immutable extends SpinifyMetrics { final DateTime? lastDisconnectAt; @override - final BigInt bytesReceived; + final fixnum.Int64 bytesReceived; + + @override + final fixnum.Int64 bytesSent; + + @override + final fixnum.Int64 messagesReceived; + + @override + final fixnum.Int64 messagesSent; + + @override + final DateTime? lastPingAt; + + @override + final fixnum.Int64 receivedPings; + + @override + final Map channels; +} + +/// {@macro metrics_channel} +@immutable +final class SpinifyMetrics$Channel$Immutable extends SpinifyMetrics$Channel { + /// {@macro metrics_channel} + const SpinifyMetrics$Channel$Immutable({ + required this.state, + required this.publicationsSent, + required this.publicationsReceived, + required this.subscribes, + required this.lastSubscribeAt, + required this.resubscribeAttempts, + required this.nextResubscribeAt, + required this.unsubscribes, + required this.lastUnsubscribeAt, + required this.ttl, + }); + @override + final SpinifySubscriptionState state; + + @override + final fixnum.Int64 publicationsSent; + + @override + final fixnum.Int64 publicationsReceived; + + @override + final int subscribes; + + @override + final DateTime? lastSubscribeAt; + + @override + final int? resubscribeAttempts; + + @override + final DateTime? nextResubscribeAt; @override - final BigInt bytesSent; + final int unsubscribes; @override - final BigInt messagesReceived; + final DateTime? lastUnsubscribeAt; @override - final BigInt messagesSent; + final DateTime? ttl; } /// {@macro metrics} @@ -210,16 +359,26 @@ final class SpinifyMetrics$Mutable extends SpinifyMetrics { DateTime? lastDisconnectAt; @override - BigInt bytesReceived = BigInt.zero; + fixnum.Int64 bytesReceived = fixnum.Int64.ZERO; @override - BigInt bytesSent = BigInt.zero; + fixnum.Int64 bytesSent = fixnum.Int64.ZERO; @override - BigInt messagesReceived = BigInt.zero; + fixnum.Int64 messagesReceived = fixnum.Int64.ZERO; @override - BigInt messagesSent = BigInt.zero; + fixnum.Int64 messagesSent = fixnum.Int64.ZERO; + + @override + DateTime? lastPingAt; + + @override + fixnum.Int64 receivedPings = fixnum.Int64.ZERO; + + @override + final Map channels = + {}; /// Freezes the metrics. SpinifyMetrics$Immutable freeze() => SpinifyMetrics$Immutable( @@ -238,5 +397,63 @@ final class SpinifyMetrics$Mutable extends SpinifyMetrics { bytesSent: bytesSent, messagesReceived: messagesReceived, messagesSent: messagesSent, + lastPingAt: lastPingAt, + receivedPings: receivedPings, + channels: Map.unmodifiable( + { + for (final entry in channels.entries) + entry.key: entry.value.freeze(), + }, + ), + ); +} + +/// {@macro metrics_channel} +final class SpinifyMetrics$Channel$Mutable extends SpinifyMetrics$Channel { + /// {@macro metrics_channel} + SpinifyMetrics$Channel$Mutable(); + + @override + SpinifySubscriptionState state = SpinifySubscriptionState$Unsubscribed(); + + @override + fixnum.Int64 publicationsSent = fixnum.Int64.ZERO; + + @override + fixnum.Int64 publicationsReceived = fixnum.Int64.ZERO; + + @override + int subscribes = 0; + + @override + DateTime? lastSubscribeAt; + + @override + int? resubscribeAttempts; + + @override + DateTime? nextResubscribeAt; + + @override + int unsubscribes = 0; + + @override + DateTime? lastUnsubscribeAt; + + @override + DateTime? ttl; + + /// Freezes the channel metrics. + SpinifyMetrics$Channel$Immutable freeze() => SpinifyMetrics$Channel$Immutable( + state: state, + publicationsSent: publicationsSent, + publicationsReceived: publicationsReceived, + subscribes: subscribes, + lastSubscribeAt: lastSubscribeAt, + resubscribeAttempts: resubscribeAttempts, + nextResubscribeAt: nextResubscribeAt, + unsubscribes: unsubscribes, + lastUnsubscribeAt: lastUnsubscribeAt, + ttl: ttl, ); } diff --git a/lib/src/model/pubspec.yaml.g.dart b/lib/src/model/pubspec.yaml.g.dart index 5113a50..42ffc33 100644 --- a/lib/src/model/pubspec.yaml.g.dart +++ b/lib/src/model/pubspec.yaml.g.dart @@ -124,13 +124,13 @@ sealed class Pubspec { /// Build date and time (UTC) static final DateTime timestamp = DateTime.utc( 2024, - 5, - 4, - 22, - 21, 6, - 924, - 885, + 12, + 10, + 22, + 54, + 467, + 344, ); /// Name @@ -440,6 +440,8 @@ sealed class Pubspec { 'benchmark_harness': r'^2.2.2', 'lints': r'^3.0.0', 'test': r'^1.24.4', + 'fake_async': r'^1.3.1', + 'mockito': r'^5.4.4', }; /// Dependency overrides diff --git a/lib/src/model/pushes_stream.dart b/lib/src/model/pushes_stream.dart deleted file mode 100644 index eca9063..0000000 --- a/lib/src/model/pushes_stream.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -import 'channel_push.dart'; - -/// Stream of received pushes from Centrifugo server for a channel. -/// {@category Event} -/// {@category Client} -/// {@category Subscription} -/// {@subCategory Push} -/// {@subCategory Channel} -@immutable -final class SpinifyPushesStream extends StreamView { - /// Stream of received events. - const SpinifyPushesStream({ - required Stream pushes, - required this.publications, - required this.messages, - required this.presenceEvents, - required this.joinEvents, - required this.leaveEvents, - }) : super(pushes); - - /// Publications stream. - final Stream publications; - - /// Messages stream. - final Stream messages; - - /// Stream of presence (join & leave) events. - final Stream presenceEvents; - - /// Join events - final Stream joinEvents; - - /// Leave events - final Stream leaveEvents; - - /// Filtered stream of data of [SpinifyEvent]. - Stream whereType() => - transform(StreamTransformer.fromHandlers( - handleData: (data, sink) => switch (data) { - T valid => sink.add(valid), - _ => null, - }, - )).asBroadcastStream(); - - @override - String toString() => 'SpinifyPushesStream{}'; -} diff --git a/lib/src/model/reply.dart b/lib/src/model/reply.dart index 6228f7d..c696728 100644 --- a/lib/src/model/reply.dart +++ b/lib/src/model/reply.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'channel_push.dart'; +import 'channel_event.dart'; import 'client_info.dart'; import 'command.dart'; import 'stream_position.dart'; @@ -264,8 +264,8 @@ final class SpinifyPresenceResult extends SpinifyReply @override String get type => 'PresenceResult'; - /// Presence - /// { Channel : ClientInfo } + /// Contains presence information - a map client IDs as keys + /// and client information as values. final Map presence; } @@ -298,6 +298,7 @@ final class SpinifyHistoryResult extends SpinifyReply required super.id, required super.timestamp, required this.since, + required this.publications, }); @override @@ -305,6 +306,9 @@ final class SpinifyHistoryResult extends SpinifyReply /// Offset final SpinifyStreamPosition since; + + /// Publications + final List publications; } /// {@macro reply} @@ -395,10 +399,10 @@ final class SpinifySubRefreshResult extends SpinifyReply /// For pushes it will have zero value. /// /// {@macro reply} -final class SpinifyError extends SpinifyReply +final class SpinifyErrorResult extends SpinifyReply with SpinifyReplyResult { /// {@macro reply} - const SpinifyError({ + const SpinifyErrorResult({ required super.id, required super.timestamp, required this.code, @@ -407,7 +411,7 @@ final class SpinifyError extends SpinifyReply }); @override - String get type => 'Error'; + String get type => 'ErrorResult'; /// Error code. final int code; diff --git a/lib/src/model/state.dart b/lib/src/model/state.dart index b702663..6f26ff7 100644 --- a/lib/src/model/state.dart +++ b/lib/src/model/state.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:meta/meta.dart'; /// {@template state} @@ -68,56 +66,8 @@ sealed class SpinifyState extends _$SpinifyStateBase { /// {@macro state} factory SpinifyState.closed({DateTime? timestamp}) = SpinifyState$Closed; - /// Restore state from JSON - /// {@macro state} - factory SpinifyState.fromJson(Map json) => switch (( - json['type']?.toString().trim().toLowerCase(), - json['timestamp'] ?? DateTime.now().microsecondsSinceEpoch, - json['url'], - )) { - ('disconnected', int timestamp, _) => SpinifyState.disconnected( - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - ), - ('connecting', int timestamp, String url) => SpinifyState.connecting( - url: url, - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - ), - ('connected', int timestamp, String url) => SpinifyState.connected( - url: url, - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - client: json['client']?.toString(), - version: json['version']?.toString(), - expires: switch (json['expires']) { - bool expires => expires, - _ => false, - }, - ttl: switch (json['ttl']) { - int ttl => DateTime.fromMicrosecondsSinceEpoch(ttl), - _ => null, - }, - pingInterval: switch (json['pingInterval']) { - int pingInterval => Duration(seconds: pingInterval), - _ => null, - }, - sendPong: switch (json['sendPong']) { - bool sendPong => sendPong, - _ => null, - }, - session: json['session']?.toString(), - node: json['node']?.toString(), - data: switch (json['data']) { - String data when data.isNotEmpty => base64Decode(data), - _ => null, - }, - ), - ('closed', int timestamp, _) => SpinifyState.closed( - timestamp: DateTime.fromMicrosecondsSinceEpoch(timestamp), - ), - _ => throw FormatException('Unknown state: $json'), - }; - - @override - String toString() => 'SpinifyState\$$type{}'; + @override + String toString() => type; } /// Disconnected @@ -137,7 +87,7 @@ final class SpinifyState$Disconnected extends SpinifyState { }) : super(timestamp ?? DateTime.now()); @override - String get type => 'Disconnected'; + String get type => 'disconnected'; @override String? get url => null; @@ -164,12 +114,7 @@ final class SpinifyState$Disconnected extends SpinifyState { disconnected(this); @override - Map toJson() => { - ...super.toJson(), - }; - - @override - int get hashCode => timestamp.millisecondsSinceEpoch * 10 + 0; + int get hashCode => 0 + timestamp.microsecondsSinceEpoch * 10; @override bool operator ==(Object other) => @@ -191,7 +136,7 @@ final class SpinifyState$Connecting extends SpinifyState { : super(timestamp ?? DateTime.now()); @override - String get type => 'Connecting'; + String get type => 'connecting'; @override final String url; @@ -218,7 +163,7 @@ final class SpinifyState$Connecting extends SpinifyState { connecting(this); @override - int get hashCode => timestamp.millisecondsSinceEpoch * 10 + 1; + int get hashCode => 1 + timestamp.microsecondsSinceEpoch * 10; @override bool operator ==(Object other) => @@ -251,7 +196,7 @@ final class SpinifyState$Connected extends SpinifyState { }) : super(timestamp ?? DateTime.now()); @override - String get type => 'Connected'; + String get type => 'connected'; @override final String url; @@ -308,21 +253,7 @@ final class SpinifyState$Connected extends SpinifyState { connected(this); @override - Map toJson() => { - ...super.toJson(), - if (client != null) 'client': client, - if (version != null) 'version': version, - 'expires': expires, - if (ttl != null) 'ttl': ttl?.microsecondsSinceEpoch, - if (pingInterval != null) 'pingInterval': pingInterval?.inSeconds, - if (sendPong != null) 'sendPong': sendPong, - if (session != null) 'session': session, - if (node != null) 'node': node, - if (data != null) 'data': base64Encode(data!), - }; - - @override - int get hashCode => timestamp.millisecondsSinceEpoch * 10 + 2; + int get hashCode => 2 + timestamp.microsecondsSinceEpoch * 10; @override bool operator ==(Object other) => @@ -344,7 +275,7 @@ final class SpinifyState$Closed extends SpinifyState { : super(timestamp ?? DateTime.now()); @override - String get type => 'Closed'; + String get type => 'closed'; @override String? get url => null; @@ -371,7 +302,7 @@ final class SpinifyState$Closed extends SpinifyState { closed(this); @override - int get hashCode => timestamp.millisecondsSinceEpoch * 10 + 3; + int get hashCode => 3 + timestamp.microsecondsSinceEpoch * 10; @override bool operator ==(Object other) => @@ -445,10 +376,4 @@ abstract base class _$SpinifyStateBase { connected: connected ?? (_) => null, closed: closed ?? (_) => null, ); - - Map toJson() => { - 'type': type, - 'timestamp': timestamp.toUtc().toIso8601String(), - if (url != null) 'url': url, - }; } diff --git a/lib/src/model/subscription_config.dart b/lib/src/model/subscription_config.dart index 259e98a..43e2a9b 100644 --- a/lib/src/model/subscription_config.dart +++ b/lib/src/model/subscription_config.dart @@ -13,7 +13,7 @@ typedef SpinifySubscriptionToken = String; /// If method returns null then subscription will be established without token. /// {@category Subscription} /// {@category Entity} -typedef SpinifySubscriptionTokenCallback = FutureOr +typedef SpinifySubscriptionTokenCallback = Future Function(); /// Callback to set subscription payload data. @@ -22,7 +22,7 @@ typedef SpinifySubscriptionTokenCallback = FutureOr /// {@category Subscription} /// {@category Entity} -typedef SpinifySubscribePayloadCallback = FutureOr?> Function(); +typedef SpinifySubscribePayloadCallback = Future?> Function(); /// {@template subscription_config} /// Subscription common options @@ -61,7 +61,10 @@ class SpinifySubscriptionConfig { this.recoverable = false, this.joinLeave = false, this.timeout = const Duration(seconds: 15), - }); + }) : assert( + (recoverable == false && since == null) || + (recoverable == true && since != null), + 'recoverable and since must be set together'); /// Create a default config /// diff --git a/lib/src/model/subscription_state.dart b/lib/src/model/subscription_state.dart index 05d9096..4df305f 100644 --- a/lib/src/model/subscription_state.dart +++ b/lib/src/model/subscription_state.dart @@ -1,8 +1,5 @@ -import 'package:fixnum/fixnum.dart' as fixnum; import 'package:meta/meta.dart'; -import 'stream_position.dart'; - /// {@template subscription_state} /// Subscription has 3 states: /// @@ -17,37 +14,32 @@ import 'stream_position.dart'; @immutable sealed class SpinifySubscriptionState extends _$SpinifySubscriptionStateBase { /// {@macro subscription_state} - const SpinifySubscriptionState( - {required super.timestamp, - required super.since, - required super.recoverable}); + const SpinifySubscriptionState({required super.timestamp}); /// Unsubscribed /// {@macro subscription_state} factory SpinifySubscriptionState.unsubscribed({ - required int code, - required String reason, DateTime? timestamp, - SpinifyStreamPosition? since, - bool recoverable, }) = SpinifySubscriptionState$Unsubscribed; /// Subscribing /// {@macro subscription_state} factory SpinifySubscriptionState.subscribing({ DateTime? timestamp, - SpinifyStreamPosition? since, - bool recoverable, }) = SpinifySubscriptionState$Subscribing; /// Subscribed /// {@macro subscription_state} factory SpinifySubscriptionState.subscribed({ + List? data, DateTime? timestamp, - SpinifyStreamPosition? since, - bool recoverable, - DateTime? ttl, }) = SpinifySubscriptionState$Subscribed; + + /// Converts this state to JSON. + Map toJson(); + + @override + String toString() => type; } /// Unsubscribed state @@ -59,22 +51,12 @@ final class SpinifySubscriptionState$Unsubscribed extends SpinifySubscriptionState { /// {@macro subscription_state} SpinifySubscriptionState$Unsubscribed({ - required this.code, - required this.reason, DateTime? timestamp, - super.since, - super.recoverable = false, }) : super(timestamp: timestamp ?? DateTime.now()); @override String get type => 'unsubscribed'; - /// Unsubscribe code. - final int code; - - /// Unsubscribe reason. - final String reason; - @override bool get isUnsubscribed => true; @@ -99,20 +81,19 @@ final class SpinifySubscriptionState$Unsubscribed unsubscribed(this); @override - Map toJson() => { - ...super.toJson(), - 'code': code, - 'reason': reason, + Map toJson() => { + 'type': type, + 'timestamp': timestamp.toUtc().toIso8601String(), }; @override - int get hashCode => Object.hash(0, timestamp, since); - - @override - bool operator ==(Object other) => identical(this, other); + int get hashCode => 0 + timestamp.microsecondsSinceEpoch * 10; @override - String toString() => r'SpinifySubscriptionState$Unsubscribed{}'; + bool operator ==(Object other) => + identical(this, other) || + other is SpinifySubscriptionState$Unsubscribed && + other.timestamp.isAtSameMomentAs(timestamp); } /// Subscribing state @@ -125,8 +106,6 @@ final class SpinifySubscriptionState$Subscribing /// {@macro subscription_state} SpinifySubscriptionState$Subscribing({ DateTime? timestamp, - super.since, - super.recoverable = false, }) : super(timestamp: timestamp ?? DateTime.now()); @override @@ -156,13 +135,19 @@ final class SpinifySubscriptionState$Subscribing subscribing(this); @override - int get hashCode => Object.hash(1, timestamp, since); + Map toJson() => { + 'type': type, + 'timestamp': timestamp.toUtc().toIso8601String(), + }; @override - bool operator ==(Object other) => identical(this, other); + int get hashCode => 1 + timestamp.microsecondsSinceEpoch * 10; @override - String toString() => r'SpinifySubscriptionState$Subscribing{}'; + bool operator ==(Object other) => + identical(this, other) || + other is SpinifySubscriptionState$Subscribing && + other.timestamp.isAtSameMomentAs(timestamp); } /// Subscribed state @@ -174,18 +159,16 @@ final class SpinifySubscriptionState$Subscribed extends SpinifySubscriptionState { /// {@macro subscription_state} SpinifySubscriptionState$Subscribed({ + this.data, DateTime? timestamp, - super.since, - super.recoverable = false, - this.ttl, }) : super(timestamp: timestamp ?? DateTime.now()); + /// Data attached to current subscription. + final List? data; + @override String get type => 'subscribed'; - /// Time to live in seconds. - final DateTime? ttl; - @override bool get isUnsubscribed => false; @@ -210,19 +193,19 @@ final class SpinifySubscriptionState$Subscribed subscribed(this); @override - Map toJson() => { - ...super.toJson(), - if (ttl != null) 'ttl': ttl?.toUtc().toIso8601String(), + Map toJson() => { + 'type': type, + 'timestamp': timestamp.toUtc().toIso8601String(), }; @override - int get hashCode => Object.hash(2, timestamp, since, recoverable, ttl); - - @override - bool operator ==(Object other) => identical(this, other); + int get hashCode => 2 + timestamp.microsecondsSinceEpoch * 10; @override - String toString() => r'SpinifySubscriptionState$Subscribed{}'; + bool operator ==(Object other) => + identical(this, other) || + other is SpinifySubscriptionState$Subscribed && + other.timestamp.isAtSameMomentAs(timestamp); } /// Pattern matching for [SpinifySubscriptionState]. @@ -234,8 +217,6 @@ typedef SpinifySubscriptionStateMatch = R abstract base class _$SpinifySubscriptionStateBase { const _$SpinifySubscriptionStateBase({ required this.timestamp, - required this.since, - required this.recoverable, }); /// Represents the current state type. @@ -244,12 +225,6 @@ abstract base class _$SpinifySubscriptionStateBase { /// Timestamp of state change. final DateTime timestamp; - /// Stream Position - final SpinifyStreamPosition? since; - - /// Whether channel is recoverable. - final bool recoverable; - /// Whether channel is unsubscribed. abstract final bool isUnsubscribed; @@ -302,18 +277,4 @@ abstract base class _$SpinifySubscriptionStateBase { subscribing: subscribing ?? (_) => null, subscribed: subscribed ?? (_) => null, ); - - Map toJson() => { - 'type': type, - 'timestamp': timestamp.toUtc().toIso8601String(), - if (since != null) - 'since': switch (since) { - (:fixnum.Int64 offset, :String epoch) => { - 'offset': offset, - 'epoch': epoch, - }, - _ => null, - }, - 'recoverable': recoverable, - }; } diff --git a/lib/src/model/subscription_states.dart b/lib/src/model/subscription_states.dart new file mode 100644 index 0000000..c20ceb4 --- /dev/null +++ b/lib/src/model/subscription_states.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'subscription_state.dart'; + +/// Stream of Spinify's [SpinifySubscriptionState] changes. +/// {@category State} +/// {@category Client} +/// {@category Subscription} +extension type SpinifySubscriptionStates( + Stream _) implements Stream { + /// Unsubscribed + SpinifySubscriptionStates unsubscribed( + {String? channel}) => + filter(); + + /// Subscribing + SpinifySubscriptionStates subscribing( + {String? channel}) => + filter(); + + /// Subscribed + SpinifySubscriptionStates subscribed( + {String? channel}) => + filter(); + + /// Filtered stream of [SpinifySubscriptionState]. + SpinifySubscriptionStates filter() => + SpinifySubscriptionStates( + transform(StreamTransformer.fromHandlers( + handleData: (data, sink) => switch (data) { + S valid => sink.add(valid), + _ => null, + }, + ))); +} diff --git a/lib/src/model/subscription_states_stream.dart b/lib/src/model/subscription_states_stream.dart deleted file mode 100644 index abdd01c..0000000 --- a/lib/src/model/subscription_states_stream.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -import 'subscription_state.dart'; - -/// Stream of Spinify's [SpinifySubscriptionState] changes. -/// {@category Subscription} -/// {@category Entity} -@immutable -final class SpinifySubscriptionStateStream - extends StreamView { - /// Stream of Spinify's [SpinifySubscriptionState] changes. - SpinifySubscriptionStateStream(super.stream); - - /// Unsubscribed - late final Stream unsubscribed = - whereType(); - - /// Subscribing - late final Stream subscribing = - whereType(); - - /// Subscribed - late final Stream subscribed = - whereType(); - - /// Filtered stream of data of [SpinifySubscriptionState]. - Stream whereType() => - transform(StreamTransformer.fromHandlers( - handleData: (data, sink) => switch (data) { - T valid => sink.add(valid), - _ => null, - }, - )).asBroadcastStream(); - - @override - String toString() => 'SpinifySubscriptionStateStream{}'; -} diff --git a/lib/src/protobuf/protobuf_codec.dart b/lib/src/protobuf/protobuf_codec.dart index c3651cc..a5866f6 100644 --- a/lib/src/protobuf/protobuf_codec.dart +++ b/lib/src/protobuf/protobuf_codec.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:meta/meta.dart'; -import '../model/channel_push.dart'; +import '../model/channel_event.dart'; import '../model/client_info.dart'; import '../model/command.dart'; import '../model/config.dart'; @@ -184,7 +184,7 @@ final class ProtobufReplyDecoder extends Converter { return _decodeReply(reply); } else if (reply.hasError()) { final error = reply.error; - return SpinifyError( + return SpinifyErrorResult( id: reply.hasId() ? reply.id : 0, timestamp: DateTime.now(), code: error.code, @@ -219,9 +219,9 @@ final class ProtobufReplyDecoder extends Converter { event = SpinifyPublication( timestamp: DateTime.now(), channel: channel, - data: push.pub.data, + data: push.pub.hasData() ? push.pub.data : const [], info: _decodeClientInfo(push.pub.info), - offset: push.pub.offset, + offset: push.pub.hasOffset() ? push.pub.offset : null, tags: push.pub.tags, ); } else if (push.hasJoin()) { @@ -254,7 +254,7 @@ final class ProtobufReplyDecoder extends Converter { timestamp: DateTime.now(), channel: channel, recoverable: push.subscribe.recoverable, - data: push.subscribe.data, + data: push.subscribe.hasData() ? push.subscribe.data : null, positioned: push.subscribe.positioned, since: ( offset: push.subscribe.offset, @@ -289,19 +289,24 @@ final class ProtobufReplyDecoder extends Converter { version: push.connect.version, expires: expBool, ttl: ttlDT, - data: push.connect.data, + data: push.connect.hasData() ? push.connect.data : null, node: push.connect.node, pingInterval: pingInterval, sendPong: push.connect.pong == true, session: push.connect.session, ); } else if (push.hasDisconnect()) { + final code = push.disconnect.code; event = SpinifyDisconnect( timestamp: DateTime.now(), - reason: push.disconnect.reason, + reason: push.disconnect.hasReason() + ? push.disconnect.reason + : 'Server disconnecting', channel: channel, - code: push.disconnect.code, - reconnect: push.disconnect.reconnect, + code: code, + reconnect: push.disconnect.hasReconnect() + ? push.disconnect.reconnect + : code < 3500 || code >= 5000 || (code >= 4000 && code < 4500), ); } else if (push.hasRefresh()) { final pb.Refresh(:expires, :ttl) = push.refresh; @@ -386,11 +391,11 @@ final class ProtobufReplyDecoder extends Converter { for (final pub in sub.publications) SpinifyPublication( timestamp: now, - // TODO(plugfox): SpinifyPublication in SubscribeResult do not + // SpinifyPublication in SubscribeResult do not // have the "channel" field - I should fill it in manually // by copying the channel from the SubscribeRequest channel: '', - data: pub.data, + data: pub.hasData() ? pub.data : const [], info: _decodeClientInfo(pub.info), offset: pub.offset, tags: pub.tags, @@ -435,7 +440,7 @@ final class ProtobufReplyDecoder extends Converter { version: connect.version, expires: expBool, ttl: ttlDT, - data: connect.data, + data: connect.hasData() ? connect.data : null, subs: switch (connect.subs) { Map map when map.isNotEmpty => { @@ -486,13 +491,27 @@ final class ProtobufReplyDecoder extends Converter { offset: history.offset, epoch: history.epoch, ), + publications: [ + for (final pub in history.publications) + SpinifyPublication( + timestamp: now, + // SpinifyPublication in HistoryResult do not + // have the "channel" field - I should fill it in manually + // by copying the channel from the SubscribeRequest + channel: '', + data: pub.hasData() ? pub.data : const [], + info: _decodeClientInfo(pub.info), + offset: pub.offset, + tags: pub.tags, + ), + ], ); } else if (reply.hasRpc()) { final rpc = reply.rpc; return SpinifyRPCResult( id: id, timestamp: now, - data: rpc.data, + data: rpc.hasData() ? rpc.data : const [], ); } else if (reply.hasRefresh()) { final refresh = reply.refresh; @@ -540,6 +559,15 @@ final class ProtobufReplyDecoder extends Converter { expires: expBool, ttl: ttlDT, ); + } else if (reply.hasError()) { + final error = reply.error; + return SpinifyErrorResult( + id: id, + timestamp: now, + code: error.code, + message: error.message, + temporary: error.temporary, + ); } else { throw UnimplementedError('Unsupported reply type'); } diff --git a/lib/src/spinify_impl.dart b/lib/src/spinify_impl.dart index b28c8f5..a58ff5f 100644 --- a/lib/src/spinify_impl.dart +++ b/lib/src/spinify_impl.dart @@ -1,23 +1,30 @@ import 'dart:async'; +import 'dart:collection'; +import 'package:fixnum/fixnum.dart' as fixnum; import 'package:meta/meta.dart'; -import 'model/channel_push.dart'; +import 'model/annotations.dart'; +import 'model/channel_event.dart'; +import 'model/channel_events.dart'; +import 'model/client_info.dart'; import 'model/command.dart'; import 'model/config.dart'; import 'model/constant.dart'; +import 'model/exception.dart'; import 'model/history.dart'; import 'model/metric.dart'; import 'model/presence_stats.dart'; -import 'model/pushes_stream.dart'; import 'model/reply.dart'; -import 'model/spinify_interface.dart'; import 'model/state.dart'; import 'model/states_stream.dart'; import 'model/stream_position.dart'; -import 'model/subscription.dart'; import 'model/subscription_config.dart'; +import 'model/subscription_state.dart'; +import 'model/subscription_states.dart'; import 'model/transport_interface.dart'; +import 'spinify_interface.dart'; +import 'subscription_interface.dart'; import 'transport_ws_pb_stub.dart' // ignore: uri_does_not_exist if (dart.library.js_util) 'transport_ws_pb_js.dart' @@ -25,6 +32,8 @@ import 'transport_ws_pb_stub.dart' if (dart.library.io) 'transport_ws_pb_vm.dart'; import 'util/backoff.dart'; +part 'subscription_impl.dart'; + /// Base class for Spinify client. abstract base class SpinifyBase implements ISpinify { /// Create a new Spinify client. @@ -83,15 +92,11 @@ abstract base class SpinifyBase implements ISpinify { /// On disconnect from the server. @mustCallSuper - Future _onDisconnected() async { - config.logger?.call( - const SpinifyLogLevel.config(), - 'disconnected', - 'Disconnected', - { - 'state': state, - }, - ); + Future _onDisconnected() async {} + + Future _doOnReady(Future Function() action) { + if (state.isConnected) return action(); + return ready().then((_) => action()); } @override @@ -138,7 +143,15 @@ base mixin SpinifyStateMixin on SpinifyBase { @override Future _onDisconnected() async { await super._onDisconnected(); - if (!state.isDisconnected) _setState(SpinifyState$Disconnected()); + if (!state.isDisconnected) { + _setState(SpinifyState$Disconnected()); + config.logger?.call( + const SpinifyLogLevel.config(), + 'disconnected', + 'Disconnected from server', + {}, + ); + } } @override @@ -156,10 +169,14 @@ base mixin SpinifyCommandMixin on SpinifyBase { completer})>{}; @override - Future send(List data) => _sendCommandAsync(SpinifySendRequest( - timestamp: DateTime.now(), - data: data, - )); + Future send(List data) => _doOnReady( + () => _sendCommandAsync( + SpinifySendRequest( + timestamp: DateTime.now(), + data: data, + ), + ), + ); Future _sendCommand(SpinifyCommand command) async { config.logger?.call( @@ -246,34 +263,39 @@ base mixin SpinifyCommandMixin on SpinifyBase { } @override + @sideEffect Future _onReply(SpinifyReply reply) async { assert( reply.id >= 0 && reply.id <= _metrics.commandId, 'Reply ID should be greater or equal to 0 ' 'and less or equal than command ID'); - if (reply.id case int id when id > 0) { - final completer = _replies.remove(id)?.completer; - assert( - completer != null, - 'Reply completer not found', - ); - assert( - completer?.isCompleted == false, - 'Reply completer already completed', - ); - completer?.complete(reply); + if (reply.isResult) { + if (reply.id case int id when id > 0) { + final completer = _replies.remove(id)?.completer; + assert( + completer != null, + 'Reply completer not found', + ); + assert( + completer?.isCompleted == false, + 'Reply completer already completed', + ); + if (reply is SpinifyErrorResult) { + completer?.completeError(SpinifyReplyException( + replyCode: reply.code, + replyMessage: reply.message, + temporary: reply.temporary, + )); + } else { + completer?.complete(reply); + } + } } await super._onReply(reply); } @override Future _onDisconnected() async { - config.logger?.call( - const SpinifyLogLevel.config(), - 'disconnected', - 'Disconnected from server', - {}, - ); late final error = StateError('Client is disconnected'); late final stackTrace = StackTrace.current; for (final tuple in _replies.values) { @@ -296,9 +318,279 @@ base mixin SpinifyCommandMixin on SpinifyBase { } } +/// Base mixin for Spinify subscription management. +base mixin SpinifySubscriptionMixin on SpinifyBase, SpinifyCommandMixin { + final StreamController _eventController = + StreamController.broadcast(); + + @override + late final SpinifyChannelEvents stream = + SpinifyChannelEvents(_eventController.stream); + + @override + ({ + Map client, + Map server + }) get subscriptions => ( + client: UnmodifiableMapView( + _clientSubscriptionRegistry), + server: UnmodifiableMapView( + _serverSubscriptionRegistry), + ); + + /// Registry of client subscriptions. + final Map _clientSubscriptionRegistry = + {}; + + /// Registry of server subscriptions. + final Map _serverSubscriptionRegistry = + {}; + + @override + SpinifySubscription? getSubscription(String channel) => + _clientSubscriptionRegistry[channel] ?? + _serverSubscriptionRegistry[channel]; + + @override + SpinifyClientSubscription? getClientSubscription(String channel) => + _clientSubscriptionRegistry[channel]; + + @override + SpinifyServerSubscription? getServerSubscription(String channel) => + _serverSubscriptionRegistry[channel]; + + @override + SpinifyClientSubscription newSubscription( + String channel, { + SpinifySubscriptionConfig? config, + bool subscribe = false, + }) { + final sub = _clientSubscriptionRegistry[channel] ?? + _serverSubscriptionRegistry[channel]; + if (sub != null) { + this.config.logger?.call( + const SpinifyLogLevel.warning(), + 'subscription_exists_error', + 'Subscription already exists', + { + 'channel': channel, + 'subscription': sub, + }, + ); + throw SpinifySubscriptionException( + channel: channel, + message: 'Subscription already exists', + ); + } + final newSub = + _clientSubscriptionRegistry[channel] = SpinifyClientSubscriptionImpl( + client: this, + channel: channel, + config: config ?? const SpinifySubscriptionConfig.byDefault(), + ); + if (subscribe) newSub.subscribe(); + return newSub; + } + + @override + Future removeSubscription( + SpinifyClientSubscription subscription) async { + final subFromRegistry = + _clientSubscriptionRegistry.remove(subscription.channel); + try { + await subFromRegistry?.unsubscribe(); + assert( + subFromRegistry != null, + 'Subscription not found in the registry', + ); + assert( + identical(subFromRegistry, subscription), + 'Subscription should be the same instance as in the registry', + ); + } on Object catch (error, stackTrace) { + config.logger?.call( + const SpinifyLogLevel.warning(), + 'subscription_remove_error', + 'Error removing subscription', + { + 'channel': subscription.channel, + 'subscription': subscription, + }, + ); + Error.throwWithStackTrace( + SpinifySubscriptionException( + channel: subscription.channel, + message: 'Error while unsubscribing', + error: error, + ), + stackTrace, + ); + } finally { + subFromRegistry?.close(); + } + } + + @override + Future _onReply(SpinifyReply reply) async { + await super._onReply(reply); + if (reply is SpinifyPush) { + // Add push to the stream. + final event = reply.event; + _eventController.add(event); // Add event to the broadcast stream. + config.logger?.call( + const SpinifyLogLevel.debug(), + 'push_received', + 'Push ${event.type} received', + { + 'event': event, + }, + ); + if (event.channel.isEmpty) { + /* ignore push without channel */ + } else if (event is SpinifySubscribe) { + // Add server subscription to the registry on subscribe event. + _serverSubscriptionRegistry.putIfAbsent( + event.channel, + () => SpinifyServerSubscriptionImpl( + client: this, + channel: event.channel, + recoverable: event.recoverable, + epoch: event.since.epoch, + offset: event.since.offset, + )) + ..onEvent(event) + .._setState(SpinifySubscriptionState.subscribed(data: event.data)); + } else if (event is SpinifyUnsubscribe) { + // Remove server subscription from the registry on unsubscribe event. + _serverSubscriptionRegistry.remove(event.channel) + ?..onEvent(event) + .._setState(SpinifySubscriptionState.unsubscribed()); + // Unsubscribe client subscription on unsubscribe event. + if (_clientSubscriptionRegistry[event.channel] + case SpinifyClientSubscriptionImpl subscription) { + subscription.onEvent(event); + if (event.code < 2500) { + // Unsubscribe client subscription on unsubscribe event. + subscription + ._unsubscribe( + code: event.code, + reason: event.reason, + sendUnsubscribe: false, + ) + .ignore(); + } else { + // Resubscribe client subscription on unsubscribe event. + subscription._resubscribe().ignore(); + } + } + } else { + // Notify subscription about new event. + final sub = _serverSubscriptionRegistry[event.channel] ?? + _clientSubscriptionRegistry[event.channel]; + sub?.onEvent(event); + if (sub == null) { + assert( + false, + 'Subscription not found for event ${event.channel}', + ); + config.logger?.call( + const SpinifyLogLevel.warning(), + 'subscription_not_found_error', + 'Subscription ${event.channel} not found for event', + { + 'channel': event.channel, + 'event': event, + }, + ); + } else if (event is SpinifyPublication && sub.recoverable) { + // Update subscription offset on publication. + if (event.offset case fixnum.Int64 newOffset when newOffset > 0) + sub.offset = newOffset; + } + } + } else if (reply is SpinifyConnectResult) { + // Update server subscriptions. + final newServerSubs = reply.subs ?? {}; + for (final entry in newServerSubs.entries) { + final MapEntry( + key: channel, + value: value + ) = entry; + final sub = _serverSubscriptionRegistry.putIfAbsent( + channel, + () => SpinifyServerSubscriptionImpl( + client: this, + channel: channel, + recoverable: value.recoverable, + epoch: value.since.epoch, + offset: value.since.offset, + )) + .._setState(SpinifySubscriptionState.subscribed(data: value.data)); + + // Notify about new publications. + for (var publication in value.publications) { + // If publication has wrong channel, fix it. + // Thats a workaround because we do not have channel + // in the publication in this server SpinifyConnectResult reply. + if (publication.channel != channel) { + assert( + publication.channel.isEmpty, + 'Publication contains wrong channel', + ); + publication = publication.copyWith(channel: channel); + } + _eventController.add(publication); + sub.onEvent(publication); + // Update subscription offset on publication. + if (sub.recoverable) { + if (publication.offset case fixnum.Int64 newOffset + when newOffset > sub.offset) { + sub.offset = newOffset; + } + } + } + } + + // Remove server subscriptions that are not in the new list. + final currentServerSubs = _serverSubscriptionRegistry.keys.toSet(); + for (final key in currentServerSubs) { + if (newServerSubs.containsKey(key)) continue; + _serverSubscriptionRegistry.remove(key) + ?.._setState(SpinifySubscriptionState.unsubscribed()) + ..close(); + } + + // We should resubscribe client subscriptions here. + for (final subscription in _clientSubscriptionRegistry.values) + subscription._resubscribe().ignore(); + } + } + + @override + Future close() async { + await super.close(); + final unsubscribed = SpinifySubscriptionState.unsubscribed(); + for (final sub in _clientSubscriptionRegistry.values) + sub + .._setState(unsubscribed) + ..close(); + for (final sub in _serverSubscriptionRegistry.values) + sub + .._setState(unsubscribed) + ..close(); + _clientSubscriptionRegistry.clear(); + _serverSubscriptionRegistry.clear(); + _eventController.close().ignore(); + } +} + /// Base mixin for Spinify client connection management (connect & disconnect). base mixin SpinifyConnectionMixin - on SpinifyBase, SpinifyCommandMixin, SpinifyStateMixin { + on + SpinifyBase, + SpinifyCommandMixin, + SpinifyStateMixin, + SpinifySubscriptionMixin { Timer? _reconnectTimer; Completer? _readyCompleter; @@ -333,19 +625,41 @@ base mixin SpinifyConnectionMixin final token = await config.getToken?.call(); assert(token == null || token.length > 5, 'Spinify JWT is too short'); final payload = await config.getPayload?.call(); + final id = _getNextCommandId(); + final now = DateTime.now(); request = SpinifyConnectRequest( - id: _getNextCommandId(), - timestamp: DateTime.now(), + id: id, + timestamp: now, token: token, data: payload, - // TODO(plugfox): Implement subscriptions. - subs: const {}, + subs: { + for (final sub in _serverSubscriptionRegistry.values) + sub.channel: SpinifySubscribeRequest( + id: id, + timestamp: now, + channel: sub.channel, + recover: sub.recoverable, + epoch: sub.epoch, + offset: sub.offset, + token: null, + data: null, + positioned: null, + recoverable: null, + joinLeave: null, + ), + }, name: config.client.name, version: config.client.version, ); } final reply = await _sendCommand(request); + + if (!state.isConnecting) + throw const SpinifyConnectionException( + message: 'Connection is not in connecting state', + ); + _setState(SpinifyState$Connected( url: url, client: reply.client, @@ -390,8 +704,36 @@ base mixin SpinifyConnectionMixin 'stackTrace': stackTrace, }, ); - _setUpReconnectTimer(); - rethrow; + + _transport?.disconnect().ignore(); + + switch (error) { + case SpinifyErrorResult result: + if (result.code == 109) { + // Token expired error. + _setUpReconnectTimer(); // Retry resubscribe + } else if (result.temporary) { + // Temporary error. + _setUpReconnectTimer(); // Retry resubscribe + } else { + // Disable resubscribe timer + //moveToUnsubscribed(result.code, result.message, false); + _setState(SpinifyState$Disconnected()); + } + case SpinifyConnectionException _: + _setUpReconnectTimer(); // Some spinify exception - retry resubscribe + rethrow; + default: + _setUpReconnectTimer(); // Unknown error - retry resubscribe + } + + Error.throwWithStackTrace( + SpinifyConnectionException( + message: 'Error connecting to server $url', + error: error, + ), + stackTrace, + ); } } @@ -515,6 +857,7 @@ base mixin SpinifyConnectionMixin { 'url': lastUrl, 'delay': delay, + 'attempt': attempt, }, ); Future.sync(() => connect(lastUrl)).ignore(); @@ -528,6 +871,7 @@ base mixin SpinifyConnectionMixin { 'url': lastUrl, 'delay': delay, + 'attempt': attempt, }, ); _metrics.nextReconnectAt = DateTime.now().add(delay); @@ -562,15 +906,20 @@ base mixin SpinifyConnectionMixin @override Future ready() async { if (state.isConnected) return; + if (state.isClosed) + throw const SpinifyConnectionException( + message: 'Connection is closed permanently', + ); return (_readyCompleter ??= Completer()).future; } @override Future disconnect() async { + // Disable reconnect because we are disconnecting manually/intentionally. _metrics.reconnectUrl = null; _tearDownReconnectTimer(); if (state.isDisconnected) return Future.value(); - await _transport?.disconnect(1000, 'Client disconnecting'); + await _transport?.disconnect(1000, 'disconnected by client'); await _onDisconnected(); } @@ -580,11 +929,31 @@ base mixin SpinifyConnectionMixin _transport = null; // Reconnect if that callback called not from disconnect method. if (_metrics.reconnectUrl != null) _setUpReconnectTimer(); - _metrics.lastDisconnectAt = DateTime.now(); - _metrics.disconnects++; + if (state.isConnected || state.isConnecting) { + _metrics.lastDisconnectAt = DateTime.now(); + _metrics.disconnects++; + } await super._onDisconnected(); } + @override + Future _onReply(SpinifyReply reply) async { + await super._onReply(reply); + if (reply + case SpinifyPush( + event: SpinifyDisconnect(:String reason, :bool reconnect) + )) { + if (reconnect) { + // Disconnect client temporarily. + await _transport?.disconnect(1000, reason); + await _onDisconnected(); + } else { + // Disconnect client permanently. + await disconnect(); + } + } + } + @override Future close() async { await _transport?.disconnect(1000, 'Client closing'); @@ -600,12 +969,11 @@ base mixin SpinifyPingPongMixin Timer? _pingTimer; /* @override - Future ping() => _bucket.push( - ClientEvent.command, - (int id, DateTime timestamp) => SpinifyPingRequest( - id: id, - timestamp: timestamp, - )); */ + Future ping() => _doOnReady( + () => _sendCommand( + SpinifyPingRequest(timestamp: DateTime.now()), + ), + ); */ /// Stop keepalive timer. @protected @@ -658,9 +1026,16 @@ base mixin SpinifyPingPongMixin @override Future _onReply(SpinifyReply reply) async { - if (reply is SpinifyServerPing) { + if (!reply.isResult && reply is SpinifyServerPing) { final command = SpinifyPingRequest(timestamp: DateTime.now()); - await _sendCommandAsync(command); + _metrics + ..lastPingAt = command.timestamp + ..receivedPings = _metrics.receivedPings + 1; + if (state case SpinifyState$Connected(:bool sendPong) when sendPong) { + // No need to handle error in a special way - + // if pong can't be sent but connection is closed anyway. + _sendCommandAsync(command).ignore(); + } config.logger?.call( const SpinifyLogLevel.debug(), 'server_ping_received', @@ -688,51 +1063,47 @@ base mixin SpinifyPingPongMixin } } -/// Base mixin for Spinify client subscription management. -base mixin SpinifyClientSubscriptionMixin on SpinifyBase { - @override - ({ - Map client, - Map server - }) get subscriptions => throw UnimplementedError(); - - @override - SpinifyClientSubscription? getSubscription(String channel) => - throw UnimplementedError(); - - @override - SpinifyClientSubscription newSubscription(String channel, - [SpinifySubscriptionConfig? config]) => - throw UnimplementedError(); - - @override - Future removeSubscription(SpinifyClientSubscription subscription) => - throw UnimplementedError(); -} - -/// Base mixin for Spinify server subscription management. -base mixin SpinifyServerSubscriptionMixin on SpinifyBase {} - /// Base mixin for Spinify client publications management. -base mixin SpinifyPublicationsMixin on SpinifyBase { +base mixin SpinifyPublicationsMixin on SpinifyBase, SpinifyCommandMixin { @override Future publish(String channel, List data) => - throw UnimplementedError(); + getSubscription(channel)?.publish(data) ?? + Future.error( + SpinifySubscriptionException( + channel: channel, + message: 'Subscription not found', + ), + StackTrace.current, + ); } /// Base mixin for Spinify client presence management. -base mixin SpinifyPresenceMixin on SpinifyBase { +base mixin SpinifyPresenceMixin on SpinifyBase, SpinifyCommandMixin { @override - Future presence(String channel) => - throw UnimplementedError(); + Future> presence(String channel) => + getSubscription(channel)?.presence() ?? + Future.error( + SpinifySubscriptionException( + channel: channel, + message: 'Subscription not found', + ), + StackTrace.current, + ); @override Future presenceStats(String channel) => - throw UnimplementedError(); + getSubscription(channel)?.presenceStats() ?? + Future.error( + SpinifySubscriptionException( + channel: channel, + message: 'Subscription not found', + ), + StackTrace.current, + ); } /// Base mixin for Spinify client history management. -base mixin SpinifyHistoryMixin on SpinifyBase { +base mixin SpinifyHistoryMixin on SpinifyBase, SpinifyCommandMixin { @override Future history( String channel, { @@ -740,21 +1111,33 @@ base mixin SpinifyHistoryMixin on SpinifyBase { SpinifyStreamPosition? since, bool? reverse, }) => - throw UnimplementedError(); + getSubscription(channel)?.history( + limit: limit, + since: since, + reverse: reverse, + ) ?? + Future.error( + SpinifySubscriptionException( + channel: channel, + message: 'Subscription not found', + ), + StackTrace.current, + ); } /// Base mixin for Spinify client RPC management. base mixin SpinifyRPCMixin on SpinifyBase, SpinifyCommandMixin { @override - Future> rpc(String method, List data) => - _sendCommand( - SpinifyRPCRequest( - id: _getNextCommandId(), - timestamp: DateTime.now(), - method: method, - data: data, - ), - ).then((reply) => reply.data); + Future> rpc(String method, [List? data]) => _doOnReady( + () => _sendCommand( + SpinifyRPCRequest( + id: _getNextCommandId(), + timestamp: DateTime.now(), + method: method, + data: data ?? const [], + ), + ).then>((reply) => reply.data), + ); } /// Base mixin for Spinify client metrics management. @@ -787,10 +1170,9 @@ final class Spinify extends SpinifyBase with SpinifyStateMixin, SpinifyCommandMixin, + SpinifySubscriptionMixin, SpinifyConnectionMixin, SpinifyPingPongMixin, - SpinifyClientSubscriptionMixin, - SpinifyServerSubscriptionMixin, SpinifyPublicationsMixin, SpinifyPresenceMixin, SpinifyHistoryMixin, @@ -805,7 +1187,4 @@ final class Spinify extends SpinifyBase /// {@macro spinify} factory Spinify.connect(String url, {SpinifyConfig? config}) => Spinify(config: config)..connect(url); - - @override - SpinifyPushesStream get stream => throw UnimplementedError(); } diff --git a/lib/src/model/spinify_interface.dart b/lib/src/spinify_interface.dart similarity index 76% rename from lib/src/model/spinify_interface.dart rename to lib/src/spinify_interface.dart index 834e99a..8a4aa4a 100644 --- a/lib/src/model/spinify_interface.dart +++ b/lib/src/spinify_interface.dart @@ -2,17 +2,18 @@ import 'dart:async'; -import 'channel_push.dart'; -import 'config.dart'; -import 'history.dart'; -import 'metric.dart'; -import 'presence_stats.dart'; -import 'pushes_stream.dart'; -import 'state.dart'; -import 'states_stream.dart'; -import 'stream_position.dart'; -import 'subscription.dart'; -import 'subscription_config.dart'; +import 'model/channel_event.dart'; +import 'model/channel_events.dart'; +import 'model/client_info.dart'; +import 'model/config.dart'; +import 'model/history.dart'; +import 'model/metric.dart'; +import 'model/presence_stats.dart'; +import 'model/state.dart'; +import 'model/states_stream.dart'; +import 'model/stream_position.dart'; +import 'model/subscription_config.dart'; +import 'subscription_interface.dart'; /// Spinify client interface. abstract interface class ISpinify @@ -75,7 +76,7 @@ abstract interface class ISpinifyAsyncMessageSender { /// Spinify event receiver interface. abstract interface class ISpinifyEventReceiver { /// Stream of received pushes from Centrifugo server for a channel. - abstract final SpinifyPushesStream stream; + abstract final SpinifyChannelEvents stream; } /// Spinify client subscriptions manager interface. @@ -83,19 +84,31 @@ abstract interface class ISpinifySubscriptionsManager { /// Create new client-side subscription. /// `newSubscription(channel, config)` allocates a new Subscription /// in the registry or throws an exception if the Subscription - /// is already there. We will discuss common Subscription options below. + /// is already there. + /// If [config] is not provided then default config will be used. + /// If [subscribe] is true then the client will subscribe to the channel + /// immediately after creation. SpinifyClientSubscription newSubscription( - String channel, [ + String channel, { SpinifySubscriptionConfig? config, - ]); + bool subscribe = false, + }); + + /// Get client or server subscription to the channel + /// from internal registry or null if not found. + SpinifySubscription? getSubscription(String channel); - /// Get subscription to the channel + /// Get client subscription to the channel /// from internal registry or null if not found. /// /// You need to call [SpinifyClientSubscription.subscribe] /// to start receiving events /// in the channel. - SpinifyClientSubscription? getSubscription(String channel); + SpinifyClientSubscription? getClientSubscription(String channel); + + /// Get server subscription to the channel + /// from internal registry or null if not found. + SpinifyServerSubscription? getServerSubscription(String channel); /// Remove the [SpinifySubscription] from internal registry /// and unsubscribe from [SpinifyClientSubscription.channel]. @@ -119,7 +132,9 @@ abstract interface class ISpinifySubscriptionsManager { /// Spinify presence owner interface. abstract interface class ISpinifyPresenceOwner { /// Fetch presence information inside a channel. - Future presence(String channel); + /// Contains presence information - a map client IDs as keys + /// and client information as values. + Future> presence(String channel); /// Fetch presence stats information inside a channel. Future presenceStats(String channel); diff --git a/lib/src/subscription_impl.dart b/lib/src/subscription_impl.dart new file mode 100644 index 0000000..1add691 --- /dev/null +++ b/lib/src/subscription_impl.dart @@ -0,0 +1,650 @@ +part of 'spinify_impl.dart'; + +@internal +abstract base class SpinifySubscriptionBase implements SpinifySubscription { + SpinifySubscriptionBase({ + required SpinifySubscriptionMixin client, + required this.channel, + required this.recoverable, + required this.epoch, + required this.offset, + }) : _clientWR = WeakReference(client), + _clientConfig = client.config { + _metrics = _client._metrics.channels + .putIfAbsent(channel, SpinifyMetrics$Channel$Mutable.new); + } + + @override + final String channel; + + /// Spinify client weak reference. + final WeakReference _clientWR; + + /// Spinify client + SpinifySubscriptionMixin get _client { + final target = _clientWR.target; + if (target == null) { + throw SpinifySubscriptionException( + channel: channel, + message: 'Spinify client is do not exist anymore', + ); + } + return target; + } + + /// Spinify channel metrics. + late final SpinifyMetrics$Channel$Mutable _metrics; + + /// Spinify client configuration. + final SpinifyConfig _clientConfig; + + /// Spinify logger. + SpinifyLogger? get _logger => _clientConfig.logger; + + final StreamController _stateController = + StreamController.broadcast(); + + final StreamController _eventController = + StreamController.broadcast(); + + Future _sendCommand( + SpinifyCommand Function(int nextId) builder, + ) => + _client._doOnReady( + () => _client._sendCommand( + builder(_client._getNextCommandId()), + ), + ); + + @override + bool recoverable; + + @override + String epoch; + + @override + fixnum.Int64 offset; + + @override + SpinifySubscriptionState get state => _metrics.state; + + @override + SpinifySubscriptionStates get states => + SpinifySubscriptionStates(_stateController.stream); + + @override + SpinifyChannelEvents get stream => + SpinifyChannelEvents(_eventController.stream); + + @sideEffect + @mustCallSuper + void onEvent(SpinifyChannelEvent event) { + assert( + event.channel == channel, + 'Subscription "$channel" received event for another channel', + ); + _eventController.add(event); + _logger?.call( + const SpinifyLogLevel.debug(), + 'subscription_event_received', + 'Subscription "$channel" received ${event.type} event', + { + 'channel': channel, + 'subscription': this, + 'event': event, + if (event is SpinifyPublication) 'publication': event, + }, + ); + } + + @mustCallSuper + void _setState(SpinifySubscriptionState state) { + final previous = _metrics.state; + if (previous == state) return; + _stateController.add(_metrics.state = state); + _logger?.call( + const SpinifyLogLevel.config(), + 'subscription_state_changed', + 'Subscription "$channel" state changed to ${state.type}', + { + 'channel': channel, + 'subscription': this, + 'previous': previous, + 'state': state, + }, + ); + } + + @mustCallSuper + @interactive + void close() { + _stateController.close().ignore(); + _eventController.close().ignore(); + assert(state.isUnsubscribed, + 'Subscription "$channel" is not unsubscribed before closing'); + } + + @override + @interactive + Future ready() async { + if (_client.isClosed) + throw SpinifySubscriptionException( + channel: channel, + message: 'Client is closed', + ); + if (_metrics.state.isSubscribed) return; + if (_stateController.isClosed) + throw SpinifySubscriptionException( + channel: channel, + message: 'Subscription is closed permanently', + ); + final state = await _stateController.stream + .firstWhere((state) => !state.isSubscribing); + if (!state.isSubscribed) + throw SpinifySubscriptionException( + channel: channel, + message: 'Subscription failed to subscribe', + ); + } + + @override + @interactive + Future history({ + int? limit, + SpinifyStreamPosition? since, + bool? reverse, + }) => + _sendCommand( + (id) => SpinifyHistoryRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + limit: limit, + since: since, + reverse: reverse, + ), + ).then( + (reply) => SpinifyHistory( + publications: List.unmodifiable( + reply.publications.map((pub) => pub.copyWith(channel: channel))), + since: reply.since, + ), + ); + + @override + @interactive + Future> presence() => + _sendCommand( + (id) => SpinifyPresenceRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + ), + ).then>((reply) => reply.presence); + + @override + @interactive + Future presenceStats() => + _sendCommand( + (id) => SpinifyPresenceStatsRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + ), + ).then( + (reply) => SpinifyPresenceStats( + channel: channel, + clients: reply.numClients, + users: reply.numUsers, + ), + ); + + @override + @interactive + Future publish(List data) => _sendCommand( + (id) => SpinifyPublishRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + data: data, + ), + ); +} + +@internal +final class SpinifyClientSubscriptionImpl extends SpinifySubscriptionBase + implements SpinifyClientSubscription { + SpinifyClientSubscriptionImpl({ + required super.client, + required super.channel, + required this.config, + }) : super( + recoverable: config.recoverable, + epoch: config.since?.epoch ?? '', + offset: config.since?.offset ?? fixnum.Int64.ZERO, + ); + + @override + final SpinifySubscriptionConfig config; + + /// Whether the subscription should recover. + bool _recover = false; + + /// Interactively subscribes to the channel. + @override + @interactive + Future subscribe() async { + // Check if the client is connected + switch (_client.state) { + case SpinifyState$Connected _: + break; + case SpinifyState$Connecting _: + case SpinifyState$Disconnected _: + await _client.ready(); + case SpinifyState$Closed _: + throw SpinifySubscriptionException( + channel: channel, + message: 'Client is closed', + ); + } + + // Check if the subscription is already subscribed + switch (state) { + case SpinifySubscriptionState$Subscribed _: + return; + case SpinifySubscriptionState$Subscribing _: + await ready(); + case SpinifySubscriptionState$Unsubscribed _: + await _resubscribe(); + } + } + + /// Interactively unsubscribes from the channel. + @override + @interactive + Future unsubscribe([ + int code = 0, + String reason = 'unsubscribe called', + ]) => + _unsubscribe( + code: code, + reason: reason, + sendUnsubscribe: true, + ); + + /// Unsubscribes from the channel. + Future _unsubscribe({ + required int code, + required String reason, + required bool sendUnsubscribe, + }) async { + final currentState = _metrics.state; + _tearDownResubscribeTimer(); + _tearDownRefreshSubscriptionTimer(); + if (currentState.isUnsubscribed) return; + _setState(SpinifySubscriptionState$Unsubscribed()); + _metrics.lastUnsubscribeAt = DateTime.now(); + _metrics.unsubscribes++; + try { + if (sendUnsubscribe && + currentState.isSubscribed && + _client.state.isConnected) { + await _sendCommand( + (id) => SpinifyUnsubscribeRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + ), + ); + } + } on Object catch (error, stackTrace) { + _logger?.call( + const SpinifyLogLevel.error(), + 'subscription_unsubscribe_error', + 'Subscription "$channel" failed to unsubscribe', + { + 'channel': channel, + 'subscription': this, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + _client._transport?.disconnect(4, 'unsubscribe error').ignore(); + if (error is SpinifyException) rethrow; + Error.throwWithStackTrace( + SpinifySubscriptionException( + channel: channel, + message: 'Error while unsubscribing', + error: error, + ), + stackTrace, + ); + } + } + + /// `SubscriptionImpl{}._resubscribe()` from `centrifuge` package + Future _resubscribe() async { + if (!_metrics.state.isUnsubscribed) return; + try { + _setState(SpinifySubscriptionState$Subscribing()); + + final token = await config.getToken?.call(); + if (token == null || token.isEmpty) { + throw SpinifySubscriptionException( + channel: channel, + message: 'Token is empty', + ); + } + + final data = await config.getPayload?.call(); + + final recover = + _recover && offset > fixnum.Int64.ZERO && epoch.isNotEmpty; + + final result = await _sendCommand( + (id) => SpinifySubscribeRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + token: token, + recoverable: recoverable, + recover: recover, + offset: recover ? offset : null, + epoch: recover ? epoch : null, + positioned: config.positioned, + joinLeave: config.joinLeave, + data: data, + ), + ); + + if (state.isUnsubscribed) { + _logger?.call( + const SpinifyLogLevel.debug(), + 'subscription_resubscribe_skipped', + 'Subscription "$channel" resubscribe skipped, ' + 'subscription is unsubscribed.', + { + 'channel': channel, + 'subscription': this, + }, + ); + await _unsubscribe( + code: 0, + reason: 'resubscribe skipped', + sendUnsubscribe: false, + ); + } + + // If subscription is recoverable and server sends recoverable flag + // then we should update epoch and offset values. + if (result.recoverable) { + _recover = true; + epoch = result.since.epoch; + offset = result.since.offset; + } + + _setState(SpinifySubscriptionState$Subscribed(data: result.data)); + + // Set up refresh subscription timer if needed. + if (result.expires) { + if (result.ttl case DateTime ttl when ttl.isAfter(DateTime.now())) { + _setUpRefreshSubscriptionTimer(ttl: ttl); + } else { + assert( + false, + 'Subscription "$channel" has invalid TTL: ${result.ttl}', + ); + } + } + + // Handle received publications and update offset. + for (final pub in result.publications) { + _client._eventController.add(pub); + onEvent(pub); + if (pub.offset case fixnum.Int64 value when value > offset) { + offset = value; + } + } + + _onSubscribed(); // Successful subscription completed + + _logger?.call( + const SpinifyLogLevel.config(), + 'subscription_subscribed', + 'Subscription "$channel" subscribed', + { + 'channel': channel, + 'subscription': this, + }, + ); + } on Object catch (error, stackTrace) { + _logger?.call( + const SpinifyLogLevel.error(), + 'subscription_resubscribe_error', + 'Subscription "$channel" failed to resubscribe', + { + 'channel': channel, + 'subscription': this, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + switch (error) { + case SpinifyErrorResult result: + if (result.code == 109) { + _setUpResubscribeTimer(); // Token expired error, retry resubscribe + } else if (result.temporary) { + _setUpResubscribeTimer(); // Temporary error, retry resubscribe + } else { + // Disable resubscribe timer and unsubscribe + _unsubscribe( + code: result.code, + reason: result.message, + sendUnsubscribe: false, + ).ignore(); + } + case SpinifySubscriptionException _: + _setUpResubscribeTimer(); // Some spinify exception, retry resubscribe + rethrow; + default: + _setUpResubscribeTimer(); // Unknown error, retry resubscribe + } + Error.throwWithStackTrace( + SpinifySubscriptionException( + channel: channel, + message: 'Failed to resubscribe to "$channel"', + error: error, + ), + stackTrace, + ); + } + } + + /// Successful subscription completed. + void _onSubscribed() { + _tearDownResubscribeTimer(); + _metrics.lastSubscribeAt = DateTime.now(); + _metrics.subscribes++; + } + + /// Resubscribe timer. + Timer? _resubscribeTimer; + + /// Set up resubscribe timer. + void _setUpResubscribeTimer() { + _resubscribeTimer?.cancel(); + final attempt = _metrics.resubscribeAttempts ?? 0; + final delay = Backoff.nextDelay( + attempt, + _client.config.connectionRetryInterval.min.inMilliseconds, + _client.config.connectionRetryInterval.max.inMilliseconds, + ); + _metrics.resubscribeAttempts = attempt + 1; + if (delay <= Duration.zero) { + if (!state.isUnsubscribed) return; + _logger?.call( + const SpinifyLogLevel.config(), + 'subscription_resubscribe_attempt', + 'Resubscibing to $channel immediately.', + { + 'channel': channel, + 'delay': delay, + 'subscription': this, + 'attempts': attempt, + }, + ); + Future.sync(subscribe).ignore(); + return; + } + _logger?.call( + const SpinifyLogLevel.debug(), + 'subscription_resubscribe_delayed', + 'Setting up resubscribe timer for $channel ' + 'after ${delay.inMilliseconds} ms.', + { + 'channel': channel, + 'delay': delay, + 'subscription': this, + 'attempts': attempt, + }, + ); + _metrics.nextResubscribeAt = DateTime.now().add(delay); + _resubscribeTimer = Timer(delay, () { + if (!state.isUnsubscribed) return; + _logger?.call( + const SpinifyLogLevel.debug(), + 'subscription_resubscribe_attempt', + 'Resubscribing to $channel after ${delay.inMilliseconds} ms.', + { + 'channel': channel, + 'subscription': this, + 'attempts': attempt, + }, + ); + Future.sync(_resubscribe).ignore(); + }); + } + + /// Tear down resubscribe timer. + void _tearDownResubscribeTimer() { + _metrics + ..resubscribeAttempts = 0 + ..nextResubscribeAt = null; + _resubscribeTimer?.cancel(); + _resubscribeTimer = null; + } + + /// Refresh subscription timer. + Timer? _refreshTimer; + + /// Set up refresh subscription timer. + void _setUpRefreshSubscriptionTimer({required DateTime ttl}) { + _tearDownRefreshSubscriptionTimer(); + _metrics.ttl = ttl; + _refreshTimer = Timer(ttl.difference(DateTime.now()), _refreshToken); + } + + /// Tear down refresh subscription timer. + void _tearDownRefreshSubscriptionTimer() { + _refreshTimer?.cancel(); + _refreshTimer = null; + _metrics.ttl = null; + } + + /// Refresh subscription token. + void _refreshToken() => runZonedGuarded( + () async { + _tearDownRefreshSubscriptionTimer(); + if (!state.isSubscribed || !_client.state.isConnected) return; + final token = await config.getToken?.call(); + if (token == null || token.isEmpty) { + throw SpinifySubscriptionException( + channel: channel, + message: 'Token is empty', + ); + } + final result = await _sendCommand( + (id) => SpinifySubRefreshRequest( + id: id, + channel: channel, + timestamp: DateTime.now(), + token: token, + ), + ); + + DateTime? newTtl; + if (result.expires) { + if (result.ttl case DateTime ttl when ttl.isAfter(DateTime.now())) { + newTtl = ttl; + _setUpRefreshSubscriptionTimer(ttl: ttl); + } else { + assert( + false, + 'Subscription "$channel" has invalid TTL: ${result.ttl}', + ); + } + } + + _logger?.call( + const SpinifyLogLevel.debug(), + 'subscription_refresh_token', + 'Subscription "$channel" token refreshed', + { + 'channel': channel, + 'subscription': this, + if (newTtl != null) 'ttl': newTtl, + }, + ); + }, + (error, stackTrace) { + _logger?.call( + const SpinifyLogLevel.error(), + 'subscription_refresh_token_error', + 'Subscription "$channel" failed to refresh token', + { + 'channel': channel, + 'subscription': this, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + + // Calculate new TTL for refresh subscription timer + late final ttl = + DateTime.now().add(Backoff.nextDelay(0, 5 * 1000, 10 * 1000)); + switch (error) { + case SpinifyErrorResult result: + if (result.temporary) { + _setUpRefreshSubscriptionTimer(ttl: ttl); + } else { + // Disable refresh subscription timer and unsubscribe + _unsubscribe( + code: result.code, + reason: result.message, + sendUnsubscribe: true, + ).ignore(); + } + case SpinifySubscriptionException _: + _setUpRefreshSubscriptionTimer(ttl: ttl); + default: + _setUpRefreshSubscriptionTimer(ttl: ttl); + } + }, + ); +} + +@internal +final class SpinifyServerSubscriptionImpl extends SpinifySubscriptionBase + implements SpinifyServerSubscription { + SpinifyServerSubscriptionImpl({ + required super.client, + required super.channel, + required super.recoverable, + required super.epoch, + required super.offset, + }); + + @override + SpinifyChannelEvents get stream => + _client.stream.filter(channel: channel); +} diff --git a/lib/src/model/subscription.dart b/lib/src/subscription_interface.dart similarity index 80% rename from lib/src/model/subscription.dart rename to lib/src/subscription_interface.dart index 0a6c8a1..d8910b8 100644 --- a/lib/src/model/subscription.dart +++ b/lib/src/subscription_interface.dart @@ -1,12 +1,16 @@ import 'dart:async'; -import 'channel_push.dart'; -import 'history.dart'; -import 'presence_stats.dart'; -import 'pushes_stream.dart'; -import 'stream_position.dart'; -import 'subscription_state.dart'; -import 'subscription_states_stream.dart'; +import 'package:fixnum/fixnum.dart' as fixnum; + +import 'model/channel_event.dart'; +import 'model/channel_events.dart'; +import 'model/client_info.dart'; +import 'model/history.dart'; +import 'model/presence_stats.dart'; +import 'model/stream_position.dart'; +import 'model/subscription_config.dart'; +import 'model/subscription_state.dart'; +import 'model/subscription_states.dart'; /// {@template subscription} /// Spinify subscription interface. @@ -36,26 +40,32 @@ import 'subscription_states_stream.dart'; /// - For server-side subscriptions see [SpinifyServerSubscription]. /// {@endtemplate} /// {@category Subscription} -sealed class SpinifySubscription { +abstract interface class SpinifySubscription { /// Channel name. abstract final String channel; /// Current subscription state. abstract final SpinifySubscriptionState state; + /// Whether subscription is recoverable using epoch and offset. + abstract final bool recoverable; + + /// Epoch of last successfully received message. + abstract final String epoch; + /// Offset of last successfully received message. - abstract final SpinifyStreamPosition? since; + abstract final fixnum.Int64 offset; /// Stream of subscription states. - abstract final SpinifySubscriptionStateStream states; + abstract final SpinifySubscriptionStates states; /// Stream of received pushes from Centrifugo server for a channel. - abstract final SpinifyPushesStream stream; + abstract final SpinifyChannelEvents stream; /// Await for subscription to be ready. /// Ready resolves when subscription successfully subscribed. /// Throws exceptions if called not in subscribing or subscribed state. - FutureOr ready(); + Future ready(); /// Publish data to current Subscription channel Future publish(List data); @@ -69,7 +79,7 @@ sealed class SpinifySubscription { }); /// Fetch presence information inside a channel. - Future presence(); + Future> presence(); /// Fetch presence stats information inside a channel. Future presenceStats(); @@ -129,11 +139,15 @@ sealed class SpinifySubscription { /// {@endtemplate} /// {@category Subscription} /// {@subCategory Client-side} -abstract class SpinifyClientSubscription extends SpinifySubscription { - /// Start subscribing to a channel +abstract interface class SpinifyClientSubscription + implements SpinifySubscription { + /// Subscription configuration. + abstract final SpinifySubscriptionConfig config; + + /// Start subscribing to a channel interactively. Future subscribe(); - /// Unsubscribe from a channel + /// Unsubscribe from a channel interactively. Future unsubscribe([ int code = 0, String reason = 'unsubscribe called', @@ -154,4 +168,5 @@ abstract class SpinifyClientSubscription extends SpinifySubscription { /// {@endtemplate} /// {@category Subscription} /// {@subCategory Server-side} -abstract class SpinifyServerSubscription extends SpinifySubscription {} +abstract interface class SpinifyServerSubscription + implements SpinifySubscription {} diff --git a/lib/src/transport_fake.dart b/lib/src/transport_fake.dart index 52fa239..7909678 100644 --- a/lib/src/transport_fake.dart +++ b/lib/src/transport_fake.dart @@ -6,15 +6,17 @@ import 'dart:math' as math; import 'package:fixnum/fixnum.dart'; +import 'model/channel_event.dart'; import 'model/command.dart'; import 'model/metric.dart'; import 'model/reply.dart'; import 'model/transport_interface.dart'; /// Create a fake Spinify transport. -SpinifyTransportBuilder $createFakeSpinifyTransport([ - void Function(ISpinifyTransport transport)? out, -]) => +SpinifyTransportBuilder $createFakeSpinifyTransport({ + SpinifyReply? Function(SpinifyCommand command)? overrideCommand, + void Function(ISpinifyTransport? transport)? out, +}) => ({ /// URL for the connection required url, @@ -31,10 +33,15 @@ SpinifyTransportBuilder $createFakeSpinifyTransport([ /// Callback for disconnect event required Future Function() onDisconnect, }) async { - final transport = SpinifyTransportFake() + final transport = SpinifyTransportFake( + overrideCommand: overrideCommand, + ) ..metrics = metrics ..onReply = onReply - ..onDisconnect = onDisconnect; + ..onDisconnect = () { + out?.call(null); + return onDisconnect(); + }; await transport._connect(url); out?.call(transport); return transport; @@ -46,7 +53,11 @@ class SpinifyTransportFake implements ISpinifyTransport { SpinifyTransportFake({ // Delay in milliseconds this.delay = 10, - }) : _random = math.Random(); + SpinifyReply? Function(SpinifyCommand command)? overrideCommand, + }) : _random = math.Random(), + _overrideCommand = overrideCommand; + + final SpinifyReply? Function(SpinifyCommand command)? _overrideCommand; /// Delay in milliseconds in the fake transport to simulate network latency. int delay; @@ -71,9 +82,14 @@ class SpinifyTransportFake implements ISpinifyTransport { Future send(SpinifyCommand command) async { if (!_isConnected) throw StateError('Not connected'); metrics - ..bytesSent += BigInt.one - ..messagesSent += BigInt.one; + ..bytesSent += 1 + ..messagesSent += 1; await _sleep(); + if (_overrideCommand != null) { + final reply = _overrideCommand.call(command); + if (reply != null) _onReply?.call(reply).ignore(); + return; + } switch (command) { case SpinifyPingRequest(:int id): _response( @@ -92,7 +108,21 @@ class SpinifyTransportFake implements ISpinifyTransport { expires: false, ttl: null, data: null, - subs: null, + subs: { + 'notification:index': SpinifySubscribeResult( + id: id, + timestamp: now, + data: null, + expires: false, + ttl: null, + positioned: false, + publications: const [], + recoverable: false, + recovered: false, + since: (epoch: '...', offset: Int64.ZERO), + wasRecovering: false, + ), + }, pingInterval: const Duration(seconds: 25), sendPong: false, session: 'fake', @@ -152,6 +182,7 @@ class SpinifyTransportFake implements ISpinifyTransport { id: id, timestamp: now, since: (epoch: '...', offset: Int64.ZERO), + publications: const [], ), ); case SpinifyRPCRequest(:int id, :String method, :List data): @@ -197,8 +228,8 @@ class SpinifyTransportFake implements ISpinifyTransport { () { if (!_isConnected) return; metrics - ..bytesReceived += BigInt.one - ..messagesReceived += BigInt.one; + ..bytesReceived += 1 + ..messagesReceived += 1; _onReply?.call(reply(DateTime.now())).ignore(); }, ); diff --git a/lib/src/transport_ws_pb_js.dart b/lib/src/transport_ws_pb_js.dart index 8aa03f3..bfce8c9 100644 --- a/lib/src/transport_ws_pb_js.dart +++ b/lib/src/transport_ws_pb_js.dart @@ -1,9 +1,58 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop' as js; +import 'dart:typed_data'; + import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart' as pb; +import 'package:web/web.dart' as web; +import 'model/channel_event.dart'; +import 'model/command.dart'; import 'model/config.dart'; import 'model/metric.dart'; import 'model/reply.dart'; import 'model/transport_interface.dart'; +import 'protobuf/client.pb.dart' as pb; +import 'protobuf/protobuf_codec.dart'; + +const _BlobCodec _blobCodec = _BlobCodec(); + +@immutable +final class _BlobCodec { + const _BlobCodec(); + + @internal + web.Blob write(Object data) { + switch (data) { + case String text: + return web.Blob([Uint8List.fromList(utf8.encode(text)).toJS].toJS); + case TypedData td: + return web.Blob([ + Uint8List.view( + td.buffer, + td.offsetInBytes, + td.lengthInBytes, + ).toJS + ].toJS); + case ByteBuffer bb: + return web.Blob([bb.asUint8List().toJS].toJS); + case List bytes: + return web.Blob([Uint8List.fromList(bytes).toJS].toJS); + case web.Blob blob: + return web.Blob([blob].toJS); + default: + throw ArgumentError.value(data, 'data', 'Invalid data type.'); + } + } + + @internal + Future> read(web.Blob blob) async { + final arrayBuffer = await blob.arrayBuffer().toDart; + final bytes = arrayBuffer.toDart.asUint8List(); + return bytes; + } +} /// Create a WebSocket Protocol Buffers transport. @internal @@ -22,5 +71,347 @@ Future $create$WS$PB$Transport({ /// Callback for disconnect event required void Function() onDisconnect, -}) => - throw UnimplementedError(); +}) async { + // ignore: close_sinks + final socket = web.WebSocket( + url, + {'centrifuge-protobuf'} + .map((e) => e.toJS) + .toList(growable: false) + .toJS, + ); + + try { + final completer = Completer(); + SpinifyTransport$WS$PB$JS? transport; + + // Fired when a connection with a WebSocket is opened. + // ignore: avoid_types_on_closure_parameters + final onOpen = (web.Event event) { + if (transport != null) return; + completer.complete(); + }.toJS; + + // Fired when a connection with a WebSocket has been closed + // because of an error, such as when some data couldn't be sent. + // ignore: avoid_types_on_closure_parameters + final onError = (web.Event event) { + if (transport != null) { + transport.disconnect(); + return; + } + switch (event) { + case web.ErrorEvent value + when value.error != null || value.message.isNotEmpty: + completer.completeError(Exception( + 'WebSocket connection error: ${value.error ?? value.message}')); + default: + completer.completeError( + Exception('WebSocket connection error: Unknown error')); + } + }.toJS; + + // Fired when a connection with a WebSocket is closed. + // ignore: avoid_types_on_closure_parameters + final onClose = (web.CloseEvent event) { + if (transport != null) { + transport.disconnect(event.code, event.reason); + return; + } + completer.completeError(Exception( + 'WebSocket connection closed: ${event.code} ${event.reason}')); + }.toJS; + + socket + ..addEventListener('open', onOpen) + ..addEventListener('error', onError) + ..addEventListener('close', onClose); + + await completer.future; + + transport = SpinifyTransport$WS$PB$JS( + socket, + config, + metrics, + onReply, + onDisconnect, + ); + + // 0 CONNECTING Socket has been created. The connection is not yet open. + // 1 OPEN The connection is open and ready to communicate. + // 2 CLOSING The connection is in the process of closing. + // 3 CLOSED The connection is closed or couldn't be opened. + assert(socket.readyState == 1, 'Socket is not open'); + return transport; + } on Object { + if (socket.readyState != 3) { + socket.close(); + } + rethrow; + } +} + +/// Create a WebSocket Protocol Buffers transport. +@internal +final class SpinifyTransport$WS$PB$JS implements ISpinifyTransport { + SpinifyTransport$WS$PB$JS( + this._socket, + SpinifyConfig config, + this._metrics, + this._onReply, + this._onDisconnect, + ) : _logger = config.logger, + _encoder = switch (config.logger) { + null => const ProtobufCommandEncoder(), + _ => ProtobufCommandEncoder(config.logger), + }, + _decoder = switch (config.logger) { + null => const ProtobufReplyDecoder(), + _ => ProtobufReplyDecoder(config.logger), + } { + _subscription = _socket.onMessage + .map((event) => event.data) + .asyncMap?>((data) { + switch (data) { + case String text: + return utf8.encode(text); + case web.Blob blob: + return _blobCodec.read(blob); + case TypedData td: + return Uint8List.view( + td.buffer, + td.offsetInBytes, + td.lengthInBytes, + ); + case ByteBuffer bb: + return bb.asInt8List(); + case List bytes: + return bytes; + default: + return null; + } + }).listen( + _onData, + cancelOnError: false, + onDone: _onDone, + ); + } + + final web.WebSocket _socket; + final Converter _encoder; + final Converter _decoder; + final SpinifyLogger? _logger; + late final StreamSubscription?> _subscription; + + int? _closeCode; + String? _closeReason; + + /// Metrics + final SpinifyMetrics$Mutable _metrics; + + /// Callback for reply messages + final void Function(SpinifyReply reply) _onReply; + + /// Callback for disconnect event + final void Function() _onDisconnect; + + /// Fired when data is received through a WebSocket. + void _onData(Object? bytes) { + if (bytes is! List || bytes.isEmpty) { + assert(false, 'Data is not byte array'); + return; + } + + _metrics + ..bytesReceived += bytes.length + ..messagesReceived += 1; + final reader = pb.CodedBufferReader(bytes); + while (!reader.isAtEnd()) { + try { + final message = pb.Reply(); + reader.readMessage(message, pb.ExtensionRegistry.EMPTY); + final reply = _decoder.convert(message); + _onReply.call(reply); + _logger?.call( + const SpinifyLogLevel.transport(), + 'transport_on_reply', + 'Reply ${reply.type}{id: ${reply.id}} received', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'bytes': bytes, + 'length': bytes.length, + 'reply': reply, + 'protobuf': message, + }, + ); + } on Object catch (error, stackTrace) { + _logger?.call( + const SpinifyLogLevel.error(), + 'transport_on_reply_error', + 'Error reading reply message', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'bytes': bytes, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + assert(false, 'Error reading message: $error'); + continue; + } + } + } + + @override + Future send(SpinifyCommand command) async { + try { + final message = _encoder.convert(command); + final commandData = message.writeToBuffer(); + final length = commandData.lengthInBytes; + final writer = pb.CodedBufferWriter() + ..writeInt32NoTag(length); //..writeRawBytes(commandData); + final bytes = writer.toBuffer() + commandData; + switch (bytes) { + case Uint8List uint8List: + _socket.send(uint8List.toJS); + case TypedData td: + _socket.send(Uint8List.view( + td.buffer, + td.offsetInBytes, + td.lengthInBytes, + ).toJS); + case List bytes: + _socket.send(Uint8List.fromList(bytes).toJS); + } + _metrics + ..bytesSent += bytes.length + ..messagesSent += 1; + _logger?.call( + const SpinifyLogLevel.transport(), + 'transport_send', + 'Command ${command.type}{id: ${command.id}} sent', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'command': command, + 'protobuf': message, + 'length': bytes.length, + 'bytes': bytes, + }, + ); + } on Object catch (error, stackTrace) { + _logger?.call( + const SpinifyLogLevel.error(), + 'transport_send_error', + 'Error sending command ${command.type}{id: ${command.id}}', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'command': command, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + rethrow; + } + } + + void _onDone() { + final timestamp = DateTime.now(); + int? code; + String? reason; + var reconnect = true; + if (_closeCode case int closeCode when closeCode > 0) { + switch (closeCode) { + case 1009: + // reconnect is true by default + code = 3; // disconnectCodeMessageSizeLimit; + reason = 'message size limit exceeded'; + reconnect = true; + case < 3000: + // We expose codes defined by Centrifuge protocol, + // hiding details about transport-specific error codes. + // We may have extra optional transportCode field in the future. + // reconnect is true by default + code = 1; // connectingCodeTransportClosed; + reason = _closeReason; + reconnect = true; + case >= 3000 && <= 3499: + // reconnect is true by default + code = closeCode; + reason = _closeReason; + reconnect = true; + case >= 3500 && <= 3999: + // application terminal codes + code = closeCode; + reason = _closeReason ?? 'application terminal code'; + reconnect = false; + case >= 4000 && <= 4499: + // custom disconnect codes + // reconnect is true by default + code = closeCode; + reason = _closeReason; + reconnect = true; + case >= 4500 && <= 4999: + // custom disconnect codes + // application terminal codes + code = closeCode; + reason = _closeReason ?? 'application terminal code'; + reconnect = false; + case >= 5000: + // reconnect is true by default + code = closeCode; + reason = _closeReason; + reconnect = true; + default: + code = closeCode; + reason = _closeReason; + reconnect = false; + } + } + code ??= 1; // connectingCodeTransportClosed + reason ??= 'transport closed'; + _onReply.call( + SpinifyPush( + timestamp: timestamp, + event: SpinifyDisconnect( + channel: '', + timestamp: timestamp, + code: code, + reason: reason, + reconnect: reconnect, + ), + ), + ); + _onDisconnect.call(); + _logger?.call( + const SpinifyLogLevel.transport(), + 'transport_disconnect', + 'Transport disconnected ' + '${reconnect ? 'temporarily' : 'permanently'} ' + 'with reason: $reason', + { + 'code': code, + 'reason': reason, + 'reconnect': reconnect, + }, + ); + } + + @override + Future disconnect([int? code, String? reason]) async { + _closeCode = code; + _closeReason = reason; + await _subscription.cancel(); + if (_socket.readyState == 3) + return; + else if (code != null && reason != null) + _socket.close(code, reason); + else if (code != null) + _socket.close(code); + else + _socket.close(); + } +} diff --git a/lib/src/transport_ws_pb_vm.dart b/lib/src/transport_ws_pb_vm.dart index 9d4d74b..e58e8fd 100644 --- a/lib/src/transport_ws_pb_vm.dart +++ b/lib/src/transport_ws_pb_vm.dart @@ -5,6 +5,7 @@ import 'dart:io' as io; import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart' as pb; +import 'model/channel_event.dart'; import 'model/command.dart'; import 'model/config.dart'; import 'model/metric.dart'; @@ -73,7 +74,7 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { _subscription = _socket.listen( _onData, cancelOnError: false, - onDone: _onDisconnect.call, + onDone: _onDone, ); } @@ -98,8 +99,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { return; } _metrics - ..bytesReceived += BigInt.from(bytes.length) - ..messagesReceived += BigInt.one; + ..bytesReceived += bytes.length + ..messagesReceived += 1; final reader = pb.CodedBufferReader(bytes); while (!reader.isAtEnd()) { try { @@ -150,8 +151,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { final bytes = writer.toBuffer() + commandData; _socket.add(bytes); _metrics - ..bytesSent += BigInt.from(bytes.length) - ..messagesSent += BigInt.one; + ..bytesSent += bytes.length + ..messagesSent += 1; _logger?.call( const SpinifyLogLevel.transport(), 'transport_send', @@ -182,10 +183,104 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { } } + void _onDone() { + final timestamp = DateTime.now(); + int? code; + String? reason; + var reconnect = true; + if (_socket + case io.WebSocket( + :int closeCode, + :String? closeReason, + ) when closeCode > 0) { + switch (closeCode) { + case 1009: + // reconnect is true by default + code = 3; // disconnectCodeMessageSizeLimit; + reason = 'message size limit exceeded'; + reconnect = true; + case < 3000: + // We expose codes defined by Centrifuge protocol, + // hiding details about transport-specific error codes. + // We may have extra optional transportCode field in the future. + // reconnect is true by default + code = 1; // connectingCodeTransportClosed; + reason = closeReason; + reconnect = true; + case >= 3000 && <= 3499: + // reconnect is true by default + code = closeCode; + reason = closeReason; + reconnect = true; + case >= 3500 && <= 3999: + // application terminal codes + code = closeCode; + reason = closeReason ?? 'application terminal code'; + reconnect = false; + case >= 4000 && <= 4499: + // custom disconnect codes + // reconnect is true by default + code = closeCode; + reason = closeReason; + reconnect = true; + case >= 4500 && <= 4999: + // custom disconnect codes + // application terminal codes + code = closeCode; + reason = closeReason ?? 'application terminal code'; + reconnect = false; + case >= 5000: + // reconnect is true by default + code = closeCode; + reason = closeReason; + reconnect = true; + default: + code = closeCode; + reason = closeReason; + reconnect = false; + } + } + code ??= 1; // connectingCodeTransportClosed + reason ??= 'transport closed'; + _onReply.call( + SpinifyPush( + timestamp: timestamp, + event: SpinifyDisconnect( + channel: '', + timestamp: timestamp, + code: code, + reason: reason, + reconnect: reconnect, + ), + ), + ); + _onDisconnect.call(); + _logger?.call( + const SpinifyLogLevel.transport(), + 'transport_disconnect', + 'Transport disconnected ' + '${reconnect ? 'temporarily' : 'permanently'} ' + 'with reason: $reason', + { + 'code': code, + 'reason': reason, + 'reconnect': reconnect, + }, + ); + } + @override Future disconnect([int? code, String? reason]) async { await _subscription.cancel(); - await _socket.close(code, reason); + if (_socket.readyState == 3) return; + if (_socket.readyState == 3) + return; + else if (code != null && reason != null) + await _socket.close(code, reason); + else if (code != null) + await _socket.close(code); + else + await _socket.close(); //assert(_socket.readyState == io.WebSocket.closed, 'Socket is not closed'); } } diff --git a/lib/src/util/backoff.dart b/lib/src/util/backoff.dart index 108d6a9..c3439c6 100644 --- a/lib/src/util/backoff.dart +++ b/lib/src/util/backoff.dart @@ -12,6 +12,9 @@ abstract final class Backoff { /// Full jitter technique. /// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + /// step - current step in backoff strategy. + /// minDelay - minimum delay in milliseconds. + /// maxDelay - maximum delay in milliseconds. static Duration nextDelay(int step, int minDelay, int maxDelay) { if (minDelay >= maxDelay) return Duration(milliseconds: maxDelay); final val = math.min(maxDelay, minDelay * math.pow(2, step.clamp(0, 31))); diff --git a/pubspec.yaml b/pubspec.yaml index 3c86512..2d75703 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,12 +45,12 @@ dependencies: # Annotations meta: ^1.9.1 - # WebSockets - ws: ^1.0.0-pre.6 - # Protocol Buffers protobuf: ^3.1.0 + # Web related libraries + web: ^1.0.0 + # Utilities crypto: ^3.0.3 fixnum: ^1.1.0 @@ -64,3 +64,10 @@ dev_dependencies: lints: ^3.0.0 test: ^1.24.4 fake_async: ^1.3.1 + # https://github.com/dart-lang/mockito/issues/732 + # https://github.com/dart-lang/mockito/pull/738 + # https://github.com/dart-lang/mockito/issues/755 + #mockito: + # git: + # url: https://github.com/dart-lang/mockito.git + # ref: master diff --git a/test/smoke/smoke_test.dart b/test/smoke/smoke_test.dart index a17cd43..500c075 100644 --- a/test/smoke/smoke_test.dart +++ b/test/smoke/smoke_test.dart @@ -6,8 +6,13 @@ import 'package:spinify/spinify.dart'; import 'package:test/test.dart'; void main() { + const url = 'ws://localhost:8000/connection/websocket'; + group('Connection', () { - const url = 'ws://localhost:8000/connection/websocket'; + // ignore: unused_element + void logger(SpinifyLogLevel level, String event, String message, + Map context) => + print('[$event] $message'); test('Connect_and_disconnect', () async { final client = Spinify(); @@ -24,9 +29,8 @@ void main() { test('Connect_and_refresh', () async { final client = Spinify( config: SpinifyConfig( - logger: (level, event, message, context) => - print('[$event] $message'), - ), + /* logger: logger, */ + ), ); await client.connect(url); expect(client.state, isA()); @@ -37,5 +41,178 @@ void main() { await client.close(); expect(client.state, isA()); }, timeout: const Timeout(Duration(minutes: 7))); + + test('Disconnect_temporarily', () async { + final client = Spinify( + config: SpinifyConfig( + connectionRetryInterval: ( + min: const Duration(milliseconds: 50), + max: const Duration(milliseconds: 150), + ), + /* logger: logger, */ + ), + ); + await client.connect(url); + expect(client.state, isA()); + await client.rpc('disconnect', utf8.encode('reconnect')); + // await client.stream.disconnect().first; + await client.states.disconnected.first; + expect(client.state, isA()); + expect( + client.metrics, + isA() + .having( + (m) => m.connects, + 'connects = 1', + equals(1), + ) + .having( + (m) => m.disconnects, + 'disconnects = 1', + equals(1), + ) + .having( + (m) => m.reconnectUrl, + 'reconnectUrl is set', + isNotNull, + ) + .having( + (m) => m.nextReconnectAt, + 'nextReconnectAt is set', + isNotNull, + ), + ); + await client.states.connecting.first; + await client.states.connected.first; + expect(client.state, isA()); + await Future.delayed(const Duration(milliseconds: 250)); + await client.close(); + expect(client.state, isA()); + }); + + test('Disconnect_permanent', () async { + final client = Spinify( + config: SpinifyConfig( + connectionRetryInterval: ( + min: const Duration(milliseconds: 50), + max: const Duration(milliseconds: 150), + ), + /* logger: logger, */ + ), + ); + await client.connect(url); + expect(client.state, isA()); + await client.rpc('disconnect'); + await client.states.disconnected.first; + expect(client.state, isA()); + expect( + client.metrics, + isA() + .having( + (m) => m.connects, + 'connects = 1', + equals(1), + ) + .having( + (m) => m.disconnects, + 'disconnects = 1', + equals(1), + ) + .having( + (m) => m.reconnectUrl, + 'reconnectUrl is not set', + isNull, + ) + .having( + (m) => m.nextReconnectAt, + 'nextReconnectAt is not set', + isNull, + ), + ); + await Future.delayed(const Duration(milliseconds: 250)); + expect(client.state, isA()); + await client.close(); + expect(client.state, isA()); + }); + }); + + group('Subscriptions', () { + test('Server_subscription', () async { + final client = Spinify(); + await client.connect(url); + expect(client.state, isA()); + final serverSubscriptions = client.subscriptions.server; + expect( + serverSubscriptions, + isA>() + .having( + (subs) => subs.keys, + 'server subscriptions', + containsAll(['notification:index']), + ) + .having( + (subs) => subs['notification:index'], + 'publications', + isA() + .having( + (sub) => sub.channel, + 'channel', + 'notification:index', + ) + .having( + (sub) => sub.state, + 'state', + isA(), + ), + ), + ); + final notification = serverSubscriptions['notification:index']; + expect( + notification, + allOf( + isNotNull, + isA() + .having((sub) => sub.state.isSubscribed, 'subscribed', isTrue), + ), + ); + notification!; + await expectLater( + notification.history, + throwsA( + isA() + .having( + (e) => e.replyCode, + 'replyCode', + equals(108), + ) + .having( + (e) => e.message.trim().toLowerCase(), + 'message', + equals('not available'), + ), + ), + ); + await expectLater(notification.presence(), completes); + await expectLater( + notification.presenceStats, + throwsA( + isA() + .having( + (e) => e.replyCode, + 'replyCode', + equals(108), + ) + .having( + (e) => e.message.trim().toLowerCase(), + 'message', + equals('not available'), + ), + ), + ); + await client.close(); + expect(client.state, isA()); + expect(notification.state.isUnsubscribed, isTrue); + expect(serverSubscriptions, isEmpty); + }); }); } diff --git a/test/unit/server_subscription_test.dart b/test/unit/server_subscription_test.dart new file mode 100644 index 0000000..e64ac48 --- /dev/null +++ b/test/unit/server_subscription_test.dart @@ -0,0 +1,83 @@ +import 'package:fake_async/fake_async.dart'; +import 'package:spinify/spinify.dart'; +import 'package:test/test.dart'; + +void main() { + group('SpinifyServerSubscription', () { + test( + 'Emulate server subscription', + () => fakeAsync( + (async) { + final client = Spinify( + config: SpinifyConfig( + transportBuilder: $createFakeSpinifyTransport( + overrideCommand: (command) => switch (command) { + SpinifyConnectRequest request => SpinifyConnectResult( + id: request.id, + timestamp: DateTime.now(), + client: 'fake', + version: '0.0.1', + expires: false, + ttl: null, + data: null, + subs: { + 'notification:index': SpinifySubscribeResult( + id: request.id, + timestamp: DateTime.now(), + data: const [], + expires: false, + ttl: null, + positioned: false, + publications: [ + SpinifyPublication( + channel: 'notification:index', + data: const [], + info: SpinifyClientInfo( + client: 'fake', + user: 'fake', + channelInfo: const [], + connectionInfo: const [], + ), + timestamp: DateTime.now(), + tags: const { + 'type': 'notification', + }, + offset: Int64.ZERO, + ), + ], + recoverable: false, + recovered: false, + since: (epoch: '...', offset: Int64.ZERO), + wasRecovering: false, + ), + }, + pingInterval: const Duration(seconds: 25), + sendPong: false, + session: 'fake', + node: 'fake', + ), + _ => null, + }, + ), + ), + )..connect('ws://localhost:8000/connection/websocket'); + async.elapse(client.config.timeout); + expect( + client.subscriptions.server, + isA>() + .having( + (s) => s.length, + 'length', + 1, + ) + .having( + (s) => s['notification:index'], + 'notification:index', + isA(), + ), + ); + }, + ), + ); + }); +} diff --git a/test/unit/spinify_test.dart b/test/unit/spinify_test.dart index 779dbae..4c10b6e 100644 --- a/test/unit/spinify_test.dart +++ b/test/unit/spinify_test.dart @@ -7,11 +7,11 @@ import 'package:test/test.dart'; void main() { group('Spinify', () { Spinify createFakeClient([ - void Function(ISpinifyTransport transport)? out, + void Function(ISpinifyTransport? transport)? out, ]) => Spinify( config: SpinifyConfig( - transportBuilder: $createFakeSpinifyTransport(out), + transportBuilder: $createFakeSpinifyTransport(out: out), ), ); @@ -164,35 +164,35 @@ void main() { expect( client.metrics, allOf([ - isA().having( + isA().having( (m) => m.state.isConnected, 'isConnected', isFalse, ), - isA().having( + isA().having( (m) => m.state, 'state', equals(client.state), ), - isA().having( + isA().having( (m) => m.connects, 'connects', 0, ), - isA().having( + isA().having( (m) => m.disconnects, 'disconnects', 0, ), - isA().having( + isA().having( (m) => m.messagesReceived, 'messagesReceived', - equals(BigInt.zero), + equals(Int64.ZERO), ), - isA().having( + isA().having( (m) => m.messagesSent, 'messagesSent', - equals(BigInt.zero), + equals(Int64.ZERO), ), ])); client.connect('ws://localhost:8000/connection/websocket'); @@ -200,35 +200,35 @@ void main() { expect( client.metrics, allOf([ - isA().having( + isA().having( (m) => m.state.isConnected, 'isConnected', isTrue, ), - isA().having( + isA().having( (m) => m.state, 'state', equals(client.state), ), - isA().having( + isA().having( (m) => m.connects, 'connects', 1, ), - isA().having( + isA().having( (m) => m.disconnects, 'disconnects', 0, ), - isA().having( + isA().having( (m) => m.messagesReceived, 'messagesReceived', - greaterThan(BigInt.zero), + greaterThan(Int64.ZERO), ), - isA().having( + isA().having( (m) => m.messagesSent, 'messagesSent', - greaterThan(BigInt.zero), + greaterThan(Int64.ZERO), ), ])); client.close(); @@ -236,27 +236,30 @@ void main() { expect( client.metrics, allOf([ - isA().having( + isA().having( (m) => m.state.isConnected, 'isConnected', isFalse, ), - isA().having( + isA().having( (m) => m.state, 'state', equals(client.state), ), - isA().having( + isA().having( (m) => m.connects, 'connects', 1, ), - isA().having( + isA().having( (m) => m.disconnects, 'disconnects', 1, ), ])); + expect(() => client.metrics.toString(), returnsNormally); + expect(() => client.metrics.toJson(), returnsNormally); + expect(client.metrics.toJson(), isA>()); })); }); } diff --git a/test/unit_test.dart b/test/unit_test.dart index 2a41207..5a0b050 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,11 +1,11 @@ -// ignore_for_file: unnecessary_lambdas - import 'package:test/test.dart'; +import 'unit/server_subscription_test.dart' as server_subscription_test; import 'unit/spinify_test.dart' as spinify_test; void main() { group('Unit', () { spinify_test.main(); + server_subscription_test.main(); }); } diff --git a/tool/echo/echo.go b/tool/echo/echo.go index 761fe2d..636742c 100644 --- a/tool/echo/echo.go +++ b/tool/echo/echo.go @@ -49,7 +49,7 @@ func waitExitSignal(n *centrifuge.Node, s *http.Server, sigCh chan os.Signal) { <-done } -var channels = []string{"public:index", "chat:index", "notification:index"} +var channels = []string{"public:index", "chat:index"} // Check whether channel is allowed for subscribing. In real case permission // check will probably be more complex than in this example. @@ -97,9 +97,15 @@ func Centrifuge() (*centrifuge.Node, error) { Subscriptions: map[string]centrifuge.SubscribeOptions{ "#" + cred.UserID: { EnableRecovery: true, - EmitPresence: true, - EmitJoinLeave: true, - PushJoinLeave: true, + EmitPresence: false, + EmitJoinLeave: false, + PushJoinLeave: false, + }, + "notification:index": { + EnableRecovery: true, + EmitPresence: false, + EmitJoinLeave: false, + PushJoinLeave: false, }, }, }, nil @@ -201,6 +207,17 @@ func Centrifuge() (*centrifuge.Node, error) { case "echo": // Return back input data. cb(centrifuge.RPCReply{Data: e.Data}, nil) + case "disconnect": + // Disconnect user + cb(centrifuge.RPCReply{}, nil) + if string(e.Data) == "reconnect" { + client.Disconnect(centrifuge.Disconnect{ + Code: 3001, + Reason: "disconnect with reconnection", + }) + } else { + client.Disconnect(centrifuge.DisconnectForceNoReconnect) + } default: // Method not found. cb(centrifuge.RPCReply{}, centrifuge.ErrorMethodNotFound)