diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic
index 4ed180b5659..f07f6deb191 100644
--- a/.config/dictionaries/project.dic
+++ b/.config/dictionaries/project.dic
@@ -320,5 +320,8 @@ xcodeproj
xctest
xctestrun
xcworkspace
+xprv
+xpub
+xpublic
xvfb
yoroi
diff --git a/.earthlyignore b/.earthlyignore
index 9cc8aa7e850..c0dad16a97f 100644
--- a/.earthlyignore
+++ b/.earthlyignore
@@ -11,6 +11,7 @@
**/*.iml
**/coverage/
**/test_reports/
+**/*.log
# node related
diff --git a/.github/workflows/build-flutter-web.yml b/.github/workflows/build-flutter-web.yml
deleted file mode 100644
index c7bfff78407..00000000000
--- a/.github/workflows/build-flutter-web.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Deploy Catalyst Voices Web App
-
-on:
- push:
- branches:
- - main
-
-permissions:
- contents: write
- pull-requests: write
- id-token: write
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
- cancel-in-progress: true
-
-env:
- AWS_REGION: eu-central-1
- AWS_ROLE_ARN: arn:aws:iam::332405224602:role/ci
- EARTHLY_TARGET: docker
- ECR_REGISTRY: 332405224602.dkr.ecr.eu-central-1.amazonaws.com
-
-jobs:
- deploy-voices-web-app:
- name: Deploy Catalyst Voices Web App
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup CI
- uses: input-output-hk/catalyst-ci/actions/setup@master
- with:
- aws_role_arn: ${{ env.AWS_ROLE_ARN }}
- aws_region: ${{ env.AWS_REGION }}
- earthly_runner_secret: ${{ secrets.EARTHLY_RUNNER_SECRET }}
-
- - name: Build Flutter Web
- uses: input-output-hk/catalyst-ci/actions/run@master
- if: always()
- continue-on-error: false
- with:
- earthfile: ./catalyst_voices/
- flags: --allow-privileged
- targets: build-web
- target_flags: --RUN_ON_PR=false --SENTRY_DSN=${{ secrets.SENTRY_DSN }}
- runner_address: ${{ secrets.EARTHLY_SATELLITE_ADDRESS }}
- artifact: "true"
-
- - name: Package Flutter Web
- uses: input-output-hk/catalyst-ci/actions/run@master
- if: always()
- continue-on-error: false
- with:
- earthfile: ./catalyst_voices/
- flags: --allow-privileged
- targets: package
- runner_address: ${{ secrets.EARTHLY_SATELLITE_ADDRESS }}
- artifact: "true"
-
- - name: Publish Flutter Web
- uses: input-output-hk/catalyst-ci/actions/run@master
- if: always()
- continue-on-error: false
- with:
- earthfile: ./catalyst_voices/
- flags: --allow-privileged
- targets: publish
- runner_address: ${{ secrets.EARTHLY_SATELLITE_ADDRESS }}
- artifact: "true"
diff --git a/.vscode/launch.recommended.json b/.vscode/launch.recommended.json
index b9de6a4e6e4..1230be7f0ca 100644
--- a/.vscode/launch.recommended.json
+++ b/.vscode/launch.recommended.json
@@ -22,7 +22,12 @@
"program": "lib/configs/main_web.dart",
"args": [
"--dart-define",
- "SENTRY_DSN=REPLACE_WITH_SENTRY_DSN_URL"
+ "SENTRY_DSN=REPLACE_WITH_SENTRY_DSN_URL",
+ // flutter_rust_bridge: https://cjycode.com/flutter_rust_bridge/manual/miscellaneous/web-cross-origin#when-flutter-run
+ "--web-header",
+ "Cross-Origin-Opener-Policy=same-origin",
+ "--web-header",
+ "Cross-Origin-Embedder-Policy=require-corp"
]
},
{
diff --git a/Earthfile b/Earthfile
index 4898523c5ff..99b54f456cb 100644
--- a/Earthfile
+++ b/Earthfile
@@ -1,8 +1,8 @@
VERSION 0.8
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.23 AS mdlint-ci
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.23 AS cspell-ci
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.23 AS postgresql-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.24 AS mdlint-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.24 AS cspell-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.24 AS postgresql-ci
FROM debian:stable-slim
diff --git a/catalyst-gateway/Earthfile b/catalyst-gateway/Earthfile
index 96ee8ec7243..1cde02848f7 100644
--- a/catalyst-gateway/Earthfile
+++ b/catalyst-gateway/Earthfile
@@ -1,6 +1,6 @@
VERSION 0.8
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.23 AS rust-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.24 AS rust-ci
#cspell: words rustfmt toolsets USERARCH stdcfgs
diff --git a/catalyst-gateway/event-db/Earthfile b/catalyst-gateway/event-db/Earthfile
index 79bc5a5a20e..dac2e2647ca 100644
--- a/catalyst-gateway/event-db/Earthfile
+++ b/catalyst-gateway/event-db/Earthfile
@@ -3,7 +3,7 @@
# the database and its associated software.
VERSION 0.8
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.23 AS postgresql-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.24 AS postgresql-ci
# cspell: words
diff --git a/catalyst-gateway/rustfmt.toml b/catalyst-gateway/rustfmt.toml
index b0f20832c9f..fa6d8c2e906 100644
--- a/catalyst-gateway/rustfmt.toml
+++ b/catalyst-gateway/rustfmt.toml
@@ -36,7 +36,7 @@ max_width = 100
# Comments:
normalize_comments = true
-normalize_doc_attributes = true
+normalize_doc_attributes = false
wrap_comments = true
comment_width = 90 # small excess is okay but prefer 80
format_code_in_doc_comments = true
@@ -65,4 +65,4 @@ condense_wildcard_suffixes = true
hex_literal_case = "Upper"
# Ignored files:
-ignore = []
+ignore = []
\ No newline at end of file
diff --git a/catalyst-gateway/tests/Earthfile b/catalyst-gateway/tests/Earthfile
index d5b0f76de80..979773202f8 100644
--- a/catalyst-gateway/tests/Earthfile
+++ b/catalyst-gateway/tests/Earthfile
@@ -1,5 +1,5 @@
VERSION 0.8
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/spectral:v3.2.23 AS spectral-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/spectral:v3.2.24 AS spectral-ci
# cspell: words oapi
# test-lint-openapi - OpenAPI linting from an artifact
diff --git a/catalyst-gateway/tests/api_tests/Earthfile b/catalyst-gateway/tests/api_tests/Earthfile
index 40551a40233..e0bb11a20cb 100644
--- a/catalyst-gateway/tests/api_tests/Earthfile
+++ b/catalyst-gateway/tests/api_tests/Earthfile
@@ -1,6 +1,6 @@
VERSION 0.8
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/python:v3.2.23 AS python-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/python:v3.2.24 AS python-ci
builder:
FROM python-ci+python-base
diff --git a/catalyst_voices/.earthlyignore b/catalyst_voices/.earthlyignore
index 9cc8aa7e850..c0dad16a97f 100644
--- a/catalyst_voices/.earthlyignore
+++ b/catalyst_voices/.earthlyignore
@@ -11,6 +11,7 @@
**/*.iml
**/coverage/
**/test_reports/
+**/*.log
# node related
diff --git a/catalyst_voices/.gitignore b/catalyst_voices/.gitignore
index ac235143a1b..a77786aa472 100644
--- a/catalyst_voices/.gitignore
+++ b/catalyst_voices/.gitignore
@@ -1,6 +1,26 @@
### Dart ###
# See https://www.dartlang.org/guides/libraries/private-files
+# Generated files from code generation tools
+*.g.dart
+*.freezed.dart
+*.chopper.dart
+*.swagger.dart
+*.openapi.dart
+*.gen.dart
+
+# Un-ignore generated files in public packages
+!**/packages/libs/**/*.g.dart
+!**/packages/libs/**/*.freezed.dart
+!**/packages/libs/**/*.chopper.dart
+!**/packages/libs/**/*.swagger.dart
+!**/packages/libs/**/*.openapi.dart
+!**/packages/libs/**/*.gen.dart
+
+# Localization (l10n) generated files
+packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_*.dart
+packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart
+
# Files and directories created by pub
.dart_tool/
.packages
diff --git a/catalyst_voices/.idea/.name b/catalyst_voices/.idea/.name
deleted file mode 100644
index 8482e6c4500..00000000000
--- a/catalyst_voices/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-catalyst_voices
\ No newline at end of file
diff --git a/catalyst_voices/.idea/libraries/Dart_Packages.xml b/catalyst_voices/.idea/libraries/Dart_Packages.xml
deleted file mode 100644
index aeb7850fae9..00000000000
--- a/catalyst_voices/.idea/libraries/Dart_Packages.xml
+++ /dev/null
@@ -1,668 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/libraries/Dart_SDK.xml b/catalyst_voices/.idea/libraries/Dart_SDK.xml
deleted file mode 100644
index 3b3c0ad641e..00000000000
--- a/catalyst_voices/.idea/libraries/Dart_SDK.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/libraries/Flutter_Plugins.xml b/catalyst_voices/.idea/libraries/Flutter_Plugins.xml
deleted file mode 100644
index b0f697111e2..00000000000
--- a/catalyst_voices/.idea/libraries/Flutter_Plugins.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/misc.xml b/catalyst_voices/.idea/misc.xml
deleted file mode 100644
index 469b00f42fa..00000000000
--- a/catalyst_voices/.idea/misc.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/modules.xml b/catalyst_voices/.idea/modules.xml
deleted file mode 100644
index 3bdcc0b0a8e..00000000000
--- a/catalyst_voices/.idea/modules.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/runConfigurations/melos_bootstrap.xml b/catalyst_voices/.idea/runConfigurations/melos_bootstrap.xml
deleted file mode 100644
index d5715306fb6..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_bootstrap.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/runConfigurations/melos_clean.xml b/catalyst_voices/.idea/runConfigurations/melos_clean.xml
deleted file mode 100644
index f45d4362496..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_clean.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/runConfigurations/melos_run_format_check.xml b/catalyst_voices/.idea/runConfigurations/melos_run_format_check.xml
deleted file mode 100644
index 08ebebf1f5b..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_run_format_check.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/runConfigurations/melos_run_metrics.xml b/catalyst_voices/.idea/runConfigurations/melos_run_metrics.xml
deleted file mode 100644
index 3e62682eadc..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_run_metrics.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/runConfigurations/melos_run_test.xml b/catalyst_voices/.idea/runConfigurations/melos_run_test.xml
deleted file mode 100644
index a4f0c21a950..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_run_test.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/runConfigurations/melos_run_test_select.xml b/catalyst_voices/.idea/runConfigurations/melos_run_test_select.xml
deleted file mode 100644
index 7d5400cbf64..00000000000
--- a/catalyst_voices/.idea/runConfigurations/melos_run_test_select.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/catalyst_voices/.idea/vcs.xml b/catalyst_voices/.idea/vcs.xml
deleted file mode 100644
index 6c0b8635858..00000000000
--- a/catalyst_voices/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/.idea/workspace.xml b/catalyst_voices/.idea/workspace.xml
deleted file mode 100644
index 0e2110b31b3..00000000000
--- a/catalyst_voices/.idea/workspace.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- "keyToString": {
- "RunOnceActivity.OpenProjectViewOnStart": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.cidr.known.project.marker": "true",
- "cidr.known.project.marker": "true",
- "dart.analysis.tool.window.visible": "false",
- "last_opened_file_path": "/Users/minikin/IOG/Code/temp-catalyst-voices/catalyst_voices",
- "settings.editor.selected.configurable": "AndroidSdkUpdater",
- "show.migrate.to.gradle.popup": "false"
- }
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1694788208043
-
-
- 1694788208043
-
-
-
-
\ No newline at end of file
diff --git a/catalyst_voices/Earthfile b/catalyst_voices/Earthfile
index 82dbe168a08..1a854036125 100644
--- a/catalyst_voices/Earthfile
+++ b/catalyst_voices/Earthfile
@@ -1,13 +1,12 @@
VERSION 0.8
IMPORT ../catalyst-gateway AS catalyst-gateway
-IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.23 AS flutter-ci
+IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.24 AS flutter-ci
# repo-catalyst-voices - Creates artifacts of all configuration files,
# packages and folders related to catalyst_voices frontend.
repo-catalyst-voices:
FROM scratch
-
WORKDIR /repo
COPY --dir . .
@@ -20,17 +19,28 @@ builder:
DO flutter-ci+BOOTSTRAP
# Generates flutter code.
-# Based on Catalyst Gateway OpenAPI specifications generates models, clients
-# and serialization logic.
+# Generates codes for Catalyst Gateway OpenAPI, Voices Localization and
+# VoicesAssets and other packages that depend on code-generator.
# It accepts [save_locally] ARG that when true place the artifacts in the
-# proper folder of `catalyst_voices_services` local code.
+# proper folders
code-generator:
ARG save_locally=false
-
+ FROM +builder
LET gen_code_path = lib/generated/catalyst_gateway
LET local_gen_code_path = packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/
- FROM +builder
+ RUN melos l10n
+ RUN melos build_runner
+
+ IF [ $save_locally = true ]
+ RUN find . \( -name "*.g.dart" -o -name "*.freezed.dart" -o -name "*.chopper.dart" -o -name "*.swagger.dart" -o -name "*.openapi.dart" -o -name "*.gen.dart" -o -name "catalyst_voices_localizations*.dart" \)
+
+ FOR generated_file IN $(find . \( -name "*.g.dart" -o -name "*.freezed.dart" -o -name "*.chopper.dart" -o -name "*.swagger.dart" -o -name "*.openapi.dart" -o -name "*.gen.dart" -o -name "catalyst_voices_localizations*.dart" \))
+ SAVE ARTIFACT $generated_file AS LOCAL $generated_file
+ END
+ ELSE
+ SAVE ARTIFACT .
+ END
WORKDIR packages/internal/catalyst_voices_services
COPY catalyst-gateway+build/doc/cat-gateway-api.json openapi/cat-gateway-api.json
DO flutter-ci+OPENAPI_CODE_GEN \
@@ -38,20 +48,12 @@ code-generator:
--GEN_CODE_PATH=$gen_code_path \
--LOCAL_GEN_CODE_PATH=$local_gen_code_path
-# Tests that the code generation is consistent
-# with the generated code currently in the repo.
-# This MUST be a test target because it requires artifacts from build targets.
-test-flutter-code-generator:
- FROM +code-generator
- # Copy generated files in the local file tree to a temporary folder
- COPY packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway /tmp/repo_generated
- # Check diff between local code and earthly artifacts
- RUN diff /tmp/repo_generated lib/generated/catalyst_gateway
+
# Runs static analysis on the code.
check-static-analysis:
- FROM +builder
- DO flutter-ci+ANALYZE
+ FROM +code-generator
+ DO flutter-ci+ANALYZE
# Runs code formatting checks.
check-code-formatting:
@@ -70,12 +72,12 @@ check-license:
# Run unit tests
test-unit:
- FROM +builder
+ FROM +code-generator
DO flutter-ci+UNIT_TESTS
# Build web version of Catalyst Voices
build-web:
- FROM +builder
+ FROM +code-generator
ARG RUN_ON_PR=true
ARG SENTRY_DSN
diff --git a/catalyst_voices/README.md b/catalyst_voices/README.md
index 831b18ad42d..9871aab483a 100644
--- a/catalyst_voices/README.md
+++ b/catalyst_voices/README.md
@@ -11,6 +11,10 @@ This repository contains the Catalyst Voices app and packages.
* [Packages](#packages)
* [Flavors](#flavors)
* [Environment variables](#environment-variables)
+ * [Code Generation](#code-generation)
+ * [Running Code Generation](#running-code-generation)
+ * [GitHub Token / PAT Setup](#github-token--pat-setup)
+ * [Security Notes](#security-notes)
* [Running Tests](#running-tests)
* [Common issues](#common-issues)
@@ -97,6 +101,42 @@ you can use the following command:
flutter build web --target apps/voices/lib/configs/main_web.dart --dart-define SENTRY_DSN=REPLACE_WITH_SENTRY_DSN_URL
```
+### Code Generation
+
+This project utilizes automatic code generation for the following components:
+
+* Catalyst Gateway OpenAPI
+* Localization files
+* Asset files
+* Navigation route files
+
+#### Running Code Generation
+
+##### Basic Generation
+
+To generate code, run the following command in the root directory:
+`earthly ./catalyst_voices+code-generator`
+
+##### Local Saving
+
+To save the generated code locally, use the `--save_locally` flag:
+`earthly ./catalyst_voices+code-generator --save_locally=true`
+
+#### GitHub Token / PAT Setup
+
+**Important** A valid `GITHUB_TOKEN`/ `PAT` is required to run the earthly target.
+
+**Token Configuration:**
+
+1. Locate the `.secret.template` file in the root directory
+2. Create a copy of this file and name it `.secret`
+3. Add your `GITHUB_TOKEN` to the `.secret` file
+
+#### Security Notes
+
+* The `.secret` file should be included in `.gitignore`
+* Verify that git does not track the `.secret` file before committing
+
## Running Tests
To run all unit and widget tests use the following command:
diff --git a/catalyst_voices/apps/voices/integration_test/Earthfile b/catalyst_voices/apps/voices/integration_test/Earthfile
index d4df74940d6..822e0559415 100644
--- a/catalyst_voices/apps/voices/integration_test/Earthfile
+++ b/catalyst_voices/apps/voices/integration_test/Earthfile
@@ -3,7 +3,7 @@ VERSION 0.8
IMPORT ../../.. AS catalyst-voices
integration-test-web:
- FROM catalyst-voices+build-web
+ FROM catalyst-voices+code-generator
ARG TARGETARCH
ARG browser
LET driver_port = 4444
@@ -21,6 +21,9 @@ integration-test-web:
# IF [ $browser = "edge" && $TARGETARCH = "amd64" ]]
# LET driver = "msedgedriver"
# END
+
+ WORKDIR /frontend/apps/voices
+
RUN ($driver --port=$driver_port > $driver.log &) && \
sleep 5 && \
flutter drive --driver=test_driver/integration_tests.dart \
@@ -35,6 +38,7 @@ integration-test-web:
WAIT
SAVE ARTIFACT $driver.log AS LOCAL $driver.log
END
+
IF [ -f fail ]
RUN --no-cache echo ""$browser" integration test failed" && \
echo "Printing "$driver" logs..." && \
diff --git a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart
index 3c5b9a0a97e..764087cb9dc 100644
--- a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart
+++ b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:catalyst_key_derivation/catalyst_key_derivation.dart';
import 'package:catalyst_voices/app/app.dart';
import 'package:catalyst_voices/configs/app_bloc_observer.dart';
import 'package:catalyst_voices/configs/sentry_service.dart';
@@ -90,6 +91,9 @@ Future bootstrap() async {
await Dependencies.instance.init();
+ // Key derivation needs to be initialized before it can be used
+ await CatalystKeyDerivation.init();
+
final router = AppRouter.init(
guards: const [
MilestoneGuard(),
diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
index 681a569e3f0..e93ed705f77 100644
--- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
+++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:catalyst_cardano/catalyst_cardano.dart';
+import 'package:catalyst_key_derivation/catalyst_key_derivation.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
@@ -66,7 +67,8 @@ final class Dependencies extends DependencyProvider {
void _registerServices() {
registerLazySingleton(() => const SecureStorage());
- registerLazySingleton(KeyDerivation.new);
+ registerLazySingleton(CatalystKeyDerivation.new);
+ registerLazySingleton(() => KeyDerivation(get()));
registerLazySingleton(VaultKeychainProvider.new);
registerLazySingleton(SecureDummyAuthStorage.new);
registerLazySingleton(Downloader.new);
diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart
index b894dbd1801..11e4598ee23 100644
--- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart
+++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart
@@ -118,7 +118,7 @@ class _BlocSummary extends StatelessWidget {
},
builder: (context, state) {
if (state == null) {
- return const Offstage();
+ return const _SummaryPlaceholder();
}
return _Summary(
@@ -131,6 +131,20 @@ class _BlocSummary extends StatelessWidget {
}
}
+class _SummaryPlaceholder extends StatelessWidget {
+ const _SummaryPlaceholder();
+
+ @override
+ Widget build(BuildContext context) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.all(32),
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+}
+
class _Summary extends StatelessWidget {
final Set roles;
final WalletInfo walletInfo;
diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart
index 52f1e03f078..700afefca60 100644
--- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart
+++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart
@@ -1,32 +1,32 @@
-import 'package:catalyst_voices/pages/treasury/treasury_campaign_setup.dart';
+import 'package:catalyst_voices/pages/treasury/treasury_dummy_topic_step.dart';
+import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class TreasuryBody extends StatelessWidget {
- final List sections;
+ final ItemScrollController itemScrollController;
const TreasuryBody({
super.key,
- required this.sections,
+ required this.itemScrollController,
});
@override
Widget build(BuildContext context) {
- return ListView.separated(
- padding: const EdgeInsets.only(top: 10),
- itemCount: sections.length,
- itemBuilder: (context, index) {
- final section = sections[index];
-
- switch (section) {
- case CampaignSetup():
- return TreasuryCampaignSetup(
- key: ValueKey('CampaignSetupSection[${section.id}]Key'),
- data: section,
- );
- }
+ return SectionsListViewBuilder(
+ builder: (context, value, child) {
+ return SectionsListView(
+ itemScrollController: itemScrollController,
+ items: value,
+ stepBuilder: (context, step) {
+ switch (step) {
+ case DummyTopicStep():
+ return TreasuryDummyTopicStep(step: step);
+ }
+ },
+ );
},
- separatorBuilder: (context, index) => const SizedBox(height: 24),
);
}
}
diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_campaign_setup.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_campaign_setup.dart
deleted file mode 100644
index b4ff9e58c39..00000000000
--- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_campaign_setup.dart
+++ /dev/null
@@ -1,87 +0,0 @@
-import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
-import 'package:catalyst_voices/widgets/widgets.dart';
-import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
-import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
-import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
-import 'package:flutter/material.dart';
-
-class TreasuryCampaignSetup extends StatelessWidget {
- final CampaignSetup data;
-
- const TreasuryCampaignSetup({
- super.key,
- required this.data,
- });
-
- @override
- Widget build(BuildContext context) {
- final controller = SectionsControllerScope.of(context);
-
- return ValueListenableBuilder(
- valueListenable: controller,
- builder: (context, value, _) {
- final isOpened = value.openedSections.contains(data.id);
- final selectedStep = value.selectedStep;
-
- return Column(
- children: [
- SegmentHeader(
- leading: ChevronExpandButton(
- onTap: () => controller.toggleSection(data.id),
- isExpanded: isOpened,
- ),
- name: data.localizedName(context),
- isSelected: selectedStep?.sectionId == data.id,
- ),
- if (isOpened)
- ...data.steps.map(
- (step) {
- return _StepDetails(
- key: ValueKey('WorkspaceStep${step.id}TileKey'),
- id: step.id,
- name: step.localizedName(context),
- desc: step.localizedDesc(context),
- isSelected: selectedStep?.sectionId == data.id &&
- selectedStep?.stepId == step.id,
- isEditable: step.isEditable,
- );
- },
- ),
- ].separatedBy(const SizedBox(height: 12)).toList(),
- );
- },
- );
- }
-}
-
-class _StepDetails extends StatelessWidget {
- final int id;
- final String name;
- final String desc;
- final bool isSelected;
- final bool isEditable;
-
- const _StepDetails({
- super.key,
- required this.id,
- required this.name,
- required this.desc,
- this.isSelected = false,
- this.isEditable = false,
- });
-
- @override
- Widget build(BuildContext context) {
- return WorkspaceTextTileContainer(
- name: name,
- isSelected: isSelected,
- headerActions: [
- VoicesTextButton(
- onTap: isEditable ? () {} : null,
- child: Text(context.l10n.stepEdit),
- ),
- ],
- content: desc,
- );
- }
-}
diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart
new file mode 100644
index 00000000000..ad89a55c21e
--- /dev/null
+++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart
@@ -0,0 +1,34 @@
+import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart';
+import 'package:catalyst_voices/widgets/widgets.dart';
+import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
+import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
+import 'package:flutter/material.dart';
+
+class TreasuryDummyTopicStep extends StatelessWidget {
+ final DummyTopicStep step;
+
+ const TreasuryDummyTopicStep({
+ super.key,
+ required this.step,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SectionStepStateBuilder(
+ id: step.sectionStepId,
+ builder: (context, value, child) {
+ return WorkspaceTextTileContainer(
+ name: step.localizedName(context),
+ isSelected: value.isSelected,
+ headerActions: [
+ VoicesTextButton(
+ onTap: step.isEditable ? () {} : null,
+ child: Text(context.l10n.stepEdit),
+ ),
+ ],
+ content: step.localizedDesc(context),
+ );
+ },
+ );
+ }
+}
diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_navigation_panel.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_navigation_panel.dart
index 68aff52426f..ca51983c7a2 100644
--- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_navigation_panel.dart
+++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_navigation_panel.dart
@@ -1,5 +1,3 @@
-import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
-import 'package:catalyst_voices/widgets/navigation/sections_menu.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';
diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart
index 3e79bb537f9..e5250fb8910 100644
--- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart
+++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart
@@ -1,19 +1,23 @@
import 'package:catalyst_voices/pages/treasury/treasury_body.dart';
import 'package:catalyst_voices/pages/treasury/treasury_details_panel.dart';
import 'package:catalyst_voices/pages/treasury/treasury_navigation_panel.dart';
-import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
const sections = [
CampaignSetup(
id: 0,
steps: [
- DummyTopicStep(id: 0, isEditable: false),
- DummyTopicStep(id: 1),
- DummyTopicStep(id: 2),
- DummyTopicStep(id: 3),
+ DummyTopicStep(
+ id: 0,
+ sectionId: 0,
+ isEditable: false,
+ ),
+ DummyTopicStep(id: 1, sectionId: 0),
+ DummyTopicStep(id: 2, sectionId: 0),
+ DummyTopicStep(id: 3, sectionId: 0),
],
),
];
@@ -29,12 +33,16 @@ class TreasuryPage extends StatefulWidget {
class _TreasuryPageState extends State {
late final SectionsController _sectionsController;
+ late final ItemScrollController _bodyItemScrollController;
@override
void initState() {
super.initState();
_sectionsController = SectionsController();
+ _bodyItemScrollController = ItemScrollController();
+
+ _sectionsController.attachItemsScrollController(_bodyItemScrollController);
_populateSections();
}
@@ -49,24 +57,19 @@ class _TreasuryPageState extends State {
Widget build(BuildContext context) {
return SectionsControllerScope(
controller: _sectionsController,
- child: const SpaceScaffold(
- left: TreasuryNavigationPanel(),
- body: TreasuryBody(sections: sections),
- right: TreasuryDetailsPanel(),
+ child: SpaceScaffold(
+ left: const TreasuryNavigationPanel(),
+ body: TreasuryBody(
+ itemScrollController: _bodyItemScrollController,
+ ),
+ right: const TreasuryDetailsPanel(),
),
);
}
void _populateSections() {
- final section = sections.firstOrNull;
- final step = section?.steps.firstOrNull;
-
- _sectionsController.value = SectionsControllerState(
+ _sectionsController.value = SectionsControllerState.initial(
sections: sections,
- openedSections: sections.map((e) => e.id).toSet(),
- selectedStep: section != null && step != null
- ? (sectionId: section.id, stepId: step.id)
- : null,
);
}
}
diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart
index 323d505f37e..5b8b811a9fe 100644
--- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart
+++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart
@@ -1,29 +1,33 @@
-import 'package:catalyst_voices/pages/workspace/workspace_form_section.dart';
+import 'package:catalyst_voices/pages/workspace/workspace_rich_text_step.dart';
+import 'package:catalyst_voices/widgets/navigation/sections_list_view.dart';
+import 'package:catalyst_voices/widgets/navigation/sections_list_view_builder.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class WorkspaceBody extends StatelessWidget {
- final List sections;
+ final ItemScrollController itemScrollController;
const WorkspaceBody({
super.key,
- required this.sections,
+ required this.itemScrollController,
});
@override
Widget build(BuildContext context) {
- return ListView.separated(
- padding: const EdgeInsets.only(top: 10),
- itemCount: sections.length,
- itemBuilder: (context, index) {
- final section = sections[index];
-
- return WorkspaceFormSection(
- key: ValueKey('WorkspaceSection[${section.id}]Key'),
- data: section,
+ return SectionsListViewBuilder(
+ builder: (context, value, child) {
+ return SectionsListView(
+ itemScrollController: itemScrollController,
+ items: value,
+ stepBuilder: (context, step) {
+ switch (step) {
+ case RichTextStep():
+ return WorkspaceRichTextStep(step: step);
+ }
+ },
);
},
- separatorBuilder: (context, index) => const SizedBox(height: 24),
);
}
}
diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_form_section.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_form_section.dart
deleted file mode 100644
index 6bdcfe6ee5c..00000000000
--- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_form_section.dart
+++ /dev/null
@@ -1,79 +0,0 @@
-import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
-import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart';
-import 'package:catalyst_voices/widgets/widgets.dart';
-import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
-import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_quill/flutter_quill.dart';
-
-class WorkspaceFormSection extends StatelessWidget {
- final WorkspaceSection data;
-
- const WorkspaceFormSection({
- super.key,
- required this.data,
- });
-
- @override
- Widget build(BuildContext context) {
- final controller = SectionsControllerScope.of(context);
-
- return ValueListenableBuilder(
- valueListenable: controller,
- builder: (context, value, _) {
- final isOpened = value.openedSections.contains(data.id);
- final selectedStep = value.selectedStep;
-
- return Column(
- children: [
- SegmentHeader(
- leading: ChevronExpandButton(
- onTap: () => controller.toggleSection(data.id),
- isExpanded: isOpened,
- ),
- name: data.localizedName(context),
- isSelected: selectedStep?.sectionId == data.id,
- ),
- if (isOpened)
- ...data.steps.whereType().map(
- (step) {
- return _StepDetails(
- key: ValueKey('WorkspaceStep${step.id}TileKey'),
- step: step,
- isSelected: selectedStep?.sectionId == data.id &&
- selectedStep?.stepId == step.id,
- isEditable: step.isEditable,
- );
- },
- ),
- ].separatedBy(const SizedBox(height: 12)).toList(),
- );
- },
- );
- }
-}
-
-class _StepDetails extends StatelessWidget {
- final RichTextStep step;
- final bool isSelected;
- final bool isEditable;
-
- const _StepDetails({
- super.key,
- required this.step,
- this.isSelected = false,
- this.isEditable = false,
- });
-
- @override
- Widget build(BuildContext context) {
- return WorkspaceTileContainer(
- isSelected: isSelected,
- content: VoicesRichText(
- title: step.localizedDesc(context),
- document: Document.fromJson(step.data.value),
- charsLimit: step.charsLimit,
- ),
- );
- }
-}
diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart
index 3d94fec3f13..a8ffc7fa7f0 100644
--- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart
+++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart
@@ -1,5 +1,3 @@
-import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
-import 'package:catalyst_voices/widgets/navigation/sections_menu.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';
diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart
index 4b8ce09825d..45db388c0f0 100644
--- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart
+++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart
@@ -15,6 +15,7 @@ import 'package:catalyst_voices/widgets/navigation/sections_controller.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
const sections = [
ProposalSetup(
@@ -22,6 +23,7 @@ const sections = [
steps: [
TitleStep(
id: 0,
+ sectionId: 0,
data: DocumentJson(title),
),
],
@@ -31,16 +33,19 @@ const sections = [
steps: [
ProblemStep(
id: 0,
+ sectionId: 1,
data: DocumentJson(problemStatement),
charsLimit: 200,
),
SolutionStep(
id: 1,
+ sectionId: 1,
data: DocumentJson(solutionStatement),
charsLimit: 200,
),
PublicDescriptionStep(
id: 2,
+ sectionId: 1,
data: DocumentJson(publicDescription),
charsLimit: 3000,
),
@@ -51,16 +56,19 @@ const sections = [
steps: [
ProblemPerspectiveStep(
id: 0,
+ sectionId: 2,
data: DocumentJson(answer),
charsLimit: 200,
),
PerspectiveRationaleStep(
id: 1,
+ sectionId: 2,
data: DocumentJson(answer),
charsLimit: 200,
),
ProjectEngagementStep(
id: 2,
+ sectionId: 2,
data: DocumentJson(answer),
charsLimit: 200,
),
@@ -71,11 +79,13 @@ const sections = [
steps: [
BonusMarkUpStep(
id: 0,
+ sectionId: 3,
data: DocumentJson(bonusMarkUp),
charsLimit: 900,
),
ValueForMoneyStep(
id: 1,
+ sectionId: 3,
data: DocumentJson(valueForMoney),
charsLimit: 2600,
),
@@ -86,10 +96,12 @@ const sections = [
steps: [
DeliveryAndAccountabilityStep(
id: 0,
+ sectionId: 4,
data: DocumentJson(deliveryAndAccountability),
),
FeasibilityChecksStep(
id: 1,
+ sectionId: 4,
data: DocumentJson(feasibilityChecks),
),
],
@@ -107,12 +119,16 @@ class WorkspacePage extends StatefulWidget {
class _WorkspacePageState extends State {
late final SectionsController _sectionsController;
+ late final ItemScrollController _bodyItemScrollController;
@override
void initState() {
super.initState();
_sectionsController = SectionsController();
+ _bodyItemScrollController = ItemScrollController();
+
+ _sectionsController.attachItemsScrollController(_bodyItemScrollController);
_populateSections();
}
@@ -127,24 +143,19 @@ class _WorkspacePageState extends State {
Widget build(BuildContext context) {
return SectionsControllerScope(
controller: _sectionsController,
- child: const SpaceScaffold(
- left: WorkspaceNavigationPanel(),
- body: WorkspaceBody(sections: sections),
- right: WorkspaceSetupPanel(),
+ child: SpaceScaffold(
+ left: const WorkspaceNavigationPanel(),
+ body: WorkspaceBody(
+ itemScrollController: _bodyItemScrollController,
+ ),
+ right: const WorkspaceSetupPanel(),
),
);
}
void _populateSections() {
- final section = sections.firstOrNull;
- final step = section?.steps.firstOrNull;
-
- _sectionsController.value = SectionsControllerState(
+ _sectionsController.value = SectionsControllerState.initial(
sections: sections,
- openedSections: {sections.first.id},
- selectedStep: section != null && step != null
- ? (sectionId: section.id, stepId: step.id)
- : null,
);
}
}
diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart
new file mode 100644
index 00000000000..9d5814abbc7
--- /dev/null
+++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart
@@ -0,0 +1,102 @@
+import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart';
+import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart';
+import 'package:catalyst_voices/widgets/widgets.dart';
+import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
+import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_quill/flutter_quill.dart';
+
+class WorkspaceRichTextStep extends StatefulWidget {
+ final RichTextStep step;
+
+ const WorkspaceRichTextStep({
+ super.key,
+ required this.step,
+ });
+
+ @override
+ State createState() => _WorkspaceRichTextStepState();
+}
+
+class _WorkspaceRichTextStepState extends State {
+ late final VoicesRichTextController _controller;
+ late final VoicesRichTextEditModeController _editModeController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ final document = Document.fromJson(widget.step.data.value);
+ final selectionOffset = document.length == 0 ? 0 : document.length - 1;
+
+ _controller = VoicesRichTextController(
+ document: document,
+ selection: TextSelection.collapsed(offset: selectionOffset),
+ );
+ _editModeController = VoicesRichTextEditModeController();
+ _editModeController.addListener(_onEditModeControllerChanged);
+ }
+
+ @override
+ void dispose() {
+ _editModeController.dispose();
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SectionStepStateBuilder(
+ id: widget.step.sectionStepId,
+ builder: (context, value, child) {
+ return WorkspaceTileContainer(
+ isSelected: value.isSelected,
+ content: child!,
+ );
+ },
+ child: VoicesRichText(
+ title: widget.step.localizedDesc(context),
+ controller: _controller,
+ editModeController: _editModeController,
+ charsLimit: widget.step.charsLimit,
+ canEditDocumentGetter: _canEditDocument,
+ onEditBlocked: _showEditBlockedRationale,
+ ),
+ );
+ }
+
+ bool _canEditDocument(Document document) {
+ final sectionsController = SectionsControllerScope.of(context);
+
+ final ids = sectionsController.value.editStepsIds;
+ final isEditing = ids.isNotEmpty;
+
+ return !isEditing;
+ }
+
+ Future _showEditBlockedRationale() async {
+ await VoicesDialog.show(
+ context: context,
+ builder: (context) {
+ return VoicesAlertDialog(
+ title: Text(context.l10n.warning),
+ subtitle: Text(context.l10n.saveBeforeEditingErrorText),
+ buttons: [
+ VoicesFilledButton(
+ child: Text(context.l10n.ok),
+ onTap: () => Navigator.of(context).pop(),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ void _onEditModeControllerChanged() {
+ final isEditMode = _editModeController.value;
+ final sectionsController = SectionsControllerScope.of(context);
+ final id = widget.step.sectionStepId;
+
+ sectionsController.editStep(id, enabled: isEditMode);
+ }
+}
diff --git a/catalyst_voices/apps/voices/lib/routes/routing/account_route.g.dart b/catalyst_voices/apps/voices/lib/routes/routing/account_route.g.dart
deleted file mode 100644
index c6fd4539508..00000000000
--- a/catalyst_voices/apps/voices/lib/routes/routing/account_route.g.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'account_route.dart';
-
-// **************************************************************************
-// GoRouterGenerator
-// **************************************************************************
-
-List get $appRoutes => [
- $accountRoute,
- ];
-
-RouteBase get $accountRoute => GoRouteData.$route(
- path: '/m4/account',
- factory: $AccountRouteExtension._fromState,
- );
-
-extension $AccountRouteExtension on AccountRoute {
- static AccountRoute _fromState(GoRouterState state) => const AccountRoute();
-
- String get location => GoRouteData.$location(
- '/m4/account',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
diff --git a/catalyst_voices/apps/voices/lib/routes/routing/coming_soon_route.g.dart b/catalyst_voices/apps/voices/lib/routes/routing/coming_soon_route.g.dart
deleted file mode 100644
index 8c843d09165..00000000000
--- a/catalyst_voices/apps/voices/lib/routes/routing/coming_soon_route.g.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'coming_soon_route.dart';
-
-// **************************************************************************
-// GoRouterGenerator
-// **************************************************************************
-
-List get $appRoutes => [
- $comingSoonRoute,
- ];
-
-RouteBase get $comingSoonRoute => GoRouteData.$route(
- path: '/',
- factory: $ComingSoonRouteExtension._fromState,
- );
-
-extension $ComingSoonRouteExtension on ComingSoonRoute {
- static ComingSoonRoute _fromState(GoRouterState state) =>
- const ComingSoonRoute();
-
- String get location => GoRouteData.$location(
- '/',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
diff --git a/catalyst_voices/apps/voices/lib/routes/routing/login_route.g.dart b/catalyst_voices/apps/voices/lib/routes/routing/login_route.g.dart
deleted file mode 100644
index 5d87880c7a8..00000000000
--- a/catalyst_voices/apps/voices/lib/routes/routing/login_route.g.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'login_route.dart';
-
-// **************************************************************************
-// GoRouterGenerator
-// **************************************************************************
-
-List get $appRoutes => [
- $loginRoute,
- ];
-
-RouteBase get $loginRoute => GoRouteData.$route(
- path: '/login',
- factory: $LoginRouteExtension._fromState,
- );
-
-extension $LoginRouteExtension on LoginRoute {
- static LoginRoute _fromState(GoRouterState state) => const LoginRoute();
-
- String get location => GoRouteData.$location(
- '/login',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
diff --git a/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.g.dart b/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.g.dart
deleted file mode 100644
index 3160be0e8b2..00000000000
--- a/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.g.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'overall_spaces_route.dart';
-
-// **************************************************************************
-// GoRouterGenerator
-// **************************************************************************
-
-List get $appRoutes => [
- $overallSpacesRoute,
- ];
-
-RouteBase get $overallSpacesRoute => GoRouteData.$route(
- path: '/m4/spaces',
- factory: $OverallSpacesRouteExtension._fromState,
- );
-
-extension $OverallSpacesRouteExtension on OverallSpacesRoute {
- static OverallSpacesRoute _fromState(GoRouterState state) =>
- const OverallSpacesRoute();
-
- String get location => GoRouteData.$location(
- '/m4/spaces',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
diff --git a/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.g.dart b/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.g.dart
deleted file mode 100644
index a3ae841792e..00000000000
--- a/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.g.dart
+++ /dev/null
@@ -1,130 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'spaces_route.dart';
-
-// **************************************************************************
-// GoRouterGenerator
-// **************************************************************************
-
-List get $appRoutes => [
- $spacesShellRouteData,
- ];
-
-RouteBase get $spacesShellRouteData => ShellRouteData.$route(
- factory: $SpacesShellRouteDataExtension._fromState,
- routes: [
- GoRouteData.$route(
- path: '/m4/discovery',
- factory: $DiscoveryRouteExtension._fromState,
- ),
- GoRouteData.$route(
- path: '/m4/workspace',
- factory: $WorkspaceRouteExtension._fromState,
- ),
- GoRouteData.$route(
- path: '/m4/voting',
- factory: $VotingRouteExtension._fromState,
- ),
- GoRouteData.$route(
- path: '/m4/funded_projects',
- factory: $FundedProjectsRouteExtension._fromState,
- ),
- GoRouteData.$route(
- path: '/m4/treasury',
- factory: $TreasuryRouteExtension._fromState,
- ),
- ],
- );
-
-extension $SpacesShellRouteDataExtension on SpacesShellRouteData {
- static SpacesShellRouteData _fromState(GoRouterState state) =>
- const SpacesShellRouteData();
-}
-
-extension $DiscoveryRouteExtension on DiscoveryRoute {
- static DiscoveryRoute _fromState(GoRouterState state) =>
- const DiscoveryRoute();
-
- String get location => GoRouteData.$location(
- '/m4/discovery',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
-
-extension $WorkspaceRouteExtension on WorkspaceRoute {
- static WorkspaceRoute _fromState(GoRouterState state) =>
- const WorkspaceRoute();
-
- String get location => GoRouteData.$location(
- '/m4/workspace',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
-
-extension $VotingRouteExtension on VotingRoute {
- static VotingRoute _fromState(GoRouterState state) => const VotingRoute();
-
- String get location => GoRouteData.$location(
- '/m4/voting',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
-
-extension $FundedProjectsRouteExtension on FundedProjectsRoute {
- static FundedProjectsRoute _fromState(GoRouterState state) =>
- const FundedProjectsRoute();
-
- String get location => GoRouteData.$location(
- '/m4/funded_projects',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
-
-extension $TreasuryRouteExtension on TreasuryRoute {
- static TreasuryRoute _fromState(GoRouterState state) => const TreasuryRoute();
-
- String get location => GoRouteData.$location(
- '/m4/treasury',
- );
-
- void go(BuildContext context) => context.go(location);
-
- Future push(BuildContext context) => context.push(location);
-
- void pushReplacement(BuildContext context) =>
- context.pushReplacement(location);
-
- void replace(BuildContext context) => context.replace(location);
-}
diff --git a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart
index 3b5c495f1d4..408077d9fd8 100644
--- a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart
+++ b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart
@@ -4,6 +4,7 @@ import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart';
import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart';
import 'package:catalyst_voices/widgets/buttons/voices_outlined_button.dart';
import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart';
+import 'package:catalyst_voices/widgets/common/animated_expand_chevron.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';
@@ -100,9 +101,10 @@ class ChevronExpandButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return isExpanded
- ? ChevronDownButton(onTap: onTap)
- : ChevronRightButton(onTap: onTap);
+ return VoicesIconButton(
+ onTap: onTap,
+ child: AnimatedExpandChevron(isExpanded: isExpanded),
+ );
}
}
diff --git a/catalyst_voices/apps/voices/lib/widgets/common/animated_expand_chevron.dart b/catalyst_voices/apps/voices/lib/widgets/common/animated_expand_chevron.dart
new file mode 100644
index 00000000000..13d56f3b923
--- /dev/null
+++ b/catalyst_voices/apps/voices/lib/widgets/common/animated_expand_chevron.dart
@@ -0,0 +1,20 @@
+import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
+import 'package:flutter/material.dart';
+
+class AnimatedExpandChevron extends StatelessWidget {
+ final bool isExpanded;
+
+ const AnimatedExpandChevron({
+ super.key,
+ required this.isExpanded,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedRotation(
+ turns: isExpanded ? 0.25 : 0,
+ duration: const Duration(milliseconds: 250),
+ child: VoicesAssets.icons.chevronRight.buildIcon(),
+ );
+ }
+}
diff --git a/catalyst_voices/apps/voices/lib/widgets/containers/space_scaffold.dart b/catalyst_voices/apps/voices/lib/widgets/containers/space_scaffold.dart
index 9ba98f4b56d..c24bf155e89 100644
--- a/catalyst_voices/apps/voices/lib/widgets/containers/space_scaffold.dart
+++ b/catalyst_voices/apps/voices/lib/widgets/containers/space_scaffold.dart
@@ -25,11 +25,9 @@ class SpaceScaffold extends StatelessWidget {
Widget build(BuildContext context) {
return SidebarScaffold(
leftRail: left,
- body: Center(
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 612),
- child: body,
- ),
+ body: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 612),
+ child: body,
),
rightRail: right,
);
diff --git a/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart b/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart
index 0781122e192..e1892893135 100644
--- a/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart
+++ b/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart
@@ -6,6 +6,7 @@ class SegmentHeader extends StatelessWidget {
final Widget? leading;
final List actions;
final bool isSelected;
+ final VoidCallback? onTap;
const SegmentHeader({
super.key,
@@ -13,6 +14,7 @@ class SegmentHeader extends StatelessWidget {
this.leading,
this.actions = const [],
this.isSelected = false,
+ this.onTap,
});
Set get _states => {
@@ -47,21 +49,24 @@ class SegmentHeader extends StatelessWidget {
style: textStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
- child: AnimatedContainer(
- duration: kThemeChangeDuration,
- constraints: const BoxConstraints(minHeight: 52),
- decoration: BoxDecoration(
- color: backgroundColor.resolve(_states),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- if (leading != null) leading!,
- Expanded(child: Text(name)),
- if (actions.isNotEmpty) ...actions,
- ],
+ child: GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: kThemeChangeDuration,
+ constraints: const BoxConstraints(minHeight: 52),
+ decoration: BoxDecoration(
+ color: backgroundColor.resolve(_states),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ if (leading != null) leading!,
+ Expanded(child: Text(name)),
+ if (actions.isNotEmpty) ...actions,
+ ],
+ ),
),
),
),
diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/section_header.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/section_header.dart
new file mode 100644
index 00000000000..dc214b43394
--- /dev/null
+++ b/catalyst_voices/apps/voices/lib/widgets/navigation/section_header.dart
@@ -0,0 +1,35 @@
+import 'package:catalyst_voices/widgets/widgets.dart';
+import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
+import 'package:flutter/material.dart';
+
+class SectionHeader extends StatelessWidget {
+ final Section section;
+
+ const SectionHeader({
+ super.key,
+ required this.section,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final controller = SectionsControllerScope.of(context);
+
+ return ValueListenableBuilder(
+ valueListenable: controller,
+ builder: (context, value, child) {
+ final isOpened = value.openedSections.contains(section.id);
+ final hasSelectedStep = value.activeSectionId == section.id;
+
+ return SegmentHeader(
+ leading: ChevronExpandButton(
+ onTap: () => controller.toggleSection(section.id),
+ isExpanded: isOpened,
+ ),
+ name: section.localizedName(context),
+ isSelected: isOpened && hasSelectedStep,
+ onTap: () => controller.focusSection(section.id),
+ );
+ },
+ );
+ }
+}
diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/section_step_state_builder.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/section_step_state_builder.dart
new file mode 100644
index 00000000000..d5424fc4664
--- /dev/null
+++ b/catalyst_voices/apps/voices/lib/widgets/navigation/section_step_state_builder.dart
@@ -0,0 +1,49 @@
+import 'package:catalyst_voices/widgets/widgets.dart';
+import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+
+final class SectionStepState extends Equatable {
+ final bool isSelected;
+
+ const SectionStepState({
+ required this.isSelected,
+ });
+
+ @override
+ List