From 504f7894e6d9ff998698aea83cc03d619206376a Mon Sep 17 00:00:00 2001 From: Mike Matiunin Date: Wed, 29 Jan 2025 01:10:42 +0400 Subject: [PATCH 1/6] Add basic clock layout --- README.md | 2 +- example/benchmark/README.md | 2 +- example/benchmark/docker-compose.yml | 2 +- example/benchmark/lib/src/help_tab.dart | 2 +- example/clock/.env.template | 0 example/clock/backend/Dockerfile | 0 example/clock/backend/analysis_options.yaml | 233 +++++++++++++++++++ example/clock/backend/bin/main.dart | 3 + example/clock/backend/lib/server.dart | 3 + example/clock/backend/pubspec.yaml | 61 +++++ example/clock/docker-compose.yml | 68 ++++++ example/clock/frontend/Dockerfile | 0 example/clock/frontend/analysis_options.yaml | 233 +++++++++++++++++++ example/clock/frontend/lib/site.dart | 3 + example/clock/frontend/pubspec.yaml | 72 ++++++ example/clock/frontend/web/index.html | 43 ++++ example/clock/frontend/web/main.dart | 4 + example/clock/frontend/web/styles.css | 14 ++ test/unit/spinify_test.dart | 61 +++++ 19 files changed, 802 insertions(+), 4 deletions(-) create mode 100644 example/clock/.env.template create mode 100644 example/clock/backend/Dockerfile create mode 100644 example/clock/backend/analysis_options.yaml create mode 100644 example/clock/backend/bin/main.dart create mode 100644 example/clock/backend/lib/server.dart create mode 100644 example/clock/backend/pubspec.yaml create mode 100644 example/clock/docker-compose.yml create mode 100644 example/clock/frontend/Dockerfile create mode 100644 example/clock/frontend/analysis_options.yaml create mode 100644 example/clock/frontend/lib/site.dart create mode 100644 example/clock/frontend/pubspec.yaml create mode 100644 example/clock/frontend/web/index.html create mode 100644 example/clock/frontend/web/main.dart create mode 100644 example/clock/frontend/web/styles.css diff --git a/README.md b/README.md index 621468d..7eff573 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Windows 11 Pro 64-bit CPU 13th Gen Intel Core i7-13700K Chrome Version 131.0.6778.86 (Official Build) (64-bit) Docker version 27.1.1 -Docker image centrifugo/centrifugo:v5 +Docker image centrifugo/centrifugo:latest Flutter 3.24.5 • Dart 3.5.4 Package spinify v0.1.0 Package centrifuge-dart v0.14.1 diff --git a/example/benchmark/README.md b/example/benchmark/README.md index 2dcede4..d9098d9 100644 --- a/example/benchmark/README.md +++ b/example/benchmark/README.md @@ -52,7 +52,7 @@ Logs: `docker-compose logs -f` services: centrifugo-benchmark: container_name: centrifugo-benchmark - image: centrifugo/centrifugo:v5 + image: centrifugo/centrifugo:latest restart: unless-stopped command: centrifugo --client_insecure --admin tty: true diff --git a/example/benchmark/docker-compose.yml b/example/benchmark/docker-compose.yml index 7421c62..c5a8e20 100644 --- a/example/benchmark/docker-compose.yml +++ b/example/benchmark/docker-compose.yml @@ -6,7 +6,7 @@ services: centrifugo-benchmark: container_name: centrifugo-benchmark - image: centrifugo/centrifugo:v5 + image: centrifugo/centrifugo:latest restart: unless-stopped command: centrifugo --client_insecure --admin tty: true diff --git a/example/benchmark/lib/src/help_tab.dart b/example/benchmark/lib/src/help_tab.dart index ca73ab4..3b67436 100644 --- a/example/benchmark/lib/src/help_tab.dart +++ b/example/benchmark/lib/src/help_tab.dart @@ -157,7 +157,7 @@ const String _helpComposeContent = ''' services: centrifugo-benchmark: container_name: centrifugo-benchmark - image: centrifugo/centrifugo:v5 + image: centrifugo/centrifugo:latest restart: unless-stopped command: centrifugo --client_insecure --admin tty: true diff --git a/example/clock/.env.template b/example/clock/.env.template new file mode 100644 index 0000000..e69de29 diff --git a/example/clock/backend/Dockerfile b/example/clock/backend/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/example/clock/backend/analysis_options.yaml b/example/clock/backend/analysis_options.yaml new file mode 100644 index 0000000..abd3e1f --- /dev/null +++ b/example/clock/backend/analysis_options.yaml @@ -0,0 +1,233 @@ +include: package:lints/recommended.yaml + +analyzer: + 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 + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + #public_member_api_docs: true + #lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + one_member_abstracts: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + unsafe_html: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true diff --git a/example/clock/backend/bin/main.dart b/example/clock/backend/bin/main.dart new file mode 100644 index 0000000..1ed3f49 --- /dev/null +++ b/example/clock/backend/bin/main.dart @@ -0,0 +1,3 @@ +import 'package:spinify_clock_backend/server.dart'; + +void main() => runServer(); diff --git a/example/clock/backend/lib/server.dart b/example/clock/backend/lib/server.dart new file mode 100644 index 0000000..afd9342 --- /dev/null +++ b/example/clock/backend/lib/server.dart @@ -0,0 +1,3 @@ +library; + +void runServer() {} diff --git a/example/clock/backend/pubspec.yaml b/example/clock/backend/pubspec.yaml new file mode 100644 index 0000000..df6aa44 --- /dev/null +++ b/example/clock/backend/pubspec.yaml @@ -0,0 +1,61 @@ +# ============================================================================== +# PROJECT METADATA +# ============================================================================== + +name: spinify_clock_backend + +description: "Spinify Clock: Backend" + +publish_to: 'none' + +version: 0.0.1+1 + +homepage: https://github.com/plugfox/spinify + +repository: https://github.com/plugfox/spinify + +issue_tracker: https://github.com/plugfox/spinify/issues + +#funding: +# - + +#topics: +# - + +platforms: + linux: + macos: + windows: + +#screenshots: +# - description: 'Screenshot 1' +# path: screenshot_1.png + + +# ============================================================================== +# PROJECT STRUCTURE +# ============================================================================== + +# https://dart.dev/tools/pub/workspaces +#workspace: +# - ../shared + +#executables: +# - + + +# ============================================================================== +# DEPENDENCIES +# ============================================================================== + +environment: + sdk: '>=3.6.1 <4.0.0' + +dependencies: + # Annotation + l: ^5.0.0 + meta: any + +dev_dependencies: + # Linting + lints: ^5.1.1 diff --git a/example/clock/docker-compose.yml b/example/clock/docker-compose.yml new file mode 100644 index 0000000..227af3e --- /dev/null +++ b/example/clock/docker-compose.yml @@ -0,0 +1,68 @@ +# Docker Compose configuration file for running Centrifugo powered clock application. +# docker compose up -d +# docker compose down +# docker compose logs -f + +services: + # Backend service + backend: + container_name: backend + restart: unless-stopped + command: + ports: + - 3080:80 + healthcheck: + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:80/health"] + interval: 3s + timeout: 3s + retries: 3 + + # Frontend service + frontend: + container_name: frontend + restart: unless-stopped + command: + ports: + - 3081:80 + healthcheck: + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:80/health"] + interval: 3s + timeout: 3s + retries: 3 + + # Centrifugo service + centrifugo: + container_name: centrifugo + image: centrifugo/centrifugo:latest + restart: unless-stopped + command: centrifugo --client_insecure --admin + tty: true + ports: + - 3082:8000 + healthcheck: + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] + interval: 3s + timeout: 3s + retries: 3 + environment: + - "CENTRIFUGO_ADMIN=true" + - "CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=80e88856-fe08-4a01-b9fc-73d1d03c2eee" + - "CENTRIFUGO_ADMIN_PASSWORD=6cec4cc2-960d-4e4a-b650-0cbd4bbf0530" + - "CENTRIFUGO_ADMIN_SECRET=70957aac-555b-4bce-b9b8-53ada3a8029e" + - "CENTRIFUGO_API_KEY=8aba9113-d67a-41c6-818a-27aaaaeb64e7" + - "CENTRIFUGO_ALLOWED_ORIGINS=*" + - "CENTRIFUGO_HEALTH=true" + - "CENTRIFUGO_HISTORY_SIZE=10" + - "CENTRIFUGO_HISTORY_TTL=300s" + - "CENTRIFUGO_FORCE_RECOVERY=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_SUBSCRIBER=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true" + - "CENTRIFUGO_LOG_LEVEL=debug" + ulimits: + nofile: + soft: 65535 + hard: 65535 diff --git a/example/clock/frontend/Dockerfile b/example/clock/frontend/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/example/clock/frontend/analysis_options.yaml b/example/clock/frontend/analysis_options.yaml new file mode 100644 index 0000000..abd3e1f --- /dev/null +++ b/example/clock/frontend/analysis_options.yaml @@ -0,0 +1,233 @@ +include: package:lints/recommended.yaml + +analyzer: + 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 + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + #public_member_api_docs: true + #lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + one_member_abstracts: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + unsafe_html: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true diff --git a/example/clock/frontend/lib/site.dart b/example/clock/frontend/lib/site.dart new file mode 100644 index 0000000..fb32b48 --- /dev/null +++ b/example/clock/frontend/lib/site.dart @@ -0,0 +1,3 @@ +library; + +void runSite() {} diff --git a/example/clock/frontend/pubspec.yaml b/example/clock/frontend/pubspec.yaml new file mode 100644 index 0000000..3ad23fd --- /dev/null +++ b/example/clock/frontend/pubspec.yaml @@ -0,0 +1,72 @@ +# ============================================================================== +# PROJECT METADATA +# ============================================================================== + +name: spinify_clock_frontend + +description: "Spinify Clock: Frontend" + +publish_to: 'none' + +version: 0.0.1+1 + +homepage: https://github.com/plugfox/spinify + +repository: https://github.com/plugfox/spinify + +issue_tracker: https://github.com/plugfox/spinify/issues + +#funding: +# - + +#topics: +# - + +platforms: + web: + +#screenshots: +# - description: 'Screenshot 1' +# path: screenshot_1.png + + +# ============================================================================== +# PROJECT STRUCTURE +# ============================================================================== + +# https://dart.dev/tools/pub/workspaces +#workspace: +# - ../shared + +#executables: +# - + + +# ============================================================================== +# DEPENDENCIES +# ============================================================================== + +environment: + sdk: '>=3.6.1 <4.0.0' + +dependencies: + # Localization + intl: ^0.20.2 + + # Annotation + l: ^5.0.0 + meta: any + + # Web + web: ^1.1.0 + +dev_dependencies: + # Linting + lints: ^5.1.1 + + # Testing + test: any + + # Code generation + build_runner: ^2.4.13 + build_web_compilers: ^4.1.1 diff --git a/example/clock/frontend/web/index.html b/example/clock/frontend/web/index.html new file mode 100644 index 0000000..54565e3 --- /dev/null +++ b/example/clock/frontend/web/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + Roadmap + + + + + + +
+ + + + + \ No newline at end of file diff --git a/example/clock/frontend/web/main.dart b/example/clock/frontend/web/main.dart new file mode 100644 index 0000000..d57a598 --- /dev/null +++ b/example/clock/frontend/web/main.dart @@ -0,0 +1,4 @@ +import 'package:web/web.dart' as web; +import 'package:spinify_clock_frontend/site.dart'; + +void main() => runSite(); diff --git a/example/clock/frontend/web/styles.css b/example/clock/frontend/web/styles.css new file mode 100644 index 0000000..d640c02 --- /dev/null +++ b/example/clock/frontend/web/styles.css @@ -0,0 +1,14 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); + +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: 'Roboto', sans-serif; +} + +#output { + padding: 20px; + text-align: center; +} diff --git a/test/unit/spinify_test.dart b/test/unit/spinify_test.dart index b79abe1..76e8eb1 100644 --- a/test/unit/spinify_test.dart +++ b/test/unit/spinify_test.dart @@ -649,6 +649,67 @@ void main() { ), ); + test( + 'Emulate_long_lifespan', + () => fakeAsync( + (async) { + var serverPingCount = 0; + var serverPongCount = 0; + final client = createFakeClient(transport: (_) async { + final ws = WebSocket$Fake(); + ws.onAdd = (bytes, sink) { + final command = ProtobufCodec.decode(pb.Command(), bytes); + if (command.hasConnect()) { + final reply = pb.Reply( + id: command.id, + connect: pb.ConnectResult( + client: 'fake', + version: '0.0.1', + expires: false, + ttl: null, + data: null, + subs: {}, + ping: 600, + pong: true, + session: 'fake', + node: 'fake', + ), + ); + scheduleMicrotask(() { + sink.add(ProtobufCodec.encode(reply)); + Timer.periodic( + Duration(milliseconds: reply.connect.ping), + (timer) { + if (ws.isClosed) { + timer.cancel(); + return; + } + serverPingCount++; + sink.add(ProtobufCodec.encode(pb.Reply())); + }, + ); + }); + } else if (command.hasPing()) { + serverPongCount++; + } + }; + return ws; + }); + unawaited(client.connect(url)); + async.elapse(client.config.timeout); + expect(client.state, isA()); + for (var day = 0; day < 5; day++) { + async.elapse(const Duration(days: 1)); + expect(client.state, isA()); + } + expect(client.state, isA()); + expect(serverPingCount, greaterThanOrEqualTo(720000)); + expect(serverPongCount, equals(serverPingCount)); + client.close(); + }, + ), + ); + test( 'ready', () => fakeAsync((async) { From 3aab263ada7c4e14240106a509d3b02eee223361 Mon Sep 17 00:00:00 2001 From: Mike Matiunin Date: Wed, 29 Jan 2025 02:05:42 +0400 Subject: [PATCH 2/6] Add clock shader --- example/clock/docker-compose.yml | 8 +- example/clock/frontend/lib/site.dart | 32 +- .../clock/frontend/lib/src/clock_layer.dart | 333 ++++++++++++++++++ example/clock/frontend/lib/src/engine.dart | 305 ++++++++++++++++ example/clock/frontend/web/index.html | 2 +- example/clock/frontend/web/main.dart | 1 + 6 files changed, 675 insertions(+), 6 deletions(-) create mode 100644 example/clock/frontend/lib/src/clock_layer.dart create mode 100644 example/clock/frontend/lib/src/engine.dart diff --git a/example/clock/docker-compose.yml b/example/clock/docker-compose.yml index 227af3e..3f01326 100644 --- a/example/clock/docker-compose.yml +++ b/example/clock/docker-compose.yml @@ -10,9 +10,9 @@ services: restart: unless-stopped command: ports: - - 3080:80 + - 3080:8080 healthcheck: - test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:80/health"] + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8080/health"] interval: 3s timeout: 3s retries: 3 @@ -23,9 +23,9 @@ services: restart: unless-stopped command: ports: - - 3081:80 + - 3081:8080 healthcheck: - test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:80/health"] + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8080"] interval: 3s timeout: 3s retries: 3 diff --git a/example/clock/frontend/lib/site.dart b/example/clock/frontend/lib/site.dart index fb32b48..b57b565 100644 --- a/example/clock/frontend/lib/site.dart +++ b/example/clock/frontend/lib/site.dart @@ -1,3 +1,33 @@ library; -void runSite() {} +import 'dart:async'; + +import 'package:l/l.dart'; +import 'package:spinify_clock_frontend/src/clock_layer.dart'; +import 'package:spinify_clock_frontend/src/engine.dart'; + +void runSite() => l.capture( + () => runZonedGuarded( + () { + final layer = ClockLayer()..setTime(DateTime.now()); + final _ = RenderingEngine.instance + ..addLayer(layer) + ..start(); + + Timer.periodic(const Duration(seconds: 1), (timer) { + layer.setTime(DateTime.now()); + }); + + l.i('Engine started'); + }, + l.e, + ), + LogOptions( + outputInRelease: true, + handlePrint: true, + printColors: false, + output: LogOutput.platform, + overrideOutput: (message) => '[${message.level}] ${message.message}', + messageFormatting: (message) => message, + ), + ); diff --git a/example/clock/frontend/lib/src/clock_layer.dart b/example/clock/frontend/lib/src/clock_layer.dart new file mode 100644 index 0000000..0753ce1 --- /dev/null +++ b/example/clock/frontend/lib/src/clock_layer.dart @@ -0,0 +1,333 @@ +// ignore_for_file: cascade_invocations + +import 'dart:js_interop'; +import 'dart:math' as math; +import 'dart:typed_data' as td; + +import 'package:intl/intl.dart' as intl; +import 'package:spinify_clock_frontend/src/engine.dart'; +import 'package:web/web.dart'; + +class ClockLayer implements ResizableLayer { + ClockLayer(); + + bool _dirty = false; + DateTime _time = DateTime(0); + static final intl.DateFormat _timeFormat = intl.DateFormat('HH:mm:ss'); + int _fontSize = 32; + + /// Set the time to display on the clock. + void setTime(DateTime time) { + if (time.second == _time.second && + time.minute == _time.minute && + time.hour == _time.hour) return; + _time = time; + if (_initialized) _updateTimeVertices(); + _dirty = true; + } + + bool _initialized = false; + late WebGLProgram _program; + late WebGLBuffer _vertexBuffer; + late WebGLBuffer _colorBuffer; + + // Shader locations + late int _positionLocation; + late int _colorLocation; + late WebGLUniformLocation _resolutionLocation; + + int _width = window.innerWidth; + int _height = window.innerHeight; + late double _centerX; + late double _centerY; + late double _radius; + + /// Римские цифры для часов + static const int _circleSegments = 24; + static const List _romanNumerals = [ + 'XII', + 'I', + 'II', + 'III', + 'IV', + 'V', + 'VI', + 'VII', + 'VIII', + 'IX', + 'X', + 'XI', + ]; + static final td.Float32List _hourColors = + td.Float32List.fromList([0, 0, 1, 1, 0, 0, 1, 1]); // Синий цвет + static final td.Float32List _minuteColors = + td.Float32List.fromList([0, 1, 0, 1, 0, 1, 0, 1]); // Зеленый цвет + static final td.Float32List _secondColors = + td.Float32List.fromList([1, 0, 0, 1, 1, 0, 0, 1]); // Красный цвет + static final td.Float32List _markColors = + td.Float32List.fromList([0.5, 0.5, 0.5, 1, 0.5, 0.5, 0.5, 1]); + static final td.Float32List _circleColors = + td.Float32List.fromList(List.filled(_circleSegments ~/ 2 * 4 * 4, 1)); + + @override + bool get isVisible => true; + + void _initializeGL(RenderContext context) { + if (_initialized) return; + + final gl = context.ctxGL; + + // Vertex shader для позиции и цвета + final vertexShader = gl.createShader(WebGL2RenderingContext.VERTEX_SHADER)!; + gl.shaderSource(vertexShader, ''' + attribute vec2 a_position; + attribute vec4 a_color; + uniform vec2 u_resolution; + varying vec4 v_color; + + void main() { + vec2 zeroToOne = a_position / u_resolution; + vec2 zeroToTwo = zeroToOne * 2.0; + vec2 clipSpace = zeroToTwo - 1.0; + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); + v_color = a_color; + } + '''); + gl.compileShader(vertexShader); + + // Fragment shader для цвета + final fragmentShader = + gl.createShader(WebGL2RenderingContext.FRAGMENT_SHADER)!; + gl.shaderSource(fragmentShader, ''' + precision mediump float; + varying vec4 v_color; + + void main() { + gl_FragColor = v_color; + } + '''); + gl.compileShader(fragmentShader); + + // Создаем и линкуем программу + _program = gl.createProgram()!; + gl.attachShader(_program, vertexShader); + gl.attachShader(_program, fragmentShader); + gl.linkProgram(_program); + + // Получаем локации атрибутов и uniform-переменных + _positionLocation = gl.getAttribLocation(_program, 'a_position'); + _colorLocation = gl.getAttribLocation(_program, 'a_color'); + _resolutionLocation = gl.getUniformLocation(_program, 'u_resolution')!; + + // Создаем буферы + _vertexBuffer = gl.createBuffer()!; + _colorBuffer = gl.createBuffer()!; + + _initialized = true; + } + + /// Рисует линии на экране + void _drawLines( + RenderContext context, + td.Float32List vertices, + td.Float32List colors, + ) { + final gl = context.ctxGL; + + // Устанавливаем вершины + gl.bindBuffer(WebGL2RenderingContext.ARRAY_BUFFER, _vertexBuffer); + gl.bufferData( + WebGL2RenderingContext.ARRAY_BUFFER, + vertices.toJS, + WebGL2RenderingContext.STATIC_DRAW, + ); + gl.enableVertexAttribArray(_positionLocation); + gl.vertexAttribPointer( + _positionLocation, + 2, + WebGL2RenderingContext.FLOAT, + false, + 0, + 0, + ); + + // Устанавливаем цвета + gl.bindBuffer(WebGL2RenderingContext.ARRAY_BUFFER, _colorBuffer); + gl.bufferData( + WebGL2RenderingContext.ARRAY_BUFFER, + colors.toJS, + WebGL2RenderingContext.STATIC_DRAW, + ); + gl.enableVertexAttribArray(_colorLocation); + gl.vertexAttribPointer( + _colorLocation, + 4, + WebGL2RenderingContext.FLOAT, + false, + 0, + 0, + ); + + // Рисуем линии + gl.drawArrays(WebGL2RenderingContext.LINES, 0, vertices.length ~/ 2); + } + + /// Создает вершины для стрелок часов + td.Float32List _createHandVertices(double angle, double length) => + td.Float32List(4) + ..[0] = _centerX // Начальная точка (центр циферблата) + ..[1] = _centerY // Начальная точка (центр циферблата) + ..[2] = _centerX + + length * math.cos(angle) // Конечная точка (конец стрелки) + ..[3] = _centerY + + length * math.sin(angle); // Конечная точка (конец стрелки) + + /// Создает вершины для циферблата + td.Float32List _createCircleVertices(int segments) { + final vertices = td.Float32List(segments * 4); + + for (var i = 0; i < segments; i++) { + final angle1 = i * 2 * math.pi / segments; + final angle2 = (i + 1) * 2 * math.pi / segments; + vertices + ..[i * 4 + 0] = _centerX + _radius * math.cos(angle1) + ..[i * 4 + 1] = _centerY + _radius * math.sin(angle1) + ..[i * 4 + 2] = _centerX + _radius * math.cos(angle2) + ..[i * 4 + 3] = _centerY + _radius * math.sin(angle2); + } + + return vertices; + } + + @override + void mount(RenderContext context) { + _initializeGL(context); + _updateDimensions(); + } + + void _updateDimensions() { + _centerX = _width / 2; + _centerY = _height / 2; + _radius = math.min(_width, _height) * 0.4; + } + + @override + void onResize(int width, int height) { + _width = width; + _height = height; + _updateDimensions(); + _updateTimeVertices(); + _fontSize = switch (math.min(_width, _height)) { + > 1024 => 64, + > 768 => 48, + > 512 => 32, + > 256 => 24, + > 128 => 16, + > 64 => 12, + > 32 => 10, + _ => 8, + }; + _dirty = true; + } + + td.Float32List _hourVertices = td.Float32List(4), + _minuteVertices = td.Float32List(4), + _secondVertices = td.Float32List(4); + + void _updateTimeVertices() { + final now = DateTime.now(); + final hours = now.hour % 12; + final minutes = now.minute; + final seconds = now.second; + + final hourAngle = (hours + minutes / 60) * 2 * math.pi / 12 - math.pi / 2; + final minuteAngle = minutes * 2 * math.pi / 60 - math.pi / 2; + final secondAngle = seconds * 2 * math.pi / 60 - math.pi / 2; + + _hourVertices = _createHandVertices(hourAngle, _radius * 0.5); + _minuteVertices = _createHandVertices(minuteAngle, _radius * 0.7); + _secondVertices = _createHandVertices(secondAngle, _radius * 0.8); + } + + @override + void update(RenderContext context, double delta) {} + + @override + void render(RenderContext context, double delta) { + if (!_dirty) return; // Skip rendering if not dirty + _dirty = false; + + final gl = context.ctxGL; + + // Очищаем и подготавливаем GL + gl.viewport(0, 0, _width, _height); + //gl.clearColor(0.1, 0.1, 0.1, 1.0); // Фон WebGL + gl.clear(WebGL2RenderingContext.COLOR_BUFFER_BIT); + //gl.lineWidth(2); + + gl.useProgram(_program); + gl.uniform2f(_resolutionLocation, _width.toDouble(), _height.toDouble()); + + // Рисуем циферблат (окружность) + final circleVertices = _createCircleVertices(_circleSegments); + _drawLines(context, circleVertices, _circleColors); + + // Рисуем часовую стрелку + _drawLines(context, _hourVertices, _hourColors); + + // Рисуем минутную стрелку + _drawLines(context, _minuteVertices, _minuteColors); + + // Рисуем секундную стрелку + _drawLines(context, _secondVertices, _secondColors); + + // Рисуем деления часов + for (var i = 0; i < 12; i++) { + final angle = i * 2 * math.pi / 12; + final outerRadius = _radius; + final innerRadius = _radius * 0.9; + + final markVertices = td.Float32List(4) + ..[0] = _centerX + innerRadius * math.cos(angle) + ..[1] = _centerY + innerRadius * math.sin(angle) + ..[2] = _centerX + outerRadius * math.cos(angle) + ..[3] = _centerY + outerRadius * math.sin(angle); + + _drawLines(context, markVertices, _markColors); + } + + // Draw text on top of the canvas + + context.ctx2D + ..clearRect(0, 0, context.width, context.height) + ..font = '${_fontSize}px Arial' + ..fillStyle = 'white'.toJS + ..textAlign = 'center' + ..textBaseline = 'middle'; + + // Draw roman numerals + for (var i = 0; i < 12; i++) { + final angle = i * 2 * math.pi / 12 - math.pi / 2; + final text = _romanNumerals[i]; + final textX = _centerX + (_radius + _fontSize / 1.5) * math.cos(angle); + final textY = _centerY + (_radius + _fontSize / 1.5) * math.sin(angle); + context.ctx2D.fillText(text, textX, textY); + } + + final text = _timeFormat.format(_time); + //final metrics = context.ctx2D.measureText(text); + context.ctx2D + // Draw time at the center of the canvas + .fillText(text, _centerX, _centerY); + } + + @override + void unmount(RenderContext context) { + if (_initialized) { + final gl = context.ctxGL; + gl.deleteProgram(_program); + gl.deleteBuffer(_vertexBuffer); + gl.deleteBuffer(_colorBuffer); + } + } +} diff --git a/example/clock/frontend/lib/src/engine.dart b/example/clock/frontend/lib/src/engine.dart new file mode 100644 index 0000000..a1c1e7f --- /dev/null +++ b/example/clock/frontend/lib/src/engine.dart @@ -0,0 +1,305 @@ +// ignore_for_file: prefer_constructors_over_static_methods + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:l/l.dart'; +import 'package:web/web.dart'; + +// Rendering context +class RenderContext { + RenderContext._({ + required int width, + required int height, + required this.canvasGL, + required this.ctxGL, + required this.canvasUI, + required this.ctx2D, + required this.resources, + }) : _width = width, + _height = height; + + /// Width of the canvas. + int get width => _width; + int _width; + + /// Height of the canvas. + int get height => _height; + int _height; + + /// WebGL canvas for rendering shaders. + final HTMLCanvasElement canvasGL; + + /// WebGL2 context. + final WebGL2RenderingContext ctxGL; + + /// 2D canvas for rendering UI. + final HTMLCanvasElement canvasUI; + + /// 2D context. + final CanvasRenderingContext2D ctx2D; + + /// Resources for rendering, such as textures, shaders and buffers. + final Map resources; + + /// Get a resource from the context. + T getResource(String key) => resources[key] as T; + + /// Set a resource in the context. + void setResource(String key, T value) => resources[key] = value; + + /// Remove a resource from the context. + void delResource(String key) => resources.remove(key); +} + +// Core rendering infrastructure +abstract interface class Layer { + /// Whether the layer is visible. + bool get isVisible; + + /// Called when the layer is mounted. + void mount(RenderContext context); + + /// Update the layer with the given delta time. + void update(RenderContext context, double delta); + + /// Render the layer with the given context and delta time. + void render(RenderContext context, double delta); + + /// Called when the layer is unmounted. + void unmount(RenderContext context); +} + +/// Layer that can be resized. +abstract interface class ResizableLayer implements Layer { + /// Called when the layer is resized. + void onResize(int width, int height); +} + +/// Rendering engine that manages layers and rendering. +class RenderingEngine { + RenderingEngine._({ + required ShadowRoot shadow, + required HTMLDivElement container, + required List layers, + required RenderContext context, + }) : _shadow = shadow, + _container = container, + _layers = layers, + _context = context; + + static RenderingEngine? _instance; + + /// Singleton instance of the rendering engine. + static RenderingEngine get instance => _instance ??= () { + final app = document.querySelector('#app'); + if (app == null) throw StateError('Failed to find app element'); + final children = app.children; + for (var i = children.length - 1; i >= 0; i--) + children.item(i)!.remove(); + + final shadow = app.attachShadow(ShadowRootInit( + mode: 'open', + clonable: false, + serializable: false, + delegatesFocus: false, + slotAssignment: 'manual', + )); + + final container = HTMLDivElement() + ..id = 'engine' + ..style.position = 'fixed' + ..style.top = '0' + ..style.left = '0' + ..style.width = '100%' + ..style.height = '100%' + ..style.overflow = 'hidden'; + + final width = window.innerWidth; + final height = window.innerHeight; + + final layers = []; + // Initialize WebGL Canvas + final canvasGL = document.createElement('canvas') as HTMLCanvasElement + ..id = 'gl-canvas' + ..width = width + ..height = height + ..style.position = 'absolute' + ..style.top = '0' + ..style.left = '0' + ..style.zIndex = '0'; + + // Get WebGL context with alpha for transparency + final ctxGL = canvasGL.getContext( + 'webgl2', + { + 'alpha': false, + 'depth': false, + 'antialias': true, // false, - for performance and pixel art + 'powerPreference': 'high-performance', + 'preserveDrawingBuffer': false, + }.jsify(), + ) as WebGL2RenderingContext; + // Initialize 2D Canvas + final canvasUI = document.createElement('canvas') as HTMLCanvasElement + ..id = 'ui-canvas' + ..width = width + ..height = height + ..style.position = 'absolute' + ..style.top = '0' + ..style.left = '0' + ..style.zIndex = '1'; + + final ctx2D = canvasUI.getContext( + '2d', + { + 'alpha': true, + 'willReadFrequently': false, + }.jsify(), + ) as CanvasRenderingContext2D; + // Append canvases to the body + shadow.append(container + ..append(canvasGL) + ..append(canvasUI)); + final engine = RenderingEngine._( + shadow: shadow, + container: container, + layers: layers, + context: RenderContext._( + width: width, + height: height, + canvasGL: canvasGL, + ctxGL: ctxGL, + canvasUI: canvasUI, + ctx2D: ctx2D, + resources: {}, + ), + ); + return engine; + }(); + + final ShadowRoot _shadow; + final HTMLDivElement _container; + final List _layers; + + bool _isClosed = false; + bool _isRunning = false; + double _lastFrameTime = 0; + + // Rendering context + final RenderContext _context; + + Timer? _healthCehckTimer; + + /// Resize the rendering engine. + void _onResize(int width, int height) { + if (_isClosed) return; + if (_context.width == width && _context.height == height) return; + l.d('Resize to $width x $height'); + _context + .._width = width + .._height = height; + _context.canvasGL + ..width = width + ..height = height; + _context.canvasUI + ..width = width + ..height = height; + // Notify layers about resize + for (final layer in _layers) { + if (layer case ResizableLayer resizableLayer) { + resizableLayer.onResize(width, height); + } + } + } + + late final JSExportedDartFunction _onResizeJS = ((Event event) { + _onResize(window.innerWidth, window.innerHeight); + }).toJS; + + /// Add a layer to the rendering engine. + void addLayer(Layer layer) { + _layers.add(layer); + layer.mount(_context); + if (layer is ResizableLayer) + layer.onResize(_context.width, _context.height); + } + + /// Remove a layer from the rendering engine. + void removeLayer(Layer layer) { + if (_layers.remove(layer)) layer.unmount(_context); + } + + /// Tick the rendering engine. + void _renderFrame(num currentTime) { + if (!_isRunning) return; + + // Calculate delta time + final deltaTime = (currentTime - _lastFrameTime) / 1000.0; + _lastFrameTime = currentTime.toDouble(); + + // Clear both contexts + //_webGl.clear(WebGL.COLOR_BUFFER_BIT | WebGL.DEPTH_BUFFER_BIT); + //_ctx2d.clearRect(0, 0, _canvas.width, _canvas.height); + + // Update and render all visible layers + for (final layer in _layers) { + if (!layer.isVisible) continue; + layer + ..update(_context, deltaTime) + ..render(_context, deltaTime); + } + + window.requestAnimationFrame(_renderFrameJS); + } + + late final JSExportedDartFunction _renderFrameJS = _renderFrame.toJS; + + /// Start the rendering engine. + void start() { + if (_isRunning) return; + + final container = _container; + + window.addEventListener('resize', _onResizeJS); + + // Health check + _healthCehckTimer?.cancel(); + _healthCehckTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_isClosed) timer.cancel(); + if (container.isConnected) return; + l.w('Engine container is not connected'); + dispose(); + }); + + // Start rendering + _isRunning = true; + _lastFrameTime = window.performance.now(); + window.requestAnimationFrame(_renderFrameJS); + } + + /// Stop the rendering engine. + void stop() { + _isRunning = false; + window.removeEventListener('resize', _onResizeJS); + _healthCehckTimer?.cancel(); + } + + /// Dispose the rendering engine. + void dispose() { + stop(); + for (final layer in _layers) layer.unmount(_context); + _layers.clear(); + _context + ..canvasGL.remove() + ..canvasUI.remove(); + final app = document.querySelector('#app'); + if (app != null) { + app.removeChild(_shadow); + final children = app.children; + for (var i = children.length - 1; i >= 0; i--) children.item(i)!.remove(); + } + _isClosed = true; + _instance = null; + } +} diff --git a/example/clock/frontend/web/index.html b/example/clock/frontend/web/index.html index 54565e3..903c495 100644 --- a/example/clock/frontend/web/index.html +++ b/example/clock/frontend/web/index.html @@ -7,7 +7,7 @@ - Roadmap + Spinify: Clock