diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..fd1d202080
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+*/example/
+*/build/
+*/.dart_tool/
+
+# We dont need git history inside the image.
+.git
+
+# Ignore the golden failure directories because they will be mapped.
+**/failures/
\ No newline at end of file
diff --git a/.github/workflows/build_clones.yaml b/.github/workflows/build_clones.yaml
new file mode 100644
index 0000000000..c86e52c01a
--- /dev/null
+++ b/.github/workflows/build_clones.yaml
@@ -0,0 +1,128 @@
+name: Build Clones
+on: [pull_request]
+jobs:
+ build_bear:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the bear project directory
+ working-directory: ./super_clones/bear
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build macos --debug
+
+ build_google_docs:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the google_docs project directory
+ working-directory: ./super_clones/google_docs
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build macos --debug
+
+ build_medium:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the medium project directory
+ working-directory: ./super_clones/medium
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build web --debug
+
+ build_obsidian:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the obsidian project directory
+ working-directory: ./super_clones/obsidian
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build macos --debug
+
+ build_slack:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the slack project directory
+ working-directory: ./super_clones/slack
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build macos --debug
+
+ build_quill:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # Run everything from within the quill project directory
+ working-directory: ./super_clones/quill
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Build the clone
+ - run: flutter build macos --debug
diff --git a/.github/workflows/cherry_pick_to_stable.yaml b/.github/workflows/cherry_pick_to_stable.yaml
new file mode 100644
index 0000000000..2a22607826
--- /dev/null
+++ b/.github/workflows/cherry_pick_to_stable.yaml
@@ -0,0 +1,57 @@
+# In general, whenever a change is merged to "main" we want to replicate that change on "stable".
+# This GitHub action watches for merges to "main", creates a new branch off of "stable", cherry
+# picks the latest commit from "main" to the new branch, and then puts up a PR with the cherry
+# pick. At that point, a reviewer waits until all tests pass, and then merges in the cherry pick
+# PR.
+#
+# Sometimes, a change to "main" shouldn't be merged to "stable". In that case, tag the original PR
+# with a "no-cherry-pick" label, and this action won't cherry pick that merge.
+name: Cherry pick to stable
+on:
+ pull_request:
+ branches:
+ - main
+ types: ["closed"]
+
+jobs:
+ cherry-pick-to-stable:
+ runs-on: ubuntu-latest
+ name: Cherry pick from main to stable
+
+ # Only cherry pick merged PRs. Don't merge PRs marked with "no-cherry-pick" label.
+ if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'no-cherry-pick') }}
+
+ # These ENV variables can be accessed directly when they're used in shell commands, e.g.:
+ #
+ # echo "My name is $NAME"
+ #
+ # When referencing ENV variables in declarative YAML, they must be accessed via "env.", e.g.:
+ #
+ # someProperty: "My name is ${{ env.NAME }}"
+ #
+ # GitHub pull request data: https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request
+ env:
+ CHERRY_PICK_BRANCH_NAME: "cherry-pick_${{ github.event.pull_request.head.ref }}_${{ github.event.pull_request.merge_commit_sha }}"
+ CHERRY_PICK_PR_TITLE: "Cherry Pick: ${{ github.event.pull_request.title }}"
+ CHERRY_PICK_PR_BODY: "Cherry Pick: Original PR - #${{ github.event.pull_request.number }}"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Cherry pick into stable
+ uses: carloscastrojumo/github-cherry-pick-action@v1.0.9
+ with:
+ branch: stable
+ cherry-pick-branch: "${{ env.CHERRY_PICK_BRANCH_NAME }}"
+ title: "${{ env.CHERRY_PICK_PR_TITLE }}"
+ body: "${{ env.CHERRY_PICK_PR_BODY }}"
+ labels: |
+ cherry-pick
+ reviewers: |
+ matthew-carroll
+ "${{ gitHub.event.pull_request.user.login }}"
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr_validation.yaml b/.github/workflows/pr_validation.yaml
index 4a7813a888..c9eed8202c 100644
--- a/.github/workflows/pr_validation.yaml
+++ b/.github/workflows/pr_validation.yaml
@@ -71,11 +71,12 @@ jobs:
# Run all tests
- run: flutter test
- test_super_editor_markdown:
+ test_goldens_super_editor:
+ if: ${{ github.base_ref == 'main' }}
runs-on: ubuntu-latest
defaults:
run:
- working-directory: ./super_editor_markdown
+ working-directory: ./super_editor
steps:
# Checkout the PR branch
- uses: actions/checkout@v3
@@ -84,16 +85,180 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: "stable"
+ architecture: x64
# Download all the packages that the app uses
- run: flutter pub get
- # TODO: Enforce static analysis
+ # Run all golden tests
+ - run: flutter test test_goldens
+
+ # Archive golden failures
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: golden-failures
+ path: "**/failures/**/*.png"
+
+ test_super_editor_spellcheck:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_editor_spellcheck
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
# Run all tests
- run: flutter test
+
+ test_super_editor_spellcheck_goldens:
+ if: ${{ github.base_ref == 'main' }}
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_editor_spellcheck
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
- test_super_text_layout:
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ architecture: x64
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Run all golden tests
+ - run: flutter test test_goldens
+
+ # Archive golden failures
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: golden-failures
+ path: "**/failures/**/*.png"
+
+ analyze_super_editor_clipboard:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_editor_clipboard
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Enforce static analysis
+ - run: flutter analyze
+
+ test_super_editor_clipboard:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_editor_clipboard
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Run all tests
+ - run: flutter test
+
+ analyze_super_keyboard:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_keyboard
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Enforce static analysis
+ - run: flutter analyze
+
+ test_super_keyboard:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_keyboard
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Run all tests
+ - run: flutter test
+
+ test_goldens_super_keyboard:
+ if: ${{ github.base_ref == 'main' }}
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_keyboard
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ architecture: x64
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
+ # Run all golden tests
+ - run: flutter test test_goldens
+
+ # Archive golden failures
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: golden-failures
+ path: "**/failures/**/*.png"
+
+ analyze_super_text_layout:
runs-on: ubuntu-latest
defaults:
run:
@@ -113,11 +278,29 @@ jobs:
# Enforce static analysis
- run: flutter analyze
+ test_super_text_layout:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./super_text_layout
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Flutter environment
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ # Download all the packages that the app uses
+ - run: flutter pub get
+
# Run all tests
- run: flutter test
test_goldens_super_text_layout:
- runs-on: macos-latest
+ if: ${{ github.base_ref == 'main' }}
+ runs-on: ubuntu-latest
defaults:
run:
working-directory: ./super_text_layout
@@ -137,7 +320,14 @@ jobs:
# Run all golden tests
- run: flutter test test_goldens
- test_attributed_text:
+ # Archive golden failures
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: golden-failures
+ path: "**/failures/**/*.png"
+
+ analyze_attributed_text:
runs-on: ubuntu-latest
defaults:
run:
@@ -155,5 +345,20 @@ jobs:
# Enforce static analysis
- run: dart analyze
+ test_attributed_text:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./attributed_text
+ steps:
+ # Checkout the PR branch
+ - uses: actions/checkout@v3
+
+ # Setup Dart
+ - uses: dart-lang/setup-dart@v1
+
+ # Install app dependencies
+ - run: dart pub get
+
# Run all tests
- run: dart test
diff --git a/.gitignore b/.gitignore
index 2f2eff8c11..44de409c06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
ios_text_example/
android_text_example/
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index afe1db5eb6..aa40f8117f 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,9 @@
Super Editor was initiated by Superlist and is implemented and maintained by the Flutter Bounty Hunters , Superlist, and the contributors.
-Feel free to join us on Slack and say hello 👋.
+
+
+Do you use Flutter's stable branch? Be sure to checkout super_editor's stable branch, for compatibility. Do you use Flutter's master branch? Be sure to checkout super_editor's main branch, for compabitility.
@@ -15,9 +17,9 @@
Super Editor & Super Text Field
-Please see the [SuperEditor README](super_editor/README.md) about how to use the packages, or run the [sample editor](super_editor/example/README.md).
+Please see the [SuperEditor README](super_editor/README.md) about how to use the packages, or run the [sample editor](super_editor/example/README.md).
-A web demo is accessible at [https://superlist.com/SuperEditor](https://superlist.com/SuperEditor/).
+A web demo is accessible at [https://superlist.com/SuperEditor](https://superlist.com/SuperEditor/).
@@ -30,3 +32,31 @@ You might notice that this is a mono-repo, which includes multiple projects. Tha
+Mono-repo Versioning
+If you have compilation errors when using the GitHub version of super_editor, try overriding dependencies for the other packages in this mono-repo, e.g., super_editor_markdown, super_text_layout, and attributed_text. This project often makes changes to multiple packages within the mono-repo, which requires that you use the latest main or stable version of every package.
+
+You can override your dependencies as follows:
+
+```yaml
+dependency_overrides:
+ super_editor:
+ git:
+ url: https://github.com/superlistapp/super_editor
+ path: super_editor
+ ref: stable # or "main"
+ super_editor_markdown:
+ git:
+ url: https://github.com/superlistapp/super_editor
+ path: super_editor_markdown
+ ref: stable
+ super_text_layout:
+ git:
+ url: https://github.com/superlistapp/super_editor
+ path: super_text_layout
+ ref: stable
+ attributed_text:
+ git:
+ url: https://github.com/superlistapp/super_editor
+ path: attributed_text
+ ref: stable
+```
diff --git a/attributed_text/CHANGELOG.md b/attributed_text/CHANGELOG.md
index c6adead31b..cccb71b117 100644
--- a/attributed_text/CHANGELOG.md
+++ b/attributed_text/CHANGELOG.md
@@ -1,3 +1,29 @@
+## [0.3.2] - June, 2024
+ * Fix crash when adding attributions that overlap others - you can now control whether a new attribution overwrites conflicting spans when you add it.
+
+## [0.3.1] - June, 2024
+ * Added query `getAllAttributionsThroughout()` to `AttributedText`.
+ * Added `copy()` to `AttributedText()`.
+ * Added ability to insert an attribution that splits an existing attribution.
+
+## [0.3.0] - Feb, 2024
+ * [BREAKING] - `AttributedText` and `SpanRange` constructors now use positional parameters istead of named parameters.
+ * [FIX] - `AttributedText` now supports differents links for different URLs in the same text blob - previously all links were sent to the same URL withing a single `AttributedText`.
+ * [FIX] - `collapseSpans` now reports the correct ending index, which was previously off by one.
+ * `AttributedText` now has a substring method and length property to avoid needing to access the inner `text` string.
+ * `AttributionSpan` now has a `range` property to get a non-directional span of text.
+ * `AttributedText` can visit and report attribution spans instead of just visiting individual attribution markers.
+ * Added query methods:
+ * `getAttributionSpans()`
+ * `getAttributionSpansByFilter()`
+ * `AttributedText` now allows you to `addAttribution()` without auto-merging with preceding and following attributions (#1198)
+
+## [0.2.2] - May, 2023
+Upgrade Dart constraints to explicitly include Dart 3. Make `markers` public on `AttributedSpans`.
+
+## [0.2.1] - January, 2023
+Add `getAttributedRange()`, which returns a range that includes a given set of attributions.
+
## [0.2.0] - July, 2022
BREAKING - Attributions in an `AttributedText` are now visited by a `AttributionVisitor` instead of a callback, and the visitor receives span markers in a more useful way.
diff --git a/attributed_text/analysis_options.yaml b/attributed_text/analysis_options.yaml
new file mode 100644
index 0000000000..31d6dd1061
--- /dev/null
+++ b/attributed_text/analysis_options.yaml
@@ -0,0 +1,9 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ exclude: [build/**]
+
+linter:
+ rules:
+ omit_local_variable_types: false
+ avoid_renaming_method_parameters: false
diff --git a/attributed_text/lib/src/attributed_spans.dart b/attributed_text/lib/src/attributed_spans.dart
index db80172634..3e121011c0 100644
--- a/attributed_text/lib/src/attributed_spans.dart
+++ b/attributed_text/lib/src/attributed_spans.dart
@@ -1,7 +1,6 @@
import 'dart:math';
import 'package:collection/collection.dart';
-import 'package:meta/meta.dart';
import 'attribution.dart';
import 'logging.dart';
@@ -44,16 +43,16 @@ class AttributedSpans {
/// with no spans.
AttributedSpans({
List? attributions,
- }) : markers = [...?attributions] {
+ }) : _markers = [...?attributions] {
_sortAttributions();
}
// markers must always be in order from lowest marker offset to highest marker offset.
- @visibleForTesting
- final List markers;
+ Iterable get markers => _markers;
+ final List _markers;
void _sortAttributions() {
- markers.sort();
+ _markers.sort();
}
/// Returns `true` if this [AttributedSpans] contains at least one
@@ -66,15 +65,21 @@ class AttributedSpans {
}) {
final attributionsToFind = Set.from(attributions);
for (int i = start; i <= end; ++i) {
+ final foundAttributions = {};
for (final attribution in attributionsToFind) {
if (hasAttributionAt(i, attribution: attribution)) {
- attributionsToFind.remove(attribution);
- }
-
- if (attributionsToFind.isEmpty) {
- return true;
+ // Store the attributions we found so far to remove them
+ // from attributionsToFind after the loop.
+ //
+ // Removing from the set while iterating throws an exception.
+ foundAttributions.add(attribution);
}
}
+ attributionsToFind.removeAll(foundAttributions);
+ if (attributionsToFind.isEmpty) {
+ // We found all attributions.
+ return true;
+ }
}
return false;
}
@@ -91,7 +96,7 @@ class AttributedSpans {
final matchingAttributions = {};
for (int i = start; i <= end; ++i) {
for (final attribution in attributions) {
- final otherAttributions = getAllAttributionsAt(start);
+ final otherAttributions = getAllAttributionsAt(i);
for (final otherAttribution in otherAttributions) {
if (otherAttribution.id == attribution.id) {
matchingAttributions.add(otherAttribution);
@@ -154,7 +159,7 @@ class AttributedSpans {
/// Returns all attributions for spans that cover the given [offset].
Set getAllAttributionsAt(int offset) {
final allAttributions = {};
- for (final marker in markers) {
+ for (final marker in _markers) {
allAttributions.add(marker.attribution);
}
@@ -248,8 +253,8 @@ class AttributedSpans {
// Note: maxStartMarkerOffset and minEndMarkerOffset are non-null because we verified
// that all desired attributions are present at the given offset.
return SpanRange(
- start: maxStartMarkerOffset!,
- end: minEndMarkerOffset!,
+ maxStartMarkerOffset!,
+ minEndMarkerOffset!,
);
}
@@ -257,39 +262,66 @@ class AttributedSpans {
/// given [offset], optionally looking specifically for a marker with
/// the given [attribution].
SpanMarker? _getStartingMarkerAtOrBefore(int offset, {Attribution? attribution}) {
- return markers //
+ return _markers //
.reversed // search from the end so its the nearest start marker
.where((marker) {
- return attribution == null ||
- (marker.attribution.id == attribution.id && marker.attribution.canMergeWith(attribution));
- })
- // .where((marker) => attribution == null || marker.attribution.id == attribution.id)
- .firstWhereOrNull((marker) => marker.isStart && marker.offset <= offset);
+ return attribution == null || (marker.attribution == attribution);
+ }).firstWhereOrNull((marker) => marker.isStart && marker.offset <= offset);
}
/// Finds and returns the nearest [end] marker that appears at or after the
/// given [offset], optionally looking specifically for a marker with
/// the given [attribution].
SpanMarker? _getEndingMarkerAtOrAfter(int offset, {Attribution? attribution}) {
- return markers
- .where((marker) =>
- attribution == null ||
- (marker.attribution.id == attribution.id && marker.attribution.canMergeWith(attribution)))
+ return _markers
+ .where((marker) => attribution == null || (marker.attribution.id == attribution.id))
.firstWhereOrNull((marker) => marker.isEnd && marker.offset >= offset);
}
/// Applies the [newAttribution] from [start] to [end], inclusive.
///
- /// If [newAttribution] spans already exist at [start] or [end], and those
- /// spans are compatible, the spans are expanded to include the new region
- /// between [start] and [end].
+ /// If [start] is less than `0`, nothing happens.
+ ///
+ /// [AttributedSpans] doesn't have any knowledge about content length, so [end] can
+ /// take any value that's desired. However, users of [AttributedSpans] should take
+ /// care to avoid values for [end] that exceed the content length.
+ ///
+ /// The effect of adding an attribution is straight forward when the text doesn't
+ /// contain any other attributions with the same ID. However, there are various
+ /// situations where [newAttribution] can't necessarily co-exist with other
+ /// attribution spans that already exist in the text.
+ ///
+ /// Attribution overlaps can take one of two forms: mergeable or conflicting.
+ ///
+ /// ## Mergeable Attribution Spans
+ /// An example of a mergeable overlap is where two bold spans overlap each
+ /// other. All bold attributions are interchangeable, so when two bold spans
+ /// overlap, those spans can be merged together into a single span.
+ ///
+ /// However, mergeable overlapping spans are not automatically merged. Instead,
+ /// this decision is left to the user of this class. If you want [AttributedSpans] to
+ /// merge overlapping mergeable spans, pass `true` for [autoMerge]. Otherwise,
+ /// if [autoMerge] is `false`, an exception is thrown when two mergeable spans
+ /// overlap each other.
+ ///
///
- /// It [newAttribution] overlaps a conflicting span, a
- /// [IncompatibleOverlappingAttributionsException] is thrown.
+ /// ## Conflicting Attribution Spans
+ /// An example of a conflicting overlap is where a black text color overlaps a red
+ /// text color. Text is either black, OR red, but never both. Therefore, the black
+ /// attribution cannot co-exist with the red attribution. Something must be done
+ /// to resolve this.
+ ///
+ /// There are two possible ways to handle conflicting overlaps. The new attribution
+ /// can overwrite the existing attribution where they overlap. Or, an exception can be
+ /// thrown. To overwrite the existing attribution with the new attribution, pass `true`
+ /// for [overwriteConflictingSpans]. Otherwise, if [overwriteConflictingSpans]
+ /// is `false`, an exception is thrown.
void addAttribution({
required Attribution newAttribution,
required int start,
required int end,
+ bool autoMerge = true,
+ bool overwriteConflictingSpans = true,
}) {
if (start < 0 || start > end) {
_log.warning("Tried to add an attribution ($newAttribution) at an invalid start/end: $start -> $end");
@@ -297,29 +329,110 @@ class AttributedSpans {
}
_log.info("Adding attribution ($newAttribution) from $start to $end");
- _log.finer("Has ${markers.length} markers before addition");
+ _log.finer("Has ${_markers.length} markers before addition");
+
+ final conflicts = <_AttributionConflict>[];
- // Ensure that no conflicting attribution overlaps the new attribution.
- // If a conflict exists, throw an exception.
+ // Check if conflicting attributions overlap the new attribution.
final matchingAttributions = getMatchingAttributionsWithin(attributions: {newAttribution}, start: start, end: end);
if (matchingAttributions.isNotEmpty) {
for (final matchingAttribution in matchingAttributions) {
- if (!newAttribution.canMergeWith(matchingAttribution)) {
- late int conflictStart;
+ bool areAttributionsMergeable = newAttribution.canMergeWith(matchingAttribution);
+ if (!areAttributionsMergeable || !autoMerge) {
+ int? conflictStart;
+ int? conflictEnd;
+
for (int i = start; i <= end; ++i) {
if (hasAttributionAt(i, attribution: matchingAttribution)) {
- conflictStart = i;
- break;
+ conflictStart ??= i;
+ conflictEnd = i;
+
+ if (areAttributionsMergeable) {
+ // Both attributions are mergeable, but the caller doesn't want to merge them.
+ throw IncompatibleOverlappingAttributionsException(
+ existingAttribution: matchingAttribution,
+ newAttribution: newAttribution,
+ conflictStart: conflictStart,
+ );
+ }
+ } else if (conflictStart != null) {
+ // We found the end of the conflict.
+ conflicts.add(_AttributionConflict(
+ newAttribution: newAttribution,
+ existingAttribution: matchingAttribution,
+ conflictStart: conflictStart,
+ conflictEnd: conflictEnd!,
+ ));
+
+ // Reset so we can find the next conflict.
+ conflictStart = null;
+ conflictEnd = null;
}
}
- throw IncompatibleOverlappingAttributionsException(
- existingAttribution: matchingAttribution,
- newAttribution: newAttribution,
- conflictStart: conflictStart,
- );
+ if (conflictStart != null && conflictEnd != null) {
+ // We found a conflict that extends to the end of the range.
+ conflicts.add(_AttributionConflict(
+ newAttribution: newAttribution,
+ existingAttribution: matchingAttribution,
+ conflictStart: conflictStart,
+ conflictEnd: conflictEnd,
+ ));
+ }
}
}
+
+ if (conflicts.isNotEmpty && !overwriteConflictingSpans) {
+ // We found conflicting attributions and we are configured not to overwrite them.
+ // For example, the user tried to apply a blue color attribution to a range of text
+ // that already has another color attribution.
+ throw IncompatibleOverlappingAttributionsException(
+ existingAttribution: conflicts.first.existingAttribution,
+ newAttribution: newAttribution,
+ conflictStart: conflicts.first.conflictStart,
+ );
+ }
+ }
+
+ // Removes any conflicting attributions. For example, consider the following text,
+ // with a blue color attribution that spans the entire text:
+ //
+ // one two three
+ // |bbbbbbbbbbbbb|
+ //
+ // We can't apply a green color attribution to the word "two", because it's already
+ // attributed with blue. So, we need to remove the blue attribution from the word "two",
+ // which results in the following text:
+ //
+ // one two three
+ // |bbbb---bbbbbb|
+ //
+ // After that, we can apply the desired attribution, because there isn't a conflicting attribution
+ // in this range anymore.
+ for (final conflict in conflicts) {
+ removeAttribution(
+ attributionToRemove: conflict.existingAttribution,
+ start: conflict.conflictStart,
+ end: conflict.conflictEnd,
+ );
+ }
+
+ if (!autoMerge) {
+ // We don't want to merge this new attribution with any other nearby attribution.
+ // Therefore, we can blindly create the new attribution range without any
+ // further adjustments, and then be done.
+ _insertMarker(SpanMarker(
+ attribution: newAttribution,
+ offset: start,
+ markerType: SpanMarkerType.start,
+ ));
+ _insertMarker(SpanMarker(
+ attribution: newAttribution,
+ offset: end,
+ markerType: SpanMarkerType.end,
+ ));
+
+ return;
}
// Start the new span, either by expanding an existing span, or by
@@ -327,11 +440,11 @@ class AttributedSpans {
final endMarkerJustBefore =
SpanMarker(attribution: newAttribution, offset: start - 1, markerType: SpanMarkerType.end);
final endMarkerAtNewStart = SpanMarker(attribution: newAttribution, offset: start, markerType: SpanMarkerType.end);
- if (markers.contains(endMarkerJustBefore)) {
+ if (_markers.contains(endMarkerJustBefore)) {
// A compatible span ends immediately before this new span begins.
// Remove the end marker so that the existing span flows into the new span.
_log.fine('A compatible span already exists immediately before the new span range. Combining the spans.');
- markers.remove(endMarkerJustBefore);
+ _markers.remove(endMarkerJustBefore);
} else if (!hasAttributionAt(start, attribution: newAttribution)) {
// The desired attribution does not yet exist at `start`, and no compatible
// span sits immediately upstream. Therefore, we need to start a new span
@@ -342,23 +455,23 @@ class AttributedSpans {
offset: start,
markerType: SpanMarkerType.start,
));
- } else if (markers.contains(endMarkerAtNewStart)) {
+ } else if (_markers.contains(endMarkerAtNewStart)) {
// There's an end marker for this span at the same place where
// the new span wants to begin. Remove the end marker so that the
// existing span flows into the new span.
_log.fine('Removing existing end marker at $start because the new span should merge with an existing span');
- markers.remove(endMarkerAtNewStart);
+ _markers.remove(endMarkerAtNewStart);
}
// Delete all markers of the same type between `range.start`
// and `range.end`.
- final markersToDelete = markers
+ final markersToDelete = _markers
.where((attribution) => attribution.attribution == newAttribution)
.where((attribution) => attribution.offset > start)
.where((attribution) => attribution.offset <= end)
.toList();
_log.fine('Removing ${markersToDelete.length} markers between $start and $end');
- markers.removeWhere((element) => markersToDelete.contains(element));
+ _markers.removeWhere((element) => markersToDelete.contains(element));
final lastDeletedMarker = markersToDelete.isNotEmpty ? markersToDelete.last : null;
@@ -383,7 +496,7 @@ class AttributedSpans {
// Only run this loop in debug mode to avoid unnecessary iteration
// in a release build (when logging should be turned off, anyway).
_log.fine('All attributions after:');
- markers.where((element) => element.attribution == newAttribution).forEach((element) {
+ _markers.where((element) => element.attribution == newAttribution).forEach((element) {
_log.fine('$element');
});
return true;
@@ -465,16 +578,16 @@ class AttributedSpans {
// Now that the end caps have been handled, remove all
// relevant attribution markers between [start, end].
- final markersToDelete = markers
+ final markersToDelete = _markers
.where((attribution) => attribution.attribution == attributionToRemove)
.where((attribution) => attribution.offset >= start)
.where((attribution) => attribution.offset <= end)
.toList();
_log.finer('removing ${markersToDelete.length} markers between $start and $end');
- markers.removeWhere((element) => markersToDelete.contains(element));
+ _markers.removeWhere((element) => markersToDelete.contains(element));
_log.finer('all attributions after:');
- markers.where((element) => element.attribution == attributionToRemove).forEach((element) {
+ _markers.where((element) => element.attribution == attributionToRemove).forEach((element) {
_log.finer(' - $element');
});
}
@@ -512,8 +625,8 @@ class AttributedSpans {
return false;
}
- final indexBefore = markers.indexOf(markerBefore);
- final nextMarker = markers.sublist(indexBefore).firstWhereOrNull((marker) {
+ final indexBefore = _markers.indexOf(markerBefore);
+ final nextMarker = _markers.sublist(indexBefore).firstWhereOrNull((marker) {
_log.finest('Comparing start marker $markerBefore to another marker $marker');
return marker.attribution == attribution && marker.offset >= markerBefore.offset && marker != markerBefore;
});
@@ -545,7 +658,7 @@ class AttributedSpans {
SpanMarkerType? type,
}) {
SpanMarker? markerBefore;
- final markerCandidates = markers
+ final markerCandidates = _markers
.where((marker) => attribution == null || marker.attribution == attribution)
.where((marker) => type == null || marker.markerType == type);
@@ -563,7 +676,7 @@ class AttributedSpans {
/// Returns the markers at the given [offset] with the given [attribution]..
Set _getMarkerAt(Attribution attribution, int offset, [SpanMarkerType? type]) {
- return markers
+ return _markers
.where((marker) => marker.attribution == attribution)
.where((marker) => marker.offset == offset)
.where((marker) => type == null || marker.markerType == type)
@@ -576,15 +689,15 @@ class AttributedSpans {
/// the same attribution at the same offset.
void _insertMarker(SpanMarker newMarker) {
int indexOfFirstMarkerAfterInsertionPoint =
- markers.indexWhere((existingMarker) => existingMarker.compareTo(newMarker) > 0);
+ _markers.indexWhere((existingMarker) => existingMarker.compareTo(newMarker) > 0);
// [indexWhere] returns -1 if no matching element is found.
final foundMarkerToInsertBefore = indexOfFirstMarkerAfterInsertionPoint >= 0;
if (foundMarkerToInsertBefore) {
- markers.insert(indexOfFirstMarkerAfterInsertionPoint, newMarker);
+ _markers.insert(indexOfFirstMarkerAfterInsertionPoint, newMarker);
} else {
// Insert the new marker at the end.
- markers.add(newMarker);
+ _markers.add(newMarker);
}
}
@@ -597,9 +710,9 @@ class AttributedSpans {
required AttributedSpans other,
required int index,
}) {
- if (markers.isNotEmpty && markers.last.offset >= index) {
+ if (_markers.isNotEmpty && _markers.last.offset >= index) {
throw Exception(
- 'Another AttributedSpans can only be appended after the final marker in this AttributedSpans. Final marker: ${markers.last}');
+ 'Another AttributedSpans can only be appended after the final marker in this AttributedSpans. Final marker: ${_markers.last}');
}
_log.fine('attributions before pushing them:');
@@ -615,7 +728,7 @@ class AttributedSpans {
final pushedSpans = other.copy()..pushAttributionsBack(pushDistance);
// Combine `this` and `other` attributions into one list.
- final List combinedAttributions = List.from(markers)..addAll(pushedSpans.markers);
+ final List combinedAttributions = List.from(_markers)..addAll(pushedSpans._markers);
_log.fine('combined attributions before merge:');
for (final marker in combinedAttributions) {
_log.fine(' - $marker');
@@ -630,7 +743,7 @@ class AttributedSpans {
_log.fine(' - $marker');
}
- markers
+ _markers
..clear()
..addAll(combinedAttributions);
}
@@ -673,7 +786,7 @@ class AttributedSpans {
/// If no [endOffset] is provided, a copy is made from [startOffset]
/// to the [offset] of the last marker in this [AttributedSpans].
AttributedSpans copyAttributionRegion(int startOffset, [int? endOffset]) {
- endOffset = endOffset ?? markers.lastOrNull?.offset ?? 0;
+ endOffset = endOffset ?? _markers.lastOrNull?.offset ?? 0;
_log.fine('start: $startOffset, end: $endOffset');
final List cutAttributions = [];
@@ -685,7 +798,7 @@ class AttributedSpans {
// Analyze all markers that appear before the start of
// the copy range so that we can insert any appropriate
// `start` markers at the beginning of the copy range.
- markers //
+ _markers //
.where((marker) => marker.offset < startOffset) //
.forEach((marker) {
_log.fine('marker before the copy region: $marker');
@@ -716,13 +829,13 @@ class AttributedSpans {
));
} else if (count < 0 || count > 1) {
throw Exception(
- 'Found an unbalanced number of `start` and `end` markers before offset: $startOffset - $markers');
+ 'Found an unbalanced number of `start` and `end` markers before offset: $startOffset - $_markers');
}
});
// Directly copy every marker that appears within the cut
// region.
- markers //
+ _markers //
.where((marker) => startOffset <= marker.offset && marker.offset <= endOffset!) //
.forEach((marker) {
_log.fine('copying "${marker.attribution}" at ${marker.offset} from original AttributionSpans to copy region.');
@@ -734,7 +847,7 @@ class AttributedSpans {
// Analyze all markers that appear after the end of
// the copy range so that we can insert any appropriate
// `end` markers at the end of the copy range.
- markers //
+ _markers //
.reversed //
.where((marker) => marker.offset > endOffset!) //
.forEach((marker) {
@@ -765,7 +878,7 @@ class AttributedSpans {
markerType: SpanMarkerType.end,
));
} else if (count < 0 || count > 1) {
- throw Exception('Found an unbalanced number of `start` and `end` markers after offset: $endOffset - $markers');
+ throw Exception('Found an unbalanced number of `start` and `end` markers after offset: $endOffset - $_markers');
}
});
@@ -780,8 +893,8 @@ class AttributedSpans {
/// Changes all spans in this [AttributedSpans] by pushing
/// them back by [offset] amount.
void pushAttributionsBack(int offset) {
- final pushedAttributions = markers.map((marker) => marker.copyWith(offset: marker.offset + offset)).toList();
- markers
+ final pushedAttributions = _markers.map((marker) => marker.copyWith(offset: marker.offset + offset)).toList();
+ _markers
..clear()
..addAll(pushedAttributions);
}
@@ -795,12 +908,12 @@ class AttributedSpans {
final contractedAttributions = [];
// Add all the markers that are unchanged.
- contractedAttributions.addAll(markers.where((marker) => marker.offset < startOffset));
+ contractedAttributions.addAll(_markers.where((marker) => marker.offset < startOffset));
_log.fine('removing $count characters starting at $startOffset');
final needToEndAttributions = {};
final needToStartAttributions = {};
- markers
+ _markers
.where((marker) => (startOffset <= marker.offset) && (marker.offset < startOffset + count))
.forEach((marker) {
// Get rid of this marker and keep track of
@@ -854,12 +967,12 @@ class AttributedSpans {
// Add all remaining markers but with an `offset`
// that is less by `count`.
contractedAttributions.addAll(
- markers
+ _markers
.where((marker) => marker.offset >= startOffset + count)
.map((marker) => marker.copyWith(offset: marker.offset - count)),
);
- markers
+ _markers
..clear()
..addAll(contractedAttributions);
}
@@ -867,7 +980,7 @@ class AttributedSpans {
/// Returns a copy of this [AttributedSpans].
AttributedSpans copy() {
return AttributedSpans(
- attributions: List.from(markers),
+ attributions: List.from(_markers),
);
}
@@ -880,7 +993,7 @@ class AttributedSpans {
}) {
_log.fine('content length: $contentLength');
_log.fine('attributions used to compute spans:');
- for (final marker in markers) {
+ for (final marker in _markers) {
_log.fine(' - $marker');
}
@@ -890,7 +1003,7 @@ class AttributedSpans {
return [];
}
- if (markers.isEmpty || markers.first.offset > contentLength - 1) {
+ if (_markers.isEmpty || _markers.first.offset > contentLength - 1) {
// There is content but no attributions that apply to it.
return [MultiAttributionSpan(attributions: {}, start: 0, end: contentLength - 1)];
}
@@ -899,8 +1012,8 @@ class AttributedSpans {
var currentSpan = MultiAttributionSpan(attributions: {}, start: 0, end: contentLength - 1);
_log.fine('walking list of markers to determine collapsed spans.');
- for (final marker in markers) {
- if (marker.offset > contentLength) {
+ for (final marker in _markers) {
+ if (marker.offset > contentLength - 1) {
// There are markers to process but we ran off the end of the requested content. Break early and handle
// committing the last span if necessary below.
_log.fine('ran out of markers within the requested contentLength, breaking early.');
@@ -965,15 +1078,15 @@ class AttributedSpans {
identical(this, other) ||
other is AttributedSpans &&
runtimeType == other.runtimeType &&
- const DeepCollectionEquality.unordered().equals(markers, other.markers);
+ const DeepCollectionEquality.unordered().equals(_markers, other._markers);
@override
- int get hashCode => markers.hashCode;
+ int get hashCode => _markers.hashCode;
@override
String toString() {
- final buffer = StringBuffer('[AttributedSpans] (${(markers.length / 2).round()} spans):');
- for (final marker in markers) {
+ final buffer = StringBuffer('[AttributedSpans] (${(_markers.length / 2).round()} spans):');
+ for (final marker in _markers) {
buffer.write('\n - $marker');
}
return buffer.toString();
@@ -1069,6 +1182,12 @@ class AttributionSpan {
final int start;
final int end;
+ /// Returns a [SpanRange] from [start] to [end].
+ SpanRange get range => SpanRange(start, end);
+
+ /// Create a returns a copy of this [AttributionSpan], which is constrained
+ /// by the given [start] and [end], i.e., the returned value's `start` is
+ /// >= [start], and its `end` is <= [end].
AttributionSpan constrain({
required int start,
required int end,
@@ -1137,6 +1256,18 @@ class MultiAttributionSpan {
@override
String toString() => '[MultiAttributionSpan] - attributions: $attributions, start: $start, end: $end';
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is MultiAttributionSpan &&
+ runtimeType == other.runtimeType &&
+ start == other.start &&
+ end == other.end &&
+ const DeepCollectionEquality().equals(attributions, other.attributions);
+
+ @override
+ int get hashCode => attributions.hashCode ^ start.hashCode ^ end.hashCode;
}
/// Returns `true` when the given [candidate] [Attribution] matches the desired condition.
@@ -1158,3 +1289,45 @@ class IncompatibleOverlappingAttributionsException implements Exception {
return 'Tried to insert attribution ($newAttribution) over a conflicting existing attribution ($existingAttribution). The overlap began at index $conflictStart';
}
}
+
+/// A conflict between the [newAttribution] and [existingAttribution] between [conflictStart] and [conflictEnd] (inclusive).
+///
+/// This means [newAttribution] and [existingAttribution] have the same id, but they can't be merged.
+class _AttributionConflict {
+ _AttributionConflict({
+ required this.newAttribution,
+ required this.existingAttribution,
+ required this.conflictStart,
+ required this.conflictEnd,
+ });
+
+ /// The new attribution that conflicts with the existing attribution.
+ final Attribution newAttribution;
+
+ /// The conflicting attribution.
+ final Attribution existingAttribution;
+
+ /// The first conflicting index.
+ final int conflictStart;
+
+ /// The last conflicting index (inclusive).
+ final int conflictEnd;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is _AttributionConflict &&
+ runtimeType == other.runtimeType &&
+ newAttribution == other.newAttribution &&
+ existingAttribution == other.existingAttribution &&
+ conflictStart == other.conflictStart &&
+ conflictEnd == other.conflictEnd;
+
+ @override
+ int get hashCode =>
+ newAttribution.hashCode ^ existingAttribution.hashCode ^ conflictStart.hashCode ^ conflictEnd.hashCode;
+
+ @override
+ String toString() =>
+ '[AttributionConflict] - newAttribution: $newAttribution existingAttribution: $existingAttribution, conflictStart: $conflictStart, conflictEnd: $conflictEnd';
+}
diff --git a/attributed_text/lib/src/attributed_text.dart b/attributed_text/lib/src/attributed_text.dart
index 9eebed3446..048b879b6c 100644
--- a/attributed_text/lib/src/attributed_text.dart
+++ b/attributed_text/lib/src/attributed_text.dart
@@ -1,3 +1,5 @@
+import 'package:collection/collection.dart';
+
import 'attributed_spans.dart';
import 'attribution.dart';
import 'logging.dart';
@@ -17,19 +19,177 @@ final _log = attributionsLog;
// TODO: there is a mixture of mutable and immutable behavior in this class.
// Pick one or the other, or offer 2 classes: mutable and immutable (#113)
class AttributedText {
- AttributedText({
- this.text = '',
+ /// The default character that's inserted in place of placeholders when converting
+ /// an [AttributedText] to plain text.
+ ///
+ /// `\uFFFC` is the unicode character for "object replacement" and it looks
+ /// like a regular space.
+ ///
+ /// `\uFFFD` is a similar character - it's the unicode character for replacing
+ /// unknown characters, and looks like: �
+ static const placeholderCharacter = '\uFFFC';
+
+ /// Constructs an [AttributedText] whose content is comprised by a combination
+ /// of [text] and [placeholders], covered by the given attributed [spans].
+ ///
+ /// [placeholders] is a map from character indices to desired placeholder objects.
+ /// The character indices in [placeholders] refer to the final indices when the
+ /// placeholders have been combined with the [text].
+ ///
+ /// Example:
+ /// - Full text: "�Hello � World!�"
+ /// - text: "Hello World!"
+ /// - placeholders:
+ /// - 0: MyPlaceholder
+ /// - 7: MyPlaceholder
+ /// - 15: MyPlaceholder
+ ///
+ /// Notice in the example above that the final placeholder index is greater
+ /// than the total length of the [text] `String`.
+ AttributedText([
+ String? text,
AttributedSpans? spans,
- }) : spans = spans ?? AttributedSpans();
+ Map? placeholders,
+ ]) : _text = text ?? "",
+ spans = spans ?? AttributedSpans() {
+ // Sort the placeholders map so that we can always assume the entries
+ // iterate in the placeholder content order. This knowledge is relied upon
+ // in multiple places in this class.
+ this.placeholders = Map.fromEntries(
+ placeholders?.entries.sorted((a, b) => a.key - b.key) ?? [],
+ );
+
+ assert(() {
+ // ^ Run this in an assert with a callback so that the validation doesn't run in
+ // production and cost processor cycles.
+ _validatePlaceholderIndices();
+ return true;
+ }());
+
+ if (this.placeholders.isEmpty) {
+ // There aren't any placeholders, so text with placeholders is the same as
+ // text without placeholders.
+ _textWithPlaceholders = _text;
+ } else {
+ // Create a 2nd plain text representation that includes stand-in characters
+ // for placeholders.
+ final buffer = StringBuffer();
+ int start = 0;
+ int insertedPlaceholders = 0;
+
+ for (final entry in this.placeholders.entries) {
+ final textSegment = _text.substring(start - insertedPlaceholders, entry.key - insertedPlaceholders);
+ buffer.write(textSegment);
+ start += textSegment.length;
+
+ buffer.write(placeholderCharacter);
+ start += 1;
+
+ insertedPlaceholders += 1;
+ }
+ if (start - insertedPlaceholders < _text.length) {
+ buffer.write(_text.substring(start - insertedPlaceholders, _text.length));
+ }
+
+ _textWithPlaceholders = buffer.toString();
+ }
+ }
+
+ void _validatePlaceholderIndices() {
+ // Ensure that none of the placeholders have negative indices.
+ assert(
+ placeholders.entries.where((entry) => entry.key < 0).isEmpty,
+ "All placeholders must have indices >= 0",
+ );
+
+ // Ensure that none of the placeholders sit beyond the end of the text and other
+ // placeholders.
+ int maxAllowableIndex = _text.length;
+ for (final entry in placeholders.entries) {
+ if (entry.key > maxAllowableIndex) {
+ throw AssertionError("Invalid placeholder index. The index is too large. ${entry.key} -> ${entry.value}.");
+ }
+
+ maxAllowableIndex += 1;
+ }
+ }
void dispose() {
_listeners.clear();
}
/// The text that this [AttributedText] attributes.
- final String text;
+ @Deprecated("Use toPlainText() instead, so you can choose whether to include placeholder characters")
+ String get text => _text;
+ final String _text;
+
+ late final String _textWithPlaceholders;
+
+ /// Returns the character or placeholder at offset zero.
+ Object get first => placeholders[0] ?? _textWithPlaceholders[0];
- /// The attributes applied to [text].
+ /// Returns the character or placeholder at the given [offset].
+ Object operator [](int offset) => placeholders[offset] ?? _textWithPlaceholders[offset];
+
+ /// Returns the character or placeholder at the end of this `AttributedText`.
+ Object get last => placeholders[length - 1] ?? _textWithPlaceholders[length - 1];
+
+ /// Returns a plain-text version of this `AttributedText`.
+ ///
+ /// Plain text has no attributions or placeholder objects.
+ ///
+ /// If [includePlaceholders] is `true`, special characters will be inserted
+ /// at every text offset where there is currently a placeholder object. By
+ /// default, the special character is [placeholderCharacter]. To use a different
+ /// character, provide a [replacementCharacter].
+ ///
+ /// if [includePlaceholders] is `false`, placeholders will be replaced
+ /// with nothing. In that case, the returned `String` will be shorter than
+ /// [length] with a difference equal to the number of placeholders in
+ /// this [AttributedText].
+ String toPlainText({
+ bool includePlaceholders = true,
+ String replacementCharacter = placeholderCharacter,
+ }) {
+ if (includePlaceholders) {
+ if (replacementCharacter != placeholderCharacter) {
+ // The caller wants to use a non-standard character to represent
+ // placeholders. Do a replace-all and return the result.
+ return _textWithPlaceholders.replaceAll(placeholderCharacter, replacementCharacter);
+ }
+
+ return _textWithPlaceholders;
+ }
+
+ return _text;
+ }
+
+ /// Placeholders that represent non-text content, e.g., inline images, that
+ /// should appear in the rendered text.
+ ///
+ /// The entries in this map are in content-order, e.g., the entry for a placeholder
+ /// at index 3 comes before an entry whose placeholder is at index 5.
+ ///
+ /// In terms of [length], each placeholder is treated as a single character.
+ late final Map placeholders;
+
+ /// Returns the `length` of this [AttributedText], which includes the length
+ /// of the plain text `String`, and the number of [placeholders].
+ int get length => _text.length + placeholders.length;
+
+ /// Returns `true` if the [length] of this [AttributedText] is zero.
+ ///
+ /// `isEmpty` is `true` if and only if both the plain text and the
+ /// placeholders are empty.
+ bool get isEmpty => _text.isEmpty && placeholders.isEmpty;
+
+ /// Returns `true` if the [length] of this [AttributedText] is greater than zero.
+ ///
+ /// `isNotEmpty` is `true` if the plain text is non-empty, or if the
+ /// placeholders are non-empty, or both.
+ bool get isNotEmpty => _text.isNotEmpty || placeholders.isNotEmpty;
+
+ /// The attributes applied across the plain text and [placeholders].
final AttributedSpans spans;
final _listeners = {};
@@ -106,7 +266,7 @@ class AttributedText {
while (index <= range.end && attributionsThroughout.isNotEmpty) {
final missingAttributions = {};
for (final attribution in attributionsThroughout) {
- if (!hasAttributionAt(index)) {
+ if (!hasAttributionAt(index, attribution: attribution)) {
missingAttributions.add(attribution);
}
}
@@ -117,6 +277,19 @@ class AttributedText {
return attributionsThroughout;
}
+ /// Returns all spans in this [AttributedText] for the given [attributions].
+ Set getAttributionSpans(Set attributions) => getAttributionSpansInRange(
+ attributionFilter: (a) => attributions.contains(a),
+ range: SpanRange(0, length),
+ );
+
+ /// Returns all spans in this [AttributedText], for attributions that are
+ /// selected by the given [filter].
+ Set getAttributionSpansByFilter(AttributionFilter filter) => getAttributionSpansInRange(
+ attributionFilter: filter,
+ range: SpanRange(0, length),
+ );
+
/// Returns spans for each attribution that (at least partially) appear
/// within the given [range], as selected by [attributionFilter].
///
@@ -126,7 +299,7 @@ class AttributedText {
/// returned, including the area that sits outside the given [range].
///
/// To obtain attribution spans that are cut down and limited to the
- /// given [range], pass [true] for [resizeSpansToFitInRange]. This setting
+ /// given [range], pass `true` for [resizeSpansToFitInRange]. This setting
/// only effects the returned spans, it does not alter the attributions
/// within this [AttributedText].
Set getAttributionSpansInRange({
@@ -151,8 +324,50 @@ class AttributedText {
/// Adds the given [attribution] to all characters within the given
/// [range], inclusive.
- void addAttribution(Attribution attribution, SpanRange range) {
- spans.addAttribution(newAttribution: attribution, start: range.start, end: range.end);
+ ///
+ /// The effect of adding an attribution is straight forward when the text doesn't
+ /// contain any other attributions with the same ID. However, there are various
+ /// situations where the [attribution] can't necessarily co-exist with other
+ /// attribution spans that already exist in the text.
+ ///
+ /// Attribution overlaps can take one of two forms: mergeable or conflicting.
+ ///
+ /// ## Mergeable Attribution Spans
+ /// An example of a mergeable overlap is where two bold spans overlap each
+ /// other. All bold attributions are interchangeable, so when two bold spans
+ /// overlap, those spans can be merged together into a single span.
+ ///
+ /// However, mergeable overlapping spans are not automatically merged. Instead,
+ /// this decision is left to the user of this class. If you want [AttributedText] to
+ /// merge overlapping mergeable spans, pass `true` for [autoMerge]. Otherwise,
+ /// if [autoMerge] is `false`, an exception is thrown when two mergeable spans
+ /// overlap each other.
+ ///
+ ///
+ /// ## Conflicting Attribution Spans
+ /// An example of a conflicting overlap is where a black text color overlaps a red
+ /// text color. Text is either black, OR red, but never both. Therefore, the black
+ /// attribution cannot co-exist with the red attribution. Something must be done
+ /// to resolve this.
+ ///
+ /// There are two possible ways to handle conflicting overlaps. The new attribution
+ /// can overwrite the existing attribution where they overlap. Or, an exception can be
+ /// thrown. To overwrite the existing attribution with the new attribution, pass `true`
+ /// for [overwriteConflictingSpans]. Otherwise, if [overwriteConflictingSpans]
+ /// is `false`, an exception is thrown.
+ void addAttribution(
+ Attribution attribution,
+ SpanRange range, {
+ bool autoMerge = true,
+ bool overwriteConflictingSpans = false,
+ }) {
+ spans.addAttribution(
+ newAttribution: attribution,
+ start: range.start,
+ end: range.end,
+ autoMerge: autoMerge,
+ overwriteConflictingSpans: overwriteConflictingSpans,
+ );
_notifyListeners();
}
@@ -163,6 +378,16 @@ class AttributedText {
_notifyListeners();
}
+ /// Returns a copy of this [AttributedText], replacing the existing
+ /// [AttributedSpans] with the given [newSpans].
+ AttributedText replaceAttributions(AttributedSpans newSpans) {
+ return AttributedText(
+ _text,
+ newSpans,
+ Map.from(placeholders),
+ );
+ }
+
/// Removes all attributions within the given [range].
void clearAttributions(SpanRange range) {
// TODO: implement this capability within AttributedSpans
@@ -188,28 +413,81 @@ class AttributedText {
_notifyListeners();
}
- /// Copies all text and attributions from [startOffset] to
- /// [endOffset], inclusive, and returns them as a new [AttributedText].
+ /// Copies all text and attributions from [range.start] to [range.end] (exclusive),
+ /// and returns them as a new [AttributedText].
+ AttributedText copyTextInRange(SpanRange range) => copyText(range.start, range.end);
+
+ /// Copies all text, attributions, and placeholders from [startOffset] to
+ /// [endOffset], exclusive, and returns them as a new [AttributedText].
AttributedText copyText(int startOffset, [int? endOffset]) {
_log.fine('start: $startOffset, end: $endOffset');
+ final placeholdersBeforeStartOffset = placeholders.entries.where((entry) => entry.key < startOffset);
+ final textStartCopyOffset = startOffset - placeholdersBeforeStartOffset.length;
+
+ final placeholdersAfterStartBeforeEndOffset = placeholders.entries.where(
+ (entry) => startOffset <= entry.key && entry.key < (endOffset ?? length),
+ );
+ final textEndCopyOffset =
+ (endOffset ?? length) - placeholdersBeforeStartOffset.length - placeholdersAfterStartBeforeEndOffset.length;
+
+ // The span marker offsets are based on the text with placeholders, so we need
+ // to copy the text with placeholders to ensure the span markers are correct.
+ final textWithPlaceholders = toPlainText();
+
// Note: -1 because copyText() uses an exclusive `start` and `end` but
// _copyAttributionRegion() uses an inclusive `start` and `end`.
- final startCopyOffset = startOffset < text.length ? startOffset : text.length - 1;
+ final startCopyOffset = startOffset < textWithPlaceholders.length ? startOffset : textWithPlaceholders.length - 1;
int endCopyOffset;
if (endOffset == startOffset) {
endCopyOffset = startCopyOffset;
} else if (endOffset != null) {
endCopyOffset = endOffset - 1;
} else {
- endCopyOffset = text.length - 1;
+ endCopyOffset = textWithPlaceholders.length - 1;
}
_log.fine('offsets, start: $startCopyOffset, end: $endCopyOffset');
+ // Create placeholders for the copied region. The indices of the placeholders
+ // need to be reduced based on the text/placeholders cut out from the
+ // beginning of this AttributedText.
+ final copiedPlaceholders = {};
+ for (final existingPlaceholder in placeholdersAfterStartBeforeEndOffset) {
+ copiedPlaceholders[existingPlaceholder.key - startOffset] = existingPlaceholder.value;
+ }
+
return AttributedText(
- text: text.substring(startOffset, endOffset),
- spans: spans.copyAttributionRegion(startCopyOffset, endCopyOffset),
+ _text.substring(textStartCopyOffset, textEndCopyOffset),
+ spans.copyAttributionRegion(startCopyOffset, endCopyOffset),
+ copiedPlaceholders,
+ );
+ }
+
+ /// Returns a plain-text substring, from [range.start] to [range.end] (exclusive).
+ ///
+ /// {@macro attributed_text_substring_range}
+ String substringInRange(SpanRange range) => substring(range.start, range.end);
+
+ /// Returns a plain-text substring, from [start] to [end] (exclusive), or the end of
+ /// this [AttributedText] if [end] isn't provided.
+ ///
+ /// {@template attributed_text_substring_range}
+ /// [AttributedText] can contain placeholders, each of which take up one character of length.
+ /// The given [range] is interpreted as a range within this [AttributedText]. If placeholders
+ /// appear within that range, then the length of the returned `String` will be less than the
+ /// length of the range.
+ /// {@endtemplate}
+ String substring(int start, [int? end]) {
+ final placeholdersBeforeStartOffset = placeholders.entries.where((entry) => entry.key < start);
+ final textStartCopyOffset = start - placeholdersBeforeStartOffset.length;
+
+ final placeholdersAfterStartBeforeEndOffset = placeholders.entries.where(
+ (entry) => start <= entry.key && entry.key < (end ?? length),
);
+ final textEndCopyOffset =
+ (end ?? length) - placeholdersBeforeStartOffset.length - placeholdersAfterStartBeforeEndOffset.length;
+
+ return _text.substring(textStartCopyOffset, textEndCopyOffset);
}
/// Returns a copy of this [AttributedText] with the [other] text
@@ -217,25 +495,32 @@ class AttributedText {
AttributedText copyAndAppend(AttributedText other) {
_log.fine('our attributions before pushing them:');
_log.fine(spans.toString());
- if (other.text.isEmpty) {
+
+ if (other.isEmpty) {
_log.fine('`other` has no text. Returning a direct copy of ourselves.');
return AttributedText(
- text: text,
- spans: spans.copy(),
+ _text,
+ spans.copy(),
+ Map.from(placeholders),
);
}
- if (text.isEmpty) {
+
+ if (isEmpty) {
_log.fine('our `text` is empty. Returning a direct copy of the `other` text.');
return AttributedText(
- text: other.text,
- spans: other.spans.copy(),
+ other._text,
+ other.spans.copy(),
+ Map.from(other.placeholders),
);
}
- final newSpans = spans.copy()..addAt(other: other.spans, index: text.length);
return AttributedText(
- text: text + other.text,
- spans: newSpans,
+ _text + other._text,
+ spans.copy()..addAt(other: other.spans, index: length),
+ {
+ ...placeholders,
+ ...other.placeholders.map((offset, placeholder) => MapEntry(offset + length, placeholder)),
+ },
);
}
@@ -273,10 +558,8 @@ class AttributedText {
_log.fine('endText: $endText');
_log.fine('creating new attributed text for insertion');
- final insertedText = AttributedText(
- text: textToInsert,
- );
- final insertTextRange = SpanRange(start: 0, end: textToInsert.length - 1);
+ final insertedText = AttributedText(textToInsert);
+ final insertTextRange = SpanRange(0, textToInsert.length - 1);
for (dynamic attribution in applyAttributions) {
insertedText.addAttribution(attribution, insertTextRange);
}
@@ -286,9 +569,29 @@ class AttributedText {
return startText.copyAndAppend(insertedText).copyAndAppend(endText);
}
- /// Copies this [AttributedText] and removes a region of text
- /// and attributions from [startOffset], inclusive,
- /// to [endOffset], exclusive.
+ AttributedText insertPlaceholders(Map placeholders) {
+ var finalText = this;
+ for (final entry in placeholders.entries) {
+ finalText = finalText.insertPlaceholder(entry.key, entry.value);
+ }
+ return finalText;
+ }
+
+ AttributedText insertPlaceholder(int index, Object placeholder) {
+ return AttributedText(_text, spans.copy(), {
+ // Insert existing placeholders that come before the new placeholder.
+ ...Map.fromEntries(placeholders.entries.where((entry) => entry.key < index)),
+ // Insert the new placeholder.
+ index: placeholder,
+ // Push back all later placeholders by 1 unit, because of the new placeholder.
+ ...Map.fromEntries(
+ placeholders.entries.where((entry) => entry.key >= index).map((entry) => MapEntry(entry.key + 1, entry.value)),
+ ),
+ });
+ }
+
+ /// Copies this [AttributedText] and removes a region of text and attributions
+ /// from [startOffset], inclusive, to [endOffset], exclusive.
AttributedText removeRegion({
required int startOffset,
required int endOffset,
@@ -296,8 +599,7 @@ class AttributedText {
_log.fine('Removing text region from $startOffset to $endOffset');
_log.fine('initial attributions:');
_log.fine(spans.toString());
- final reducedText = (startOffset > 0 ? text.substring(0, startOffset) : '') +
- (endOffset < text.length ? text.substring(endOffset) : '');
+ final reducedText = substring(0, startOffset) + substring(endOffset, length);
AttributedSpans contractedAttributions = spans.copy()
..contractAttributions(
@@ -309,14 +611,42 @@ class AttributedText {
_log.fine(contractedAttributions.toString());
return AttributedText(
- text: reducedText,
- spans: contractedAttributions,
+ reducedText,
+ contractedAttributions,
+ Map.fromEntries(
+ placeholders.entries
+ .where((entry) => entry.key < startOffset || endOffset <= entry.key) //
+ .map(
+ (entry) => entry.key >= endOffset //
+ ? MapEntry(entry.key - (endOffset - startOffset), entry.value)
+ : entry,
+ ),
+ ),
);
}
+ /// Visits all attributions in this [AttributedText] by calling [visitor] whenever
+ /// an attribution begins or ends.
+ ///
+ /// If multiple attributions begin or end at the same index, then all of those attributions
+ /// are reported together.
+ ///
+ /// **Only Reports Beginnings and Endings:**
+ ///
+ /// This visitation method does not report all applied attributions at a given index. It
+ /// only reports attributions that begin or end at a specific index.
+ ///
+ /// For example:
+ ///
+ /// Bold: |xxxxxxxxxxxx|
+ /// Italics: |------xxxxxx|
+ ///
+ /// Bold is attributed throughout the range. Italics begins at index `6`. When [visitor]
+ /// is notified about italics beginning at `6`, visitor is NOT notified that bold applies
+ /// at that same index.
void visitAttributions(AttributionVisitor visitor) {
- final startingAttributions = Set();
- final endingAttributions = Set();
+ final startingAttributions = {};
+ final endingAttributions = {};
int currentIndex = -1;
visitor.onVisitBegin();
@@ -350,21 +680,80 @@ class AttributedText {
visitor.onVisitEnd();
}
+ /// Visits attributions in this [AttributedText], reporting every changing group of
+ /// attributions to the given [visitor].
+ ///
+ /// See [computeAttributionSpans] for an example.
+ ///
+ /// See also:
+ ///
+ /// * [visitAttributions], to visit attributions markers instead of attribution groups.
+ /// * [computeAttributionSpans], to work with a list of [MultiAttributionSpan]s instead
+ /// of visiting each span with a callback.
+ void visitAttributionSpans(AttributionSpanVisitor visitor) {
+ final collapsedSpans = computeAttributionSpans();
+ for (final span in collapsedSpans) {
+ visitor(span);
+ }
+ }
+
+ /// Collapses all attribution markers down into a series of attribution groups,
+ /// starting at the beginning of this [AttributedText], until the end.
+ ///
+ /// A new group of attributions begin wherever an attribution begins or ends.
+ ///
+ /// For example:
+ ///
+ /// Bold: |----xxxxxxxxxxxx------------|
+ /// Italics: |-------xxxxxxxxxxxxx--------|
+ /// Strikethru: |-----------xxxxxxxxxxxxx----|
+ ///
+ /// Given the above attributions, the given [visitor] would be notified of the following
+ /// groups:
+ ///
+ /// 1. [0, 4] - No attributions
+ /// 2. [5, 8] - Bold
+ /// 3. [9, 12] - Bold, Italics
+ /// 4. [13, 16] - Bold, Italics, Strikethru
+ /// 5. [17, 20] - Italics, Strikethru
+ /// 6. [21, 24] - Strikethru
+ /// 7. [25, 28] - No attributions
+ ///
+ /// Attribution groups are useful when computing all style variations for [AttributedText].
+ Iterable computeAttributionSpans() {
+ return spans.collapseSpans(contentLength: length);
+ }
+
+ /// Returns a copy of this [AttributedText].
+ AttributedText copy() {
+ return AttributedText(
+ _text,
+ spans.copy(),
+ Map.from(placeholders),
+ );
+ }
+
@override
bool operator ==(Object other) {
return identical(this, other) ||
- other is AttributedText && runtimeType == other.runtimeType && text == other.text && spans == other.spans;
+ other is AttributedText &&
+ runtimeType == other.runtimeType &&
+ _text == other._text &&
+ spans == other.spans &&
+ (const DeepCollectionEquality()).equals(placeholders, other.placeholders);
}
@override
- int get hashCode => text.hashCode ^ spans.hashCode;
+ int get hashCode => _text.hashCode ^ spans.hashCode ^ placeholders.hashCode;
@override
String toString() {
- return '[AttributedText] - "$text"\n' + spans.toString();
+ return '[AttributedText] - "$_text"\n$spans\n$placeholders';
}
}
+typedef AttributionSpanVisitor = void Function(MultiAttributionSpan span);
+
/// Visits every [index] in the the given [AttributedText] which has at least
/// one start or end marker, passing the attributions that start or end at the [index].
///
@@ -422,7 +811,12 @@ class CallbackAttributionVisitor implements AttributionVisitor {
}
@override
- void visitAttributions(AttributedText fullText, int index, Set startingAttributions, Set endingAttributions) {
+ void visitAttributions(
+ AttributedText fullText,
+ int index,
+ Set startingAttributions,
+ Set endingAttributions,
+ ) {
_onVisitAttributions(fullText, index, startingAttributions, endingAttributions);
}
diff --git a/attributed_text/lib/src/logging.dart b/attributed_text/lib/src/logging.dart
index 20c59305a2..d5af7fff75 100644
--- a/attributed_text/lib/src/logging.dart
+++ b/attributed_text/lib/src/logging.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: avoid_print
+
import 'package:logging/logging.dart' as logging;
class LogNames {
@@ -16,7 +18,7 @@ void initLoggers(logging.Level level, Set loggers) {
if (level == logging.Level.OFF) {
return;
}
-
+
logging.hierarchicalLoggingEnabled = true;
for (final logger in loggers) {
diff --git a/attributed_text/lib/src/span_range.dart b/attributed_text/lib/src/span_range.dart
index 57e526588d..74e87f6c14 100644
--- a/attributed_text/lib/src/span_range.dart
+++ b/attributed_text/lib/src/span_range.dart
@@ -3,7 +3,7 @@
/// This was copied from Flutter so that attributed_text could be distributed
/// as a Dart package, instead of a Flutter package.
class SpanRange {
- /// Creates a text range.
+ /// Creates a span range.
///
/// The [start] and [end] arguments must not be null. Both the [start] and
/// [end] must either be greater than or equal to zero or both exactly -1.
@@ -11,15 +11,15 @@ class SpanRange {
/// The text included in the range includes the character at [start], but not
/// the one at [end].
///
- /// Instead of creating an empty text range, consider using the [empty]
+ /// Instead of creating an empty span range, consider using the [empty]
/// constant.
- const SpanRange({
- required this.start,
- required this.end,
- }) : assert(start >= -1),
+ const SpanRange(
+ this.start,
+ this.end,
+ ) : assert(start >= -1),
assert(end >= -1);
- /// A text range that starts and ends at offset.
+ /// A span range that starts and ends at offset.
///
/// The [offset] argument must be non-null and greater than or equal to -1.
const SpanRange.collapsed(int offset)
@@ -27,8 +27,8 @@ class SpanRange {
start = offset,
end = offset;
- /// A text range that contains nothing and is not in the text.
- static const SpanRange empty = SpanRange(start: -1, end: -1);
+ /// A span range that contains nothing and is not in the text.
+ static const SpanRange empty = SpanRange(-1, -1);
/// The index of the first character in the range.
///
@@ -77,5 +77,5 @@ class SpanRange {
int get hashCode => start.hashCode ^ end.hashCode;
@override
- String toString() => 'TextRange(start: $start, end: $end)';
+ String toString() => 'SpanRange(start: $start, end: $end)';
}
diff --git a/attributed_text/lib/src/test_tools.dart b/attributed_text/lib/src/test_tools.dart
index b65de393a8..7887c277ba 100644
--- a/attributed_text/lib/src/test_tools.dart
+++ b/attributed_text/lib/src/test_tools.dart
@@ -6,7 +6,9 @@ import 'attribution.dart';
class ExpectedSpans {
static const bold = NamedAttribution('bold');
static const italics = NamedAttribution('italics');
+ static const underline = NamedAttribution('underline');
static const strikethrough = NamedAttribution('strikethrough');
+ static const hashTag = NamedAttribution('hashTag');
ExpectedSpans(
List spanTemplates,
@@ -60,6 +62,7 @@ class ExpectedSpans {
}
if (!spans.hasAttributionAt(characterIndex, attribution: namedAttribution)) {
+ // ignore: avoid_print
print("SPAN MISMATCH: missing $namedAttribution at $characterIndex");
}
expect(spans.hasAttributionAt(characterIndex, attribution: namedAttribution), true);
diff --git a/attributed_text/pubspec.yaml b/attributed_text/pubspec.yaml
index 29a11e764f..e4eca427b1 100644
--- a/attributed_text/pubspec.yaml
+++ b/attributed_text/pubspec.yaml
@@ -1,15 +1,15 @@
name: attributed_text
description: Text with metadata spans for easy text editing and styling.
-version: 0.2.0
+version: 0.3.2
homepage: https://github.com/superlistapp/super_editor
environment:
- sdk: ">=2.12.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
dependencies:
characters: ^1.2.0
collection: ^1.15.0
- logging: ^1.0.1
+ logging: ^1.3.0
meta: ^1.7.0
test: ^1.19.4
diff --git a/attributed_text/test/attributed_spans_test.dart b/attributed_text/test/attributed_spans_test.dart
index adc5ce286d..3a8de3f95b 100644
--- a/attributed_text/test/attributed_spans_test.dart
+++ b/attributed_text/test/attributed_spans_test.dart
@@ -157,95 +157,142 @@ void main() {
);
});
+ test('hasAttributionsWithin can look for multiple attributions at the same time', () {
+ final spans = AttributedSpans()
+ ..addAttribution(newAttribution: ExpectedSpans.bold, start: 0, end: 8)
+ ..addAttribution(newAttribution: ExpectedSpans.italics, start: 1, end: 5)
+ ..addAttribution(newAttribution: ExpectedSpans.strikethrough, start: 5, end: 9);
+
+ ExpectedSpans(
+ [
+ 'bbbbbbbbb_',
+ '_iiiii____',
+ '_____sssss',
+ ],
+ ).expectSpans(spans);
+
+ expect(
+ spans.hasAttributionsWithin(attributions: {
+ ExpectedSpans.bold,
+ ExpectedSpans.italics,
+ ExpectedSpans.strikethrough,
+ }, start: 0, end: 9),
+ true,
+ );
+
+ expect(
+ spans.hasAttributionsWithin(attributions: {
+ ExpectedSpans.bold,
+ ExpectedSpans.italics,
+ }, start: 0, end: 9),
+ true,
+ );
+
+ expect(
+ spans.hasAttributionsWithin(attributions: {
+ ExpectedSpans.bold,
+ ExpectedSpans.italics,
+ }, start: 0, end: 4),
+ true);
+
+ expect(
+ spans.hasAttributionsWithin(attributions: {
+ ExpectedSpans.bold,
+ ExpectedSpans.strikethrough,
+ }, start: 0, end: 4),
+ false,
+ );
+ });
+
group('getAttributedRange', () {
test('returns the range of a single attribution for an offset in the middle of a span', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold}, 5);
- expect(range, SpanRange(start: 4, end: 9));
+ expect(range, const SpanRange(4, 9));
});
test('returns the range of a single attribution for an offset at the beginning of a span', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold}, 4);
- expect(range, SpanRange(start: 4, end: 9));
+ expect(range, const SpanRange(4, 9));
});
test('returns the range of a single attribution for an offset at the end of a span', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold}, 9);
- expect(range, SpanRange(start: 4, end: 9));
+ expect(range, const SpanRange(4, 9));
});
test('returns the range for multiple attributions for an offset in the middle of the overlapping range', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics}, 5);
- expect(range, SpanRange(start: 4, end: 7));
+ expect(range, const SpanRange(4, 7));
});
test('returns the range for multiple attributions for an offset at the beginning of the overlapping range', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics}, 4);
- expect(range, SpanRange(start: 4, end: 7));
+ expect(range, const SpanRange(4, 7));
});
test('returns the range for multiple attributions for an offset at the end of the overlapping range', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
],
);
final range = spans.getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics}, 7);
- expect(range, SpanRange(start: 4, end: 7));
+ expect(range, const SpanRange(4, 7));
});
test('throws when given an empty attribution set', () {
@@ -257,8 +304,8 @@ void main() {
test('throws when any attribution is not present at the given offset', () {
final spans = AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 10, markerType: SpanMarkerType.end),
],
);
@@ -554,10 +601,211 @@ void main() {
newAttribution: _LinkAttribution(url: 'https://pub.dev'),
start: 4,
end: 12,
+ overwriteConflictingSpans: false,
);
}, throwsA(isA()));
});
+ test('overwrites incompatible attributions at the beginning of the span', () {
+ // Starting value:
+ // |aaaaaaa|
+ //
+ // Ending value:
+ // |-----aa|
+ // |bbbbb--|
+
+ final spans = AttributedSpans(
+ attributions: _createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 6,
+ ),
+ );
+
+ // Add an overlapping link at the beginning.
+ spans.addAttribution(
+ newAttribution: _LinkAttribution(url: 'https://pub.dev'),
+ start: 0,
+ end: 4,
+ );
+
+ expect(
+ spans,
+ AttributedSpans(
+ attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://pub.dev'),
+ startOffset: 0,
+ endOffset: 4,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 5,
+ endOffset: 6,
+ ),
+ ],
+ ),
+ );
+ });
+
+ test('splits incompatible attributions at the middle of the text', () {
+ // Starting value:
+ // |aaaaaaa----------|
+ // |----------bbbbbbb|
+ //
+ // Ending value:
+ // |aaaa-------------|
+ // |-------------bbbb|
+ // |----ccccccccc----|
+
+ final spans = AttributedSpans(attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 6,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://pub.dev'),
+ startOffset: 10,
+ endOffset: 16,
+ ),
+ ]);
+
+ // Add an overlapping at the middle.
+ spans.addAttribution(
+ newAttribution: _LinkAttribution(url: 'https://google.com'),
+ start: 4,
+ end: 12,
+ );
+
+ expect(
+ spans,
+ AttributedSpans(
+ attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 3,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://google.com'),
+ startOffset: 4,
+ endOffset: 12,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://pub.dev'),
+ startOffset: 13,
+ endOffset: 16,
+ ),
+ ],
+ ),
+ );
+ });
+
+ test('overwrites incompatible attributions at the end of the span', () {
+ // Starting value:
+ // |aaaaaaa------|
+ //
+ // Ending value:
+ // |aaaa---------|
+ // |----bbbbbbbbb|
+
+ final spans = AttributedSpans(
+ attributions: _createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 6,
+ ),
+ );
+
+ // Add an overlapping link at the end.
+ spans.addAttribution(
+ newAttribution: _LinkAttribution(url: 'https://pub.dev'),
+ start: 4,
+ end: 12,
+ );
+
+ expect(
+ spans,
+ AttributedSpans(
+ attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 3,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://pub.dev'),
+ startOffset: 4,
+ endOffset: 12,
+ ),
+ ],
+ ),
+ );
+ });
+
+ test('overwrites multiple incompatible attributions at the midle of the text', () {
+ // Starting value:
+ // |aaaaaa--------------------|
+ // |----------bbbbbb----------|
+ // |--------------------cccccc|
+ //
+ // Ending value:
+ // |aaaa----------------------|
+ // |-----------------------ccc|
+ // |----ddddddddddddddddddd---|
+
+ final spans = AttributedSpans(
+ attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 5,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://pub.dev'),
+ startOffset: 10,
+ endOffset: 15,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://google.com'),
+ startOffset: 20,
+ endOffset: 25,
+ ),
+ ],
+ );
+
+ // Add a link overlapping with all existing spans.
+ spans.addAttribution(
+ newAttribution: _LinkAttribution(url: 'https://youtube.com'),
+ start: 4,
+ end: 22,
+ );
+
+ expect(
+ spans,
+ AttributedSpans(
+ attributions: [
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://flutter.dev'),
+ startOffset: 0,
+ endOffset: 3,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://youtube.com'),
+ startOffset: 4,
+ endOffset: 22,
+ ),
+ ..._createSpanMarkersForAttribution(
+ attribution: _LinkAttribution(url: 'https://google.com'),
+ startOffset: 23,
+ endOffset: 25,
+ )
+ ],
+ ),
+ );
+ });
+
test('compatible attributions are merged', () {
final spans = AttributedSpans();
@@ -760,8 +1008,30 @@ class _LinkAttribution implements Attribution {
}
@override
- bool operator ==(Object other) => identical(this, other) || other is _LinkAttribution && runtimeType == other.runtimeType && url == other.url;
+ bool operator ==(Object other) =>
+ identical(this, other) || other is _LinkAttribution && runtimeType == other.runtimeType && url == other.url;
@override
int get hashCode => url.hashCode;
}
+
+/// Creates start and end markers for the [attribution], starting at [startOffset]
+/// and ending at [endOffset].
+List _createSpanMarkersForAttribution({
+ required Attribution attribution,
+ required int startOffset,
+ required int endOffset,
+}) {
+ return [
+ SpanMarker(
+ attribution: attribution,
+ offset: startOffset,
+ markerType: SpanMarkerType.start,
+ ),
+ SpanMarker(
+ attribution: attribution,
+ offset: endOffset,
+ markerType: SpanMarkerType.end,
+ ),
+ ];
+}
diff --git a/attributed_text/test/attributed_text_placeholders_test.dart b/attributed_text/test/attributed_text_placeholders_test.dart
new file mode 100644
index 0000000000..562fd4ccdb
--- /dev/null
+++ b/attributed_text/test/attributed_text_placeholders_test.dart
@@ -0,0 +1,809 @@
+import 'package:attributed_text/attributed_text.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group("AttributedAttributed > placeholders >", () {
+ group("construction >", () {
+ test("reports invalid placeholder positions", () {
+ // Index less than zero.
+ expect(
+ () => AttributedText("Hello, World!", null, {
+ -1: const _FakePlaceholder("bad-index"),
+ }),
+ throwsA(isA()),
+ );
+
+ // Index beyond length.
+ expect(
+ () => AttributedText("Hello, World!", null, {
+ 14: const _FakePlaceholder("bad-index"),
+ }),
+ throwsA(isA()),
+ );
+ });
+
+ test("is order agnostic", () {
+ // Ensure that constructors don't below up when the placeholder
+ // entry order isn't the same as content order, and also ensure that
+ // equality doesn't care about the map order.
+
+ final order1 = AttributedText("Hello, World!", null, {
+ 3: const _FakePlaceholder("first"),
+ 6: const _FakePlaceholder("second"),
+ 9: const _FakePlaceholder("third"),
+ });
+
+ final order2 = AttributedText("Hello, World!", null, {
+ 6: const _FakePlaceholder("second"),
+ 3: const _FakePlaceholder("first"),
+ 9: const _FakePlaceholder("third"),
+ });
+
+ final order3 = AttributedText("Hello, World!", null, {
+ 6: const _FakePlaceholder("second"),
+ 9: const _FakePlaceholder("third"),
+ 3: const _FakePlaceholder("first"),
+ });
+
+ final order4 = AttributedText("Hello, World!", null, {
+ 9: const _FakePlaceholder("third"),
+ 6: const _FakePlaceholder("second"),
+ 3: const _FakePlaceholder("first"),
+ });
+
+ expect(order1, equals(order2));
+ expect(order1, equals(order3));
+ expect(order1, equals(order4));
+ });
+ });
+
+ group("length >", () {
+ test("only a single placeholder", () {
+ expect(
+ AttributedText(
+ "",
+ null,
+ {
+ 0: const _FakePlaceholder("only"),
+ },
+ ).length,
+ 1,
+ );
+ });
+
+ test("only multiple placeholders", () {
+ expect(
+ AttributedText(
+ "",
+ null,
+ {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ },
+ ).length,
+ 3,
+ );
+ });
+
+ test("text with a single placeholder", () {
+ expect(
+ AttributedText(
+ "Hello, world! ",
+ null,
+ {
+ 14: const _FakePlaceholder("trailing"),
+ },
+ ).length,
+ 15,
+ );
+ });
+
+ test("text with multiple placeholders", () {
+ expect(
+ AttributedText(
+ "Hello, world! ",
+ null,
+ {
+ 0: const _FakePlaceholder("leading"),
+ 6: const _FakePlaceholder("middle"),
+ 16: const _FakePlaceholder("trailing"),
+ },
+ ).length,
+ 17,
+ );
+ });
+ });
+
+ test("reports characters and placeholders at indices", () {
+ final text1 = AttributedText("HelloWorld", null, {
+ 0: const _FakePlaceholder("one"),
+ 6: const _FakePlaceholder("two"),
+ 12: const _FakePlaceholder("three"),
+ });
+
+ expect(text1.first, const _FakePlaceholder("one"));
+ expect(text1[1], "H");
+ expect(text1[6], const _FakePlaceholder("two"));
+ expect(text1[11], "d");
+ expect(text1.last, const _FakePlaceholder("three"));
+
+ final text2 = AttributedText("Hello World", null, {
+ 0: const _FakePlaceholder("one"),
+ });
+ expect(text2[11], "d");
+ expect(text2.last, "d");
+ });
+
+ test("reports plain text value", () {
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }).toPlainText(replacementCharacter: "�"),
+ "�",
+ );
+
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }).toPlainText(replacementCharacter: "�"),
+ "���",
+ );
+
+ expect(
+ AttributedText("HelloWorld", null, {
+ 0: const _FakePlaceholder("one"),
+ 6: const _FakePlaceholder("two"),
+ 12: const _FakePlaceholder("three"),
+ }).toPlainText(replacementCharacter: "�"),
+ "�Hello�World�",
+ );
+ });
+
+ group("plain text substring >", () {
+ test("when placeholders are not in range", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 0: const _FakePlaceholder("leading"),
+ 6: const _FakePlaceholder("middle"),
+ 15: const _FakePlaceholder("trailing"),
+ }).substring(1, 6),
+ "Hello",
+ );
+ });
+
+ test("with placeholders in the range", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 0: const _FakePlaceholder("leading"),
+ 6: const _FakePlaceholder("middle"),
+ 15: const _FakePlaceholder("trailing"),
+ }).substring(0, 7),
+ "Hello",
+ );
+ });
+ });
+
+ group("equality >", () {
+ test("only a single placeholder", () {
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ equals(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ ),
+ );
+
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ isNot(
+ equals(
+ AttributedText("", null),
+ ),
+ ),
+ );
+
+ expect(
+ AttributedText("", null),
+ isNot(
+ equals(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ ),
+ ),
+ );
+ });
+
+ test("some of multiple placeholders", () {
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ equals(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ ),
+ );
+
+ expect(
+ AttributedText("", null),
+ isNot(
+ equals(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ ),
+ ),
+ );
+
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ isNot(
+ equals(
+ AttributedText("", null),
+ ),
+ ),
+ );
+ });
+
+ test("some text and a placeholder", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ equals(
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ ),
+ );
+
+ expect(
+ AttributedText("Hello, world!", null),
+ isNot(
+ equals(
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ ),
+ ),
+ );
+
+ expect(
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ isNot(
+ equals(
+ AttributedText("Hello, world!", null),
+ ),
+ ),
+ );
+ });
+ });
+
+ group("attribution queries >", () {
+ test('finds spans that exceed plain text length', () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 12, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 11: const _FakePlaceholder("trailing1"),
+ 12: const _FakePlaceholder("trailing2"),
+ },
+ );
+
+ final ranges = attributedText.getAttributionSpans({ExpectedSpans.bold});
+
+ expect(ranges.length, 1);
+ expect(
+ ranges,
+ [
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 0, end: 12),
+ ],
+ );
+ });
+ });
+
+ group("full copy >", () {
+ test("only a single placeholder", () {
+ expect(
+ AttributedText(
+ "",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder("only"),
+ }).copy(),
+ AttributedText(
+ "",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder("only"),
+ }),
+ );
+ });
+
+ test("some of multiple placeholders", () {
+ expect(
+ AttributedText(
+ "",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 1, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 2, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }).copy(),
+ AttributedText(
+ "",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 1, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 2, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ );
+ });
+
+ test("some text and a placeholder", () {
+ expect(
+ AttributedText(
+ "Hello, world!",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 5, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 5: const _FakePlaceholder("middle"),
+ }).copy(),
+ AttributedText(
+ "Hello, world!",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 5, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ );
+ });
+ });
+
+ group("copy span >", () {
+ test("only a single placeholder", () {
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }).copyText(0, 1),
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ );
+ });
+
+ test("some of multiple placeholders", () {
+ expect(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }).copyText(1, 3),
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("two"),
+ 1: const _FakePlaceholder("three"),
+ }),
+ );
+ });
+
+ test("some text and a leading placeholder", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 0: const _FakePlaceholder("leading"),
+ }).copyText(0, 6),
+ AttributedText("Hello", null, {
+ 0: const _FakePlaceholder("leading"),
+ }),
+ );
+ });
+
+ test("some text and a middle placeholder", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }).copyText(0, 6),
+ AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ );
+ });
+
+ test("some text and a trailing placeholder", () {
+ expect(
+ AttributedText("Hello, world!", null, {
+ 13: const _FakePlaceholder("trailing"),
+ }).copyText(7, 14),
+ AttributedText("world!", null, {
+ 6: const _FakePlaceholder("trailing"),
+ }),
+ );
+ });
+
+ test("empty text with leading placeholder (with attributions)", () {
+ // Create an empty text containing a placeholder with an attribution around it.
+ final text = AttributedText(
+ '',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(
+ attribution: _bold,
+ offset: 0,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: _bold,
+ offset: 0,
+ markerType: SpanMarkerType.end,
+ ),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder('leading'),
+ },
+ );
+
+ expect(
+ text.copyText(0),
+ AttributedText(
+ "",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 0: const _FakePlaceholder("leading"),
+ },
+ ),
+ );
+ });
+ });
+
+ group("copy and append >", () {
+ test("only a single placeholder", () {
+ expect(
+ AttributedText("Hello").copyAndAppend(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ ),
+ AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("only"),
+ }),
+ );
+ });
+
+ test("some of multiple placeholders", () {
+ expect(
+ AttributedText("Hello").copyAndAppend(
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("one"),
+ 1: const _FakePlaceholder("two"),
+ 2: const _FakePlaceholder("three"),
+ }),
+ ),
+ AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("one"),
+ 6: const _FakePlaceholder("two"),
+ 7: const _FakePlaceholder("three"),
+ }),
+ );
+ });
+
+ test("some text and a leading placeholder", () {
+ expect(
+ AttributedText("Hello").copyAndAppend(
+ AttributedText(", world!", null, {
+ 0: const _FakePlaceholder("middle"),
+ }),
+ ),
+ AttributedText("Hello, world!", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ );
+ });
+
+ test("some text and a middle placeholder", () {
+ expect(
+ AttributedText("Hello").copyAndAppend(
+ AttributedText(", world!", null, {
+ 2: const _FakePlaceholder("middle"),
+ }),
+ ),
+ AttributedText("Hello, world!", null, {
+ 7: const _FakePlaceholder("middle"),
+ }),
+ );
+ });
+
+ test("some text and a trailing placeholder", () {
+ expect(
+ AttributedText("Hello").copyAndAppend(
+ AttributedText(", world!", null, {
+ 8: const _FakePlaceholder("trailing"),
+ }),
+ ),
+ AttributedText("Hello, world!", null, {
+ 13: const _FakePlaceholder("trailing"),
+ }),
+ );
+ });
+
+ test("at end of text (with attributions) with leading placeholder", () {
+ // Note: We include attributions in this test because in a client app the presence
+ // of attributions unearthed a bug where we were still using the plain text
+ // length, when we should have been using the text + placeholders length.
+ expect(
+ AttributedText(
+ "Hello",
+ AttributedSpans(attributions: const [
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 1, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 5, markerType: SpanMarkerType.end),
+ ]),
+ {
+ 0: const _FakePlaceholder("leading"),
+ },
+ ).copyAndAppend(
+ AttributedText(", world!"),
+ ),
+ AttributedText(
+ "Hello, world!",
+ AttributedSpans(attributions: const [
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 1, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 5, markerType: SpanMarkerType.end),
+ ]),
+ {
+ 0: const _FakePlaceholder("leading"),
+ },
+ ),
+ );
+ });
+
+ test("at end of text (with attributions) with trailing placeholder", () {
+ // Note: We include attributions in this test because in a client app the presence
+ // of attributions unearthed a bug where we were still using the plain text
+ // length, when we should have been using the text + placeholders length.
+ expect(
+ AttributedText(
+ "Hello",
+ AttributedSpans(attributions: const [
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.end),
+ ]),
+ {
+ 5: const _FakePlaceholder("trailing"),
+ },
+ ).copyAndAppend(
+ AttributedText(", world!"),
+ ),
+ AttributedText(
+ "Hello, world!",
+ AttributedSpans(attributions: const [
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.end),
+ ]),
+ {
+ 5: const _FakePlaceholder("trailing"),
+ },
+ ),
+ );
+ });
+ });
+
+ test("insert attributed text >", () {
+ final empty = AttributedText("");
+ final hello = empty.insert(
+ textToInsert: AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ startOffset: 0,
+ );
+ final helloWorld = hello.insert(
+ textToInsert: AttributedText(", World!", null, {
+ 8: const _FakePlaceholder("trailing"),
+ }),
+ startOffset: 6,
+ );
+
+ expect(
+ hello,
+ AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("middle"),
+ }),
+ );
+
+ expect(
+ helloWorld,
+ AttributedText("Hello, World!", null, {
+ 5: const _FakePlaceholder("middle"),
+ 14: const _FakePlaceholder("trailing"),
+ }),
+ );
+ });
+
+ group("insert placeholders >", () {
+ test("multiple placeholders", () {
+ expect(
+ AttributedText("Hello, World!").insertPlaceholders({
+ 0: const _FakePlaceholder("leading"),
+ 6: const _FakePlaceholder("middle"),
+ 14: const _FakePlaceholder("trailing"),
+ }),
+ AttributedText("Hello, World!", null, {
+ 0: const _FakePlaceholder("leading"),
+ 6: const _FakePlaceholder("middle"),
+ 14: const _FakePlaceholder("trailing"),
+ }),
+ );
+ });
+
+ test("individual placeholder", () {
+ expect(
+ AttributedText().insertPlaceholder(0, const _FakePlaceholder("only")),
+ AttributedText("", null, {
+ 0: const _FakePlaceholder("only"),
+ }),
+ );
+
+ expect(
+ AttributedText("Hello").insertPlaceholder(5, const _FakePlaceholder("only")),
+ AttributedText("Hello", null, {
+ 5: const _FakePlaceholder("only"),
+ }),
+ );
+ });
+ });
+
+ test("remove region >", () {
+ expect(
+ AttributedText(
+ "Hello, World!",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 4, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 5: const _FakePlaceholder("middle"),
+ },
+ ).removeRegion(startOffset: 0, endOffset: 5),
+ AttributedText(
+ ", World!",
+ null,
+ {
+ 0: const _FakePlaceholder("middle"),
+ },
+ ),
+ );
+
+ expect(
+ AttributedText(
+ "Hello, World!",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 5, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 5: const _FakePlaceholder("middle"),
+ },
+ ).removeRegion(startOffset: 3, endOffset: 10),
+ AttributedText(
+ "Helrld!",
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: _bold, offset: 2, markerType: SpanMarkerType.end),
+ ],
+ ),
+ ),
+ );
+ });
+
+ group("collapses spans >", () {
+ test("when placeholders sit beyond plain text", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 11, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.underline, offset: 12, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.underline, offset: 12, markerType: SpanMarkerType.end),
+ ],
+ ),
+ {
+ 11: const _FakePlaceholder("trailing1"),
+ 12: const _FakePlaceholder("trailing2"),
+ },
+ );
+
+ final spans = attributedText.computeAttributionSpans().toList();
+
+ expect(spans.length, 2);
+
+ expect(spans[0].attributions, {ExpectedSpans.bold});
+ expect(spans[0].start, 0);
+ expect(spans[0].end, 11);
+
+ expect(spans[1].attributions, {ExpectedSpans.underline});
+ expect(spans[1].start, 12);
+ expect(spans[1].end, 12);
+ });
+ });
+ });
+}
+
+const _bold = NamedAttribution("bold");
+
+class _FakePlaceholder {
+ const _FakePlaceholder(this.name);
+
+ final String name;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is _FakePlaceholder && runtimeType == other.runtimeType && name == other.name;
+
+ @override
+ int get hashCode => name.hashCode;
+}
diff --git a/attributed_text/test/attributed_text_test.dart b/attributed_text/test/attributed_text_test.dart
index 3fa92637d0..9671ea9933 100644
--- a/attributed_text/test/attributed_text_test.dart
+++ b/attributed_text/test/attributed_text_test.dart
@@ -1,12 +1,13 @@
import 'package:attributed_text/attributed_text.dart';
+import 'package:collection/collection.dart';
import 'package:test/test.dart';
void main() {
group('Attributed Text', () {
test('Bug 145 - insert character at beginning of styled text', () {
final initialText = AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
@@ -20,63 +21,180 @@ void main() {
applyAttributions: {ExpectedSpans.bold},
);
- expect(newText.text, 'aabcdefghij');
+ expect(newText.toPlainText(), 'aabcdefghij');
expect(
- newText.hasAttributionsWithin(attributions: {ExpectedSpans.bold}, range: const SpanRange(start: 0, end: 10)),
+ newText.hasAttributionsWithin(attributions: {ExpectedSpans.bold}, range: const SpanRange(0, 10)),
true,
);
});
+ group('fragments', () {
+ test('can be copied as attributed text with a SpanRange', () {
+ final text = AttributedText(
+ "this that other",
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final slice = text.copyTextInRange(const SpanRange(5, 9));
+ expect(slice.toPlainText(), "that");
+ expect(slice.length, 4);
+ expect(slice.getAttributedRange({ExpectedSpans.bold}, 0), const SpanRange(0, 1));
+ expect(slice.getAttributedRange({ExpectedSpans.italics}, 2), const SpanRange(2, 3));
+ });
+
+ test('can be copied as attributed text with start and end bounds', () {
+ final text = AttributedText(
+ "this that other",
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final slice = text.copyText(5, 9);
+ expect(slice.toPlainText(), "that");
+ expect(slice.length, 4);
+ expect(slice.getAttributedRange({ExpectedSpans.bold}, 0), const SpanRange(0, 1));
+ expect(slice.getAttributedRange({ExpectedSpans.italics}, 2), const SpanRange(2, 3));
+ });
+
+ test('can be copied as plain text with a SpanRange', () {
+ final text = AttributedText(
+ "this that other",
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final substring = text.substringInRange(const SpanRange(5, 9));
+ expect(substring, "that");
+ });
+
+ test('can be copied as plain text with start and end bounds', () {
+ final text = AttributedText(
+ "this that other",
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final substring = text.substring(5, 9);
+ expect(substring, "that");
+ });
+ });
+
group('span manipulation', () {
test('combines overlapping spans when adding from left to right', () {
// Note: span overlaps at the boundary had a bug that was filed in #582.
- final text = AttributedText(text: '01234567');
- text.addAttribution(ExpectedSpans.bold, SpanRange(start: 0, end: 4));
- text.addAttribution(ExpectedSpans.bold, SpanRange(start: 4, end: 8));
+ final text = AttributedText('01234567');
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(0, 4));
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(4, 8));
// Ensure that the spans were merged into a single span.
expect(text.spans.markers.length, 2);
expect(
text.spans.markers.first,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
);
expect(
text.spans.markers.last,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
);
});
test('combines overlapping spans when adding from left to right', () {
- final text = AttributedText(text: '01234567');
- text.addAttribution(ExpectedSpans.bold, SpanRange(start: 4, end: 8));
- text.addAttribution(ExpectedSpans.bold, SpanRange(start: 0, end: 4));
+ final text = AttributedText('01234567');
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(4, 8));
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(0, 4));
// Ensure that the spans were merged into a single span.
expect(text.spans.markers.length, 2);
expect(
text.spans.markers.first,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
);
expect(
text.spans.markers.last,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
);
});
- test('combines back-to-back spans after addition', () {
- final text = AttributedText(text: 'ABCD');
- text.addAttribution(ExpectedSpans.bold, const SpanRange(start: 0, end: 1));
- text.addAttribution(ExpectedSpans.bold, const SpanRange(start: 2, end: 3));
+ test('automatically combines back-to-back spans after addition', () {
+ final text = AttributedText('ABCD');
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(0, 1));
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(2, 3));
// Ensure that we only have a single span
expect(text.spans.markers.length, 2);
expect(
text.spans.markers.first,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
);
expect(
text.spans.markers.last,
- SpanMarker(attribution: ExpectedSpans.bold, offset: 3, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 3, markerType: SpanMarkerType.end),
+ );
+ });
+
+ test('keeps back-to-back spans separate when requested', () {
+ final text = AttributedText('#john#sally');
+ text.addAttribution(ExpectedSpans.hashTag, const SpanRange(0, 4));
+ text.addAttribution(ExpectedSpans.hashTag, const SpanRange(5, 10), autoMerge: false);
+
+ // Ensure that the hash tag spans were kept separate
+ expect(text.spans.markers.length, 2 * 2);
+
+ final markers = text.spans.markers.toList();
+
+ // #john
+ expect(
+ markers[0],
+ const SpanMarker(attribution: ExpectedSpans.hashTag, offset: 0, markerType: SpanMarkerType.start),
+ );
+ expect(
+ markers[1],
+ const SpanMarker(attribution: ExpectedSpans.hashTag, offset: 4, markerType: SpanMarkerType.end),
+ );
+
+ // #sally
+ expect(
+ markers[2],
+ const SpanMarker(attribution: ExpectedSpans.hashTag, offset: 5, markerType: SpanMarkerType.start),
+ );
+ expect(
+ markers[3],
+ const SpanMarker(attribution: ExpectedSpans.hashTag, offset: 10, markerType: SpanMarkerType.end),
+ );
+ });
+
+ test('throws exception when compatible attributions overlap but auto-merge is false', () {
+ final text = AttributedText('#john#sally');
+ text.addAttribution(ExpectedSpans.hashTag, const SpanRange(0, 4));
+
+ expect(
+ () => text.addAttribution(ExpectedSpans.hashTag, const SpanRange(0, 10), autoMerge: false),
+ throwsA(isA()),
);
});
});
@@ -84,12 +202,12 @@ void main() {
test('notifies listeners when style changes', () {
bool listenerCalled = false;
- final text = AttributedText(text: 'abcdefghij');
+ final text = AttributedText('abcdefghij');
text.addListener(() {
listenerCalled = true;
});
- text.addAttribution(ExpectedSpans.bold, const SpanRange(start: 1, end: 1));
+ text.addAttribution(ExpectedSpans.bold, const SpanRange(1, 1));
expect(listenerCalled, isTrue);
});
@@ -98,8 +216,8 @@ void main() {
test("equivalent AttributedText are equal", () {
expect(
AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
@@ -110,8 +228,8 @@ void main() {
),
equals(
AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
@@ -127,8 +245,8 @@ void main() {
test("different text are not equal", () {
expect(
AttributedText(
- text: 'jihgfedcba',
- spans: AttributedSpans(
+ 'jihgfedcba',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
@@ -138,8 +256,8 @@ void main() {
),
) ==
AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
@@ -155,8 +273,8 @@ void main() {
test("different spans are not equal", () {
expect(
AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
@@ -166,8 +284,8 @@ void main() {
),
) ==
AttributedText(
- text: 'abcdefghij',
- spans: AttributedSpans(
+ 'abcdefghij',
+ AttributedSpans(
attributions: [
const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
const SpanMarker(attribution: ExpectedSpans.bold, offset: 5, markerType: SpanMarkerType.end),
@@ -180,58 +298,634 @@ void main() {
});
group('attribution queries', () {
+ test('finds all spans for single attribution throughout text', () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 3, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final ranges = attributedText.getAttributionSpans({ExpectedSpans.bold});
+
+ expect(ranges.length, 3);
+ expect(
+ ranges,
+ [
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 2, end: 3),
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 6, end: 7),
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 9, end: 10),
+ ],
+ );
+ });
+
+ test('finds all spans for multiple attributions throughout text', () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 3, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 5, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 9, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final ranges = attributedText.getAttributionSpans({ExpectedSpans.bold, ExpectedSpans.italics});
+
+ expect(ranges.length, 4);
+ expect(
+ ranges,
+ [
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 2, end: 3),
+ const AttributionSpan(attribution: ExpectedSpans.italics, start: 5, end: 7),
+ const AttributionSpan(attribution: ExpectedSpans.bold, start: 6, end: 7),
+ const AttributionSpan(attribution: ExpectedSpans.italics, start: 9, end: 10),
+ ],
+ );
+ });
+
+ test('returns empty list when searching for non-existent attribution spans', () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 3, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final ranges = attributedText.getAttributionSpans({ExpectedSpans.italics});
+
+ expect(ranges.length, 0);
+ });
+
test('finds all bold text around a character', () {
final attributedText = AttributedText(
- text: 'Hello world',
- spans: AttributedSpans(
+ 'Hello world',
+ AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
],
),
);
final range = attributedText.getAttributedRange({ExpectedSpans.bold}, 5);
- expect(range, SpanRange(start: 4, end: 9));
+ expect(range, const SpanRange(4, 9));
});
test('finds all bold and italics text around a character', () {
final attributedText = AttributedText(
- text: 'Hello world',
- spans: AttributedSpans(
+ 'Hello world',
+ AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 9, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 10, markerType: SpanMarkerType.end),
],
),
);
final range = attributedText.getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics}, 5);
- expect(range, SpanRange(start: 4, end: 7));
+ expect(range, const SpanRange(4, 7));
+ });
+
+ test(
+ 'finds all bold, italic and strikethrough text within a word that also includes a span with only bold and italics',
+ () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 1, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 3, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final range = attributedText
+ .getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics, ExpectedSpans.strikethrough}, 2);
+ expect(range, const SpanRange(1, 3));
+ });
+
+ group('getAllAttributionsThroughout', () {
+ test('returns empty list if the range does not have any attributions', () {
+ final attributedText = AttributedText('Text without attributions');
+ expect(attributedText.getAllAttributionsThroughout(const SpanRange(5, 12)), isEmpty);
+ });
+
+ test('returns attributions that apply to the entirety of the range', () {
+ // Create a text with the following attributions:
+ // - bold: applied throught the entire text.
+ // - underline: applied to the word "with",
+ // - italics: applied from the begining of the text until "wi|th".
+ // - strikethrough: applied from "wi|th" until the end of the text.
+
+ final attributedText = AttributedText(
+ 'Text with attributions',
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.underline, offset: 5, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.italics, offset: 6, markerType: SpanMarkerType.end),
+ SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 6, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: ExpectedSpans.underline, offset: 8, markerType: SpanMarkerType.end),
+ SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 21, markerType: SpanMarkerType.end),
+ SpanMarker(attribution: ExpectedSpans.bold, offset: 21, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ expect(
+ attributedText.getAllAttributionsThroughout(const SpanRange(5, 8)),
+ {ExpectedSpans.bold, ExpectedSpans.underline},
+ );
+ });
+ });
+ });
+
+ group("attribution visitation", () {
+ test("visits full-length attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ _AttributionVisit(0, {ExpectedSpans.bold}, {}),
+ _AttributionVisit(10, {}, {ExpectedSpans.bold}),
+ ];
+
+ attributedText.visitAttributions(
+ CallbackAttributionVisitor(
+ visitAttributions: (
+ AttributedText fullText,
+ int index,
+ Set startingAttributions,
+ Set endingAttributions,
+ ) {
+ expect(_AttributionVisit(index, startingAttributions, endingAttributions), expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ ),
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+
+ test("visits partial-length attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ _AttributionVisit(2, {ExpectedSpans.bold}, {}),
+ _AttributionVisit(8, {}, {ExpectedSpans.bold}),
+ ];
+
+ attributedText.visitAttributions(
+ CallbackAttributionVisitor(
+ visitAttributions: (
+ AttributedText fullText,
+ int index,
+ Set startingAttributions,
+ Set endingAttributions,
+ ) {
+ expect(_AttributionVisit(index, startingAttributions, endingAttributions), expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ ),
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+
+ test("visits overlapping attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ _AttributionVisit(0, {ExpectedSpans.bold}, {}),
+ _AttributionVisit(4, {ExpectedSpans.italics}, {}),
+ _AttributionVisit(6, {}, {ExpectedSpans.bold}),
+ _AttributionVisit(10, {}, {ExpectedSpans.italics}),
+ ];
+
+ attributedText.visitAttributions(
+ CallbackAttributionVisitor(
+ visitAttributions: (
+ AttributedText fullText,
+ int index,
+ Set startingAttributions,
+ Set endingAttributions,
+ ) {
+ expect(_AttributionVisit(index, startingAttributions, endingAttributions), expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ ),
+ );
+
+ expect(expectedVisits, isEmpty);
});
- test('finds all bold, italic and strikethrough text within a word that also includes a span with only bold and italics', () {
+ test("visits multiple starting and ending attributions", () {
final attributedText = AttributedText(
- text: 'Hello world',
- spans: AttributedSpans(
+ 'Hello world',
+ AttributedSpans(
attributions: [
- SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.bold, offset: 4, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 0, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.end),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 1, markerType: SpanMarkerType.start),
- SpanMarker(attribution: ExpectedSpans.strikethrough, offset: 3, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 8, markerType: SpanMarkerType.end),
],
),
);
- final range = attributedText.getAttributedRange({ExpectedSpans.bold, ExpectedSpans.italics, ExpectedSpans.strikethrough}, 2);
- expect(range, SpanRange(start: 1, end: 3));
+ final expectedVisits = [
+ _AttributionVisit(2, {ExpectedSpans.bold, ExpectedSpans.italics}, {}),
+ _AttributionVisit(8, {}, {ExpectedSpans.bold, ExpectedSpans.italics}),
+ ];
+
+ attributedText.visitAttributions(
+ CallbackAttributionVisitor(
+ visitAttributions: (
+ AttributedText fullText,
+ int index,
+ Set startingAttributions,
+ Set endingAttributions,
+ ) {
+ expect(_AttributionVisit(index, startingAttributions, endingAttributions), expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ ),
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+ });
+
+ group("attribution span visitation", () {
+ test("visits full-length attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ MultiAttributionSpan(attributions: {ExpectedSpans.bold}, start: 0, end: 10),
+ ];
+
+ attributedText.visitAttributionSpans(
+ (span) {
+ expect(span, expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+
+ test("visits partial-length attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ const MultiAttributionSpan(attributions: {}, start: 0, end: 1),
+ MultiAttributionSpan(attributions: {ExpectedSpans.bold}, start: 2, end: 8),
+ const MultiAttributionSpan(attributions: {}, start: 9, end: 10),
+ ];
+
+ attributedText.visitAttributionSpans(
+ (span) {
+ expect(span, expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+
+ test("visits overlapping attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 4, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 10, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ MultiAttributionSpan(attributions: {ExpectedSpans.bold}, start: 0, end: 3),
+ MultiAttributionSpan(attributions: {ExpectedSpans.bold, ExpectedSpans.italics}, start: 4, end: 6),
+ MultiAttributionSpan(attributions: {ExpectedSpans.italics}, start: 7, end: 10),
+ ];
+
+ attributedText.visitAttributionSpans(
+ (span) {
+ expect(span, expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+
+ test("visits multiple starting and ending attributions", () {
+ final attributedText = AttributedText(
+ 'Hello world',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.bold, offset: 8, markerType: SpanMarkerType.end),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 2, markerType: SpanMarkerType.start),
+ const SpanMarker(attribution: ExpectedSpans.italics, offset: 8, markerType: SpanMarkerType.end),
+ ],
+ ),
+ );
+
+ final expectedVisits = [
+ const MultiAttributionSpan(attributions: {}, start: 0, end: 1),
+ MultiAttributionSpan(attributions: {ExpectedSpans.bold, ExpectedSpans.italics}, start: 2, end: 8),
+ const MultiAttributionSpan(attributions: {}, start: 9, end: 10),
+ ];
+
+ attributedText.visitAttributionSpans(
+ (span) {
+ expect(span, expectedVisits.first);
+ expectedVisits.removeAt(0);
+ },
+ );
+
+ expect(expectedVisits, isEmpty);
+ });
+ });
+
+ group('collapseSpans', () {
+ const boldAttribution = NamedAttribution('bold');
+
+ test('returns a single span for text without attributions', () {
+ final text = AttributedText('Hello World');
+
+ final spans = text.computeAttributionSpans().toList();
+
+ // Ensure a single span containing the whole text was returned.
+ expect(spans.length, 1);
+ expect(spans[0].attributions, isEmpty);
+ expect(spans[0].start, 0);
+ expect(spans[0].end, text.length - 1);
+ });
+
+ test('returns a single span for text with an attribution containing the whole text', () {
+ final text = AttributedText(
+ 'Hello World',
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.start,
+ offset: 0,
+ ),
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.end,
+ offset: 10,
+ ),
+ ],
+ ),
+ );
+
+ final spans = text.computeAttributionSpans().toList();
+
+ // Ensure a single span containing the whole text was returned.
+ expect(spans.length, 1);
+ expect(spans[0].attributions, isNotEmpty);
+ expect(spans[0].start, 0);
+ expect(spans[0].end, text.length - 1);
+ });
+
+ test('returns two spans for text with an attribution from the beginning until half of the text', () {
+ // Create a text with a bold attribution in "Hello ".
+ final text = AttributedText(
+ 'Hello World',
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.start,
+ offset: 0,
+ ),
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.end,
+ offset: 5,
+ ),
+ ],
+ ),
+ );
+
+ final spans = text.computeAttributionSpans().toList();
+
+ // Ensure two spans were returned.
+ // The first containing the attribution and the second without any attributions.
+ expect(spans.length, 2);
+ expect(spans[0].attributions, isNotEmpty);
+ expect(spans[0].start, 0);
+ expect(spans[0].end, 5);
+ expect(spans[1].attributions, isEmpty);
+ expect(spans[1].start, 6);
+ expect(spans[1].end, text.length - 1);
+ });
+
+ test('handles markers which end after the end of the text', () {
+ // Create a text with a bold attribution in "World".
+ // The marker end offset is bigger than the last character index (the text lenght is 11).
+ final text = AttributedText(
+ 'Hello World',
+ AttributedSpans(
+ attributions: const [
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.start,
+ offset: 6,
+ ),
+ SpanMarker(
+ attribution: boldAttribution,
+ markerType: SpanMarkerType.end,
+ offset: 11,
+ ),
+ ],
+ ),
+ );
+
+ final spans = text.computeAttributionSpans().toList();
+
+ // Ensure two spans were returned. The first containing no attributions and
+ // the second containing the attribution.
+ expect(spans.length, 2);
+ expect(spans[0].attributions, isEmpty);
+ expect(spans[0].start, 0);
+ expect(spans[0].end, 5);
+ expect(spans[1].attributions, isNotEmpty);
+ expect(spans[1].start, 6);
+ expect(spans[1].end, 10);
+ });
+ });
+
+ group("copy >", () {
+ test("copies an AttributedText without any attributions", () {
+ final attributedText = AttributedText(
+ 'Sample Text',
+ );
+
+ expect(attributedText.copy(), AttributedText('Sample Text'));
+ });
+
+ test("copies an AttributedText with attributions", () {
+ final attributedText = AttributedText(
+ 'abcdefghij',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(
+ attribution: ExpectedSpans.bold,
+ offset: 2,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.italics,
+ offset: 4,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.bold,
+ offset: 5,
+ markerType: SpanMarkerType.end,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.italics,
+ offset: 7,
+ markerType: SpanMarkerType.end,
+ ),
+ ],
+ ),
+ );
+
+ expect(
+ attributedText.copy(),
+ AttributedText(
+ 'abcdefghij',
+ AttributedSpans(
+ attributions: [
+ const SpanMarker(
+ attribution: ExpectedSpans.bold,
+ offset: 2,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.italics,
+ offset: 4,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.bold,
+ offset: 5,
+ markerType: SpanMarkerType.end,
+ ),
+ const SpanMarker(
+ attribution: ExpectedSpans.italics,
+ offset: 7,
+ markerType: SpanMarkerType.end,
+ ),
+ ],
+ ),
+ ),
+ );
});
});
});
}
+
+class _AttributionVisit {
+ _AttributionVisit(
+ this.index,
+ this.startingAttributions,
+ this.endingAttributions,
+ );
+
+ final int index;
+ final Set startingAttributions;
+ final Set endingAttributions;
+
+ @override
+ String toString() =>
+ "[_AttributionVisit] - index: $index, starting: $startingAttributions, ending: $endingAttributions";
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is _AttributionVisit &&
+ runtimeType == other.runtimeType &&
+ index == other.index &&
+ const DeepCollectionEquality().equals(startingAttributions, other.startingAttributions) &&
+ const DeepCollectionEquality().equals(endingAttributions, other.endingAttributions);
+
+ @override
+ int get hashCode => index.hashCode ^ startingAttributions.hashCode ^ endingAttributions.hashCode;
+}
diff --git a/doc/website/bin/super_editor_docs.dart b/doc/website/bin/super_editor_docs.dart
new file mode 100644
index 0000000000..0a6c63d500
--- /dev/null
+++ b/doc/website/bin/super_editor_docs.dart
@@ -0,0 +1,36 @@
+import 'dart:io';
+
+import 'package:static_shock/static_shock.dart';
+
+Future main(List arguments) async {
+ // Configure the static website generator.
+ final staticShock = StaticShock()
+ // Here, you can directly hook into the StaticShock pipeline. For example,
+ // you can copy an "images" directory from the source set to build set:
+ ..pick(DirectoryPicker.parse("images"))
+ // All 3rd party behavior is added through plugins, even the behavior
+ // shipped with Static Shock.
+ ..plugin(const MarkdownPlugin())
+ ..plugin(const JinjaPlugin())
+ ..plugin(const PrettyUrlsPlugin())
+ ..plugin(const RedirectsPlugin())
+ ..plugin(const SassPlugin())
+ ..plugin(DraftingPlugin(
+ showDrafts: arguments.contains("preview"),
+ ))
+ ..plugin(const PubPackagePlugin({
+ "super_editor",
+ "super_editor_markdown",
+ "super_editor_quill",
+ "super_text_layout",
+ "attributed_text",
+ }))
+ ..plugin(
+ GitHubContributorsPlugin(
+ authToken: Platform.environment["github_doc_website_token"],
+ ),
+ );
+
+ // Generate the static website.
+ await staticShock.generateSite();
+}
diff --git a/doc/website/pubspec.lock b/doc/website/pubspec.lock
new file mode 100644
index 0000000000..3001d6576e
--- /dev/null
+++ b/doc/website/pubspec.lock
@@ -0,0 +1,715 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+ url: "https://pub.dev"
+ source: hosted
+ version: "73.0.0"
+ _macros:
+ dependency: transitive
+ description: dart
+ source: sdk
+ version: "0.3.2"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.8.0"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.6.1"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ cli_pkg:
+ dependency: transitive
+ description:
+ name: cli_pkg
+ sha256: f812467b5d6a5f26ad0fba5dcfc95133df02edbae47dfa4ade3df5d2b5afdcf2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.10.0"
+ cli_repl:
+ dependency: transitive
+ description:
+ name: cli_repl
+ sha256: a2ee06d98f211cb960c777519cb3d14e882acd90fe5e078668e3ab4baab0ddd4
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.3"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ coverage:
+ dependency: transitive
+ description:
+ name: coverage
+ sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.2"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ dart_rss:
+ dependency: transitive
+ description:
+ name: dart_rss
+ sha256: "73539d4b7153b47beef8b51763ca55dcb6fc0bb412b29e0f5e74e93fabfd1ac6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ fbh_front_matter:
+ dependency: transitive
+ description:
+ name: fbh_front_matter
+ sha256: "18b2f355326ff2b7ebd64eb1d969c091895ffa755ab55ede5de68f59cf115024"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.1"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ github:
+ dependency: transitive
+ description:
+ name: github
+ sha256: "57f6ad78591f9638e903409977443093f862d25062a6b582a3c89e4ae44e4814"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.24.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ grinder:
+ dependency: transitive
+ description:
+ name: grinder
+ sha256: e1996e485d2b56bb164a8585679758d488fbf567273f51c432c8733fee1f6188
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.5"
+ html_unescape:
+ dependency: transitive
+ description:
+ name: html_unescape
+ sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ intl:
+ dependency: transitive
+ description:
+ name: intl
+ sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.19.0"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ jinja:
+ dependency: transitive
+ description:
+ name: jinja
+ sha256: ee379abb8106faec4898d7a8b712f7085474c8d8dea43d06c52def04fbe5aa67
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.0"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
+ lints:
+ dependency: "direct dev"
+ description:
+ name: lints
+ sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ macros:
+ dependency: transitive
+ description:
+ name: macros
+ sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2-main.4"
+ markdown:
+ dependency: transitive
+ description:
+ name: markdown
+ sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.2.2"
+ mason_logger:
+ dependency: transitive
+ description:
+ name: mason_logger
+ sha256: "1fdf5c76870eb6fc3611ed6fbae1973a3794abe581ea5e22e68af2f73c688b93"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.16"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.16+1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.15.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.6"
+ native_stack_traces:
+ dependency: transitive
+ description:
+ name: native_stack_traces
+ sha256: "64d2f4bcf3b69326fb9bc91b4dd3a06f94bb5bbc3a65e25ae6467ace0b34bfd3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.7"
+ native_synchronization:
+ dependency: transitive
+ description:
+ name: native_synchronization
+ sha256: ff200fe0a64d733ff7d4dde2005271c297db81007604c8cd21037959858133ab
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ node_interop:
+ dependency: transitive
+ description:
+ name: node_interop
+ sha256: "3af2420c728173806f4378cf89c53ba9f27f7f67792b898561bff9d390deb98e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ node_preamble:
+ dependency: transitive
+ description:
+ name: node_preamble
+ sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.2"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.1"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.2"
+ protobuf:
+ dependency: transitive
+ description:
+ name: protobuf
+ sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ pub_updater:
+ dependency: transitive
+ description:
+ name: pub_updater
+ sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.0"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ puppeteer:
+ dependency: transitive
+ description:
+ name: puppeteer
+ sha256: a6752d4f09b510ae41911bfd0997f957e723d38facf320dd9ee0e5661108744a
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.13.0"
+ retry:
+ dependency: transitive
+ description:
+ name: retry
+ sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+ sass:
+ dependency: transitive
+ description:
+ name: sass
+ sha256: "0303cb84a5655e72f6e2442f0ae4c8f7068f2c97a3ed60e582cd59c7c37a13b2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.77.8"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.2"
+ shelf_packages_handler:
+ dependency: transitive
+ description:
+ name: shelf_packages_handler
+ sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ shelf_static:
+ dependency: transitive
+ description:
+ name: shelf_static
+ sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ source_map_stack_trace:
+ dependency: transitive
+ description:
+ name: source_map_stack_trace
+ sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ source_maps:
+ dependency: transitive
+ description:
+ name: source_maps
+ sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.12"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.1"
+ static_shock:
+ dependency: "direct main"
+ description:
+ path: "packages/static_shock"
+ ref: HEAD
+ resolved-ref: "0764a11f18b8ad041b475d1b20dcdb5a15719f9c"
+ url: "https://github.com/flutter-bounty-Hunters/static_shock"
+ source: git
+ version: "0.0.12"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test:
+ dependency: "direct dev"
+ description:
+ name: test
+ sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.25.8"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.3"
+ test_core:
+ dependency: transitive
+ description:
+ name: test_core
+ sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.5"
+ test_process:
+ dependency: transitive
+ description:
+ name: test_process
+ sha256: "217f19b538926e4922bdb2a01410100ec4e3beb4cc48eae5ae6b20037b07bbd6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ textwrap:
+ dependency: transitive
+ description:
+ name: textwrap
+ sha256: "780b164d83dfed30b475b1310d288145c5e0109193c3c507cf015d38e4adc844"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.2"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.5"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ web_socket:
+ dependency: transitive
+ description:
+ name: web_socket
+ sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.6"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ webkit_inspection_protocol:
+ dependency: transitive
+ description:
+ name: webkit_inspection_protocol
+ sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.5.4"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+sdks:
+ dart: ">=3.4.0 <4.0.0"
diff --git a/doc/website/source/_data.yaml b/doc/website/source/_data.yaml
new file mode 100644
index 0000000000..6ccacaaa7d
--- /dev/null
+++ b/doc/website/source/_data.yaml
@@ -0,0 +1,27 @@
+# Configuration for the package that this website documents.
+package:
+ name: super_editor
+ title: Super Editor
+ description: A document editing toolkit for Flutter
+ is_on_pub: true
+ github:
+ url: https://github.com/superlistapp/super_editor
+ organization: superlistapp
+ name: super_editor
+ discord: https://discord.gg/8hna2VD32s
+ sponsorship: https://flutterbountyhunters.com
+
+# Configuration of the GitHub plugin for loading info about GitHub repositories.
+github:
+ contributors:
+ repositories:
+ - { organization: superlistapp, name: super_editor }
+
+navigation:
+ items:
+ - title: Super Editor
+ url: /super-editor/guides
+ - title: Super Reader
+ url: /super-reader-guides
+ - title: Super Text Field
+ url: /super-text-field-guides
\ No newline at end of file
diff --git a/doc/website/source/_includes/components/navbar.jinja b/doc/website/source/_includes/components/navbar.jinja
new file mode 100644
index 0000000000..c567291f11
--- /dev/null
+++ b/doc/website/source/_includes/components/navbar.jinja
@@ -0,0 +1,27 @@
+
+
+
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/_data.yaml b/doc/website/source/super-editor/guides/_data.yaml
new file mode 100644
index 0000000000..731923b498
--- /dev/null
+++ b/doc/website/source/super-editor/guides/_data.yaml
@@ -0,0 +1,114 @@
+layout: layouts/docs_page.jinja
+
+base_path: super-editor/guides/
+
+navigation:
+ show_contributors: false
+
+ header: Super Editor
+
+ items:
+ - title: Getting Started
+ items:
+ - title: Quickstart
+ url: super-editor/guides/getting-started/quickstart
+ - title: Tour
+ url: super-editor/guides/getting-started/tour
+
+ - title: Document Editors
+ items:
+ - title: Overview
+ url: super-editor/guides/document-editors/overview
+
+ - title: Chat
+ items:
+ - title: Overview
+ url: super-editor/guides/chat/overview
+ - title: Screen Scaffold
+ url: super-editor/guides/chat/screen-scaffold
+ - title: Keyboard Toolbar
+ url: super-editor/guides/chat/keyboard-toolbar
+
+ - title: AI / GPT
+ items:
+ - title: Overview
+ url: super-editor/guides/ai/overview
+ - title: Fade-In Content
+ url: super-editor/guides/ai/fade-in-content
+
+ - title: Basics
+ items:
+ - title: Build an Editor
+ url: super-editor/guides/basics/build-an-editor
+ - title: Show a Hint
+ url: super-editor/guides/basics/show-a-hint
+
+ - title: Built-In Content
+ items:
+ - title: Paragraphs
+ url: super-editor/guides/built-in-content/paragraphs
+ - title: Headers
+ url: super-editor/guides/built-in-content/headers
+ - title: Blockquotes
+ url: super-editor/guides/built-in-content/blockquotes
+ - title: List Items
+ url: super-editor/guides/built-in-content/list-items
+ - title: Tasks
+ url: super-editor/guides/built-in-content/tasks
+ - title: Images
+ url: super-editor/guides/built-in-content/images
+ - title: Horizontal Rules
+ url: super-editor/guides/built-in-content/horizontal-rules
+
+ - title: Markdown
+ items:
+ - title: Import
+ url: super-editor/guides/markdown/import
+ - title: Export
+ url: super-editor/guides/markdown/export
+ - title: As You Type
+ url: super-editor/guides/markdown/as-you-type
+
+ - title: Quill Deltas
+ items:
+ - title: Import
+ url: super-editor/guides/quill/import
+ - title: Export
+ url: super-editor/guides/quill/export
+ - title: Multi-Player
+ url: super-editor/guides/quill/multi-player
+
+ - title: Styling
+ items:
+ - title: Dark Mode & Light Mode
+ url: super-editor/guides/styling/dark-mode-and-light-mode
+ - title: Style a Document
+ url: super-editor/guides/styling/style-a-document
+ - title: Text Underlines
+ url: super-editor/guides/styling/text-underlines
+
+ - title: Editing UI
+ items:
+ - title: Overlay Controls
+ url: super-editor/guides/editing-ui/overlay-controls
+ - title: Add a Popover Toolbar
+ url: super-editor/guides/editing-ui/add-a-popover-toolbar
+ - title: Add a Mobile Keyboard Toolbar
+ url: super-editor/guides/editing-ui/add-a-mobile-keyboard-toolbar
+
+ - title: The Viewport
+ items:
+ - title: Auto-Scrolling
+ url: super-editor/guides/viewport/auto-scrolling
+ - title: Embed in a Scrollview
+ url: super-editor/guides/viewport/embed-in-a-scrollview
+
+ - title: Custom Content
+ items:
+ - title: Add New Type of Content
+ url: super-editor/guides/custom-content/add-new-type-of-content
+
+ - title: Custom Rules
+ items:
+ - title: Un-Deletable Content
+ url: super-editor/guides/custom-rules/undeletable-content
diff --git a/doc/website/source/super-editor/guides/add-a-mobile-keyboard-toolbar.md b/doc/website/source/super-editor/guides/add-a-mobile-keyboard-toolbar.md
new file mode 100644
index 0000000000..7d2a999610
--- /dev/null
+++ b/doc/website/source/super-editor/guides/add-a-mobile-keyboard-toolbar.md
@@ -0,0 +1,4 @@
+---
+title: Add a Mobile Keyboard Toolbar
+---
+# Add a Mobile Keyboard Toolbar
diff --git a/doc/website/source/super-editor/guides/add-a-popover-toolbar.md b/doc/website/source/super-editor/guides/add-a-popover-toolbar.md
new file mode 100644
index 0000000000..2bf20f85fa
--- /dev/null
+++ b/doc/website/source/super-editor/guides/add-a-popover-toolbar.md
@@ -0,0 +1,276 @@
+---
+title: Add a Popover Toolbar
+---
+
+# Add a Popover Toolbar
+
+To display a Popover Toolbar, it is recomended to use an `OverlayPortal`. To do that, start by wraping `SuperEditor` with an `OverlayPortal` and give it a toolbar builder:
+
+```dart
+class MyApp extends StatefulWidget {
+ State createState() => MyAppState();
+}
+
+class MyAppState extends State {
+ /// Controls the visibility of the toolbar.
+ final _popoverToolbarController = OverlayPortalController();
+
+ @override
+ Widget build(BuildContext context) {
+ return OverlayPortal(
+ controller: _popoverToolbarController,
+ overlayChildBuilder: _buildPopoverToolbar,
+ child: SuperEditor(),
+ );
+ }
+
+ Widget _buildPopoverToolbar() {
+ return const SizedBox();
+ }
+}
+```
+
+# Showing the toolbar
+
+Usually, a Popover Toolbar is displayed when the user selects some content. To do that, listen for selection changes to show or hide the toolbar:
+
+```dart
+class MyAppState extends State {
+ // ...
+ late final MutableDocument _document;
+ late final MutableDocumentComposer _composer;
+ // ...
+
+ @override
+ void initState() {
+ super.initState();
+ _document = MutableDocument.empty();
+
+ _composer = MutableDocumentComposer();
+ _composer.selectionNotifier.addListener(_hideOrShowToolbar);
+ }
+
+ void _hideOrShowToolbar() {
+ final selection = _composer.selection;
+ if (selection == null) {
+ // Nothing is selected. We don't want to show a toolbar in this case.
+ _popoverToolbarController.hide();
+ return;
+ }
+
+ if (selection.isCollapsed) {
+ // We only want to show the toolbar when a span of text
+ // is selected. Therefore, we ignore collapsed selections.
+ _popoverToolbarController.hide();
+ return;
+ }
+
+ // We have an expanded selection. Show the toolbar.
+ _popoverToolbarController.show();
+ }
+
+ // ...
+}
+```
+
+# Aligning the toolbar with the content
+
+By default, no alignment is enforced to the toolbar. To align it with the content, and make it follow the content, it is recomended to use the `follow_the_leader` package.
+
+Start by adding `follow_the_leader` to your dependencies in your `pubspec.yaml`.
+
+```yaml
+dependencies:
+ follow_the_leader: latest_version
+```
+
+Wrap `SuperEditor` with a `KeyedSubtree` widget to delimit the viewport area and assign a `GlobalKey` to it. This is used to prevent the toolbar from going off-screen.
+
+```dart
+class MyAppState extends State {
+ // ...
+ final GlobalKey _viewportKey = GlobalKey();
+ // ...
+
+ @override
+ Widget build(BuildContext context) {
+ return OverlayPortal(
+ // ...
+ child: KeyedSubtree(
+ key: _viewportKey,
+ child: SuperEditor(),
+ ),
+ );
+ }
+}
+```
+
+Create a `SelectionLayerLinks` instance and pass it to the `SuperEditor`. This object holds the links that make it possible to follow the content.
+
+```dart
+class MyAppState extends State {
+ // ...
+ final SelectionLayerLinks _selectionLayerLinks = SelectionLayerLinks();
+ // ...
+
+ @override
+ Widget build(BuildContext context) {
+ return OverlayPortal(
+ // ...
+ child: KeyedSubtree(
+ // ...
+ child: SuperEditor(
+ selectionLayerLinks: _selectionLayerLinks,
+ ),
+ ),
+ );
+ }
+}
+```
+
+Create a `FollowerBoundary` to configure the boundary of the area where the toolbar is allowed to be.
+
+```dart
+class MyAppState extends State {
+ // ...
+ late FollowerBoundary _screenBoundary;
+ // ...
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+
+ // Confine the toolbar to the bounds of the widget attached
+ // to the _viewportKey.
+ _screenBoundary = WidgetFollowerBoundary(
+ boundaryKey: _viewportKey,
+ devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
+ );
+ }
+}
+```
+
+Create a `FollowerAligner` to configure how the toolbar should be aligned with the selected content.
+
+```dart
+class MyAppState extends State {
+ // ...
+ late final FollowerAligner _toolbarAligner;
+ // ...
+
+ @override
+ void initState() {
+ super.initState();
+ // Place the toolbar above the content by default.
+ _toolbarAligner = CupertinoPopoverToolbarAligner(_viewportKey);
+ };
+
+ // ...
+}
+```
+
+Finally, wrap the toolbar with a `Follower` widget.
+
+```dart
+class MyAppState extends State {
+ // ...
+ Widget _buildPopoverToolbar() {
+ return Follower.withAligner(
+ // Make the toolbar follow the expanded selection.
+ link: _selectionLayerLinks.expandedSelectionBoundsLink,
+
+ // Configure how the toolbar is aligned to the content.
+ aligner: _toolbarAligner,
+
+ // Configure the boundary where the toolbar is allowed
+ // to be displayed.
+ boundary: _screenBoundary,
+
+ showWhenUnlinked: false,
+ child: _buildToolbarContent(),
+ );
+ }
+
+ Widget _buildToolbarContent() {
+ return const SizedBox();
+ }
+
+ // ...
+}
+```
+
+# Showing different toolbars depending on the content
+
+To show different toolbar depending on the content, check the type of the selected node and show/hide the appropriate toolbars.
+
+```dart
+class MyAppState extends State {
+ // ...
+ void _hideOrShowToolbar() {
+ // ...
+ if (selection.base.nodeId != selection.extent.nodeId) {
+ // Since we want to show different toolbars depending on the content,
+ // we don't show the toolbar if more than one node is selected.
+ _popoverToolbarController.hide();
+ _imageToolbarController.hide();
+ return;
+ }
+
+ // Grab the selected node to check its type.
+ final selectedNode = _document.getNodeById(selection.extent.nodeId);
+
+ if (selectedNode is ImageNode) {
+ // The selected node is an image. Show the image toolbar and hide
+ // the text toolbar.
+ _popoverToolbarController.hide();
+ _imageToolbarController.show();
+ return;
+ }
+
+ // The currently selected node isn't an image. Hide the image toolbar
+ // if it's visible.
+ _imageToolbarController.hide();
+
+ if (selectedNode is TextNode) {
+ // The selected node is a text node, e.g., a paragraph, a list item,
+ // a task, etc. Show the text toolbar and hide the image toolbar.
+ _imageToolbarController.show();
+ _popoverToolbarController.show();
+ return;
+ }
+
+ // The currently selected node isn't a text node. Hide the text toolbar
+ // if it's visible.
+ _popoverToolbarController.hide();
+ }
+}
+```
+
+# Sharing the editor focus with the toolbar
+
+In order to make it possible for the user to interact with focusable items in the toolbar, while keeping the editor focused (with non-primary focus), it is necessary to share focus between the editor and the popover toolbar.
+
+To do that, create a `FocusNode` for the popover, and setup focus sharing by using a `SuperEditorPopover` widget.
+
+```dart
+class MyAppState extends State {
+ // ...
+ final FocusNode _popoverFocusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _popoverFocusNode.dispose();
+ super.dispose();
+ }
+
+ // ...
+ Widget _buildToolbarContent() {
+ return SuperEditorPopover(
+ popoverFocusNode: _popoverFocusNode,
+ editorFocusNode: _editorFocusNode,
+ // The toolbar content.
+ child: SizedBox(),
+ );
+ }
+}
+```
diff --git a/doc/website/source/super-editor/guides/add-new-type-of-content.md b/doc/website/source/super-editor/guides/add-new-type-of-content.md
new file mode 100644
index 0000000000..4e5bedd56b
--- /dev/null
+++ b/doc/website/source/super-editor/guides/add-new-type-of-content.md
@@ -0,0 +1,18 @@
+---
+title: Add a New Type of Content
+---
+# Add a New Type of Content
+Super Editor ships with a number of standard content types, including paragraphs, images, ordered
+and unordered list items, blockquotes, and horizontal rules. However, you may need to implement
+editor support for other types of content. Adding new content requires implementing a number
+of new behaviors.
+
+## Define a new node type
+
+## Define a new view model, component, and builder
+
+## Add edit requests and commands (optional)
+
+## Add edit reactions (optional)
+
+## Add keyboard handlers (optional)
diff --git a/doc/website/source/super-editor/guides/ai/fade-in-content.md b/doc/website/source/super-editor/guides/ai/fade-in-content.md
new file mode 100644
index 0000000000..2f297ec5d4
--- /dev/null
+++ b/doc/website/source/super-editor/guides/ai/fade-in-content.md
@@ -0,0 +1,141 @@
+---
+title: Fade-In GPT Content
+---
+ChatGPT-style large language models (LLM) produces text and content in snippets.
+The answer to a single question might includes 50 pieces of information that are
+generated by the LLM. The chat-style experience adds each new piece to the existing
+answer, making it appear that the AI is "writing" the answer in real-time.
+
+It's common in these experiences to fade-in the content as its generated.
+Super Editor includes a couple of tools that can be used to achieve this
+effect, when feeding LLM content to a `SuperEditor` or `SuperReader`.
+
+## How to Fade-In Content
+The following example demonstrates how to fade content into a `SuperReader`.
+The `SuperReader` widget is typically used for LLMs because LLM output isn't
+typically meant to be editable.
+
+First, create a `SuperReader` with a `Document` that will hold the LLM output.
+Include a `FadeInStyler`, which will control the opacity of new content.
+
+```dart
+class MyState extends State {
+ late final MutableDocument _document;
+ late final MutableDocumentComposer _composer;
+ late final Editor _editor;
+ late final FadeInStyler _fadeInStylePhase;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _document = MutableDocument.empty();
+ _composer = MutableDocumentComposer(
+ initialSelection: DocumentSelection.collapsed(
+ position: DocumentPosition(
+ nodeId: _document.first.id,
+ nodePosition: TextNodePosition(offset: 0),
+ ),
+ ),
+ );
+ _editor = createDefaultDocumentEditor(document: _document, composer: _composer);
+ _fadeInStylePhase = FadeInStyler(this);
+ }
+
+ @override
+ void dispose() {
+ _fadeInStylePhase.dispose();
+ _composer.dispose();
+ _editor.dispose();
+ _document.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SuperReader(
+ editor: _editor,
+ customStylePhases: [
+ _fadeInStylePhase,
+ ],
+ );
+ }
+}
+```
+
+With a `SuperReader` in place, take the output from an LLM and feed it into the
+`Editor` however you'd like. The way you feed content into the `Editor` depends on
+the format of the output from the LLM. You can feed the LLM text into the `Editor`
+as plain text, or you can parse the LLM text into styled `AttributedText` and
+feed that to the `Editor`, or you can also parse out entire blocks like images and
+horizontal rules from the LLM and insert those as nodes in the `Editor`.
+
+The most important part about feeding content from the LLM to the `Editor` is to
+include the current timestamp when inserted. This timestamp is used by the `FadeInStyler`
+to choose the opacity for the content.
+
+```dart
+// Insert plain text.
+_editor.execute([
+ InsertPlainTextAtEndOfDocumentRequest(
+ "...this is a snippet from the LLM...",
+ createdAt: DateTime.now(),
+ ),
+]);
+
+// Insert styled text.
+_editor.execute([
+ InsertStyledTextAtEndOfDocumentRequest(
+ AttributedText(
+ "...this is a bold snippet...",
+ AttributedSpans(attributions: [
+ const SpanMarker(
+ attribution: boldAttribution,
+ offset: 0,
+ markerType: SpanMarkerType.start,
+ ),
+ const SpanMarker(
+ attribution: boldAttribution,
+ offset: 27,
+ markerType: SpanMarkerType.end,
+ ),
+ ]),
+ ),
+ createdAt: DateTime.now(),
+ ),
+]);
+
+// Insert a block node.
+_editor.execute([
+ InsertNodeAtEndOfDocumentRequest(
+ ImageNode(
+ id: Editor.createNodeId(),
+ imageUrl: "https://somewhere.com/image.png",
+ metadata: {
+ NodeMetadata.createdAt: DateTime.now(),
+ },
+ ),
+ ),
+]);
+```
+
+With a `FadeInStyler` plugged into `SuperReader`, and content insertions that
+include a `CreatedAtAttribution`, the inserted content will fade-in within the
+`SuperReader` widget.
+
+## How to Control the Fade-In
+When using the `FadeInStyler` to fade-in content, the fade-in effect is configurable.
+
+```dart
+// Customize fade-in times and curves.
+final styler = FadeInStyler(
+ tickerProvider,
+ blockNodeFadeInDuration: const Duration(milliseconds: 1500),
+ textSnippetFadeInDuration: const Duration(milliseconds: 250),
+ fadeCurve: Curves.easeInOut,
+);
+```
+
+The `FadeInStyler` supports different timing for text snippets vs blocks, like images.
+The fade animation curve is also configurable.
diff --git a/doc/website/source/super-editor/guides/ai/overview.md b/doc/website/source/super-editor/guides/ai/overview.md
new file mode 100644
index 0000000000..45d92492db
--- /dev/null
+++ b/doc/website/source/super-editor/guides/ai/overview.md
@@ -0,0 +1,12 @@
+---
+title: AI / GPT Overview
+---
+ChatGPT-style experiences are all the rage right now. These experiences include
+a large language model (LLM), which generates text based on a query, and a UI
+that presents this text in a chat-style format.
+
+Super Editor doesn't include any LLM-specific functionality, but Super Editor
+does offer some tools to help construct experiences that are similar to
+ChatGPT, Gemini, etc.
+
+ * [Fade-in content as the LLM generates it](/super-editor/guides/ai/fade-in-content)
diff --git a/doc/website/source/super-editor/guides/assemble-a-document.md b/doc/website/source/super-editor/guides/assemble-a-document.md
new file mode 100644
index 0000000000..9c69024ffd
--- /dev/null
+++ b/doc/website/source/super-editor/guides/assemble-a-document.md
@@ -0,0 +1,56 @@
+---
+title: Assemble a Document
+---
+# Assemble a Document
+In Super Editor, a document is typically represented by an instance of `MutableDocument`. The
+easiest way to assemble a `MutableDocument` is by [de-serializing Markdown](/guides/document-from-markdown).
+However, there are situations where you might need to construct a `MutableDocument` directly. This
+guide shows you how.
+
+## What is a Document?
+In Super Editor, a `Document` is a series of nodes - specifically `DocumentNode`s. Different types of
+`DocumentNode`s are available for different types of content, such as `ParagraphNode` and `ImageNode`.
+
+Assembling a `Document` means assembling a list of desired `DocumentNode`s.
+
+## Construct a `MutableDocument` with Content
+The `MutableDocument` constructor accepts a list of `DocumentNode`s as initial content.
+
+```dart
+final document = MutableDocument(
+ nodes: [
+ ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText(text: "Hello, world!"),
+ ),
+ ],
+);
+```
+
+## Alter an existing `MutableDocument`
+A `MutableDocument` can be altered after construction.
+
+You can insert nodes.
+
+```dart
+document.insertNodeAt(1, ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText(text: "New paragraph"),
+),);
+```
+
+You can move nodes.
+
+```dart
+document.moveNode(nodeId: "node1", targetIndex: 2);
+```
+
+You can remove nodes.
+
+```dart
+document.deleteNodeAt(2);
+```
+
+If your goal is to use a `MutableDocument` in an editor experience, consider wrapping the
+`MutableDocument` in an `Editor`, and then use the standard edit pipeline to alter the document's
+content.
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/dark-mode-and-light-mode.md b/doc/website/source/super-editor/guides/dark-mode-and-light-mode.md
new file mode 100644
index 0000000000..4674f11d5e
--- /dev/null
+++ b/doc/website/source/super-editor/guides/dark-mode-and-light-mode.md
@@ -0,0 +1,79 @@
+---
+title: Dark Mode & Light Mode
+---
+# Dark Mode & Light Mode
+In Super Editor, there's no explicit concept of light mode and dark mode. Instead, you can implement
+your own light mode and dark mode by switching out stylesheets.
+
+Build a `SuperEditor` widget with a configurable stylesheet.
+
+```dart
+class _MyAppState extends State {
+ Stylesheet _stylesheet = _lightStylesheet;
+
+ Widget build(BuildContext context) {
+ return SuperEditor(
+ stylesheet: _stylesheet,
+ // ...other properties
+ );
+ }
+}
+```
+
+Define the stylesheet you want to use for light mode, and another stylesheet for dark mode.
+
+```dart
+// Super Editor comes with a standard stylesheet. You
+// can copy that stylesheet and adjust for your light
+// mode stylesheet.
+final _lightStylesheet = defaultStylesheet.copyWith();
+
+// For dark mode, you can define an entirely new stylesheet,
+// or you can copy and adjust your light mode stylesheet.
+//
+// Note the background color of your editor is controlled
+// by your widget tree outside of SuperEditor, which is
+// why this stylesheet doesn't include a background color.
+final _darkStylesheet = _lightStylesheet.copyWith(
+ addRulesAfter: [
+ // Make all text a very light gray.
+ StyleRule(
+ BlockSelector.all, (doc, docNode) {
+ return {
+ "textStyle": const TextStyle(
+ color: Color(0xFFCCCCCC),
+ ),
+ };
+ },
+ ),
+ // Make the headers a medium gray.
+ StyleRule(
+ const BlockSelector("header1"), (doc, docNode) {
+ return {
+ "textStyle": const TextStyle(
+ color: Color(0xFF888888),
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header2"), (doc, docNode) {
+ return {
+ "textStyle": const TextStyle(
+ color: Color(0xFF888888),
+ ),
+ };
+ },
+ ),
+ ],
+);
+```
+
+When you're ready to switch brightness modes, rebuild your `SuperEditor` widget with the other
+stylesheet.
+
+```dart
+setState(() {
+ _stylesheet = _darkStylesheet;
+});
+```
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/document-from-markdown.md b/doc/website/source/super-editor/guides/document-from-markdown.md
new file mode 100644
index 0000000000..5cfa0073b7
--- /dev/null
+++ b/doc/website/source/super-editor/guides/document-from-markdown.md
@@ -0,0 +1,30 @@
+---
+title: Document from Markdown
+---
+# Document from Markdown
+Super Editor supports conversion to and from Markdown.
+
+## Add a dependency on super_editor_markdown
+Markdown support is maintained in a separate package. To get started, add `super_editor_markdown`
+to your `pubspec.yaml`.
+
+```yaml
+dependencies:
+ super_editor_markdown:
+```
+
+## De-serialize Markdown
+De-serialize a Markdown `String` with the supplied top-level function.
+
+```dart
+final document = deserializeMarkdownToDocument(markdown);
+```
+
+The de-serialized document is a `MutableDocument`. Check other guides to find out how to use it.
+
+## Serialize Markdown
+Serialize a `Document` to a Markdown `String`.
+
+```dart
+final markdown = serializeDocumentToMarkdown(document);
+```
diff --git a/doc/website/source/super-editor/guides/index.md b/doc/website/source/super-editor/guides/index.md
new file mode 100644
index 0000000000..0162c487be
--- /dev/null
+++ b/doc/website/source/super-editor/guides/index.md
@@ -0,0 +1,11 @@
+---
+title: Super Editor
+---
+Welcome to Super Editor, a document editor for Flutter apps.
+
+Our mission with Super Editor is to make it possible to build any and all document
+editors with Flutter. Whether you're building a productivity app, journaling app, note
+taking app, or any other app that requires rich text layout and editing, Super Editor
+is here to help!
+
+Get started with Super Editor with our [Quickstart](/super-editor-guides/quickstart) guide.
diff --git a/doc/website/source/super-editor/guides/markdown/parsing.md b/doc/website/source/super-editor/guides/markdown/parsing.md
new file mode 100644
index 0000000000..7705963ae4
--- /dev/null
+++ b/doc/website/source/super-editor/guides/markdown/parsing.md
@@ -0,0 +1,113 @@
+---
+title: Parsing Markdown Documents
+contentRenderers:
+ - jinja
+ - markdown
+---
+Super Editor supports parsing of Markdown documents into Super Editor documents.
+
+To get started with parsing Markdown documents, add the Super Editor Markdown package:
+
+```yaml
+dependencies:
+ super_editor: any
+ super_editor_markdown: ^{{ pub.super_editor_markdown.version }}
+```
+
+Parse a Markdown document by calling the provided global function:
+
+```dart
+final superEditorDocument = deserializeMarkdownToDocument(markdownText);
+```
+
+## Custom Parsing
+Super Editor Markdown offers a few options to customize Markdown parsing.
+
+### Pre-Configured Syntax
+Super Editor Markdown has a concept called `MarkdownSyntax`, which represents an entire
+set of syntax preferences. The `deserializeMarkdownToDocument()` function accepts a
+`MarkdownSyntax`.
+
+There are only two options:
+ * `MarkdownSyntax.superEditor`: Standard Markdown syntax, along with a number of custom
+ Markdown syntaxes, including paragraph alignment with notation like `:---` for left alignment
+ and `:---:` for center alignment.
+ * `MarkdownSyntax.normal`: Standard Markdown syntax as defined by the `markdown` package.
+
+By default, the `MarkdownSyntax.superEditor` option is used. To restrict parsing to the normal
+syntax, pass that option into `deserializeMarkdownToDocument()`.
+
+```dart
+final superEditorDocument = deserializeMarkdownToDocument(
+ markdownText,
+ syntax: MarkdownSyntax.normal,
+);
+```
+
+### Encode HTML or Leave Alone
+When parsing Markdown text, HTML-based symbols can be converted to HTML escape codes,
+or not.
+
+For example, when encoding HTML characters, given the following Markdown:
+
+```markdown
+Flutter & Dart are > Android
+```
+
+The parsed output would become:
+
+```
+Flutter & Dart are gt; Android
+```
+
+Using HTML escape codes ensures that non-HTML text isn't treated as HTML.
+
+By default `deserializeMarkdownToDocument()` doesn't make these conversions.
+
+To automatically convert characters to HTML escape codes, pass `true` for `encodeHtml`.
+
+```dart
+final superEditorDocument = deserializeMarkdownToDocument(
+ markdownText,
+ encodeHtml: true,
+);
+```
+
+### Custom Markdown Blocks.
+Markdown is sometimes extended with custom block syntaxes. These are non-standard syntaxes,
+and they're not understood by standard parsers, like the `markdown` package parser. However,
+the `markdown` package parser accepts `BlockSyntax` objects to parse custom Markdown blocks,
+and Super Editor Markdown forwards those `BlockSyntax`s.
+
+To parse custom Markdown block syntaxes, pass your `BlockSyntax`s to
+`deserializeMarkdownToDocument()`:
+
+```dart
+final superEditorDocument = deserializeMarkdownToDocument(
+ markdownText,
+ customBockSyntax: [
+ const TableSyntax(),
+ ],
+);
+```
+
+### Custom Super Editor Nodes
+When parsing custom Markdown syntaxes, you'll need to tell Super Editor Markdown how to
+convert those syntaxes into Super Editor `DocumentNode`s. Also, sometimes you might want
+to deserialize a standard Markdown syntax into a Super Editor configuration that's different
+from how Super Editor handles that syntax by default.
+
+To customize how Markdown converts into Super Editor documents, provide custom
+`ElementToNodeConverter`s to `deserializeMarkdownToDocument()`.
+
+```dart
+final superEditorDocument = deserializeMarkdownToDocument(
+ markdownText,
+ customBockSyntax: [
+ const TableSyntax(),
+ ],
+ customElementToNodeConverters: [
+ const MarkdownTableToNodeConverter(),
+ ],
+);
+```
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/markdown/serializing.md b/doc/website/source/super-editor/guides/markdown/serializing.md
new file mode 100644
index 0000000000..7df42a583b
--- /dev/null
+++ b/doc/website/source/super-editor/guides/markdown/serializing.md
@@ -0,0 +1,65 @@
+---
+title: Serializing Super Editor Document to Markdown
+contentRenderers:
+ - jinja
+ - markdown
+---
+Super Editor supports serializing Super Editor documents to Markdown documents.
+
+To get started with serializing Markdown documents, add the Super Editor Markdown package:
+
+```yaml
+dependencies:
+ super_editor: any
+ super_editor_markdown: ^{{ pub.super_editor_markdown.version }}
+```
+
+Serialize a Super Editor document to Markdown document by calling the provided global function:
+
+```dart
+final markdown = serializeDocumentToMarkdown(superEditorDocument);
+```
+
+## Custom Serialization
+Sometimes an app uses non-standard Markdown. Super Editor Markdown provides customization
+control to generate that non-standard Markdown.
+
+### Pre-Configured Syntax
+Super Editor Markdown has a concept called `MarkdownSyntax`, which represents an entire
+set of syntax preferences. The `serializeDocumentToMarkdown()` function accepts a
+`MarkdownSyntax`.
+
+There are only two options:
+* `MarkdownSyntax.superEditor`: Standard Markdown syntax, along with a number of custom
+ Markdown syntaxes, including paragraph alignment with notation like `:---` for left alignment
+ and `:---:` for center alignment.
+* `MarkdownSyntax.normal`: Standard Markdown syntax as defined by the `markdown` package.
+
+By default, the `MarkdownSyntax.superEditor` option is used. To restrict serializing to the normal
+syntax, pass `MarkdownSyntax.normal` into `serializeDocumentToMarkdown()`.
+
+```dart
+final markdown = serializeDocumentToMarkdown(
+ superEditorDocument,
+ syntax: MarkdownSyntax.normal,
+);
+```
+
+### Custom Super Editor Node Converters
+Super Editor documents are serialized to Markdown by converting every `DocumentNode` in the
+Super Editor document into a block-level Markdown syntax. You might want to convert these nodes
+to Markdown in a different way, or you might have your own custom `DocumentNode`s, which require
+explicit instructions from you about how to turn them into Markdown blocks.
+
+To control how various `DocumentNode`s serialize to Markdown blocks, provide
+`customNodeSerializers` of type `DocumentNodeMarkdownSerializer` to
+`serializeDocumentToMarkdown`:
+
+```dart
+final markdown = serializeDocumentToMarkdown(
+ superEditorDocument,
+ syntax: [
+ const TableNodeToMarkdownConverter(),
+ ],
+);
+```
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/quickstart.md b/doc/website/source/super-editor/guides/quickstart.md
new file mode 100644
index 0000000000..d406f8e5ba
--- /dev/null
+++ b/doc/website/source/super-editor/guides/quickstart.md
@@ -0,0 +1,69 @@
+---
+title: Super Editor Quickstart
+contentRenderers: ["jinja", "markdown"]
+---
+# Super Editor Quickstart
+Super Editor comes with sane defaults to help you get started with an editor experience, quickly. These defaults include support for images, list items, blockquotes, and horizontal rules, as well as selection gestures, and various keyboard shortcuts.
+
+Drop in the default editor and start editing.
+
+## Add super_editor to your project
+To use super_editor, add a dependency in your pubspec.yaml.
+
+```yaml
+dependencies:
+ super_editor: {{ pub.super_editor.version }}
+```
+
+## Display an editor
+A visual editor first requires a logical editor. A logical editor holds an underlying document, which the user edits, and a composer to manage the user's selection.
+
+Initialize the logical editor.
+
+```dart
+class MyApp extends StatefulWidget {
+ State createState() => _MyApp();
+}
+
+class _MyApp extends State {
+ late final Editor _editor;
+ late final MutableDocument _document;
+ late final MutableDocumentComposer _composer;
+
+ void initState() {
+ super.initState();
+
+ _document = MutableDocument.empty();
+
+ _composer = MutableDocumentComposer();
+
+ _editor = Editor();
+ }
+
+ void dispose() {
+ _editor.dispose();
+ _composer.dispose();
+ _document.dispose();
+
+ super.dispose();
+ }
+}
+```
+
+With the logical pieces ready, you can now display a visual editor. Build a SuperEditor widget and return it from your build() method.
+
+```dart
+class _MyApp extends State {
+ // ...
+
+ Widget build(BuildContext context) {
+ return SuperEditor(
+ editor: _editor,
+ );
+ }
+}
+```
+
+That's all it takes to get started with your very own editor. Run your app, tap in the editor, and start typing!
+
+The next step is configuration. Check out the other guides for more help.
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/quill/parsing.md b/doc/website/source/super-editor/guides/quill/parsing.md
new file mode 100644
index 0000000000..a828f211d6
--- /dev/null
+++ b/doc/website/source/super-editor/guides/quill/parsing.md
@@ -0,0 +1,190 @@
+---
+title: Parsing Quill Delta Documents
+contentRenderers:
+ - jinja
+ - markdown
+---
+Super Editor supports parsing of Quill Delta documents into Super Editor documents.
+
+To get started with parsing Quill documents, add the Super Editor Quill package:
+
+```yaml
+dependencies:
+ super_editor: any
+ super_editor_quill: ^{{ pub.super_editor_quill.version }}
+```
+
+A Quill Delta document is a JSON object, which contains a list of `ops`. You can parse
+a full document, or you can parse just a list of `ops`. Global functions are provided
+for both cases.
+
+Parse a complete Quill Delta document with `parseQuillDeltaDocument()`:
+
+```dart
+final quillDocument = getMyQuillDocument();
+
+final superEditorDocument = parseQuillDeltaDocument(quillDocument);
+```
+
+Parse a list of Quill Delta operations with `parseQuillDeltaOps()`:
+
+```dart
+final quillOps = getMyQuillOps();
+
+final superEditorDocument = parseQuillDeltaOps(quillOps);
+```
+
+Both parsing methods produce a `MutableDocument`, which you can then use within
+a logical `Editor`, and then within a visual `SuperEditor` widget.
+
+## Custom Parsing
+Super Editor Quill supports multiple customizations for document parsing.
+
+### Custom Blocks, Attributes, and Embeds.
+It's common among Quill-based editors to use custom inline attributes, custom
+inline embeds, and custom block formats. Super Editor Quill supports custom
+parsing behaviors within each of the global parsing methods.
+
+Add support for your custom blocks, attributes, and embeds by passing those
+artifacts to the parser methods:
+
+```dart
+final superEditorDocument = parseQuillDeltaDocument(
+ quillDocument,
+ blockFormats: {
+ const MyVideoBlockFormat(),
+ ...defaultBlockFormats
+ },
+ inlineFormats: {
+ const MyUserTagFormat(),
+ ...defaultInlineFormats
+ },
+ inlineEmbedFormats: [
+ const MyMathFormulaFormat(),
+ ],
+);
+```
+
+#### Custom Blocks
+A "block" is a standalone unit within a document, e.g., a paragraph, image, video, etc.
+
+To parse a custom block, implement `BlockDeltaFormat`.
+
+The following example implements a hypothetical video block:
+
+```dart
+class VideoBlockFormat implements BlockDeltaFormat {
+ const VideoBlockFormat();
+
+ @override
+ List? applyTo(Operation operation, Editor editor) {
+ // Pull out the data based on your custom video Delta format...
+ if (!operation.hasAttribute('video')) {
+ return null;
+ }
+ final videoUrl = operation['video'];
+ if (videoUrl is! String) {
+ return null;
+ }
+
+ // This example assumes that your editor supports a `InsertVideoRequest`.
+ editor.executeRequests([
+ InsertVideoRequest(videoUrl),
+ ]);
+ }
+}
+```
+
+#### Custom Inline Attributes
+Inline attributes are metadata that applies to spans of text. Typically this metadata represents styles,
+such as bold, italic, and underline. These attributes can also mix styling with logical information
+such as for links.
+
+To parse a custom inline attribute, implement `InlineDeltaFormat`.
+
+The following example implements a hypothetical user tag inline style:
+
+```dart
+class UserTagFormat implements InlineDeltaFormat {
+ @override
+ Attribution? from(Operation operation) {
+ // Pull out the data based on your custom user tag Delta attribute format...
+ if (!operation.hasAttribute('tag')) {
+ return null;
+ }
+ final tag = operation['tag'];
+ if (tag is! Map) {
+ return null;
+ }
+
+ final userId = tag['userId'];
+ final userName = tag['userName'];
+ if (userId is! String || userName is! String) {
+ return null;
+ }
+
+ // This hypothetical example assumes that your editor supports a
+ // `InsertUserTagRequest`.
+ editor.executeRequests([
+ InsertUserTagRequest(
+ id: userId,
+ name: userName,
+ ),
+ ]);
+
+ return attribution;
+ }
+}
+```
+
+#### Custom Inline Embeds
+Inline embeds are pieces of non-text content that are placed within lines of text. For example,
+a math formula, or a tiny bitmap image.
+
+Currently, Super Editor doesn't support inline widgets. Therefore, the actions that you can take
+with inline embeds, is limited. However, Super Editor Quill supports parsing those embeds.
+
+To parse a custom inline embed, implement `InlineEmbedFormat`.
+
+The following hypothetical example parses an inline bitmap image:
+
+```dart
+class InlineImageFormat implements InlineEmbedFormat {
+ @override
+ bool insert(Editor editor, DocumentComposer composer, Map embed) {
+ // Pull out the data based on your custom Delta inline image format...
+ final url = embed['image'];
+ if (url is! String) {
+ return false;
+ }
+
+ // TODO: take whatever action you'd like with the inline image URL.
+
+ return true;
+ }
+}
+```
+
+### Custom Editor
+When Super Editor Quill parses a Quill document, it internally creates an `Editor`, `MutableDocument`,
+and a `MutableDocumentComposer`, which is then used to run every content insertion listed in the Quill
+document.
+
+Apps with custom formats might need to take unusual actions to insert those blocks, attributes, and
+inline embeds through an `Editor` and into the `MutableDocument`. For this reason, Super Editor Quill
+allows you to pass your own `Editor`, with custom request handlers and editables, into the global
+parsers.
+
+```dart
+final superEditorDocument = MutableDocument.empty();
+final editor = Editor(/* custom config */);
+
+final quillDocument = getQuillDocument();
+
+// Parse the `quillDocument` into the given `customEditor`, which will use
+// the request handlers that you chose to register with your `editor`.
+parseQuillDeltaDocument(
+ quillDocument,
+ customEditor: editor,
+);
+```
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/quill/serializing.md b/doc/website/source/super-editor/guides/quill/serializing.md
new file mode 100644
index 0000000000..f643d41aa1
--- /dev/null
+++ b/doc/website/source/super-editor/guides/quill/serializing.md
@@ -0,0 +1,113 @@
+---
+title: Serializing Super Editor to Quill Deltas
+contentRenderers:
+ - jinja
+ - markdown
+---
+Super Editor supports exporting its document to the Quill Delta format. The process
+of going from a Super Editor document to Quill Deltas is known as "serializing" your
+Super Editor document.
+
+To get started with serializing Quill documents, add the Super Editor Quill package:
+
+```yaml
+dependencies:
+ super_editor: any
+ super_editor_quill: ^{{ pub.super_editor_quill.version }}
+```
+
+The `super_editor_quill` package adds an extension method to `MutableDocument`s, which
+serializes the document to Quill Deltas.
+
+To serialize your document to Quill Deltas, call `toQuillDeltas()` on your document:
+
+```dart
+final MutableDocument mySuperEditorDocument = getMySuperEditorDocument();
+
+final quillDocument = mySuperEditorDocument.toQuillDeltas();
+```
+
+## Custom Blocks, Attributes, and Inline Embeds
+It's common among Quill use-cases to extend the Quill Delta format to include new types of
+blocks, attributes, and inline embeds. Super Editor Quill allows you to provide your own such
+serializers.
+
+Pass your custom serializers to the `toQuillDeltas()` method:
+
+```dart
+final quillDocument = mySuperEditorDocument.toQuillDeltas(
+ serializers: [
+ const MyMathFormulaSerializer(),
+ const MyUserTagSerializer(),
+ ...defaultDeltaSerializers,
+ ],
+);
+```
+
+### Custom Block Serializers
+To serialize a Super Editor `DocumentNode` into a Quill Delta block, implement a
+`DeltaSerializer`.
+
+The following hypothetical example implements a Quill serializer for a video block:
+
+```dart
+class VideoBlockSerializer implements DeltaSerializer {
+ const VideoBlockSerializer();
+
+ @override
+ bool serialize(DocumentNode node, Delta deltas) {
+ if (node is! VideoNode) {
+ return false;
+ }
+
+ deltas.operations.add(
+ Operation.insert({
+ "video": node.url,
+ }),
+ );
+
+ return true;
+ }
+}
+```
+
+### Custom Inline Serializers
+Serializing text includes both the serialization of a block (the paragraph), as well as any number of
+inline attributes (e.g., bold, italic, links). Therefore, there are no standalone inline serializers.
+Instead, to serialize custom inline attributes, you should extend the existing text serializer.
+
+The following hypothetical example extends `TextBlockDeltaSerializer` and adds support for a user tag:
+
+```dart
+class MyAppParagraphSerializer extends TextBlockDeltaSerializer {
+ @override
+ @protected
+ Map getInlineAttributesFor(Set superEditorAttributions) {
+ // Collect any standard Quill Delta attributes that exist in the current
+ // span of text.
+ final inlineAttributes = super.getInlineAttributesFor(superEditorAttribions);
+
+ // This hypothetical example assumes that your editor has the concept
+ // of a `UserTagAttribution`.
+ final userTag = superEditorAttributions.whereType().firstOrNull;
+ if (userTag != null) {
+ // This inline span includes a user tag. Configure the Quill Delta attributes
+ // based on our custom specifications.
+ inlineAttributes['tag'] = {
+ 'userId': userTag.id,
+ 'userName': userTag.name,
+ };
+ }
+
+ return inlineAttributes;
+ }
+}
+```
+
+### Custom Inline Embeds
+Currently, Super Editor doesn't support the concept of inline widgets, which means there's no
+structure that would yield inline embeds. Therefore, no support currently exists for serializing
+inline embeds.
+
+In the future, when Super Editor adjusts the definition of `AttributedText` to support inline
+non-text content, `super_editor_quill` will be updated, accordingly.
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/style-a-document.md b/doc/website/source/super-editor/guides/style-a-document.md
new file mode 100644
index 0000000000..17330e540e
--- /dev/null
+++ b/doc/website/source/super-editor/guides/style-a-document.md
@@ -0,0 +1,136 @@
+---
+title: Style a Document
+---
+# Style a Document
+Super Editor includes support for rudimentary stylesheets, which make it easy to apply sweeping
+styles across all document content.
+
+A `Stylesheet` is a priority list of `StyleRule`s. Each `StyleRule` has a `BlockSelector`, which determines for which nodes the rule applies. Think of `BlockSelector`s as rudimentary css selectors. `BlockSelector` can match all nodes, nodes of a specific type, nodes that appear after a specific node type, and so on. A `StyleRule` also includes a `Styler`, which is a function that returns the style metadata.
+
+`SuperEditor` includes sane defaults for common node types, but you can define your own styles by providing a custom `StyleSheet`.
+
+## Creating a custom stylesheet
+
+The easiest way is to create a custom stylesheet is to copy the `defaultStylesheet` and add your rules at the end. For example, to make all level one headers green, create the following stylesheet:
+
+```dart
+const myStyleSheet = defaultStylesheet.copyWith(
+ addRulesAfter: [
+ StyleRule(
+ // Matches all level one headers.
+ const BlockSelector("header1"),
+ (document, node) {
+ return {
+ Styles.textStyle: const TextStyle(color: Colors.green),
+ };
+ },
+ ),
+ ],
+);
+```
+
+Then pass it to `SuperEditor`.
+
+```dart
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return SuperEditor(
+ // ...
+ stylesheet: myStyleSheet,
+ );
+ }
+}
+```
+
+See the `Styles` class for the list of keys to the style metadata used by `SuperEditor`.
+
+## Multiple matching rules
+
+Multiple `StyleRule`s can match a single node. When that happens, `SuperEditor` attempts to merge them, by looking at each key. For example, consider the following stylesheet:
+
+```dart
+const myStyleSheet = defaultStylesheet.copyWith(
+ addRulesAfter: [
+ StyleRule(
+ // Matches all level one headers.
+ const BlockSelector("header1"),
+ (document, node) {
+ return {
+ Styles.textStyle: const TextStyle(color: Colors.green),
+ };
+ },
+ ),
+ StyleRule(
+ // Matches all nodes.
+ BlockSelector.all,
+ (document, node) {
+ return {
+ Styles.textStyle: const TextStyle(fontSize: 14),
+ };
+ },
+ )
+ ],
+);
+```
+
+Both styles will be applied. Each level one header will have green text with a font size of 14px.
+
+If the styles can't be merged, the first one wins. For example, consider the following stylesheet:
+
+```dart
+const myStyleSheet = defaultStylesheet.copyWith(
+ addRulesAfter: [
+ StyleRule(
+ // Matches all nodes.
+ BlockSelector.all,
+ (document, node) {
+ return {
+ Styles.textAlign: TextAlign.center,
+ };
+ },
+ ),
+ StyleRule(
+ // Matches all nodes.
+ BlockSelector.all,
+ (document, node) {
+ return {
+ Styles.textAlign: TextAlign.right,
+ };
+ },
+ )
+ ],
+);
+```
+
+Since we cannot match two different text alignments, the first one is used. All nodes will be center-aligned.
+
+However, non-conflicting keys are preserved. For example, consider the following stylesheet:
+
+```dart
+const myStyleSheet = defaultStylesheet.copyWith(
+ addRulesAfter: [
+ StyleRule(
+ // Matches all nodes.
+ BlockSelector.all,
+ (document, node) {
+ return {
+ Styles.textAlign: TextAlign.center,
+ };
+ },
+ ),
+ StyleRule(
+ // Matches all nodes.
+ BlockSelector.all,
+ (document, node) {
+ return {
+ Styles.textAlign: TextAlign.right,
+ Styles.textStyle: const TextStyle(color: Colors.green),
+ };
+ },
+ )
+ ],
+);
+```
+
+`SuperEditor` keeps the text alignment from the first rule, ignores the text alignment from the second rule, and keeps the text style from the second rule. As a result, all nodes will be center-aligned and have green text.
\ No newline at end of file
diff --git a/doc/website/source/super-editor/guides/text-underlines.md b/doc/website/source/super-editor/guides/text-underlines.md
new file mode 100644
index 0000000000..186e86f8dd
--- /dev/null
+++ b/doc/website/source/super-editor/guides/text-underlines.md
@@ -0,0 +1,167 @@
+---
+title: Text Underlines
+---
+Underlines in Flutter text don't support any styles. They're always the same
+thickness, the same distance from the text, the same color as the text, and
+have the same square end-caps. It should be possible to control these styles,
+but Flutter doesn't expose the lower level text layout controls.
+
+Editors require custom underline painting for styles and design languages that
+don't exactly match the standard text underline. Super Editor supports custom
+painting of underlines by manually positioning the painted lines beneath the
+relevant spans of text.
+
+## Special Underlines
+Super Editor treats some underlines as special. These include:
+
+ * The user's composing region.
+ * Spelling errors.
+ * Grammar errors.
+
+For these special underlines, please see other guides and references to
+work with them.
+
+## Custom Underlines
+Super Editor supports painting custom text underlines.
+
+### Attribute the Text
+First, attribute the desired text with a `CustomUnderlineAttribution`, which
+specifies the visual type of underline. Super Editor includes some pre-defined
+type names, but you can use any name.
+
+```dart
+final underlineAttribution = CustomUnderlineAttribution(
+ CustomUnderlineAttribution.standard,
+);
+
+AttributedText(
+ "This text includes an underline.",
+ AttributedSpans(
+ attributions: [
+ SpanMarker(attribution: underlineAttribution, offset: 22, markerType: SpanMarkerType.start),
+ SpanMarker(attribution: underlineAttribution, offset: 30, markerType: SpanMarkerType.end),
+ ],
+ ),
+)
+```
+
+### Style the Underlines
+Add a style rule to your stylesheet, which specifies all underline styles.
+
+```dart
+final myStylesheet = defaultStylesheet.copyWith(
+ addRulesBefore: [
+ StyleRule(
+ BlockSelector.all,
+ (doc, docNode) {
+ return {
+ // The `underlineStyles` key is used to identify a collection of
+ // underline styles.
+ //
+ // Within the `CustomUnderlineStyles`, you should add an entry
+ // for every underline type name that your app uses, and then
+ // specify the `UnderlineStyle` to paint that underline.
+ UnderlineStyler.underlineStyles: CustomUnderlineStyles({
+ // In this example, we specify only one underline style. This
+ // style is for the `standard` underline type, and it paints
+ // a green squiggly underline.
+ CustomUnderlineAttribution.standard: SquiggleUnderlineStyle(
+ color: Colors.green,
+ ),
+ // You can add more types and styles here...
+ }),
+ };
+ },
+ ),
+ ],
+);
+```
+
+### Custom Styles
+Super Editor provides a few underline styles, which offer some configuration,
+including `StraightUnderlineStyle`, `DottedUnderlineStyle`, and `SquiggleUnderlineStyle`.
+However, these may not meet your needs.
+
+To paint your own underline, you need to create two classes: a subclass of `UnderlineStyle`
+and a `CustomPainter` that actually does the painting.
+
+The `UnderlineStyle` subclass is like a view-model, and the `CustomPainter` uses
+properties from the `UnderlineStyle` to decide how to paint the underline.
+
+For example, the following is the implementation of `StraightUnderlineStyle`.
+
+```dart
+class StraightUnderlineStyle implements UnderlineStyle {
+ const StraightUnderlineStyle({
+ this.color = const Color(0xFF000000),
+ this.thickness = 2,
+ this.capType = StrokeCap.square,
+ });
+
+ final Color color;
+ final double thickness;
+ final StrokeCap capType;
+
+ @override
+ CustomPainter createPainter(List underlines) {
+ return StraightUnderlinePainter(underlines: underlines, color: color, thickness: thickness, capType: capType);
+ }
+}
+```
+
+The job of the `UnderlineStyle` is to take a collection of properties and
+pass them in some form to a `CustomPainter`. In the case of `StraightUnderlineStyle`,
+the properties are passed to a `StraightUnderlinePainter`. The `createPainter()`
+method is called by Super Editor at the appropriate time.
+
+To complete the example, the following is the implementation of `StraightUnderlinePainter`.
+
+```dart
+class StraightUnderlinePainter extends CustomPainter {
+ const StraightUnderlinePainter({
+ required List underlines,
+ this.color = const Color(0xFF000000),
+ this.thickness = 2,
+ this.capType = StrokeCap.square,
+ }) : _underlines = underlines;
+
+ final List _underlines;
+
+ final Color color;
+ final double thickness;
+ final StrokeCap capType;
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ if (_underlines.isEmpty) {
+ return;
+ }
+
+ final linePaint = Paint()
+ ..style = PaintingStyle.stroke
+ ..color = color
+ ..strokeWidth = thickness
+ ..strokeCap = capType;
+ for (final underline in _underlines) {
+ canvas.drawLine(underline.start, underline.end, linePaint);
+ }
+ }
+
+ @override
+ bool shouldRepaint(StraightUnderlinePainter oldDelegate) {
+ return color != oldDelegate.color ||
+ thickness != oldDelegate.thickness ||
+ capType != oldDelegate.capType ||
+ !const DeepCollectionEquality().equals(_underlines, oldDelegate._underlines);
+ }
+}
+```
+
+By providing your own version of these two classes, you can paint any underline you desire.
+
+With your own `UnderlineStyle` defined, use it in your stylesheet as discussed previously.
+
+As you implement your own underline painting, you might be confused where some of these
+underline classes come from. Note that some of them are lower level than Super Editor -
+they come from the `super_text_layout` package, which is another package in the
+Super Editor mono repo.
\ No newline at end of file
diff --git a/golden_runner/.gitignore b/golden_runner/.gitignore
new file mode 100644
index 0000000000..96486fd930
--- /dev/null
+++ b/golden_runner/.gitignore
@@ -0,0 +1,30 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.packages
+build/
diff --git a/golden_runner/.metadata b/golden_runner/.metadata
new file mode 100644
index 0000000000..2b69774845
--- /dev/null
+++ b/golden_runner/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "f8afcd5aa01b3ae6c55cb6e4c9fa4171e27a92f6"
+ channel: "master"
+
+project_type: package
diff --git a/golden_runner/CHANGELOG.md b/golden_runner/CHANGELOG.md
new file mode 100644
index 0000000000..41cc7d8192
--- /dev/null
+++ b/golden_runner/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/golden_runner/LICENSE b/golden_runner/LICENSE
new file mode 100644
index 0000000000..ba75c69f7f
--- /dev/null
+++ b/golden_runner/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/golden_runner/README.md b/golden_runner/README.md
new file mode 100644
index 0000000000..13e9cf4570
--- /dev/null
+++ b/golden_runner/README.md
@@ -0,0 +1,41 @@
+This package contains a tool to run golden tests and update golden files in a docker container.
+
+The command should be run from the root of the package being tested.
+
+## Activate the package:
+
+```console
+dart pub global activate --source path ./golden_runner
+```
+
+## Run golden tests:
+
+```
+# run all tests
+flutter pub run ../golden_runner/tool/goldens test
+
+# run a single test
+flutter pub run ../golden_runner/tool/goldens test --plain-name "something"
+
+# run all tests in a directory
+flutter pub run ../golden_runner/tool/goldens test test_goldens/my_dir
+
+# run a single test in a directory
+flutter pub run ../golden_runner/tool/goldens test --plain-name "something" test_goldens/my_dir
+```
+
+## Update golden files:
+
+```
+# update all goldens
+flutter pub run ../golden_runner/tool/goldens update
+
+# update all goldens in a directory
+flutter pub run ../golden_runner/tool/goldens update test_goldens/my_dir
+
+# update a single golden
+flutter pub run ../golden_runner/tool/goldens update --plain-name "something"
+
+# update a single golden in a directory
+flutter pub run ../golden_runner/tool/goldens update --plain-name "something" test_goldens/my_dir
+```
diff --git a/golden_runner/analysis_options.yaml b/golden_runner/analysis_options.yaml
new file mode 100644
index 0000000000..a5744c1cfb
--- /dev/null
+++ b/golden_runner/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/golden_runner/bin/goldens.dart b/golden_runner/bin/goldens.dart
new file mode 100644
index 0000000000..32a696c56d
--- /dev/null
+++ b/golden_runner/bin/goldens.dart
@@ -0,0 +1,17 @@
+import 'dart:io';
+
+// ignore: depend_on_referenced_packages
+import 'package:args/command_runner.dart';
+import 'package:golden_runner/golden_runner.dart';
+
+Future main(List arguments) async {
+ final runner = CommandRunner("goldens", "A tool to run and update golden tests using docker")
+ ..addCommand(GoldenTestCommand())
+ ..addCommand(UpdateGoldensCommand());
+
+ try {
+ await runner.run(arguments);
+ } on UsageException catch (e) {
+ stdout.write(e);
+ }
+}
diff --git a/golden_runner/lib/golden_runner.dart b/golden_runner/lib/golden_runner.dart
new file mode 100644
index 0000000000..0fc9aef473
--- /dev/null
+++ b/golden_runner/lib/golden_runner.dart
@@ -0,0 +1,3 @@
+library golden_runner;
+
+export 'src/commands.dart';
diff --git a/golden_runner/lib/src/commands.dart b/golden_runner/lib/src/commands.dart
new file mode 100644
index 0000000000..a44131022c
--- /dev/null
+++ b/golden_runner/lib/src/commands.dart
@@ -0,0 +1,356 @@
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart' as path;
+
+/// A [Command] which runs golden tests.
+///
+/// The command run the tests in a Linux Docker container. It expects to be running in a "golden_tester" directory.
+///
+/// Usage: `flutter pub run test `
+///
+/// Options:
+///
+/// `--plain-name "test-name"`: Runs only the tests containing the given value in the test name.
+///
+/// The target can be a directory or a file. This argument is optional.
+///
+/// This is intended to be added as an [CommandRunner] command.
+class GoldenTestCommand extends Command {
+ GoldenTestCommand() {
+ argParser.addOption(
+ 'plain-name',
+ help: 'A plain-text substring of the names of tests to run',
+ );
+ }
+
+ @override
+ String get name => 'test';
+
+ @override
+ String get description => 'Runs golden tests';
+
+ @override
+ Future run() async {
+ final args = argResults!;
+
+ // The tool must run from the root of the package being tested.
+ // For example, /super_editor/super_text_layout.
+ // We take the last part of the directory as the package directory.
+ final packageDirectory = path.split(Directory.current.path).last;
+
+ // Builds the image used to run the container.
+ // We can build the image even if it already exists.
+ // Docker will cache each step used in the Dockerfile, so subsequent builds will be faster.
+ await _buildDockerImage();
+
+ // Arguments that are placed after 'flutter test test_goldens'.
+ final cmdArguments = [];
+
+ final name = args['plain-name'];
+ if (name is String) {
+ cmdArguments
+ ..add('--plain-name')
+ ..add(name);
+ }
+
+ stdout.writeln('Running golden tests');
+
+ // Other arguments passed at the end of the command.
+ // For example, the test directory.
+ final rest = [...args.rest];
+
+ late String testDirOrTestFileName;
+ late String testBaseDirectory;
+
+ if (rest.isNotEmpty) {
+ // An argument was passed after the command options.
+ // For example, in "flutter pub run tool/goldens test my_test_dir", "my_test_dir" is the first argument
+ // after the command.
+ // Use the first argument after the command options as the test directory or test file name
+ // and remove it from the rest.
+ testDirOrTestFileName = rest.removeAt(0);
+
+ if (path.extension(testDirOrTestFileName).isNotEmpty) {
+ // A test file was given.
+ // Extract the directory name so we can list the sub-directories.
+ testBaseDirectory = path.dirname(testDirOrTestFileName);
+ } else {
+ // A test directory was given.
+ // Don't try to extract the directory name because it can return an empty string if it's the root directory.
+ // For example, passing "test_goldens" to dirname would return "".
+ testBaseDirectory = testDirOrTestFileName;
+ }
+ } else {
+ testDirOrTestFileName = 'test_goldens';
+ testBaseDirectory = testDirOrTestFileName;
+ }
+
+ final dirs = _findAllTestDirectories(testBaseDirectory);
+
+ final volumeMappings = _generateFailureDirectoriesMappings(packageDirectory, dirs);
+
+ // Runs the container.
+ //
+ // --rm: Removes the container when it exits.
+ //
+ // --workdir: Sets the process working directory to the given package directory in the container.
+ await _runProcess(
+ executable: 'docker',
+ arguments: [
+ 'run',
+ '--rm',
+ ...volumeMappings,
+ '--workdir',
+ '/golden_tester/$packageDirectory',
+ 'supereditor_golden_tester',
+ 'flutter',
+ 'test',
+ testDirOrTestFileName,
+ ...rest,
+ ...cmdArguments,
+ ],
+ description: 'Golden tests',
+ throwOnError: false,
+ );
+
+ // After running the tests, we don't need the image anymore. Remove it.
+ await _removeDockerImage();
+
+ // Mapping the failure directories causes them to be created automatically, even without any failing test.
+ // Remove all the empty failure directories.
+ for (final dirName in dirs) {
+ final dir = Directory('$dirName/failures');
+ if (dir.existsSync() && dir.listSync().isEmpty) {
+ dir.deleteSync();
+ }
+ }
+ }
+
+ /// Returns a list of docker command line arguments to configure the volume mappings for the test failure directories.
+ ///
+ /// [testDirectories] must be a list of relative paths to the working directory.
+ ///
+ /// This mappings are used so when a failure happens, the failure images are save in the host OS.
+ List _generateFailureDirectoriesMappings(String packageDirectory, List testDirectories) {
+ final mappings = [];
+
+ for (final dir in testDirectories) {
+ mappings.add('-v');
+ mappings.add('${Directory.current.path}/$dir/failures:/golden_tester/$packageDirectory/$dir/failures');
+ }
+
+ return mappings;
+ }
+
+ /// Returns a list of sub-directories inside a root test directory as relative paths to the working directory.
+ ///
+ /// For example, "test_goldens/editor", "test_goldens/components".
+ ///
+ /// Ignores "failures" directories.
+ List _findAllTestDirectories(String rootTestDir) {
+ final dir = Directory(rootTestDir);
+ final subDirs = dir
+ .listSync(recursive: true) //
+ .whereType()
+ // Ensure we use linux path separator.
+ // The tool can run in a host OS which uses a different path separator.
+ // Without this, the volume ma
+ .map((e) => e.path.replaceAll(path.separator, '/'))
+ .where((e) => !e.endsWith('failures'))
+ .toList();
+ return [rootTestDir, ...subDirs];
+ }
+}
+
+/// A [Command] which updates golden files.
+///
+/// The command run the tests in a Linux Docker container. It expects to be running in a "golden_tester" directory.
+///
+/// Usage: `flutter pub run update `
+///
+/// Options:
+///
+/// `--plain-name "test-name"`: Update only the tests containing the given value in the test name.
+///
+/// The target can be a directory or a file. This argument is optional.
+///
+/// This is intended to be added as an [CommandRunner] command.
+class UpdateGoldensCommand extends Command {
+ UpdateGoldensCommand() {
+ argParser.addOption(
+ 'plain-name',
+ help: 'A plain-text substring of the names of tests to run',
+ );
+ }
+
+ @override
+ String get description => 'Updates golden files';
+
+ @override
+ String get name => 'update';
+
+ @override
+ Future run() async {
+ final args = argResults!;
+
+ // The tool must run from the root of the package being tested.
+ // For example, /super_editor/super_text_layout.
+ // We take the last part of the directory as the package directory.
+ final packageDirectory = path.split(Directory.current.path).last;
+
+ // Builds the image used to run the container.
+ // We can build the image even if it already exists.
+ // Docker will cache each step used in the Dockerfile, so subsequent builds will be faster.
+ await _buildDockerImage();
+
+ // Arguments that are placed after 'flutter test test_goldens'.
+ final cmdArguments = [];
+
+ final name = args['plain-name'];
+ if (name is String) {
+ cmdArguments
+ ..add('--plain-name')
+ ..add(name);
+ }
+
+ stdout.writeln('Updating golden files');
+
+ // Other arguments passed at the end of the command.
+ // For example, the test directory.
+ final rest = [...args.rest];
+
+ late String testDirOrTestFileName;
+ late String testBaseDirectory;
+
+ if (rest.isNotEmpty) {
+ // An argument was passed after the command options.
+ // For example, in "flutter pub run tool/goldens test my_test_dir", "my_test_dir" is the first argument
+ // after the command.
+ // Use the first argument after the command options as the test directory or test file name
+ // and remove it from the rest.
+ testDirOrTestFileName = rest.removeAt(0);
+
+ if (path.extension(testDirOrTestFileName).isNotEmpty) {
+ // A test file was given.
+ // Extract the directory name so we can list the sub-directories.
+ testBaseDirectory = path.dirname(testDirOrTestFileName);
+ } else {
+ // A test directory was given.
+ // Don't try to extract the directory name because it can return an empty string if it's the root directory.
+ // For example, passing "test_goldens" to dirname would return "".
+ testBaseDirectory = testDirOrTestFileName;
+ }
+ } else {
+ testDirOrTestFileName = 'test_goldens';
+ testBaseDirectory = testDirOrTestFileName;
+ }
+
+ // Runs the container.
+ //
+ // --rm: Removes the container when it exits.
+ //
+ // -v: Mounts the directory containing the tests of the host machine into the container.
+ // This is used to write the new golden files directly on the host OS.
+ //
+ // --workdir: Sets the working directory to /super_editor/super_text_layout in the container.
+ await _runProcess(
+ executable: 'docker',
+ arguments: [
+ 'run',
+ '--rm',
+ '-v',
+ '${Directory.current.path}/$testBaseDirectory:/golden_tester/$packageDirectory/$testBaseDirectory',
+ '--workdir',
+ '/golden_tester/$packageDirectory',
+ 'supereditor_golden_tester',
+ 'flutter',
+ 'test',
+ '--update-goldens',
+ testDirOrTestFileName,
+ ...rest,
+ ...cmdArguments,
+ ],
+ description: 'Update goldens',
+ throwOnError: false,
+ );
+
+ // After running the tests, we don't need the image anymore. Remove it.
+ await _removeDockerImage();
+ }
+}
+
+/// Builds a linux docker image to run the tests.
+///
+/// The golden_tester.Dockerfile is used to build this image.
+Future _buildDockerImage() async {
+ stdout.write('building image');
+
+ await _runProcess(
+ executable: 'docker',
+ arguments: [
+ 'build',
+ '-f',
+ './golden_tester.Dockerfile',
+ '-t',
+ 'supereditor_golden_tester',
+ '.',
+ ],
+ // We need to use the repository root as the working directory to be able to copy all of the files
+ // in this repository, not just the package directory.
+ workingDirectory: '../',
+ description: 'Image build',
+ );
+}
+
+/// Removes the image built by the golden tester.
+Future _removeDockerImage() async {
+ await _runProcess(
+ executable: 'docker',
+ arguments: [
+ 'image', 'rm', //
+ '-f',
+ 'supereditor_golden_tester',
+ ],
+ description: 'Removing image',
+ throwOnError: false,
+ );
+}
+
+/// Runs [executable] with the given [arguments].
+///
+/// [executable] could be an absolute path or it could be resolved from the PATH.
+///
+/// The [arguments] must contain any modifiers, like `-`, `--` or `/`.
+///
+/// Use [workingDirectory] to set the working directory for the process.
+///
+/// The child process stdout and stderr are written to the current process stdout.
+///
+/// If [throwOnError] is `true`, throws an exception if the process exits with a non-zero exit code.
+///
+/// If [throwOnError] is `false`, the function returns the exit code.
+Future _runProcess({
+ required String executable,
+ required List arguments,
+ required String description,
+ String? workingDirectory,
+ bool throwOnError = true,
+}) async {
+ final process = await Process.start(
+ executable,
+ arguments,
+ workingDirectory: workingDirectory,
+ );
+
+ await stdout.addStream(process.stdout);
+ await stderr.addStream(process.stderr);
+
+ final exitCode = await process.exitCode;
+
+ if (exitCode != 0 && throwOnError) {
+ throw Exception('$description failed');
+ }
+
+ return exitCode;
+}
diff --git a/golden_runner/pubspec.yaml b/golden_runner/pubspec.yaml
new file mode 100644
index 0000000000..2f03de03be
--- /dev/null
+++ b/golden_runner/pubspec.yaml
@@ -0,0 +1,17 @@
+name: golden_runner
+description: Commands to test and update goldens in a Docker container
+version: 0.0.1
+homepage:
+
+executables:
+ goldens:
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+
+dependencies:
+ args: ^2.3.1
+ meta: ^1.8.0
+ path: ^1.8.3
+
+flutter:
diff --git a/golden_tester.Dockerfile b/golden_tester.Dockerfile
new file mode 100644
index 0000000000..b61f350cbd
--- /dev/null
+++ b/golden_tester.Dockerfile
@@ -0,0 +1,24 @@
+FROM ubuntu:latest
+
+ENV FLUTTER_HOME=${HOME}/sdks/flutter
+ENV PATH ${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin
+
+USER root
+
+RUN apt update
+
+RUN apt install -y git curl unzip
+
+# Print the Ubuntu version. Useful when there are failing tests.
+RUN cat /etc/lsb-release
+
+# Invalidate the cache when flutter pushes a new commit.
+ADD https://api.github.com/repos/flutter/flutter/git/refs/heads/master ./flutter-latest-master
+
+RUN git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME}
+
+RUN flutter doctor
+
+# Copy the whole repo.
+# We need this because we use local dependencies.
+COPY ./ /golden_tester
diff --git a/super_clones/bear/.gitignore b/super_clones/bear/.gitignore
new file mode 100644
index 0000000000..79c113f9b5
--- /dev/null
+++ b/super_clones/bear/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/super_clones/bear/.metadata b/super_clones/bear/.metadata
new file mode 100644
index 0000000000..6e13c77392
--- /dev/null
+++ b/super_clones/bear/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
+ base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
+ - platform: macos
+ create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
+ base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/super_clones/bear/README.md b/super_clones/bear/README.md
new file mode 100644
index 0000000000..a5c36eee5b
--- /dev/null
+++ b/super_clones/bear/README.md
@@ -0,0 +1,16 @@
+# bear
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/super_clones/bear/analysis_options.yaml b/super_clones/bear/analysis_options.yaml
new file mode 100644
index 0000000000..0d2902135c
--- /dev/null
+++ b/super_clones/bear/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/super_clones/bear/lib/app.dart b/super_clones/bear/lib/app.dart
new file mode 100644
index 0000000000..5de4af84cc
--- /dev/null
+++ b/super_clones/bear/lib/app.dart
@@ -0,0 +1,19 @@
+import 'package:bear/features/home_screen.dart';
+import 'package:flutter/material.dart';
+
+class DashApp extends StatelessWidget {
+ const DashApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Dash',
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
+ useMaterial3: true,
+ ),
+ home: const HomeScreen(),
+ debugShowCheckedModeBanner: false,
+ );
+ }
+}
diff --git a/super_clones/bear/lib/features/home_screen.dart b/super_clones/bear/lib/features/home_screen.dart
new file mode 100644
index 0000000000..9e191e95a4
--- /dev/null
+++ b/super_clones/bear/lib/features/home_screen.dart
@@ -0,0 +1,53 @@
+import 'package:bear/infrastructure/editor/editor.dart';
+import 'package:flutter/material.dart';
+
+class HomeScreen extends StatefulWidget {
+ const HomeScreen({super.key});
+
+ @override
+ State createState() => _HomeScreenState();
+}
+
+class _HomeScreenState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _buildToolbar(),
+ _buildNoteList(),
+ _buildDivider(),
+ Expanded(
+ child: _buildEditor(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildToolbar() {
+ return Container(
+ width: 180,
+ color: const Color(0xFF303033),
+ );
+ }
+
+ Widget _buildNoteList() {
+ return Container(
+ width: 285,
+ color: Colors.white,
+ );
+ }
+
+ Widget _buildDivider() {
+ return Container(
+ width: 1,
+ color: const Color(0xFFDDDDDD),
+ );
+ }
+
+ Widget _buildEditor() {
+ return TextEditor();
+ }
+}
diff --git a/super_clones/bear/lib/infrastructure/editor/components.dart b/super_clones/bear/lib/infrastructure/editor/components.dart
new file mode 100644
index 0000000000..b344b40c4f
--- /dev/null
+++ b/super_clones/bear/lib/infrastructure/editor/components.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:super_editor/super_editor.dart';
+
+const dashComponentBuilders = [
+ DashListItemComponentBuilder(),
+ ...defaultComponentBuilders,
+];
+
+class DashListItemComponentBuilder implements ComponentBuilder {
+ const DashListItemComponentBuilder();
+
+ @override
+ SingleColumnLayoutComponentViewModel? createViewModel(Document document, DocumentNode node) {
+ if (node is! ListItemNode) {
+ return null;
+ }
+ if (node.type != ListItemType.unordered) {
+ return null;
+ }
+
+ return UnorderedListItemComponentViewModel(
+ nodeId: node.id,
+ indent: node.indent,
+ text: node.text,
+ textStyleBuilder: noStyleBuilder,
+ selectionColor: const Color(0x00000000),
+ );
+ }
+
+ @override
+ Widget? createComponent(
+ SingleColumnDocumentComponentContext componentContext, SingleColumnLayoutComponentViewModel componentViewModel) {
+ if (componentViewModel is! UnorderedListItemComponentViewModel) {
+ return null;
+ }
+
+ return UnorderedListItemComponent(
+ componentKey: componentContext.componentKey,
+ text: componentViewModel.text,
+ styleBuilder: componentViewModel.textStyleBuilder,
+ indent: componentViewModel.indent,
+ indentCalculator: (TextStyle textStyle, int indent) {
+ return (textStyle.fontSize! * 0.60) * 3 * (indent + 1);
+ },
+ dotStyle: const ListItemDotStyle(
+ color: Colors.red,
+ size: Size(6, 6),
+ ),
+ textSelection: componentViewModel.selection,
+ selectionColor: componentViewModel.selectionColor,
+ highlightWhenEmpty: componentViewModel.highlightWhenEmpty,
+ underlines: componentViewModel.createUnderlines(),
+ );
+ }
+}
diff --git a/super_clones/bear/lib/infrastructure/editor/editor.dart b/super_clones/bear/lib/infrastructure/editor/editor.dart
new file mode 100644
index 0000000000..547819bd25
--- /dev/null
+++ b/super_clones/bear/lib/infrastructure/editor/editor.dart
@@ -0,0 +1,236 @@
+import 'package:bear/infrastructure/editor/components.dart';
+import 'package:bear/infrastructure/editor/stylesheet.dart';
+import 'package:flutter/material.dart';
+import 'package:super_editor/super_editor.dart';
+
+class TextEditor extends StatefulWidget {
+ const TextEditor({super.key});
+
+ @override
+ State createState() => _TextEditorState();
+}
+
+class _TextEditorState extends State {
+ late final MutableDocument _document;
+ final _composer = MutableDocumentComposer();
+ late final Editor _editor;
+
+ bool _isTopToolbarVisible = true;
+ bool _isFormattingToolbarVisible = false;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _document = deserializeMarkdownToDocument(_testDocumentContent);
+ _editor = createDefaultDocumentEditor(document: _document, composer: _composer)
+ ..addListener(
+ FunctionalEditListener(_hideTopToolbarOnContentChange),
+ );
+ }
+
+ /// Inspects every editor change and hides the top toolbar whenever the user makes
+ /// a content change, e.g., types some text, deletes some text.
+ void _hideTopToolbarOnContentChange(changeList) {
+ for (final change in changeList) {
+ if (change is! SelectionChangeEvent && change is! ComposingRegionChangeEvent) {
+ // We assume any non-selection and non-composing event is a content change.
+ // Hide the toolbar.
+ _hideTopToolbar();
+ return;
+ }
+ }
+ }
+
+ void _showTopToolbar() {
+ if (_isTopToolbarVisible) {
+ return;
+ }
+
+ setState(() {
+ _isTopToolbarVisible = true;
+ });
+ }
+
+ void _hideTopToolbar() {
+ if (!_isTopToolbarVisible) {
+ return;
+ }
+
+ setState(() {
+ _isTopToolbarVisible = false;
+ });
+ }
+
+ void _toggleFormattingToolbar() {
+ if (_isFormattingToolbarVisible) {
+ _hideFormattingToolbar();
+ } else {
+ _showFormattingToolbar();
+ }
+ }
+
+ void _showFormattingToolbar() {
+ if (_isFormattingToolbarVisible) {
+ return;
+ }
+
+ setState(() {
+ _isFormattingToolbarVisible = true;
+ });
+ }
+
+ void _hideFormattingToolbar() {
+ if (!_isFormattingToolbarVisible) {
+ return;
+ }
+
+ setState(() {
+ _isFormattingToolbarVisible = false;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ onPointerHover: (_) => _showTopToolbar(),
+ behavior: HitTestBehavior.opaque,
+ child: ColoredBox(
+ color: Colors.white,
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: SuperEditor(
+ editor: _editor,
+ document: _document,
+ composer: _composer,
+ componentBuilders: dashComponentBuilders,
+ stylesheet: dashStylesheet,
+ ),
+ ),
+ Positioned(
+ left: 0,
+ right: 0,
+ top: 0,
+ child: _buildTopToolbar(),
+ ),
+ AnimatedPositioned(
+ left: 0,
+ right: 0,
+ bottom: _isFormattingToolbarVisible ? 24 : -50,
+ duration: const Duration(milliseconds: 200),
+ curve: Curves.easeInOutCirc,
+ child: _buildFormattingToolbar(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildTopToolbar() {
+ return AbsorbPointer(
+ // ^ Absorb pointer so that when the user hovers over this bar, the user can drag the
+ // window around, instead of tapping through to the document and placing moving the caret.
+ child: AnimatedOpacity(
+ opacity: _isTopToolbarVisible ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 250),
+ child: SizedBox(
+ height: 54,
+ child: Row(
+ children: [
+ const Spacer(),
+ IconButton(
+ onPressed: _toggleFormattingToolbar,
+ icon: const Icon(
+ Icons.format_bold,
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(
+ Icons.info_outline,
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(
+ Icons.more_vert,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFormattingToolbar() {
+ return Center(
+ child: IconTheme(
+ data: IconThemeData(
+ size: 18,
+ color: Color(0xFF777777),
+ ),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: const Color(0xFFDDDDDD),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.text_fields),
+ ),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.list),
+ ),
+ const SizedBox(width: 24),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.format_bold),
+ ),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.format_italic),
+ ),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.highlight),
+ ),
+ const SizedBox(width: 24),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.link),
+ ),
+ IconButton(
+ onPressed: () {},
+ icon: Icon(Icons.image_outlined),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+const _testDocumentContent = '''# Editor Decorations
+## Padding
+* There's about 150px of padding on top.
+* ~600px padding on the bottom
+* Horizontally centered with a max document width
+
+## Top Bar
+* Shows itself when the mouse moves
+* Disappears when the user starts typing
+* Bottom border has nuanced appearance rules
+ * If the top bar is currently visible, but the bottom border isn't
+ * Any scrolling will cause the bottom bar to fade in
+''';
diff --git a/super_clones/bear/lib/infrastructure/editor/stylesheet.dart b/super_clones/bear/lib/infrastructure/editor/stylesheet.dart
new file mode 100644
index 0000000000..c4b33e10bc
--- /dev/null
+++ b/super_clones/bear/lib/infrastructure/editor/stylesheet.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:super_editor/super_editor.dart';
+
+final dashStylesheet = Stylesheet(
+ rules: [
+ ...defaultStylesheet.rules,
+ StyleRule(
+ BlockSelector.all,
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ color: Color(0xFF444444),
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header1"),
+ (doc, docNode) {
+ return {
+ Styles.padding: const CascadingPadding.only(top: 40),
+ Styles.textStyle: const TextStyle(
+ fontSize: 24,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header2"),
+ (doc, docNode) {
+ return {
+ Styles.padding: const CascadingPadding.only(top: 16),
+ Styles.textStyle: const TextStyle(
+ fontSize: 18,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("listItem"),
+ (doc, docNode) {
+ return {
+ Styles.padding: const CascadingPadding.only(top: 10),
+ Styles.textStyle: const TextStyle(
+ fontSize: 14,
+ ),
+ };
+ },
+ ),
+ ],
+ inlineTextStyler: defaultStylesheet.inlineTextStyler,
+);
diff --git a/super_clones/bear/lib/main.dart b/super_clones/bear/lib/main.dart
new file mode 100644
index 0000000000..a3e043e076
--- /dev/null
+++ b/super_clones/bear/lib/main.dart
@@ -0,0 +1,6 @@
+import 'package:bear/app.dart';
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const DashApp());
+}
diff --git a/super_clones/bear/macos/.gitignore b/super_clones/bear/macos/.gitignore
new file mode 100644
index 0000000000..746adbb6b9
--- /dev/null
+++ b/super_clones/bear/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/super_clones/bear/macos/Flutter/Flutter-Debug.xcconfig b/super_clones/bear/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000000..4b81f9b2d2
--- /dev/null
+++ b/super_clones/bear/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/super_clones/bear/macos/Flutter/Flutter-Release.xcconfig b/super_clones/bear/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000000..5caa9d1579
--- /dev/null
+++ b/super_clones/bear/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/super_clones/bear/macos/Flutter/GeneratedPluginRegistrant.swift b/super_clones/bear/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000000..8236f5728c
--- /dev/null
+++ b/super_clones/bear/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,12 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import url_launcher_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+}
diff --git a/super_clones/bear/macos/Podfile b/super_clones/bear/macos/Podfile
new file mode 100644
index 0000000000..c795730db8
--- /dev/null
+++ b/super_clones/bear/macos/Podfile
@@ -0,0 +1,43 @@
+platform :osx, '10.14'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/super_clones/bear/macos/Podfile.lock b/super_clones/bear/macos/Podfile.lock
new file mode 100644
index 0000000000..4768d52553
--- /dev/null
+++ b/super_clones/bear/macos/Podfile.lock
@@ -0,0 +1,22 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+
+SPEC CHECKSUMS:
+ FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
+ url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
+
+PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
+
+COCOAPODS: 1.15.2
diff --git a/super_clones/bear/macos/Runner.xcodeproj/project.pbxproj b/super_clones/bear/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..7de4a61974
--- /dev/null
+++ b/super_clones/bear/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,803 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ A022DCBB263A55F92B3D890F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5902A74075BFE0C3D48C8D48 /* Pods_Runner.framework */; };
+ EC107CE66F186F0DD5FA70C2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F38700AE193CFFFB919C20F7 /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 10C3413F8C897B6E53597A9B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 2D0B665771D6AE17F3FA431C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* bear.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bear.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 3BAE771960643842B59230E0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 56147A70B00CF7F4D1B3BF3D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 5902A74075BFE0C3D48C8D48 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ AA46F0ABA89EFCB9A55708F4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ B273427908DDEB7225FB9520 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ F38700AE193CFFFB919C20F7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EC107CE66F186F0DD5FA70C2 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A022DCBB263A55F92B3D890F /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 331C80D6294CF71000263BE5 /* RunnerTests */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ E477D87D0C74F54658FF46B2 /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* bear.app */,
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 5902A74075BFE0C3D48C8D48 /* Pods_Runner.framework */,
+ F38700AE193CFFFB919C20F7 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ E477D87D0C74F54658FF46B2 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3BAE771960643842B59230E0 /* Pods-Runner.debug.xcconfig */,
+ 56147A70B00CF7F4D1B3BF3D /* Pods-Runner.release.xcconfig */,
+ 10C3413F8C897B6E53597A9B /* Pods-Runner.profile.xcconfig */,
+ AA46F0ABA89EFCB9A55708F4 /* Pods-RunnerTests.debug.xcconfig */,
+ 2D0B665771D6AE17F3FA431C /* Pods-RunnerTests.release.xcconfig */,
+ B273427908DDEB7225FB9520 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 5906E7559FCC3CD340AE5906 /* [CP] Check Pods Manifest.lock */,
+ 331C80D1294CF70F00263BE5 /* Sources */,
+ 331C80D2294CF70F00263BE5 /* Frameworks */,
+ 331C80D3294CF70F00263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 1339FF43BED015CAAAC10DC7 /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ 16F0E29558E0C96104A0342E /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* bear.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C80D4294CF70F00263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 331C80D4294CF70F00263BE5 /* RunnerTests */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C80D3294CF70F00263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 1339FF43BED015CAAAC10DC7 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 16F0E29558E0C96104A0342E /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+ 5906E7559FCC3CD340AE5906 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C80D1294CF70F00263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+ };
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 331C80DB294CF71000263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AA46F0ABA89EFCB9A55708F4 /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.bear.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bear.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bear";
+ };
+ name = Debug;
+ };
+ 331C80DC294CF71000263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 2D0B665771D6AE17F3FA431C /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.bear.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bear.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bear";
+ };
+ name = Release;
+ };
+ 331C80DD294CF71000263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = B273427908DDEB7225FB9520 /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.bear.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bear.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bear";
+ };
+ name = Profile;
+ };
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C80DB294CF71000263BE5 /* Debug */,
+ 331C80DC294CF71000263BE5 /* Release */,
+ 331C80DD294CF71000263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/super_clones/bear/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/super_clones/bear/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/super_clones/bear/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/super_clones/bear/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/super_clones/bear/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..39fbc67f7d
--- /dev/null
+++ b/super_clones/bear/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/bear/macos/Runner.xcworkspace/contents.xcworkspacedata b/super_clones/bear/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/super_clones/bear/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/super_clones/bear/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/super_clones/bear/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/super_clones/bear/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/super_clones/bear/macos/Runner/AppDelegate.swift b/super_clones/bear/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..b3c1761412
--- /dev/null
+++ b/super_clones/bear/macos/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Cocoa
+import FlutterMacOS
+
+@main
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..a2ec33f19f
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000000..82b6f9d9a3
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000000..13b35eba55
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000000..0a3f5fa40f
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000000..bdb57226d5
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000000..f083318e09
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000000..326c0e72c9
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000000..2f1632cfdd
Binary files /dev/null and b/super_clones/bear/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/super_clones/bear/macos/Runner/Base.lproj/MainMenu.xib b/super_clones/bear/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000000..80e867a4e0
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/bear/macos/Runner/Configs/AppInfo.xcconfig b/super_clones/bear/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000000..7be447b7ad
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = bear
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.bear
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.
diff --git a/super_clones/bear/macos/Runner/Configs/Debug.xcconfig b/super_clones/bear/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000000..36b0fd9464
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/super_clones/bear/macos/Runner/Configs/Release.xcconfig b/super_clones/bear/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000000..dff4f49561
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/super_clones/bear/macos/Runner/Configs/Warnings.xcconfig b/super_clones/bear/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000000..42bcbf4780
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/super_clones/bear/macos/Runner/DebugProfile.entitlements b/super_clones/bear/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000000..dddb8a30c8
--- /dev/null
+++ b/super_clones/bear/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/super_clones/bear/macos/Runner/Info.plist b/super_clones/bear/macos/Runner/Info.plist
new file mode 100644
index 0000000000..4789daa6a4
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/super_clones/bear/macos/Runner/MainFlutterWindow.swift b/super_clones/bear/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000000..18ff6ede42
--- /dev/null
+++ b/super_clones/bear/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,24 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+
+ // Remove the title bar and expand content to fill the entire window.
+ self.setContentSize(NSSize(width: 1000, height: 650))
+ self.styleMask.update(with: StyleMask.fullSizeContentView)
+ self.titleVisibility = TitleVisibility.hidden
+ self.titlebarAppearsTransparent = true
+ self.backgroundColor = NSColor.white
+ self.toolbar = NSToolbar()
+ self.toolbarStyle = ToolbarStyle.unified
+ }
+}
diff --git a/super_clones/bear/macos/Runner/Release.entitlements b/super_clones/bear/macos/Runner/Release.entitlements
new file mode 100644
index 0000000000..852fa1a472
--- /dev/null
+++ b/super_clones/bear/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/super_clones/bear/macos/RunnerTests/RunnerTests.swift b/super_clones/bear/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000000..61f3bd1fc5
--- /dev/null
+++ b/super_clones/bear/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Cocoa
+import FlutterMacOS
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/super_clones/bear/pubspec.lock b/super_clones/bear/pubspec.lock
new file mode 100644
index 0000000000..b73b2dbd24
--- /dev/null
+++ b/super_clones/bear/pubspec.lock
@@ -0,0 +1,672 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "67.0.0"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.1"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ attributed_text:
+ dependency: transitive
+ description:
+ name: attributed_text
+ sha256: "177ea01f58a8d8df279f4066834375a2009bdd304d559c084bb06f784b258477"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.5"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.1"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ coverage:
+ dependency: transitive
+ description:
+ name: coverage
+ sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.8.0"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.3"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.28"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test_robots:
+ dependency: transitive
+ description:
+ name: flutter_test_robots
+ sha256: "3b00f2081148bde55190997c2772f934ad2f4529cbcfc4ccfa593f8ddc117a28"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.24"
+ flutter_test_runners:
+ dependency: transitive
+ description:
+ name: flutter_test_runners
+ sha256: cc575117ed66a79185a26995399d7048341517a1bd21188cb43753739627832d
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.4"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ follow_the_leader:
+ dependency: transitive
+ description:
+ name: follow_the_leader
+ sha256: "2e4c4ebe6b3f1942b2385904b118ba8ba117fae0b30c8c453be0b64a271dd07a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.2"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.1"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.0.2"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.10"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ linkify:
+ dependency: transitive
+ description:
+ name: linkify
+ sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ markdown:
+ dependency: transitive
+ description:
+ name: markdown
+ sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.3.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.17"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.16.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
+ node_preamble:
+ dependency: transitive
+ description:
+ name: node_preamble
+ sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ overlord:
+ dependency: transitive
+ description:
+ name: overlord
+ sha256: "532f5685ac09ee805d97ce89794a4eeda41672c32955b4a835bdfce93e720a05"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.1"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.1"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ shelf_packages_handler:
+ dependency: transitive
+ description:
+ name: shelf_packages_handler
+ sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ shelf_static:
+ dependency: transitive
+ description:
+ name: shelf_static
+ sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ source_map_stack_trace:
+ dependency: transitive
+ description:
+ name: source_map_stack_trace
+ sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ source_maps:
+ dependency: transitive
+ description:
+ name: source_maps
+ sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.12"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.12.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ super_editor:
+ dependency: "direct main"
+ description:
+ path: "../../super_editor"
+ relative: true
+ source: path
+ version: "0.3.0-dev.38"
+ super_keyboard:
+ dependency: transitive
+ description:
+ name: super_keyboard
+ sha256: e3accebf33635f760efbd4d3c13f6484242a09e773ce8e711f4aa745d52b73b1
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.0"
+ super_text_layout:
+ dependency: "direct overridden"
+ description:
+ path: "../../super_text_layout"
+ relative: true
+ source: path
+ version: "0.1.19"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test:
+ dependency: transitive
+ description:
+ name: test
+ sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.26.2"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.6"
+ test_core:
+ dependency: transitive
+ description:
+ name: test_core
+ sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.11"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.2"
+ url_launcher:
+ dependency: transitive
+ description:
+ name: url_launcher
+ sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.1"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.3"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.0"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.1"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ uuid:
+ dependency: transitive
+ description:
+ name: uuid
+ sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.5.1"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.3.0"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.5"
+ webkit_inspection_protocol:
+ dependency: transitive
+ description:
+ name: webkit_inspection_protocol
+ sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+sdks:
+ dart: ">=3.8.0-0 <4.0.0"
+ flutter: ">=3.27.0"
diff --git a/super_clones/bear/pubspec.yaml b/super_clones/bear/pubspec.yaml
new file mode 100644
index 0000000000..6995a4037f
--- /dev/null
+++ b/super_clones/bear/pubspec.yaml
@@ -0,0 +1,61 @@
+name: bear
+description: "A Flutter clone of Bear"
+publish_to: "none"
+
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.6.0
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ cupertino_icons: ^1.0.6
+ super_editor:
+ path: ../../super_editor
+
+dependency_overrides:
+ super_editor:
+ path: ../../super_editor
+ super_text_layout:
+ path: ../../super_text_layout
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_lints: ^3.0.0
+
+flutter:
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/super_clones/google_docs/.gitignore b/super_clones/google_docs/.gitignore
new file mode 100644
index 0000000000..79c113f9b5
--- /dev/null
+++ b/super_clones/google_docs/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/super_clones/google_docs/.metadata b/super_clones/google_docs/.metadata
new file mode 100644
index 0000000000..779f429981
--- /dev/null
+++ b/super_clones/google_docs/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "c0a481e31dcf8d9bd2dadc134aad0afe9e411d80"
+ channel: "master"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: android
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: ios
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: linux
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: macos
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: web
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ - platform: windows
+ create_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+ base_revision: c0a481e31dcf8d9bd2dadc134aad0afe9e411d80
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/super_clones/google_docs/README.md b/super_clones/google_docs/README.md
new file mode 100644
index 0000000000..c95428ec29
--- /dev/null
+++ b/super_clones/google_docs/README.md
@@ -0,0 +1,6 @@
+# Docs Example
+
+A Super Editor example app that approximates the UX of Google Docs.
+
+The purpose of this example app is to ensure that common desktop word processing use-cases are
+achievable with Super Editor.
diff --git a/super_clones/google_docs/analysis_options.yaml b/super_clones/google_docs/analysis_options.yaml
new file mode 100644
index 0000000000..0d2902135c
--- /dev/null
+++ b/super_clones/google_docs/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/super_clones/google_docs/android/.gitignore b/super_clones/google_docs/android/.gitignore
new file mode 100644
index 0000000000..6f568019d3
--- /dev/null
+++ b/super_clones/google_docs/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/super_clones/google_docs/android/app/build.gradle b/super_clones/google_docs/android/app/build.gradle
new file mode 100644
index 0000000000..b61cebd956
--- /dev/null
+++ b/super_clones/google_docs/android/app/build.gradle
@@ -0,0 +1,67 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+android {
+ namespace "com.example.example_docs"
+ compileSdk flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.example.example_docs"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion flutter.targetSdkVersion
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {}
diff --git a/super_clones/google_docs/android/app/src/debug/AndroidManifest.xml b/super_clones/google_docs/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000000..399f6981d5
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/main/AndroidManifest.xml b/super_clones/google_docs/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..78b2b6e6be
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/main/kotlin/com/example/example_docs/MainActivity.kt b/super_clones/google_docs/android/app/src/main/kotlin/com/example/example_docs/MainActivity.kt
new file mode 100644
index 0000000000..c09774501b
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/kotlin/com/example/example_docs/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.example_docs
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity()
diff --git a/super_clones/google_docs/android/app/src/main/res/drawable-v21/launch_background.xml b/super_clones/google_docs/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000000..f74085f3f6
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/main/res/drawable/launch_background.xml b/super_clones/google_docs/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000000..304732f884
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/super_clones/google_docs/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..db77bb4b7b
Binary files /dev/null and b/super_clones/google_docs/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/super_clones/google_docs/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/super_clones/google_docs/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..17987b79bb
Binary files /dev/null and b/super_clones/google_docs/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/super_clones/google_docs/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/super_clones/google_docs/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..09d4391482
Binary files /dev/null and b/super_clones/google_docs/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/super_clones/google_docs/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/super_clones/google_docs/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..d5f1c8d34e
Binary files /dev/null and b/super_clones/google_docs/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/super_clones/google_docs/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/super_clones/google_docs/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..4d6372eebd
Binary files /dev/null and b/super_clones/google_docs/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/super_clones/google_docs/android/app/src/main/res/values-night/styles.xml b/super_clones/google_docs/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000000..06952be745
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/main/res/values/styles.xml b/super_clones/google_docs/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..cb1ef88056
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/android/app/src/profile/AndroidManifest.xml b/super_clones/google_docs/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000000..399f6981d5
--- /dev/null
+++ b/super_clones/google_docs/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/super_clones/google_docs/android/build.gradle b/super_clones/google_docs/android/build.gradle
new file mode 100644
index 0000000000..bc157bd1a1
--- /dev/null
+++ b/super_clones/google_docs/android/build.gradle
@@ -0,0 +1,18 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
diff --git a/super_clones/google_docs/android/gradle.properties b/super_clones/google_docs/android/gradle.properties
new file mode 100644
index 0000000000..598d13fee4
--- /dev/null
+++ b/super_clones/google_docs/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx4G
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/super_clones/google_docs/android/gradle/wrapper/gradle-wrapper.properties b/super_clones/google_docs/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..e1ca574ef0
--- /dev/null
+++ b/super_clones/google_docs/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
diff --git a/super_clones/google_docs/android/settings.gradle b/super_clones/google_docs/android/settings.gradle
new file mode 100644
index 0000000000..1d6d19b7f8
--- /dev/null
+++ b/super_clones/google_docs/android/settings.gradle
@@ -0,0 +1,26 @@
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }
+ settings.ext.flutterSdkPath = flutterSdkPath()
+
+ includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "7.3.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+}
+
+include ":app"
diff --git a/super_clones/google_docs/assets/images/docs_logo.png b/super_clones/google_docs/assets/images/docs_logo.png
new file mode 100644
index 0000000000..eb4cd1248a
Binary files /dev/null and b/super_clones/google_docs/assets/images/docs_logo.png differ
diff --git a/super_clones/google_docs/ios/.gitignore b/super_clones/google_docs/ios/.gitignore
new file mode 100644
index 0000000000..7a7f9873ad
--- /dev/null
+++ b/super_clones/google_docs/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/super_clones/google_docs/ios/Flutter/AppFrameworkInfo.plist b/super_clones/google_docs/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000000..9625e105df
--- /dev/null
+++ b/super_clones/google_docs/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 11.0
+
+
diff --git a/super_clones/google_docs/ios/Flutter/Debug.xcconfig b/super_clones/google_docs/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000000..ec97fc6f30
--- /dev/null
+++ b/super_clones/google_docs/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/super_clones/google_docs/ios/Flutter/Release.xcconfig b/super_clones/google_docs/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000000..c4855bfe20
--- /dev/null
+++ b/super_clones/google_docs/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/super_clones/google_docs/ios/Podfile b/super_clones/google_docs/ios/Podfile
new file mode 100644
index 0000000000..fdcc671eb3
--- /dev/null
+++ b/super_clones/google_docs/ios/Podfile
@@ -0,0 +1,44 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '11.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/super_clones/google_docs/ios/Podfile.lock b/super_clones/google_docs/ios/Podfile.lock
new file mode 100644
index 0000000000..76356722e9
--- /dev/null
+++ b/super_clones/google_docs/ios/Podfile.lock
@@ -0,0 +1,22 @@
+PODS:
+ - Flutter (1.0.0)
+ - url_launcher_ios (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+
+SPEC CHECKSUMS:
+ Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+ url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
+
+PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
+
+COCOAPODS: 1.12.1
diff --git a/super_clones/google_docs/ios/Runner.xcodeproj/project.pbxproj b/super_clones/google_docs/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..0413024639
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,725 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 3FF9FC0B28AE90185295F58A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7C95A96CC051122C7CCB49E /* Pods_RunnerTests.framework */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ A91EC9E3EFDBAB9137BA593C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 720EA7F52BFD549285F1C931 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 1BE6B197E0ADF30C0C46E916 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 2DA3C79B912813040E70B57A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 48EE20398213D9FBF506F82A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 720EA7F52BFD549285F1C931 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 77B999B93EC9D0BB1E5789D8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D2DDE7117EF5F16629F366B5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ E665B59E41A605DFD197B01A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ F7C95A96CC051122C7CCB49E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 62889E8CDB517A5365C01660 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3FF9FC0B28AE90185295F58A /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A91EC9E3EFDBAB9137BA593C /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ C184FCB9552E23670DFA170B /* Pods */,
+ D15D70BBF26BCCB42346F97A /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ C184FCB9552E23670DFA170B /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 77B999B93EC9D0BB1E5789D8 /* Pods-Runner.debug.xcconfig */,
+ 48EE20398213D9FBF506F82A /* Pods-Runner.release.xcconfig */,
+ D2DDE7117EF5F16629F366B5 /* Pods-Runner.profile.xcconfig */,
+ 1BE6B197E0ADF30C0C46E916 /* Pods-RunnerTests.debug.xcconfig */,
+ E665B59E41A605DFD197B01A /* Pods-RunnerTests.release.xcconfig */,
+ 2DA3C79B912813040E70B57A /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ D15D70BBF26BCCB42346F97A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 720EA7F52BFD549285F1C931 /* Pods_Runner.framework */,
+ F7C95A96CC051122C7CCB49E /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 73BC784FE3F145CFE5B69972 /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ 62889E8CDB517A5365C01660 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 1157AED947F6BE0DF96F18BF /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 0ACD19CA4D70B7FC39924647 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1430;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 0ACD19CA4D70B7FC39924647 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 1157AED947F6BE0DF96F18BF /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 73BC784FE3F145CFE5B69972 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F9S7W35N3F;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 1BE6B197E0ADF30C0C46E916 /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = E665B59E41A605DFD197B01A /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 2DA3C79B912813040E70B57A /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F9S7W35N3F;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F9S7W35N3F;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleDocs;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/super_clones/google_docs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..87131a09be
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcworkspace/contents.xcworkspacedata b/super_clones/google_docs/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/super_clones/google_docs/ios/Runner/AppDelegate.swift b/super_clones/google_docs/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..70693e4a8c
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..d36b1fab2d
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000000..dc9ada4725
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000000..7353c41ecf
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000000..797d452e45
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000000..6ed2d933e1
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000000..4cd7b0099c
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000000..fe730945a0
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000000..321773cd85
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000000..797d452e45
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000000..502f463a9b
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000000..0ec3034392
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000000..0ec3034392
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000000..e9f5fea27c
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000000..84ac32ae7d
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000000..8953cba090
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000000..0467bf12aa
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000000..0bedcf2fd4
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000000..89c2725b70
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/super_clones/google_docs/ios/Runner/Base.lproj/LaunchScreen.storyboard b/super_clones/google_docs/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..f2e259c7c9
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/ios/Runner/Base.lproj/Main.storyboard b/super_clones/google_docs/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000000..f3c28516fb
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/super_clones/google_docs/ios/Runner/Info.plist b/super_clones/google_docs/ios/Runner/Info.plist
new file mode 100644
index 0000000000..0aa8cf88f5
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Example Docs
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ example_docs
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+
diff --git a/super_clones/google_docs/ios/Runner/Runner-Bridging-Header.h b/super_clones/google_docs/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000000..308a2a560b
--- /dev/null
+++ b/super_clones/google_docs/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/super_clones/google_docs/ios/RunnerTests/RunnerTests.swift b/super_clones/google_docs/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000000..86a7c3b1b6
--- /dev/null
+++ b/super_clones/google_docs/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/super_clones/google_docs/lib/app.dart b/super_clones/google_docs/lib/app.dart
new file mode 100644
index 0000000000..747e7bfb27
--- /dev/null
+++ b/super_clones/google_docs/lib/app.dart
@@ -0,0 +1,231 @@
+import 'package:example_docs/app_menu.dart';
+import 'package:example_docs/editor.dart';
+import 'package:example_docs/theme.dart';
+import 'package:example_docs/toolbar.dart';
+import 'package:flutter/material.dart';
+import 'package:super_editor/super_editor.dart';
+
+class DocsApp extends StatelessWidget {
+ const DocsApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Docs',
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+ useMaterial3: true,
+ ),
+ home: const MainScreen(),
+ debugShowCheckedModeBanner: false,
+ );
+ }
+}
+
+class MainScreen extends StatefulWidget {
+ const MainScreen({
+ super.key,
+ });
+
+ @override
+ State createState() => _MainScreenState();
+}
+
+class _MainScreenState extends State {
+ final FocusNode _editorFocusNode = FocusNode();
+ late MutableDocument _document;
+ late MutableDocumentComposer _composer;
+ late Editor _editor;
+
+ int _zoomLevel = 100;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _document = _createInitialDocument();
+ _composer = MutableDocumentComposer();
+ _editor = createDefaultDocumentEditor(document: _document, composer: _composer);
+ }
+
+ @override
+ void dispose() {
+ _editor.dispose();
+ _composer.dispose();
+ _document.dispose();
+ _editorFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFf9fbfd),
+ body: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _AppHeaderPane(
+ editorFocusNode: _editorFocusNode,
+ document: _document,
+ editor: _editor,
+ composer: _composer,
+ onZoomChange: (zoom) => setState(() {
+ _zoomLevel = zoom;
+ }),
+ ),
+ const SizedBox(height: 4),
+ const Divider(height: 1, thickness: 1, color: Color(0xFFc4c7c5)),
+ Expanded(
+ child: Transform.scale(
+ alignment: Alignment.topCenter,
+ scale: _zoomLevel / 100.0,
+ child: DocsEditor(
+ focusNode: _editorFocusNode,
+ document: _document,
+ composer: _composer,
+ editor: _editor,
+ ),
+ ),
+ ),
+ ],
+ ));
+ }
+}
+
+/// The pane that appears at the top of the app, which includes the document title, app menu,
+/// and editor toolbar.
+class _AppHeaderPane extends StatelessWidget {
+ const _AppHeaderPane({
+ required this.editorFocusNode,
+ required this.document,
+ required this.editor,
+ required this.composer,
+ required this.onZoomChange,
+ });
+
+ final FocusNode editorFocusNode;
+ final Document document;
+ final Editor editor;
+ final MutableDocumentComposer composer;
+ final void Function(int zoom) onZoomChange;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
+ child: Column(
+ children: [
+ _buildTitleAndMenuBar(),
+ const SizedBox(height: 16),
+ DocsEditorToolbar(
+ editorFocusNode: editorFocusNode,
+ document: document,
+ editor: editor,
+ composer: composer,
+ onZoomChange: onZoomChange,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTitleAndMenuBar() {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(width: 8),
+ Image.asset("assets/images/docs_logo.png"),
+ const SizedBox(width: 4),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildDocumentTitleAndActions(),
+ _buildMenus(),
+ ],
+ )
+ ],
+ );
+ }
+
+ Widget _buildDocumentTitleAndActions() {
+ return const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: menuButtonHorizontalPadding), // Push title to the right to match first letter of first menu item
+ Text(
+ "Some Document",
+ style: TextStyle(
+ color: Colors.black,
+ fontSize: 18,
+ letterSpacing: -0.8,
+ ),
+ ),
+ SizedBox(width: 24),
+ Icon(Icons.star_border, size: 18, color: titleActionIconColor),
+ SizedBox(width: 12),
+ Icon(Icons.drive_folder_upload, size: 18, color: titleActionIconColor),
+ SizedBox(width: 12),
+ Icon(Icons.cloud_done_outlined, size: 18, color: titleActionIconColor),
+ ],
+ );
+ }
+
+ Widget _buildMenus() {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildMenuButton("File"),
+ _buildMenuButton("Edit"),
+ _buildMenuButton("View"),
+ _buildMenuButton("Insert"),
+ _buildMenuButton("Format"),
+ _buildMenuButton("Tools"),
+ _buildMenuButton("Extensions"),
+ _buildMenuButton("Help"),
+ ],
+ );
+ }
+
+ Widget _buildMenuButton(String label) {
+ return DocsAppMenu(
+ label: label,
+ items: const [
+ // TODO: create options for each menu that matches Google Docs
+ DocsAppMenuItem(id: "new", label: "New"),
+ DocsAppMenuItem(id: "open", label: "Open"),
+ DocsAppMenuItem(id: "copy", label: "Make a Copy"),
+ ],
+ onSelected: (_) {},
+ );
+ }
+}
+
+// Creates the document that's initially displayed when the app launches.
+MutableDocument _createInitialDocument() {
+ return MutableDocument(
+ nodes: [
+ ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText("Welcome to a Super Editor version of Docs!"),
+ metadata: {
+ "blockType": header1Attribution,
+ },
+ ),
+ ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText("By: The Super Editor Team"),
+ ),
+ ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText(
+ "This is an example document editor experience, which is meant to mimic the UX of Google Docs. We created this example app to ensure that common desktop word processing UX can be built with Super Editor."),
+ ),
+ ParagraphNode(
+ id: Editor.createNodeId(),
+ text: AttributedText(
+ "A typical desktop word processor is comprised of a pane at the top of the window, which includes some combination of information about the current document, as well as toolbars that present editing options. The remainder of the window is filled by an editable document."),
+ ),
+ ],
+ );
+}
diff --git a/super_clones/google_docs/lib/app_menu.dart b/super_clones/google_docs/lib/app_menu.dart
new file mode 100644
index 0000000000..8429c819c5
--- /dev/null
+++ b/super_clones/google_docs/lib/app_menu.dart
@@ -0,0 +1,314 @@
+import 'package:flutter/material.dart';
+import 'package:follow_the_leader/follow_the_leader.dart';
+import 'package:overlord/overlord.dart';
+import 'package:super_editor/super_editor.dart';
+
+import 'theme.dart';
+
+/// An application-level popup menu.
+///
+/// A menu button is displayed with the given [label].
+///
+/// When the user clicks the menu button, a popover appears, which displays the given [items] as
+/// a vertical list. The popover is left-aligned with the menu button, and appears immediately below
+/// the menu button.
+///
+/// The popover list height is based on the following rules:
+///
+/// 1. The popover is displayed as tall as all items in the list, if there's enough room, or
+/// 2. The popover is displayed as tall as the available space and becomes scrollable.
+///
+/// The popover list includes keyboard selection behaviors:
+///
+/// * Pressing UP/DOWN moves the "active" item selection up/down.
+/// * Pressing UP with the first item active moves the active item selection to the last item.
+/// * Pressing DOWN with the last item active moves the active item selection to the first item.
+/// * Pressing ENTER selects the currently active item and closes the popover list.
+class DocsAppMenu extends StatefulWidget {
+ const DocsAppMenu({
+ super.key,
+ this.parentFocusNode,
+ this.boundaryKey,
+ required this.label,
+ required this.items,
+ required this.onSelected,
+ });
+
+ /// The [FocusNode], to which the popover list's [FocusNode] will be added as a child.
+ ///
+ /// In Flutter, [FocusNode]s have parents and children. This relationship allows an
+ /// entire ancestor path to "have focus", but only the lowest level descendant
+ /// in that path has "primary focus". This path is important because various
+ /// widgets alter their presentation or behavior based on whether or not they
+ /// currently have focus, even if they only have "non-primary focus".
+ ///
+ /// When the popover list of items is visible, that list will have primary focus.
+ /// Moreover, because the popover list is built in an `Overlay`, none of your
+ /// widgets are in the natural focus path for that popover list. Therefore, if you
+ /// need your widget tree to retain focus while the popover list is visible, then
+ /// you need to provide the [FocusNode] that the popover list should use as its
+ /// parent, thereby retaining focus for your widgets.
+ final FocusNode? parentFocusNode;
+
+ /// A [GlobalKey] to a widget that determines the bounds where the popover list can be displayed.
+ ///
+ /// As the popover list follows the selected item, it can be displayed off-screen if this [DocsAppMenu]
+ /// is close to the bottom of the screen.
+ ///
+ /// Passing a [boundaryKey] causes the popover list to be confined to the bounds of the widget
+ /// bound to the [boundaryKey].
+ ///
+ /// If `null`, the popover list is confined to the screen bounds, defined by the result of `MediaQuery.sizeOf`.
+ final GlobalKey? boundaryKey;
+
+ /// The name of the menu, which is displayed on the menu button.
+ final String label;
+
+ /// The items that will be displayed in the popover list.
+ ///
+ /// For each item, its [DocsAppMenuItem.label] is displayed.
+ final List items;
+
+ /// Called when the user selects an item on the popover list.
+ final void Function(DocsAppMenuItem? value) onSelected;
+
+ @override
+ State createState() => _DocsAppMenuState();
+}
+
+class _DocsAppMenuState extends State {
+ /// Shows and hides the popover.
+ final PopoverController _popoverController = PopoverController();
+
+ /// The [FocusNode] of the popover list.
+ final FocusNode _popoverFocusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _popoverController.dispose();
+ _popoverFocusNode.dispose();
+ super.dispose();
+ }
+
+ void _onItemSelected(DocsAppMenuItem? value) {
+ _popoverController.close();
+ widget.onSelected(value);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopoverScaffold(
+ controller: _popoverController,
+ buttonBuilder: _buildButton,
+ popoverFocusNode: _popoverFocusNode,
+ parentFocusNode: widget.parentFocusNode,
+ boundaryKey: widget.boundaryKey,
+ popoverGeometry: DocsAppMenuPopoverGeometry(),
+ popoverBuilder: (context) => DocsAppMenuPopoverAppearance(
+ child: ItemSelectionList(
+ focusNode: _popoverFocusNode,
+ value: null,
+ items: widget.items,
+ itemBuilder: _buildPopoverListItem,
+ onItemSelected: _onItemSelected,
+ onCancel: () => _popoverController.close(),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildButton(BuildContext context) {
+ return DocsMenuButton(
+ label: widget.label,
+ onTap: () => _popoverController.open(),
+ );
+ }
+
+ Widget _buildPopoverListItem(BuildContext context, DocsAppMenuItem item, bool isActive, VoidCallback onTap) {
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ color: isActive ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
+ child: Text(
+ item.label,
+ style: const TextStyle(
+ color: Colors.black,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// A [PopoverGeometry] designed for a Docs app menu.
+///
+/// This [PopoverGeometry] aligns the top-left corner of the popover with the bottom-left corner
+/// of the menu button that launched it.
+class DocsAppMenuPopoverGeometry extends PopoverGeometry {
+ @override
+ PopoverAligner get aligner => FunctionalPopoverAligner(
+ (Rect globalLeaderRect, Size followerSize, Size screenSize, GlobalKey? boundaryKey) {
+ final boundsBox = boundaryKey?.currentContext?.findRenderObject() as RenderBox?;
+ final bounds = boundsBox != null
+ ? Rect.fromPoints(
+ boundsBox.localToGlobal(Offset.zero),
+ boundsBox.localToGlobal(boundsBox.size.bottomRight(Offset.zero)),
+ )
+ : Offset.zero & screenSize;
+ late FollowerAlignment alignment;
+
+ if (globalLeaderRect.bottom + followerSize.height < bounds.bottom) {
+ // The follower fits below the leader.
+ alignment = const FollowerAlignment(
+ leaderAnchor: Alignment.bottomLeft,
+ followerAnchor: Alignment.topLeft,
+ );
+ } else if (globalLeaderRect.top - followerSize.height > bounds.top) {
+ // The follower fits above the leader.
+ alignment = const FollowerAlignment(
+ leaderAnchor: Alignment.bottomLeft,
+ followerAnchor: Alignment.topLeft,
+ );
+ } else {
+ // There isn't enough room to fully display the follower below or above the leader.
+ // Pin the popover list to the bottom, letting the follower cover the leader.
+ alignment = const FollowerAlignment(
+ leaderAnchor: Alignment.bottomLeft,
+ followerAnchor: Alignment.topLeft,
+ );
+ }
+
+ return alignment;
+ },
+ );
+
+ @override
+ BoxConstraints get constraints => const BoxConstraints.tightFor(width: 250);
+}
+
+/// The shape and color of a popover menu.
+class DocsAppMenuPopoverAppearance extends StatefulWidget {
+ const DocsAppMenuPopoverAppearance({
+ super.key,
+ required this.child,
+ });
+
+ final Widget child;
+
+ @override
+ State createState() => _DocsAppMenuPopoverAppearanceState();
+}
+
+class _DocsAppMenuPopoverAppearanceState extends State
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _animationController;
+ late final Animation _containerFadeInAnimation;
+
+ @override
+ void initState() {
+ super.initState();
+ _animationController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+
+ _containerFadeInAnimation = CurvedAnimation(
+ parent: _animationController,
+ curve: Curves.easeInOut,
+ );
+
+ _animationController.forward();
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ borderRadius: const BorderRadius.only(
+ topRight: Radius.circular(4),
+ bottomLeft: Radius.circular(4),
+ bottomRight: Radius.circular(4),
+ ),
+ clipBehavior: Clip.hardEdge,
+ color: Colors.white,
+ elevation: 8,
+ child: FadeTransition(
+ opacity: _containerFadeInAnimation,
+ child: widget.child,
+ ),
+ );
+ }
+}
+
+/// An app menu button, such as a button that reads "File" or "Edit" at the top of
+/// an app.
+class DocsMenuButton extends StatelessWidget {
+ const DocsMenuButton({
+ super.key,
+ required this.label,
+ required this.onTap,
+ });
+
+ /// The text displayed on the button.
+ final String label;
+
+ /// Called when the user taps the button.
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return TextButton(
+ onPressed: onTap,
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.black,
+ surfaceTintColor: const Color(0xFFedf2fa),
+ padding: const EdgeInsets.symmetric(horizontal: menuButtonHorizontalPadding, vertical: 12),
+ minimumSize: Size.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ visualDensity: VisualDensity.compact,
+ textStyle: const TextStyle(
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ child: Text(label),
+ );
+ }
+}
+
+/// An item that's displayed within a popover app menu, such as when the user clicks on
+/// an app's "File" menu.
+///
+/// Two [DocsAppMenuItem]s are considered to be equal if they have the same [id].
+class DocsAppMenuItem {
+ const DocsAppMenuItem({
+ required this.id,
+ required this.label,
+ });
+
+ /// The value that identifies this item.
+ final String id;
+
+ /// The text that is displayed.
+ final String label;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is DocsAppMenuItem && runtimeType == other.runtimeType && id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
+}
diff --git a/super_clones/google_docs/lib/editor.dart b/super_clones/google_docs/lib/editor.dart
new file mode 100644
index 0000000000..c032e18530
--- /dev/null
+++ b/super_clones/google_docs/lib/editor.dart
@@ -0,0 +1,113 @@
+import 'package:flutter/widgets.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:super_editor/super_editor.dart';
+
+import 'theme.dart';
+
+/// An editable document within a Docs app.
+///
+/// This is the primary editing experience for the app. A [DocsEditor] takes up
+/// all the space beneath the app header pane.
+class DocsEditor extends StatefulWidget {
+ const DocsEditor({
+ super.key,
+ this.focusNode,
+ required this.document,
+ required this.composer,
+ required this.editor,
+ });
+
+ final FocusNode? focusNode;
+ final MutableDocument document;
+ final MutableDocumentComposer composer;
+ final Editor editor;
+
+ @override
+ State createState() => _DocsEditorState();
+}
+
+class _DocsEditorState extends State {
+ late FocusNode _editorFocusNode;
+
+ @override
+ void initState() {
+ super.initState();
+ _editorFocusNode = widget.focusNode ?? FocusNode();
+ }
+
+ @override
+ void didUpdateWidget(covariant DocsEditor oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.focusNode != widget.focusNode) {
+ if (oldWidget.focusNode == null) {
+ _editorFocusNode.dispose();
+ }
+ _editorFocusNode = widget.focusNode ?? FocusNode();
+ }
+ }
+
+ @override
+ void dispose() {
+ if (widget.focusNode == null) {
+ _editorFocusNode.dispose();
+ }
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ListenableBuilder(
+ listenable: _editorFocusNode,
+ builder: (context, child) {
+ return SuperEditor(
+ focusNode: _editorFocusNode,
+ editor: widget.editor,
+ stylesheet: defaultStylesheet.copyWith(
+ addRulesAfter: docsStylesheet,
+ inlineTextStyler: _applyFontFamily,
+ ),
+ selectionStyle: _editorFocusNode.hasPrimaryFocus //
+ ? _standardEditorSelectionStyle
+ : _unfocusedEditorSelectionStyle,
+ selectionPolicies: const SuperEditorSelectionPolicies(
+ clearSelectionWhenEditorLosesFocus: false,
+ clearSelectionWhenImeConnectionCloses: false,
+ ),
+ documentOverlayBuilders: const [
+ DefaultCaretOverlayBuilder(
+ displayCaretWithExpandedSelection: false,
+ ),
+ ],
+ componentBuilders: [
+ ...defaultComponentBuilders,
+ TaskComponentBuilder(widget.editor),
+ ],
+ );
+ },
+ );
+ }
+
+ TextStyle _applyFontFamily(Set attributions, TextStyle existingStyle) {
+ TextStyle styles = defaultInlineTextStyler(attributions, existingStyle);
+
+ final fontFamilyAttribution = attributions.whereType().firstOrNull;
+ if (fontFamilyAttribution != null) {
+ styles = GoogleFonts.getFont(
+ fontFamilyAttribution.fontFamily,
+ textStyle: styles,
+ );
+ }
+
+ return styles;
+ }
+}
+
+// Selection styles when the editor has focus.
+const _standardEditorSelectionStyle = defaultSelectionStyle;
+
+// Selection styles when the editor doesn't have focus.
+final _unfocusedEditorSelectionStyle = SelectionStyles(
+ selectionColor: const Color(0xFFDDDDDD),
+ highlightEmptyTextBlocks: defaultSelectionStyle.highlightEmptyTextBlocks,
+);
diff --git a/super_clones/google_docs/lib/infrastructure/color_selector.dart b/super_clones/google_docs/lib/infrastructure/color_selector.dart
new file mode 100644
index 0000000000..828a6f89c2
--- /dev/null
+++ b/super_clones/google_docs/lib/infrastructure/color_selector.dart
@@ -0,0 +1,354 @@
+import 'package:example_docs/infrastructure/selectable_grid.dart';
+import 'package:example_docs/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:overlord/overlord.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// A selection control, which displays a button with the selected color, and upon tap, displays a
+/// color picker with the available colors, from which the user can select a different color.
+///
+/// Includes the following keyboard selection behaviors:
+///
+/// * Pressing UP/DOWN moves the "active" color selection up/down.
+/// * Pressing LEFT/RIGHT moves the "active" color selection left/right.
+/// * Pressing ENTER selects the currently active color.
+class ColorSelector extends StatefulWidget {
+ const ColorSelector({
+ super.key,
+ this.parentFocusNode,
+ this.tapRegionGroupId,
+ this.boundaryKey,
+ this.selectedColor,
+ this.colors = defaultColors,
+ this.columnCount = 10,
+ this.showClearButton = false,
+ required this.onSelected,
+ required this.colorButtonBuilder,
+ });
+
+ /// The [FocusNode], to which the color picker's [FocusNode] will be added as a child.
+ ///
+ /// See [PopoverScaffold.parentFocusNode] for more information.
+ final FocusNode? parentFocusNode;
+
+ /// A group ID for a tap region that is shared with the color picker.
+ ///
+ /// Tapping on a [TapRegion] with the same [tapRegionGroupId]
+ /// won't invoke [onTapOutside].
+ final String? tapRegionGroupId;
+
+ /// A [GlobalKey] to a widget that determines the bounds where the color picker can be displayed.
+ ///
+ /// See [PopoverScaffold.boundaryKey] for more information.
+ final GlobalKey? boundaryKey;
+
+ /// The currently selected color or `null` if no color is selected.
+ final Color? selectedColor;
+
+ /// The colors that will be displayed in the color picker.
+ ///
+ /// Each color is displayed as a circle.
+ final List colors;
+
+ /// Defines the number of columns that the color picker should have.
+ final int columnCount;
+
+ /// Whether or not the color picker should display a "Clear" button.
+ ///
+ /// Pressing that button call [onSelected] with a `null` value.
+ final bool showClearButton;
+
+ /// Called when the user selects an item on the color picker.
+ final void Function(Color? value) onSelected;
+
+ /// Builds the button of this [ColorSelector].
+ final Widget Function(BuildContext context, Color? selectedColor) colorButtonBuilder;
+
+ @override
+ State createState() => _ColorSelectorState();
+}
+
+class _ColorSelectorState extends State {
+ /// Shows and hides the popover.
+ final PopoverController _popoverController = PopoverController();
+
+ /// The [FocusNode] of the color picker.
+ final FocusNode _popoverFocusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _popoverController.dispose();
+ _popoverFocusNode.dispose();
+ super.dispose();
+ }
+
+ void _onItemSelected(Color? value) {
+ _popoverController.close();
+ widget.onSelected(value);
+ }
+
+ /// Decides a foreground color for a [background] color based on the brightness of the [background].
+ ///
+ /// Returns [Colors.white] if [background] is a dark color and [Colors.black] otherwise.
+ Color _getColorForCheckIcon(Color background) {
+ // Adapted from https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color/3943023#3943023.
+ final intensity = (0.299 * background.red) + (0.587 * background.green) + (0.114 * background.blue);
+ return intensity > 130 ? Colors.black : Colors.white;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopoverScaffold(
+ controller: _popoverController,
+ tapRegionGroupId: widget.tapRegionGroupId,
+ buttonBuilder: _buildButton,
+ popoverFocusNode: _popoverFocusNode,
+ parentFocusNode: widget.parentFocusNode,
+ boundaryKey: widget.boundaryKey,
+ popoverBuilder: (context) => Material(
+ elevation: 8,
+ borderRadius: BorderRadius.circular(12),
+ clipBehavior: Clip.hardEdge,
+ color: Colors.white,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (widget.showClearButton) //
+ _buildClearButton(),
+ if (widget.showClearButton) //
+ const SizedBox(height: 3),
+ _buildColorGrid(),
+ _buildCustomColorsButton(),
+ _buildFooterButtons(),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildButton(BuildContext context) {
+ return TextButton(
+ onPressed: () => _popoverController.open(),
+ style: defaultToolbarButtonStyle,
+ child: widget.colorButtonBuilder(context, widget.selectedColor),
+ );
+ }
+
+ Widget _buildClearButton() {
+ return SizedBox(
+ width: 243,
+ height: 32,
+ child: TextButton.icon(
+ onPressed: () => _onItemSelected(null),
+ style: ButtonStyle(
+ foregroundColor: WidgetStateProperty.all(Colors.black),
+ backgroundColor: WidgetStateProperty.resolveWith(getButtonColor),
+ padding: WidgetStateProperty.all(const EdgeInsets.all(5)),
+ shape: WidgetStateProperty.all(
+ const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(5)),
+ ),
+ ),
+ ),
+ icon: const Icon(Icons.format_color_reset),
+ label: const SizedBox(
+ width: double.infinity,
+ child: Text(
+ 'None',
+ textAlign: TextAlign.left,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildColorGrid() {
+ return ConstrainedBox(
+ constraints: BoxConstraints(
+ maxHeight: (widget.colors.length / widget.columnCount * 20),
+ maxWidth: 243,
+ ),
+ child: SelectableGrid(
+ focusNode: _popoverFocusNode,
+ value: widget.selectedColor,
+ items: widget.colors,
+ itemBuilder: _buildPopoverGridItem,
+ onItemSelected: _onItemSelected,
+ onCancel: () => _popoverController.close(),
+ columnCount: widget.columnCount,
+ mainAxisExtent: 18,
+ ),
+ );
+ }
+
+ Widget _buildPopoverGridItem(BuildContext context, Color item, bool isActive, VoidCallback onTap) {
+ return InkWell(
+ onTap: onTap,
+ customBorder: const CircleBorder(),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ alignment: Alignment.centerLeft,
+ decoration: BoxDecoration(
+ color: item,
+ shape: BoxShape.circle,
+ boxShadow: [
+ if (isActive) //
+ const BoxShadow(
+ blurRadius: 3,
+ )
+ ],
+ ),
+ ),
+ if (item == widget.selectedColor) //
+ Icon(
+ Icons.check,
+ size: 15,
+ color: _getColorForCheckIcon(item),
+ )
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCustomColorsButton() {
+ return SizedBox(
+ width: 243,
+ height: 24,
+ child: TextButton(
+ onPressed: () {},
+ style: ButtonStyle(
+ foregroundColor: WidgetStateProperty.all(Colors.black),
+ backgroundColor: WidgetStateProperty.resolveWith(getButtonColor),
+ padding: WidgetStateProperty.all(const EdgeInsets.all(5)),
+ shape: WidgetStateProperty.all(
+ const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(5)),
+ ),
+ ),
+ ),
+ child: const SizedBox(
+ width: double.infinity,
+ child: Text(
+ 'Custom Colors',
+ textAlign: TextAlign.left,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFooterButtons() {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextButton(
+ onPressed: () {},
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.add_circle_outline),
+ ),
+ TextButton(
+ onPressed: () {},
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.colorize),
+ ),
+ ],
+ );
+ }
+}
+
+const defaultColors = [
+ Colors.black,
+ Color(0xFF434343),
+ Color(0xFF666666),
+ Color(0xFF999999),
+ Color(0xFFB7B7B7),
+ Color(0xFFCCCCCC),
+ Color(0xFFD9D9D9),
+ Color(0xFFEFEFEF),
+ Color(0xFFF3F3F3),
+ Color(0xFFDFE0E3),
+ //
+ Color(0xFF980201),
+ Color(0xFFFF0000),
+ Color(0xFFFF9900),
+ Color(0xFFFFFF00),
+ Color(0xFF01FF00),
+ Color(0xFF02FFFF),
+ Color(0xFF4A86E8),
+ Color(0xFF0602FF),
+ Color(0xFF9901FF),
+ Color(0xFFFF00FF),
+ //
+ Color(0xFFE6B8AF),
+ Color(0xFFF4CCCC),
+ Color(0xFFFCE5CD),
+ Color(0xFFFFF2CC),
+ Color(0xFFD9EAD3),
+ Color(0xFFD0E0E3),
+ Color(0xFFC9DAF8),
+ Color(0xFFCFE2F3),
+ Color(0xFFD9D2E9),
+ Color(0xFFEAD1DC),
+ //
+ Color(0xFFDD7E6A),
+ Color(0xFFEA9999),
+ Color(0xFFF9CB9C),
+ Color(0xFFFFE599),
+ Color(0xFFB6D7A8),
+ Color(0xFFA2C4C9),
+ Color(0xFFA4C2F4),
+ Color(0xFF9FC5E8),
+ Color(0xFFB4A7D6),
+ Color(0xFFD5A6BD),
+ //
+ Color(0xFFCC4125),
+ Color(0xFFE06666),
+ Color(0xFFF6B26B),
+ Color(0xFFFFD965),
+ Color(0xFF93C47E),
+ Color(0xFF76A5AF),
+ Color(0xFF6D9EEB),
+ Color(0xFF6FA8DC),
+ Color(0xFF8E7CC3),
+ Color(0xFFC27BA0),
+ //
+ Color(0xFFA61C01),
+ Color(0xFFCC0200),
+ Color(0xFFE69139),
+ Color(0xFFF1C233),
+ Color(0xFF6AA84F),
+ Color(0xFF45808E),
+ Color(0xFF3C78D8),
+ Color(0xFF3D85C6),
+ Color(0xFF674EA7),
+ Color(0xFFA64D78),
+ //
+ Color(0xFF85200C),
+ Color(0xFF990201),
+ Color(0xFFB45F07),
+ Color(0xFFBF9001),
+ Color(0xFF38761D),
+ Color(0xFF144F5C),
+ Color(0xFF1155CC),
+ Color(0xFF0B5394),
+ Color(0xFF351D75),
+ Color(0xFF741B46),
+ //
+ Color(0xFF5B0E03),
+ Color(0xFF660202),
+ Color(0xFF783E03),
+ Color(0xFF7F6001),
+ Color(0xFF274E13),
+ Color(0xFF0C343D),
+ Color(0xFF1B4487),
+ Color(0xFF093763),
+ Color(0xFF20124D),
+ Color(0xFF4C1230),
+ //
+];
diff --git a/super_clones/google_docs/lib/infrastructure/icon_selector.dart b/super_clones/google_docs/lib/infrastructure/icon_selector.dart
new file mode 100644
index 0000000000..62baa438f1
--- /dev/null
+++ b/super_clones/google_docs/lib/infrastructure/icon_selector.dart
@@ -0,0 +1,158 @@
+import 'package:example_docs/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:overlord/overlord.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// A selection control, which displays a button with the selected icon, and upon tap, displays a
+/// popover list of available icons, from which the user can select a different icon.
+///
+/// Includes the following keyboard selection behaviors:
+///
+/// * Pressing UP/DOWN moves the "active" icon selection up/down.
+/// * Pressing UP with the first icon active moves the active icon selection to the last icon.
+/// * Pressing DOWN with the last icon active moves the active icon selection to the first icon.
+/// * Pressing ENTER selects the currently active icon.
+class IconSelector extends StatefulWidget {
+ const IconSelector({
+ super.key,
+ this.parentFocusNode,
+ this.tapRegionGroupId,
+ this.boundaryKey,
+ this.selectedIcon,
+ required this.icons,
+ required this.onSelected,
+ });
+
+ /// The [FocusNode], to which the popover list's [FocusNode] will be added as a child.
+ ///
+ /// See [PopoverScaffold.parentFocusNode] for more information.
+ final FocusNode? parentFocusNode;
+
+ /// A group ID for a tap region that is shared with the popover list.
+ ///
+ /// Tapping on a [TapRegion] with the same [tapRegionGroupId]
+ /// won't invoke [onTapOutside].
+ final String? tapRegionGroupId;
+
+ /// A [GlobalKey] to a widget that determines the bounds where the popover list can be displayed.
+ ///
+ /// See [PopoverScaffold.boundaryKey] for more information.
+ final GlobalKey? boundaryKey;
+
+ /// The currently selected icon or `null` if no icon is selected.
+ final IconItem? selectedIcon;
+
+ /// The icons that will be displayed in the popover list.
+ final List icons;
+
+ /// Called when the user selects an icon on the popover list.
+ final void Function(IconItem? value) onSelected;
+
+ @override
+ State createState() => _IconSelectorState();
+}
+
+class _IconSelectorState extends State {
+ /// Shows and hides the popover.
+ final PopoverController _popoverController = PopoverController();
+
+ /// The [FocusNode] of the popover list.
+ final FocusNode _popoverFocusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _popoverController.dispose();
+ _popoverFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopoverScaffold(
+ tapRegionGroupId: widget.tapRegionGroupId,
+ controller: _popoverController,
+ buttonBuilder: _buildButton,
+ popoverFocusNode: _popoverFocusNode,
+ parentFocusNode: widget.parentFocusNode,
+ popoverGeometry: const PopoverGeometry(
+ constraints: BoxConstraints(minHeight: 40),
+ aligner: FunctionalPopoverAligner(popoverAligner),
+ ),
+ popoverBuilder: (context) => Material(
+ elevation: 8,
+ borderRadius: BorderRadius.circular(4),
+ clipBehavior: Clip.hardEdge,
+ color: Colors.white,
+ child: Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: ItemSelectionList(
+ axis: Axis.horizontal,
+ focusNode: _popoverFocusNode,
+ value: widget.selectedIcon,
+ items: widget.icons,
+ itemBuilder: _buildItem,
+ onItemSelected: _onItemSelected,
+ onCancel: () => _popoverController.close(),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildItem(BuildContext context, IconItem item, bool isActive, VoidCallback onTap) {
+ return Container(
+ height: 30,
+ width: 30,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: item == widget.selectedIcon
+ ? toolbarButtonSelectedColor
+ : isActive
+ ? Colors.grey.withValues(alpha: 0.2)
+ : Colors.transparent,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ child: Icon(item.icon),
+ ),
+ );
+ }
+
+ Widget _buildButton(BuildContext context) {
+ return TextButton(
+ onPressed: () => _popoverController.open(),
+ style: defaultToolbarButtonStyle,
+ child: widget.selectedIcon == null //
+ ? const SizedBox()
+ : Icon(widget.selectedIcon!.icon),
+ );
+ }
+
+ void _onItemSelected(IconItem? value) {
+ _popoverController.close();
+ widget.onSelected(value);
+ }
+}
+
+/// An option that is displayed as an icon by a [IconSelector].
+///
+/// Two [IconItem]s are considered to be equal if they have the same [id].
+class IconItem {
+ const IconItem({
+ required this.id,
+ required this.icon,
+ });
+
+ /// The value that identifies this item.
+ final String id;
+
+ /// The icon that is displayed.
+ final IconData icon;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is IconItem && runtimeType == other.runtimeType && id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
+}
diff --git a/super_clones/google_docs/lib/infrastructure/increment_decrement_field.dart b/super_clones/google_docs/lib/infrastructure/increment_decrement_field.dart
new file mode 100644
index 0000000000..5fe990996a
--- /dev/null
+++ b/super_clones/google_docs/lib/infrastructure/increment_decrement_field.dart
@@ -0,0 +1,148 @@
+import 'dart:math';
+
+import 'package:example_docs/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// A Widget that allows the user to increment or decrement a [value].
+///
+/// Displays a textfield with the current [value], surrounded by the increment and
+/// decrement buttons.
+///
+/// Selects all the content on the textfield upon focus.
+class IncrementDecrementField extends StatefulWidget {
+ const IncrementDecrementField({
+ super.key,
+ required this.value,
+ required this.onChange,
+ });
+
+ /// The current value.
+ final int value;
+
+ /// Called when the user presses one of the buttons or when the user
+ /// presses ENTER on the textfield.
+ ///
+ /// If the user clears the textfield, no change is reported. Instead,
+ /// the textfield value is reset to the current [value].
+ final void Function(int value) onChange;
+
+ @override
+ State createState() => _IncrementDecrementFieldState();
+}
+
+class _IncrementDecrementFieldState extends State {
+ final ImeAttributedTextEditingController _controller = ImeAttributedTextEditingController();
+ final FocusNode _focusNode = FocusNode();
+
+ @override
+ void initState() {
+ super.initState();
+ _controller
+ ..text = AttributedText(widget.value.toString())
+ ..onPerformActionPressed = _onPerformAction;
+
+ _focusNode.addListener(_onFocusChange);
+ }
+
+ @override
+ void didUpdateWidget(covariant IncrementDecrementField oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.value != widget.value) {
+ _controller.text = AttributedText(widget.value.toString());
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ _focusNode.dispose();
+ super.dispose();
+ }
+
+ void _onPerformAction(TextInputAction action) {
+ if (action == TextInputAction.done) {
+ final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
+ if (value != null) {
+ widget.onChange(value);
+ }
+
+ _controller.text = AttributedText(widget.value.toString());
+ }
+ }
+
+ void _onIncrement() {
+ final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
+ if (value == null) {
+ return;
+ }
+
+ widget.onChange(value + 1);
+ }
+
+ void _onDecrement() {
+ final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
+ if (value == null) {
+ return;
+ }
+
+ widget.onChange(max(value - 1, 1));
+ }
+
+ void _onFocusChange() {
+ if (_focusNode.hasFocus) {
+ _controller.selectAll();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextButton(
+ onPressed: _onDecrement,
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.remove),
+ ),
+ const SizedBox(width: 4),
+ SizedBox(
+ height: 24,
+ width: 32,
+ child: SuperDesktopTextField(
+ focusNode: _focusNode,
+ textAlign: TextAlign.center,
+ inputSource: TextInputSource.ime,
+ textInputAction: TextInputAction.done,
+ textController: _controller,
+ minLines: 1,
+ maxLines: 1,
+ textStyleBuilder: (attributions) => const TextStyle(
+ fontSize: 12,
+ color: Colors.black,
+ ),
+ decorationBuilder: (context, child) {
+ return Container(
+ height: 24,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(4),
+ border: Border.all(
+ color: _focusNode.hasFocus ? const Color(0xFF0B57D0) : const Color(0xFF747775),
+ width: 1,
+ ),
+ ),
+ child: Center(child: child),
+ );
+ },
+ ),
+ ),
+ const SizedBox(width: 4),
+ TextButton(
+ onPressed: _onIncrement,
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.add),
+ )
+ ],
+ );
+ }
+}
diff --git a/super_clones/google_docs/lib/infrastructure/selectable_grid.dart b/super_clones/google_docs/lib/infrastructure/selectable_grid.dart
new file mode 100644
index 0000000000..1b60db813c
--- /dev/null
+++ b/super_clones/google_docs/lib/infrastructure/selectable_grid.dart
@@ -0,0 +1,276 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// A grid where the user can navigate between its items and select one of them.
+///
+/// Includes the following keyboard selection behaviors:
+///
+/// * Pressing UP/DOWN moves the "active" item selection up/down.
+/// * Pressing LEFT/RIGHT moves the "active" item selection left/right.
+/// * Pressing ENTER selects the currently active item.
+class SelectableGrid extends StatefulWidget {
+ const SelectableGrid({
+ super.key,
+ this.focusNode,
+ required this.value,
+ required this.items,
+ required this.itemBuilder,
+ required this.columnCount,
+ this.mainAxisExtent,
+ this.onItemActivated,
+ required this.onItemSelected,
+ this.onCancel,
+ });
+
+ /// The [FocusNode] of the grid.
+ final FocusNode? focusNode;
+
+ /// The currently selected value or `null` if no item is selected.
+ final GridItemType? value;
+
+ /// The items that will be displayed on the grid.
+ ///
+ /// For each item, [itemBuilder] is called to build its visual representation.
+ final List items;
+
+ /// Builds each item on the grid.
+ ///
+ /// This method is called for each item in [items], to build its visual representation.
+ ///
+ /// The provided `onTap` must be called when the item is tapped.
+ final SelectableGridItemBuilder itemBuilder;
+
+ /// How many columns the grid must have.
+ final int columnCount;
+
+ /// The extent of each item on the grid.
+ final double? mainAxisExtent;
+
+ /// Called when the user activates an item on the grid.
+ ///
+ /// The activation can be performed by:
+ /// 1. Pressing UP ARROW or DOWN ARROW.
+ /// 2. Pressing LEFT ARROW or RIGHT ARROW.
+ final ValueChanged? onItemActivated;
+
+ /// Called when the user selects an item on the grid.
+ ///
+ /// The selection can be performed by:
+ /// 1. Tapping on an item in the grid.
+ /// 2. Pressing ENTER when the grid has an active item.
+ final ValueChanged onItemSelected;
+
+ /// Called when the user presses ESCAPE.
+ final VoidCallback? onCancel;
+
+ @override
+ State> createState() => _SelectableGridState();
+}
+
+class _SelectableGridState extends State>
+ with SingleTickerProviderStateMixin {
+ final ScrollController _scrollController = ScrollController();
+
+ /// Holds keys to each item on the grid.
+ ///
+ /// Used to scroll the grid to reveal the active item.
+ final List _itemKeys = [];
+
+ int? _activeIndex;
+
+ @override
+ void initState() {
+ super.initState();
+ _activateSelectedItem();
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose();
+
+ super.dispose();
+ }
+
+ void _activateSelectedItem() {
+ final selectedItem = widget.value;
+
+ if (selectedItem == null) {
+ _activeIndex = null;
+ return;
+ }
+
+ int selectedItemIndex = widget.items.indexOf(selectedItem);
+ if (selectedItemIndex < 0) {
+ // A selected item was provided, but it isn't included in the list of items.
+ _activeIndex = null;
+ return;
+ }
+
+ // The grid was just displayed.
+ // Jump to the active item without animation.
+ _activateItem(selectedItemIndex, animationDuration: Duration.zero);
+ }
+
+ /// Activates the item at [itemIndex] and ensure it's visible on screen.
+ ///
+ /// The active item is selected when the user presses ENTER.
+ void _activateItem(int? itemIndex, {required Duration animationDuration}) {
+ _activeIndex = itemIndex;
+ if (itemIndex != null) {
+ widget.onItemActivated?.call(widget.items[itemIndex]);
+ }
+
+ // This method might be called before the widget was rendered.
+ // For example, when the widget is created with a selected item,
+ // this item is immediately activated, before the rendering pipeline is
+ // executed. Therefore, the RenderBox won't be available at the same frame.
+ //
+ // Scrolls on the next frame to let the popover be laid-out first,
+ // so we can access its RenderBox.
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ if (!mounted) {
+ return;
+ }
+ _scrollToShowActiveItem(animationDuration);
+ });
+ }
+
+ /// Scrolls the scrollable to display the selected item.
+ void _scrollToShowActiveItem(Duration animationDuration) {
+ if (_activeIndex == null) {
+ return;
+ }
+
+ final key = _itemKeys[_activeIndex!];
+
+ final childRenderBox = key.currentContext?.findRenderObject() as RenderBox?;
+ if (childRenderBox == null) {
+ return;
+ }
+
+ childRenderBox.showOnScreen(
+ rect: Offset.zero & childRenderBox.size,
+ duration: animationDuration,
+ curve: Curves.easeIn,
+ );
+ }
+
+ KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
+ if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
+ return KeyEventResult.ignored;
+ }
+
+ if (!const [
+ LogicalKeyboardKey.enter,
+ LogicalKeyboardKey.numpadEnter,
+ LogicalKeyboardKey.arrowDown,
+ LogicalKeyboardKey.arrowUp,
+ LogicalKeyboardKey.arrowLeft,
+ LogicalKeyboardKey.arrowRight,
+ LogicalKeyboardKey.escape,
+ ].contains(event.logicalKey)) {
+ return KeyEventResult.ignored;
+ }
+
+ if (event.logicalKey == LogicalKeyboardKey.escape) {
+ widget.onCancel?.call();
+ return KeyEventResult.handled;
+ }
+
+ if (event.logicalKey == LogicalKeyboardKey.enter || event.logicalKey == LogicalKeyboardKey.numpadEnter) {
+ if (_activeIndex == null) {
+ // The user pressed ENTER without an active item.
+ // Clear the selected item.
+ widget.onItemSelected(null);
+ return KeyEventResult.handled;
+ }
+
+ widget.onItemSelected(widget.items[_activeIndex!]);
+
+ return KeyEventResult.handled;
+ }
+
+ // The user pressed an arrow key. Update the active item.
+ int? newActiveIndex;
+ if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+ if (_activeIndex == null || _activeIndex! >= widget.items.length - 1) {
+ // We don't have an active item or we are at the end of the list. Activate the first item.
+ newActiveIndex = 0;
+ } else {
+ // Activate the next item.
+ newActiveIndex = _activeIndex! + 1;
+ }
+ }
+
+ if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+ if (_activeIndex == null || _activeIndex! <= 0) {
+ // We don't have an active item or we are at the beginning of the list. Activate the last item.
+ newActiveIndex = widget.items.length - 1;
+ } else {
+ // Activate the previous item.
+ newActiveIndex = _activeIndex! - 1;
+ }
+ }
+
+ if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+ newActiveIndex = (_activeIndex ?? 0) + widget.columnCount;
+ if (newActiveIndex >= widget.items.length - 1) {
+ // We don't have an active item or we are at the end of the list. Activate the first item.
+ newActiveIndex = 0;
+ }
+ }
+
+ if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+ newActiveIndex = (_activeIndex ?? 0) - widget.columnCount;
+ if (newActiveIndex <= 0) {
+ // We don't have an active item or we are at the beginning of the list. Activate the last item.
+ newActiveIndex = widget.items.length - 1;
+ }
+ }
+
+ setState(() {
+ _activateItem(newActiveIndex, animationDuration: const Duration(milliseconds: 100));
+ });
+
+ return KeyEventResult.handled;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ _itemKeys.clear();
+
+ for (int i = 0; i < widget.items.length; i++) {
+ _itemKeys.add(GlobalKey());
+ }
+ return Focus(
+ focusNode: widget.focusNode,
+ onKeyEvent: _onKeyEvent,
+ child: GridView.builder(
+ clipBehavior: Clip.none,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: widget.columnCount,
+ mainAxisSpacing: 2,
+ mainAxisExtent: widget.mainAxisExtent,
+ ),
+ itemCount: widget.items.length,
+ itemBuilder: (context, index) {
+ return widget.itemBuilder(
+ context,
+ widget.items[index],
+ _activeIndex == index,
+ () => widget.onItemSelected(widget.items[index]),
+ );
+ },
+ ),
+ );
+ }
+}
+
+/// Builds a grid item.
+///
+/// [isActive] is `true` if [item] is the currently active item on the grid, or `false` otherwise.
+///
+/// The active item is the currently focused item in the grid, which can be selected by pressing ENTER.
+///
+/// The provided [onTap] must be called when the button is tapped.
+typedef SelectableGridItemBuilder = Widget Function(BuildContext context, T item, bool isActive, VoidCallback onTap);
diff --git a/super_clones/google_docs/lib/infrastructure/text_item_selector.dart b/super_clones/google_docs/lib/infrastructure/text_item_selector.dart
new file mode 100644
index 0000000000..3228581a8c
--- /dev/null
+++ b/super_clones/google_docs/lib/infrastructure/text_item_selector.dart
@@ -0,0 +1,234 @@
+import 'package:example_docs/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:overlord/overlord.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// A selection control, which displays a button with the selected text, and upon tap, displays a
+/// popover list of available texts, from which the user can select a different text.
+///
+/// Includes the following keyboard selection behaviors:
+///
+/// * Pressing UP/DOWN moves the "active" text selection up/down.
+/// * Pressing UP with the first text active moves the active text selection to the last text.
+/// * Pressing DOWN with the last text active moves the active text selection to the first text.
+/// * Pressing ENTER selects the currently active text.
+class TextItemSelector extends StatefulWidget {
+ const TextItemSelector({
+ super.key,
+ required this.parentFocusNode,
+ this.tapRegionGroupId,
+ this.boundaryKey,
+ this.selectedText,
+ required this.items,
+ this.popoverGeometry,
+ this.buttonSize,
+ this.itemBuilder = defaultPopoverListItemBuilder,
+ this.separatorBuilder,
+ required this.onSelected,
+ });
+
+ /// The [FocusNode], to which the popover list's [FocusNode] will be added as a child.
+ ///
+ /// See [PopoverScaffold.parentFocusNode] for more information.
+ final FocusNode parentFocusNode;
+
+ /// A group ID for a tap region that is shared with the popover list.
+ ///
+ /// Tapping on a [TapRegion] with the same [tapRegionGroupId]
+ /// won't invoke [onTapOutside].
+ final String? tapRegionGroupId;
+
+ /// A [GlobalKey] to a widget that determines the bounds where the popover list can be displayed.
+ ///
+ /// See [PopoverScaffold.boundaryKey] for more information.
+ final GlobalKey? boundaryKey;
+
+ /// The currently selected text or `null` if no text is selected.
+ ///
+ /// This value is used to build the button.
+ final TextItem? selectedText;
+
+ /// The items that will be displayed in the popover list.
+ ///
+ /// For each item, its [TextItem.label] is displayed.
+ final List items;
+
+ /// Builds each item on the list.
+ ///
+ /// Defaults to [defaultPopoverListItemBuilder].
+ final SelectableListItemBuilder itemBuilder;
+
+ /// Builds a separator between each item.
+ ///
+ /// If `null`, no separator is displayed.
+ final IndexedWidgetBuilder? separatorBuilder;
+
+ /// The desired size of the button.
+ ///
+ /// If `null` a default fixed size is used.
+ final Size? buttonSize;
+
+ /// Controls the size and position of the popover.
+ ///
+ /// The popover is first sized, then positioned.
+ final PopoverGeometry? popoverGeometry;
+
+ /// Called when the user selects an item on the popover list.
+ final void Function(TextItem? value) onSelected;
+
+ @override
+ State createState() => _TextItemSelectorState();
+}
+
+class _TextItemSelectorState extends State {
+ final PopoverController _popoverController = PopoverController();
+ final WidgetStatesController _buttonStatesController = WidgetStatesController();
+ final FocusNode _focusNode = FocusNode();
+
+ @override
+ void initState() {
+ super.initState();
+ _popoverController.addListener(_onPopoverVisibilityChange);
+ }
+
+ @override
+ void dispose() {
+ _focusNode.dispose();
+ _popoverController.dispose();
+ _buttonStatesController.dispose();
+ super.dispose();
+ }
+
+ void _onItemSelected(TextItem? value) {
+ if (value == null) {
+ return;
+ }
+ widget.onSelected(value);
+ _popoverController.close();
+ }
+
+ void _onPopoverVisibilityChange() {
+ _buttonStatesController.update(WidgetState.pressed, _popoverController.shouldShow);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopoverScaffold(
+ popoverFocusNode: _focusNode,
+ parentFocusNode: widget.parentFocusNode,
+ tapRegionGroupId: widget.tapRegionGroupId,
+ controller: _popoverController,
+ buttonBuilder: _buildButton,
+ popoverBuilder: _buildPopover,
+ popoverGeometry:
+ widget.popoverGeometry ?? const PopoverGeometry(aligner: FunctionalPopoverAligner(popoverAligner)),
+ );
+ }
+
+ Widget _buildButton(BuildContext context) {
+ final size = WidgetStateProperty.all(widget.buttonSize ?? const Size(97, 30));
+ return Stack(
+ alignment: Alignment.centerLeft,
+ children: [
+ TextButton(
+ statesController: _buttonStatesController,
+ onPressed: () => _popoverController.open(),
+ style: defaultToolbarButtonStyle.copyWith(
+ fixedSize: size,
+ minimumSize: size,
+ maximumSize: size,
+ ),
+ child: SizedBox(
+ width: (widget.buttonSize?.width ?? 97) - 30,
+ child: Text(
+ widget.selectedText?.label ?? '',
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.left,
+ style: TextStyle(
+ color: Colors.black.withValues(alpha: 0.7),
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 10),
+ const Positioned(
+ right: 0,
+ child: Icon(Icons.arrow_drop_down),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildPopover(BuildContext context) {
+ return Material(
+ elevation: 8,
+ borderRadius: BorderRadius.circular(4),
+ clipBehavior: Clip.hardEdge,
+ color: Colors.white,
+ child: SizedBox(
+ child: ItemSelectionList(
+ focusNode: _focusNode,
+ value: widget.selectedText,
+ items: widget.items,
+ itemBuilder: widget.itemBuilder,
+ separatorBuilder: widget.separatorBuilder,
+ onItemSelected: _onItemSelected,
+ onCancel: () => _popoverController.close(),
+ ),
+ ),
+ );
+ }
+}
+
+Widget defaultPopoverListItemBuilder(BuildContext context, TextItem item, bool isActive, VoidCallback onTap) {
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ color: isActive ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ constraints: const BoxConstraints(minHeight: 32),
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Row(
+ children: [
+ Text(
+ item.label,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(
+ color: Colors.black,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+}
+
+/// An option that is displayed as text by a [TextItemSelector].
+///
+/// Two [TextItem]s are considered to be equal if they have the same [id].
+class TextItem {
+ const TextItem({
+ required this.id,
+ required this.label,
+ });
+
+ /// The value that identifies this item.
+ final String id;
+
+ /// The text that is displayed.
+ final String label;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is TextItem && runtimeType == other.runtimeType && id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
+}
diff --git a/super_clones/google_docs/lib/main.dart b/super_clones/google_docs/lib/main.dart
new file mode 100644
index 0000000000..4974e62daa
--- /dev/null
+++ b/super_clones/google_docs/lib/main.dart
@@ -0,0 +1,6 @@
+import 'package:example_docs/app.dart';
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const DocsApp());
+}
diff --git a/super_clones/google_docs/lib/theme.dart b/super_clones/google_docs/lib/theme.dart
new file mode 100644
index 0000000000..e70596a003
--- /dev/null
+++ b/super_clones/google_docs/lib/theme.dart
@@ -0,0 +1,135 @@
+import 'package:flutter/material.dart';
+import 'package:follow_the_leader/follow_the_leader.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// The background color of the window panes, such as the background of the
+/// app header/ribbon.
+const windowBackgroundColor = Color(0xFFf9fbfd);
+
+/// The color of the icons that appear next to the document title.
+const titleActionIconColor = Color(0xFF444746);
+
+/// The horizontal padding of the primary app menu buttons, e.g., "File", "Edit".
+const menuButtonHorizontalPadding = 8.0;
+
+/// The background color of the app toolbar, i.e., the toolbar with options for font
+/// family, font size, text alignment.
+const toolbarBackgroundColor = Color(0xFFedf2fa);
+
+/// The background color of a selected button on the toolbar, i.e., the color of a
+/// bold button when the selection is bold.
+const toolbarButtonSelectedColor = Color(0xFFd3e3fd);
+
+/// The background color of a hovered button on the toolbar.
+const toolbarButtonHoveredColor = Color(0xFFE1E6ED);
+
+/// The background color of a pressed button on the toolbar.
+const toolbarButtonPressedColor = Color(0xFFDAE0E6);
+
+/// The color of the vertical divider of the toolbar.
+const toolbarDividerColor = Color(0xFFC7C7C7);
+
+/// Computes the background color for toolbar buttons.
+Color? getButtonColor(Set states) {
+ if (states.contains(WidgetState.pressed)) {
+ return toolbarButtonPressedColor;
+ }
+
+ if (states.contains(WidgetState.selected)) {
+ return toolbarButtonSelectedColor;
+ }
+
+ if (states.contains(WidgetState.hovered)) {
+ return toolbarButtonHoveredColor;
+ }
+
+ return Colors.transparent;
+}
+
+final defaultToolbarButtonStyle = ButtonStyle(
+ backgroundColor: WidgetStateProperty.resolveWith(getButtonColor),
+ overlayColor: WidgetStateProperty.all(Colors.transparent),
+ foregroundColor: WidgetStateProperty.all(Colors.black),
+ fixedSize: WidgetStateProperty.all(const Size(30, 30)),
+ minimumSize: WidgetStateProperty.all(const Size(30, 30)),
+ maximumSize: WidgetStateProperty.all(const Size(30, 30)),
+ iconSize: WidgetStateProperty.all(18),
+ padding: WidgetStateProperty.all(EdgeInsets.zero),
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4.0),
+ ),
+ ),
+ shadowColor: WidgetStateProperty.all(Colors.transparent),
+);
+
+final docsStylesheet = [
+ StyleRule(
+ const BlockSelector("header1"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header2"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header3"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header4"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header5"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+ StyleRule(
+ const BlockSelector("header6"),
+ (doc, docNode) {
+ return {
+ Styles.textStyle: const TextStyle(
+ height: 1.0,
+ ),
+ };
+ },
+ ),
+];
+
+FollowerAlignment popoverAligner(Rect globalLeaderRect, Size followerSize, Size screenSize, GlobalKey? boundaryKey) {
+ return const FollowerAlignment(
+ leaderAnchor: Alignment.bottomLeft,
+ followerAnchor: Alignment.topLeft,
+ followerOffset: Offset(0, 1),
+ );
+}
diff --git a/super_clones/google_docs/lib/toolbar.dart b/super_clones/google_docs/lib/toolbar.dart
new file mode 100644
index 0000000000..dacf6f7b27
--- /dev/null
+++ b/super_clones/google_docs/lib/toolbar.dart
@@ -0,0 +1,1563 @@
+import 'dart:math';
+
+import 'package:example_docs/infrastructure/icon_selector.dart';
+import 'package:example_docs/infrastructure/color_selector.dart';
+import 'package:example_docs/infrastructure/text_item_selector.dart';
+import 'package:example_docs/infrastructure/increment_decrement_field.dart';
+import 'package:example_docs/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:follow_the_leader/follow_the_leader.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:overlord/overlord.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// The application toolbar that includes document editing options such as font family,
+/// font size, text alignment, etc.
+///
+/// The toolbar is divided by groups of children. Depending on the size of the screen,
+/// some of the groups are hidden and a button to show the hidden groups is displayed.
+/// Upon tapping this button, a popover is displayed revealing the hidden groups.
+///
+/// All children of a visible group are displayed and no child is displayed for a
+/// hidden group.
+class DocsEditorToolbar extends StatefulWidget {
+ const DocsEditorToolbar({
+ super.key,
+ required this.document,
+ required this.editor,
+ required this.composer,
+ required this.editorFocusNode,
+ required this.onZoomChange,
+ });
+
+ final Document document;
+ final Editor editor;
+ final MutableDocumentComposer composer;
+ final FocusNode editorFocusNode;
+ final void Function(int zoom) onZoomChange;
+
+ @override
+ State createState() => _DocsEditorToolbarState();
+}
+
+class _DocsEditorToolbarState extends State {
+ /// Groups the additional toolbar options popover, which is shown by tapping
+ /// the "more items" button with the popovers shown by the toolbar items,
+ /// like the color picker.
+ static const _tapRegionGroupId = 'docs_toolbar';
+
+ final FocusNode _urlFocusNode = FocusNode();
+ final PopoverController _linkPopoverController = PopoverController();
+ ImeAttributedTextEditingController? _urlController;
+
+ final PopoverController _searchPopoverController = PopoverController();
+ final FocusNode _searchFocusNode = FocusNode();
+
+ TextItem _selectedZoom = const TextItem(id: '100', label: '100%');
+
+ @override
+ void initState() {
+ super.initState();
+ widget.composer.selectionNotifier.addListener(_onSelectionChanged);
+
+ _urlController = ImeAttributedTextEditingController() //
+ ..onPerformActionPressed = _onUrlFieldPerformAction
+ ..text = AttributedText("https://");
+ }
+
+ @override
+ void didUpdateWidget(covariant DocsEditorToolbar oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.composer.selectionNotifier != widget.composer.selectionNotifier) {
+ oldWidget.composer.selectionNotifier.addListener(_onSelectionChanged);
+ widget.composer.selectionNotifier.addListener(_onSelectionChanged);
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.composer.selectionNotifier.removeListener(_onSelectionChanged);
+ _urlFocusNode.dispose();
+ _linkPopoverController.dispose();
+ _searchPopoverController.dispose();
+ _searchFocusNode.dispose();
+ super.dispose();
+ }
+
+ void _onSelectionChanged() {
+ // Rebuild to update the visuals, because information like which buttons are toggled
+ // depends on the selection.
+ setState(() {});
+ }
+
+ void _onUrlFieldPerformAction(TextInputAction action) {
+ if (action == TextInputAction.done) {
+ _applyLink();
+ }
+ }
+
+ void _onChangeZoomLevelRequested(TextItem? zoomLevel) {
+ if (zoomLevel == null) {
+ return;
+ }
+
+ widget.onZoomChange(int.parse(zoomLevel.id));
+
+ setState(() {
+ _selectedZoom = zoomLevel;
+ });
+ }
+
+ /// Converts the currently selected text node into a new type of
+ /// text node, represented by [newType].
+ ///
+ /// For example: convert a paragraph to a blockquote, or a header
+ /// to a list item.
+ void _convertTextToNewType(String? newType) {
+ final existingTextType = _getCurrentTextType();
+
+ if (existingTextType == newType) {
+ // The text is already the desired type. Return.
+ return;
+ }
+
+ // Apply a new block type to an existing paragraph node.
+ widget.editor.execute([
+ ChangeParagraphBlockTypeRequest(
+ nodeId: widget.composer.selection!.extent.nodeId,
+ blockType: _getBlockTypeAttribution(newType),
+ ),
+ ]);
+ }
+
+ /// Changes the font family of the current selected range to reflect [newFontFamily].
+ ///
+ /// If [newFontFamily] is `null`, the font family attributions are removed and
+ /// the default font family is applied.
+ void _onChangeFontFamilyRequested(String? newFontFamily) {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final fontFamilyAttributions = widget.document.getAttributionsByType(selection);
+
+ widget.editor.execute([
+ for (final existingAttribution in fontFamilyAttributions) //
+ RemoveTextAttributionsRequest(documentRange: selection, attributions: {existingAttribution}),
+ if (newFontFamily != null) //
+ AddTextAttributionsRequest(
+ documentRange: selection,
+ attributions: {FontFamilyAttribution(newFontFamily)},
+ ),
+ ]);
+
+ // Rebuild to update the selected font on the toolbar.
+ setState(() {});
+ }
+
+ /// Changes the font size of the current selected range to reflect [newFontSize].
+ void _onChangeFontSizeRequested(int newFontSize) {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final fontSizeAttributions = widget.document.getAttributionsByType(selection);
+
+ widget.editor.execute([
+ for (final existingAttribution in fontSizeAttributions) //
+ RemoveTextAttributionsRequest(documentRange: selection, attributions: {existingAttribution}),
+ AddTextAttributionsRequest(
+ documentRange: selection,
+ attributions: {FontSizeAttribution(newFontSize.toDouble())},
+ ),
+ ]);
+
+ // Rebuild to update the font size on the toolbar.
+ setState(() {});
+ }
+
+ /// Toggles the bold attribution on the current selected range.
+ void _onToggleBoldRequested() {
+ _toggleAttribution(boldAttribution);
+ }
+
+ /// Toggles the italics attribution on the current selected range.
+ void _onToggleItalicsRequested() {
+ _toggleAttribution(italicsAttribution);
+ }
+
+ /// Toggles the underline attribution on the current selected range.
+ void _onToggleUnderlineRequested() {
+ _toggleAttribution(underlineAttribution);
+ }
+
+ /// Changes the color of the current selected range to reflect [newColor].
+ ///
+ /// If [newColor] is `null`, the color attributions are removed and
+ /// the default color is applied.
+ void _onChangeTextColorRequested(Color? newColor) {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final colorAttributions = widget.document.getAttributionsByType(selection);
+
+ widget.editor.execute([
+ for (final existingAttribution in colorAttributions) //
+ RemoveTextAttributionsRequest(documentRange: selection, attributions: {existingAttribution}),
+ if (newColor != null) //
+ AddTextAttributionsRequest(
+ documentRange: selection,
+ attributions: {ColorAttribution(newColor)},
+ ),
+ ]);
+
+ // Rebuild to update the color on the toolbar button.
+ setState(() {});
+ }
+
+ /// Changes the background color of the current selected range to reflect [newColor].
+ ///
+ /// If [newColor] is `null`, the background color attributions are removed and
+ /// the default color is applied.
+ void _onChangeBackgroundColorRequested(Color? newColor) {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final colorAttributions = widget.document.getAttributionsByType(selection);
+
+ widget.editor.execute([
+ for (final existingAttribution in colorAttributions) //
+ RemoveTextAttributionsRequest(documentRange: selection, attributions: {existingAttribution}),
+ if (newColor != null) //
+ AddTextAttributionsRequest(
+ documentRange: selection,
+ attributions: {BackgroundColorAttribution(newColor)},
+ ),
+ ]);
+
+ // Rebuild to update the background color on the toolbar button.
+ setState(() {});
+ }
+
+ /// Applies the link entered on the URL textfield to the current
+ /// selected range.
+ void _applyLink() {
+ final url = _urlController!.text.toPlainText(includePlaceholders: false);
+
+ final selection = widget.composer.selection!;
+ final baseOffset = (selection.base.nodePosition as TextPosition).offset;
+ final extentOffset = (selection.extent.nodePosition as TextPosition).offset;
+ final selectionStart = min(baseOffset, extentOffset);
+ final selectionEnd = max(baseOffset, extentOffset);
+ final selectionRange = TextRange(start: selectionStart, end: selectionEnd - 1);
+
+ final textNode = widget.document.getNodeById(selection.extent.nodeId) as TextNode;
+ final text = textNode.text;
+
+ final trimmedRange = _trimTextRangeWhitespace(text, selectionRange);
+
+ final linkAttribution = LinkAttribution.fromUri(Uri.parse(url));
+
+ widget.editor.execute([
+ AddTextAttributionsRequest(
+ documentRange: DocumentRange(
+ start: DocumentPosition(
+ nodeId: textNode.id,
+ nodePosition: TextNodePosition(offset: trimmedRange.start),
+ ),
+ end: DocumentPosition(
+ nodeId: textNode.id,
+ nodePosition: TextNodePosition(offset: trimmedRange.end),
+ ),
+ ),
+ attributions: {linkAttribution},
+ ),
+ ]);
+
+ // Clear the field and hide the URL bar
+ _urlController!.clearTextAndSelection();
+ _urlFocusNode.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
+ _linkPopoverController.close();
+ setState(() {});
+ }
+
+ /// Changes the alignment of the current selected text node
+ /// to reflect [newAlignment].
+ void _changeAlignment(TextAlign? newAlignment) {
+ if (widget.composer.selection == null || newAlignment == null) {
+ return;
+ }
+
+ widget.editor.execute([
+ ChangeParagraphAlignmentRequest(
+ nodeId: widget.composer.selection!.extent.nodeId,
+ alignment: newAlignment,
+ ),
+ ]);
+ }
+
+ /// Converts the selected node to a [TaskNode], or to a
+ /// [ParagraphNode] if it's already a [TaskNode].
+ void _onToggleTaskNodeRequested() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final node = widget.document.getNodeById(selection.extent.nodeId);
+ if (node is TaskNode) {
+ widget.editor.execute([
+ DeleteUpstreamAtBeginningOfNodeRequest(node),
+ ]);
+ } else {
+ widget.editor.execute([
+ ConvertParagraphToTaskRequest(nodeId: selection.extent.nodeId),
+ ]);
+ }
+ }
+
+ /// Converts the selected node to a unordered [ListItemNode],
+ /// or to a [ParagraphNode] if it's already a [ListItemNode].
+ void _onToggleUnorderedListItemRequested() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final node = widget.document.getNodeById(selection.extent.nodeId);
+ if (node is ListItemNode) {
+ widget.editor.execute([
+ ConvertListItemToParagraphRequest(nodeId: node.id, paragraphMetadata: node.metadata),
+ ]);
+ } else {
+ widget.editor.execute([
+ ConvertParagraphToListItemRequest(
+ nodeId: selection.extent.nodeId,
+ type: ListItemType.unordered,
+ ),
+ ]);
+ }
+ }
+
+ /// Converts the selected node to a ordered [ListItemNode],
+ /// or to a [ParagraphNode] if it's already a [ListItemNode].
+ void _onToggleOrderedListItemRequested() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+
+ final node = widget.document.getNodeById(selection.extent.nodeId);
+ if (node is ListItemNode) {
+ widget.editor.execute([
+ ConvertListItemToParagraphRequest(nodeId: node.id, paragraphMetadata: node.metadata),
+ ]);
+ } else {
+ widget.editor.execute([
+ ConvertParagraphToListItemRequest(
+ nodeId: selection.extent.nodeId,
+ type: ListItemType.ordered,
+ ),
+ ]);
+ }
+ }
+
+ /// Removes all attributions from the selected range.
+ void _onClearFormattingRequested() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return;
+ }
+ }
+
+ /// Shows a dialog with an alert that the selected feature
+ /// isn't implemented yet.
+ void _showNotImplementedAlert() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Docs Demo'),
+ content: const Text('Feature not implemented yet'),
+ actions: [
+ TextButton(
+ child: const Text('OK'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ /// Toggles the given [attribution] on the selected range.
+ void _toggleAttribution(Attribution attribution) {
+ if (widget.composer.selection == null) {
+ return;
+ }
+
+ widget.editor.execute([
+ ToggleTextAttributionsRequest(
+ documentRange: widget.composer.selection!,
+ attributions: {attribution},
+ ),
+ ]);
+
+ // Rebuild to update the toggled buttons on the toolbar.
+ setState(() {});
+ }
+
+ /// Reacts to the change of the alignment on the toolbar.
+ void _onChangeAlignmentRequested(IconItem? selectedItem) {
+ if (selectedItem != null) {
+ setState(() {
+ _changeAlignment(TextAlign.values.firstWhere((e) => e.name == selectedItem.id));
+ });
+ }
+ }
+
+ /// Reacts to the change of the block type on the toolbar.
+ void _onChangeBlockTypeRequested(TextItem? selectedItem) {
+ if (selectedItem != null) {
+ setState(() {
+ _convertTextToNewType(selectedItem.id);
+ });
+ }
+ }
+
+ /// Given [text] and a [range] within the [text], the [range] is
+ /// shortened on both sides to remove any trailing whitespace and
+ /// the new range is returned.
+ SpanRange _trimTextRangeWhitespace(AttributedText text, TextRange range) {
+ int startOffset = range.start;
+ int endOffset = range.end;
+
+ final plainText = text.toPlainText();
+ while (startOffset < range.end && plainText[startOffset] == ' ') {
+ startOffset += 1;
+ }
+ while (endOffset > startOffset && plainText[endOffset] == ' ') {
+ endOffset -= 1;
+ }
+
+ // Add 1 to the end offset because SpanRange treats the end offset to be exclusive.
+ return SpanRange(startOffset, endOffset + 1);
+ }
+
+ bool _doesSelectionHaveAttributions(Set attributions) {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return false;
+ }
+
+ if (selection.isCollapsed) {
+ return widget.composer.preferences.currentAttributions.containsAll(attributions);
+ }
+
+ return widget.document.doesSelectedTextContainAttributions(selection, attributions);
+ }
+
+ /// Returns the text alignment of the currently selected text node.
+ ///
+ /// Throws an exception if the currently selected node is not a text node.
+ TextAlign _getCurrentTextAlignment() {
+ if (widget.composer.selection == null) {
+ return TextAlign.left;
+ }
+ final selectedNode = widget.document.getNodeById(widget.composer.selection!.extent.nodeId);
+ if (selectedNode is ParagraphNode) {
+ final align = selectedNode.getMetadataValue('textAlign');
+ switch (align) {
+ case 'left':
+ return TextAlign.left;
+ case 'center':
+ return TextAlign.center;
+ case 'right':
+ return TextAlign.right;
+ case 'justify':
+ return TextAlign.justify;
+ default:
+ return TextAlign.left;
+ }
+ } else {
+ throw Exception('Invalid node type: $selectedNode');
+ }
+ }
+
+ /// Returns the text [Attribution] associated with the given
+ /// block type, e.g., `"header1"` -> [header1Attribution].
+ Attribution? _getBlockTypeAttribution(String? newType) {
+ return switch (newType) {
+ BlockTypes.header1 => header1Attribution,
+ BlockTypes.header2 => header2Attribution,
+ BlockTypes.header3 => header3Attribution,
+ BlockTypes.blockquote => blockquoteAttribution,
+ BlockTypes.paragraph => paragraphAttribution,
+ _ => null,
+ };
+ }
+
+ /// Returns whether or not the currently selected node is a paragraph.
+ bool _isParagraphNode() {
+ final selection = widget.composer.selection;
+
+ if (selection == null) {
+ return false;
+ }
+
+ if (selection.base.nodeId != selection.extent.nodeId) {
+ return false;
+ }
+
+ final selectedNode = widget.document.getNodeById(selection.extent.nodeId);
+ return selectedNode is ParagraphNode;
+ }
+
+ /// Returns the text type of the selected node, i.e, a header,
+ /// a blockquote, etc.
+ String? _getCurrentTextType() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return null;
+ }
+
+ final selectedNode = widget.document.getNodeById(selection.extent.nodeId);
+ if (selectedNode is ParagraphNode) {
+ return (selectedNode.getMetadataValue('blockType') as NamedAttribution).id;
+ }
+
+ return null;
+ }
+
+ /// Returns all attributions of the currently selected range, if the selection is expanded,
+ /// or the current composer attributes, if the selection is collapsed.
+ Set _getAllAttributions() {
+ final selection = widget.composer.selection;
+ if (selection == null) {
+ return {};
+ }
+
+ if (selection.isCollapsed) {
+ return widget //
+ .composer
+ .preferences
+ .currentAttributions;
+ }
+
+ return widget.document.getAllAttributions(selection);
+ }
+
+ TextStyle _getDefaultTextStyleForBlockType(TextItem item) {
+ return switch (item.id) {
+ BlockTypes.header1 => const TextStyle(
+ color: Color(0xFF333333),
+ fontSize: 38,
+ fontWeight: FontWeight.bold,
+ ),
+ BlockTypes.header2 => const TextStyle(
+ color: Color(0xFF333333),
+ fontSize: 26,
+ fontWeight: FontWeight.bold,
+ ),
+ BlockTypes.header3 => const TextStyle(
+ color: Color(0xFF333333),
+ fontSize: 22,
+ fontWeight: FontWeight.bold,
+ ),
+ BlockTypes.paragraph => const TextStyle(
+ color: Colors.black,
+ fontSize: 18,
+ height: 1.4,
+ ),
+ BlockTypes.blockquote => const TextStyle(
+ color: Colors.grey,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ height: 1.4,
+ ),
+ _ => const TextStyle(
+ color: Colors.black,
+ fontSize: 18,
+ height: 1.4,
+ )
+ };
+ }
+
+ void _showUrlPopover() {
+ _linkPopoverController.open();
+ _urlFocusNode.requestFocus();
+ }
+
+ /// Computes how many button groups should be visible for the given [width].
+ int _computeVisibleGroupCount(double width) {
+ return switch (width) {
+ >= 1300 => 5,
+ >= 1000 => 4,
+ >= 900 => 3,
+ >= 600 => 2,
+ _ => 1,
+ };
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final width = MediaQuery.sizeOf(context).width;
+ final visibleGroupCount = _computeVisibleGroupCount(width);
+ final attributions = _getAllAttributions();
+
+ return Material(
+ color: toolbarBackgroundColor,
+ shape: const StadiumBorder(),
+ child: SizedBox(
+ width: double.infinity,
+ height: 40,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 9.0),
+ child: _GroupedToolbarItems(
+ tapRegionGroupId: _tapRegionGroupId,
+ visibleGroupCount: visibleGroupCount,
+ groups: [
+ _WidgetGroup(
+ widgets: [
+ const SizedBox(width: 1),
+ _buildSearchPopoverButton(expanded: width > 1250),
+ const SizedBox(width: 7),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Undo',
+ child: const Icon(Icons.undo),
+ ),
+ const SizedBox(width: 1),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Redo',
+ child: const Icon(Icons.redo),
+ ),
+ const SizedBox(width: 1),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Print',
+ child: const Icon(Icons.print_outlined),
+ ),
+ const SizedBox(width: 1),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Spellcheck',
+ child: const Icon(Icons.spellcheck),
+ ),
+ const SizedBox(width: 1),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Paint Formating',
+ child: const Icon(Icons.format_paint_outlined),
+ ),
+ _buildZoomSelector(),
+ ],
+ ),
+ _WidgetGroup(
+ widgets: [
+ _buildBlockTypeSelector(),
+ _buildFontFamilySelector(attributions),
+ ],
+ ),
+ _WidgetGroup(
+ widgets: [
+ _buildFontSizeSelector(attributions),
+ ToolbarImageButton(
+ onPressed: _onToggleBoldRequested,
+ selected: _doesSelectionHaveAttributions({boldAttribution}),
+ hint: 'Bold',
+ child: const Icon(Icons.format_bold),
+ ),
+ const SizedBox(width: 2),
+ ToolbarImageButton(
+ onPressed: _onToggleItalicsRequested,
+ selected: _doesSelectionHaveAttributions({italicsAttribution}),
+ hint: 'Italic',
+ child: const Icon(Icons.format_italic),
+ ),
+ const SizedBox(width: 2),
+ ToolbarImageButton(
+ onPressed: _onToggleUnderlineRequested,
+ selected: _doesSelectionHaveAttributions({underlineAttribution}),
+ hint: 'Underline',
+ child: const Icon(Icons.format_underline),
+ ),
+ const SizedBox(width: 2),
+ _buildColorButton(attributions),
+ const SizedBox(width: 2),
+ _buildBackgroundColorButton(attributions),
+ ],
+ ),
+ _WidgetGroup(
+ widgets: [
+ _buildLinkButton(),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Add comment',
+ child: const Icon(Icons.add_comment_outlined),
+ ),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Add photo',
+ child: const Icon(Icons.add_photo_alternate_outlined),
+ ),
+ ],
+ ),
+ _WidgetGroup(
+ widgets: [
+ if (_isParagraphNode()) //
+ _buildAlignmentSelector(),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Line spacing',
+ child: const Icon(Icons.format_line_spacing),
+ ),
+ ToolbarImageButton(
+ onPressed: _onToggleTaskNodeRequested,
+ hint: 'Checklist',
+ child: const Icon(Icons.checklist),
+ ),
+ ToolbarImageButton(
+ onPressed: _onToggleUnorderedListItemRequested,
+ hint: 'Bulleted list',
+ child: const Icon(Icons.format_list_bulleted),
+ ),
+ ToolbarImageButton(
+ onPressed: _onToggleOrderedListItemRequested,
+ hint: 'Numbered list',
+ child: const Icon(Icons.format_list_numbered),
+ ),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Decrease indent',
+ child: const Icon(Icons.format_indent_decrease),
+ ),
+ ToolbarImageButton(
+ onPressed: _showNotImplementedAlert,
+ hint: 'Increase indent',
+ child: const Icon(Icons.format_indent_increase),
+ ),
+ ToolbarImageButton(
+ onPressed: _onClearFormattingRequested,
+ hint: 'Clear formatting',
+ child: const Icon(Icons.format_clear),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Builds the search button, which upon tap shows a popover with
+ /// the available actions.
+ Widget _buildSearchPopoverButton({required bool expanded}) {
+ return PopoverScaffold(
+ parentFocusNode: widget.editorFocusNode,
+ popoverFocusNode: _searchFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ onTapOutside: (controller) => _searchPopoverController.close(),
+ controller: _searchPopoverController,
+ popoverGeometry: const PopoverGeometry(
+ aligner: FunctionalPopoverAligner(_searchPopoverAligner),
+ ),
+ buttonBuilder: (context) => _buildSearchButton(expanded: expanded),
+ popoverBuilder: (context) => _buildSearchPopover(),
+ );
+ }
+
+ /// Builds the button that triggers the search popover.
+ Widget _buildSearchButton({required bool expanded}) {
+ if (!expanded) {
+ return ToolbarImageButton(
+ onPressed: () => _searchPopoverController.open(),
+ hint: 'Search menus',
+ size: const Size(44, 30),
+ child: const Icon(Icons.search),
+ );
+ }
+
+ return Tooltip(
+ message: 'Search menus',
+ waitDuration: _tooltipDelay,
+ child: TextButton.icon(
+ onPressed: () {
+ _searchPopoverController.open();
+ _searchFocusNode.requestFocus();
+ },
+ icon: const Icon(
+ Icons.search,
+ size: 18,
+ ),
+ label: const Text('Menus'),
+ style: ButtonStyle(
+ backgroundColor: WidgetStateProperty.all(Colors.white),
+ overlayColor: WidgetStateProperty.all(Colors.transparent),
+ foregroundColor: WidgetStateProperty.all(Colors.black),
+ fixedSize: WidgetStateProperty.all(const Size(100, 30)),
+ minimumSize: WidgetStateProperty.all(const Size(100, 30)),
+ textStyle: WidgetStateProperty.all(
+ const TextStyle(fontWeight: FontWeight.w200),
+ ),
+ mouseCursor: WidgetStateProperty.all(SystemMouseCursors.text),
+ ),
+ ),
+ );
+ }
+
+ /// Builds the search popover with the available actions.
+ Widget _buildSearchPopover() {
+ return Material(
+ borderRadius: BorderRadius.circular(8.0),
+ elevation: 5,
+ color: Colors.white,
+ child: SizedBox(
+ width: 350,
+ height: 160,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ const Icon(Icons.search, size: 18),
+ const SizedBox(width: 13),
+ SuperTextField(
+ hintBuilder: (context) => const Text('Menus (Options + /)'),
+ hintBehavior: HintBehavior.displayHintUntilTextEntered,
+ ),
+ ],
+ ),
+ ),
+ ItemSelectionList<_ActionMenu>(
+ value: _ActionMenu(
+ icon: Icons.format_bold,
+ label: 'Bold',
+ ),
+ focusNode: _searchFocusNode,
+ onCancel: () => _searchPopoverController.close(),
+ items: [
+ _ActionMenu(
+ icon: Icons.format_bold,
+ label: 'Bold',
+ ),
+ _ActionMenu(
+ icon: Icons.format_italic,
+ label: 'Italic',
+ ),
+ _ActionMenu(
+ icon: Icons.format_underline,
+ label: 'Underline',
+ ),
+ ],
+ itemBuilder: (context, item, isActive, onTap) => SizedBox(
+ height: 40,
+ child: ColoredBox(
+ color: isActive ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Icon(item.icon, size: 18),
+ const SizedBox(width: 13),
+ Expanded(child: Text(item.label)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ onItemSelected: (_) => _searchPopoverController.close(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Builds the zoom button, which upon tap shows a popover for the user
+ /// to select the zoom.
+ Widget _buildZoomSelector() {
+ return Tooltip(
+ message: 'Zoom',
+ waitDuration: _tooltipDelay,
+ child: TextItemSelector(
+ parentFocusNode: widget.editorFocusNode,
+ selectedText: _selectedZoom,
+ buttonSize: const Size(77, 30),
+ popoverGeometry: const PopoverGeometry(
+ constraints: BoxConstraints.tightFor(width: 77),
+ aligner: FunctionalPopoverAligner(popoverAligner),
+ ),
+ items: const [
+ TextItem(id: '50', label: '50%'),
+ TextItem(id: '75', label: '75%'),
+ TextItem(id: '90', label: '90%'),
+ TextItem(id: '100', label: '100%'),
+ TextItem(id: '125', label: '125%'),
+ TextItem(id: '150', label: '150%'),
+ TextItem(id: '200', label: '200%'),
+ ],
+ onSelected: _onChangeZoomLevelRequested,
+ ),
+ );
+ }
+
+ /// Builds the block type button, which upon tap shows a popover for the user
+ /// to change the block type of the currently selected node.
+ Widget _buildBlockTypeSelector() {
+ final currentBlockType = _getCurrentTextType();
+
+ return Tooltip(
+ message: 'Styles',
+ waitDuration: _tooltipDelay,
+ child: TextItemSelector(
+ parentFocusNode: widget.editorFocusNode,
+ selectedText: currentBlockType != null //
+ ? _blockTypes.where((e) => e.id == currentBlockType).firstOrNull
+ : null,
+ buttonSize: const Size(122, 30),
+ popoverGeometry: const PopoverGeometry(
+ constraints: BoxConstraints.tightFor(width: 220),
+ aligner: FunctionalPopoverAligner(popoverAligner),
+ ),
+ items: _blockTypes,
+ onSelected: _onChangeBlockTypeRequested,
+ itemBuilder: (context, item, isActive, onTap) => DecoratedBox(
+ decoration: BoxDecoration(
+ color: isActive ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ constraints: const BoxConstraints(minHeight: 71),
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.only(right: 20.0),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: 24,
+ child: currentBlockType != null && item.id == currentBlockType
+ ? const Icon(
+ Icons.check,
+ size: 18,
+ )
+ : null,
+ ),
+ Text(
+ item.label,
+ overflow: TextOverflow.ellipsis,
+ style: _getDefaultTextStyleForBlockType(item),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ separatorBuilder: (context, index) => const Divider(height: 1),
+ ),
+ );
+ }
+
+ /// Builds the font button, which upon tap shows a popover for the user
+ /// to change the font family of the selection.
+ Widget _buildFontFamilySelector(Set attributions) {
+ const defaultFont = 'Roboto';
+
+ final selectedFont = attributions.whereType().firstOrNull?.fontFamily ?? defaultFont;
+ final textItem = TextItem(id: selectedFont, label: selectedFont);
+
+ return Tooltip(
+ message: 'Font',
+ waitDuration: _tooltipDelay,
+ child: TextItemSelector(
+ parentFocusNode: widget.editorFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ selectedText: textItem,
+ items: _availableFonts.map((fontFamily) => TextItem(id: fontFamily, label: fontFamily)).toList(),
+ onSelected: (value) => _onChangeFontFamilyRequested(value?.id),
+ buttonSize: const Size(97, 30),
+ popoverGeometry: const PopoverGeometry(
+ constraints: BoxConstraints.tightFor(width: 247),
+ aligner: FunctionalPopoverAligner(popoverAligner),
+ ),
+ itemBuilder: (context, item, isActive, onTap) => DecoratedBox(
+ decoration: BoxDecoration(
+ color: isActive ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ constraints: const BoxConstraints(minHeight: 32),
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.only(right: 20.0),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: 24,
+ child: item == textItem
+ ? const Icon(
+ Icons.check,
+ size: 18,
+ )
+ : null,
+ ),
+ Text(
+ item.id,
+ overflow: TextOverflow.ellipsis,
+ style: GoogleFonts.getFont(
+ item.id,
+ textStyle: const TextStyle(
+ color: Colors.black,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Builds the font size button, which upon tap shows a popover for the user
+ /// to change the font size of the selection.
+ Widget _buildFontSizeSelector(Set attributions) {
+ const defaultFontSize = 18;
+
+ final fontAttribution = attributions.whereType().firstOrNull;
+ return Tooltip(
+ message: 'Font size',
+ waitDuration: _tooltipDelay,
+ child: IncrementDecrementField(
+ value: fontAttribution?.fontSize.toInt() ?? defaultFontSize,
+ onChange: _onChangeFontSizeRequested,
+ ),
+ );
+ }
+
+ /// Builds a color button, which changes the text color.
+ Widget _buildColorButton(Set attributions) {
+ final colorAttribution = attributions.whereType().firstOrNull;
+
+ return Tooltip(
+ message: 'Text color',
+ waitDuration: _tooltipDelay,
+ child: ColorSelector(
+ parentFocusNode: widget.editorFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ onSelected: _onChangeTextColorRequested,
+ selectedColor: colorAttribution?.color ?? Colors.black,
+ colorButtonBuilder: (_, color) => _buildTextColorIcon(color),
+ ),
+ );
+ }
+
+ /// Builds the button icon with a rectangle of the selected [color].
+ Widget _buildTextColorIcon(Color? color) {
+ return Stack(
+ children: [
+ const Icon(Icons.format_color_text),
+ Positioned(
+ bottom: 0,
+ left: 1,
+ child: Container(
+ width: 16,
+ height: 4,
+ color: color ?? Colors.black,
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// Builds a color button, which changes the text background color.
+ Widget _buildBackgroundColorButton(Set attributions) {
+ final colorAttribution = attributions.whereType().firstOrNull;
+
+ return Tooltip(
+ message: 'Highlight color',
+ waitDuration: _tooltipDelay,
+ child: ColorSelector(
+ parentFocusNode: widget.editorFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ onSelected: _onChangeBackgroundColorRequested,
+ showClearButton: true,
+ selectedColor: colorAttribution?.color ?? Colors.black,
+ colorButtonBuilder: (_, color) => _buildBackgroundColorIcon(color),
+ ),
+ );
+ }
+
+ /// Builds the button icon with a rectangle of the selected [color].
+ Widget _buildBackgroundColorIcon(Color? color) {
+ return Stack(
+ children: [
+ const Icon(Icons.format_color_fill),
+ Positioned(
+ bottom: 0,
+ left: 1,
+ child: Container(
+ width: 16,
+ height: 4,
+ color: color ?? Colors.black,
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// Builds the link button, which upon tap shows a popover for the user
+ /// to enter a URL.
+ Widget _buildLinkButton() {
+ return Tooltip(
+ message: 'Insert Link',
+ waitDuration: _tooltipDelay,
+ child: PopoverScaffold(
+ parentFocusNode: widget.editorFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ onTapOutside: (controller) => _linkPopoverController.close(),
+ controller: _linkPopoverController,
+ buttonBuilder: (context) => TextButton(
+ onPressed: _showUrlPopover,
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.link),
+ ),
+ popoverBuilder: (context) => _buildLinkPopover(),
+ ),
+ );
+ }
+
+ Widget _buildLinkPopover() {
+ return Material(
+ shape: const StadiumBorder(),
+ elevation: 5,
+ clipBehavior: Clip.hardEdge,
+ child: Container(
+ width: 400,
+ height: 40,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Row(
+ children: [
+ Expanded(
+ child: SuperTextField(
+ focusNode: _urlFocusNode,
+ textController: _urlController,
+ minLines: 1,
+ maxLines: 1,
+ inputSource: TextInputSource.ime,
+ hintBehavior: HintBehavior.displayHintUntilTextEntered,
+ hintBuilder: (context) {
+ return const Text(
+ "enter a url...",
+ style: TextStyle(
+ color: Colors.grey,
+ fontSize: 16,
+ ),
+ );
+ },
+ textStyleBuilder: (_) {
+ return const TextStyle(
+ color: Colors.black,
+ fontSize: 16,
+ );
+ },
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ iconSize: 20,
+ splashRadius: 16,
+ padding: EdgeInsets.zero,
+ onPressed: () {
+ setState(() {
+ _urlFocusNode.unfocus();
+ _urlController!.clearTextAndSelection();
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Builds the alignment button, which upon tap shows a popover for the user
+ /// to change the paragraph alignment.
+ Widget _buildAlignmentSelector() {
+ final alignment = _getCurrentTextAlignment();
+ return Tooltip(
+ message: 'Alignment',
+ waitDuration: _tooltipDelay,
+ child: IconSelector(
+ parentFocusNode: widget.editorFocusNode,
+ tapRegionGroupId: _tapRegionGroupId,
+ selectedIcon: IconItem(
+ id: alignment.name,
+ icon: _getTextAlignIcon(alignment),
+ ),
+ icons: const [TextAlign.left, TextAlign.center, TextAlign.right, TextAlign.justify]
+ .map(
+ (alignment) => IconItem(
+ icon: _getTextAlignIcon(alignment),
+ id: alignment.name,
+ ),
+ )
+ .toList(),
+ onSelected: _onChangeAlignmentRequested,
+ ),
+ );
+ }
+}
+
+/// The content of a toolbar, divided by groups of widgets.
+///
+/// Only the groups with index less than [visibleGroupCount]
+/// are displayed. When there is any hidden groups, a button is
+/// displayed to show a popover with the remaining groups.
+class _GroupedToolbarItems extends StatefulWidget {
+ const _GroupedToolbarItems({
+ required this.groups,
+ required this.visibleGroupCount,
+ this.tapRegionGroupId,
+ });
+
+ /// All of the groups available.
+ final List<_WidgetGroup> groups;
+
+ /// The number of groups to be displayed.
+ ///
+ /// All groups with an index greater of equal to [visibleGroupCount]
+ /// are hidden.
+ final int visibleGroupCount;
+
+ /// A group ID for a tap region that is shared with the toolbar items
+ /// that display popovers.
+ ///
+ /// When the popover of hidden groups is displayed, a [TapRegion] is used
+ /// to close this popover upon tapping outside of it. If a popover child
+ /// also displays other popovers, tapping on the child's popover triggers
+ /// [TapRegion.onTapOutside] of the hidden groups popover, closing the popover.
+ /// To prevent that, provide a [tapRegionGroupId] with the same value as
+ /// child's [TapRegion] groupId.
+ final String? tapRegionGroupId;
+
+ @override
+ State<_GroupedToolbarItems> createState() => _GroupedToolbarItemsState();
+}
+
+class _GroupedToolbarItemsState extends State<_GroupedToolbarItems> {
+ final PopoverController _popoverController = PopoverController();
+
+ @override
+ void dispose() {
+ _popoverController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ _GroupedRow(
+ groups: widget.groups,
+ visibleGroupCount: widget.visibleGroupCount,
+ ),
+ if (widget.visibleGroupCount < widget.groups.length) //
+ PopoverScaffold(
+ tapRegionGroupId: widget.tapRegionGroupId,
+ onTapOutside: (controller) => _popoverController.close(),
+ controller: _popoverController,
+ popoverGeometry: PopoverGeometry(aligner: FunctionalPopoverAligner(_aditionalItensAligner)),
+ buttonBuilder: (context) => TextButton(
+ onPressed: () => _popoverController.open(),
+ style: defaultToolbarButtonStyle,
+ child: const Icon(Icons.more_vert),
+ ),
+ popoverBuilder: (context) => _buildAditionalOptionsPopover(),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildAditionalOptionsPopover() {
+ return Material(
+ elevation: 8,
+ borderRadius: BorderRadius.circular(4),
+ color: toolbarBackgroundColor,
+ child: SizedBox(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 9.0),
+ child: Wrap(
+ children: [
+ for (int i = widget.visibleGroupCount; i < widget.groups.length; i++) //
+ ...[
+ ...widget.groups[i].widgets,
+ if (i < widget.groups.length - 1) //
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ child: Container(
+ height: 30,
+ width: 1,
+ color: toolbarDividerColor,
+ ),
+ )
+ ]
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Aligns the "aditional items" popover top-right with the button bottom-right.
+ FollowerAlignment _aditionalItensAligner(
+ Rect globalLeaderRect, Size followerSize, Size screenSize, GlobalKey? boundaryKey) {
+ return const FollowerAlignment(
+ leaderAnchor: Alignment.bottomRight,
+ followerAnchor: Alignment.topRight,
+ followerOffset: Offset(0, 6),
+ );
+ }
+}
+
+/// A row that takes groups of widgets and displays a subset of them separated
+/// by a vertical divider.
+///
+/// Only the groups with index less than [visibleGroupCount] are displayed.
+/// When the visibility of a group changes, its opacity and size is animated.
+///
+/// A group is either entirely visible or entirely invisible.
+class _GroupedRow extends StatelessWidget {
+ const _GroupedRow({
+ required this.groups,
+ required this.visibleGroupCount,
+ });
+
+ /// The groups of widgets to be displayed.
+ final List<_WidgetGroup> groups;
+
+ /// The number of groups that should be visible.
+ final int visibleGroupCount;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ for (int i = 0; i < groups.length; i++) //
+ AnimatedOpacity(
+ opacity: i < visibleGroupCount ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 250),
+ child: AnimatedSize(
+ duration: const Duration(milliseconds: 250),
+ child: SizedBox(
+ width: i < visibleGroupCount ? null : 0,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ...groups[i].widgets,
+ if (i < visibleGroupCount - 1 && i < groups.length - 1) //
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 2.0),
+ child: VerticalDivider(
+ width: 1,
+ color: toolbarDividerColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+/// A group of widgets that should be either all visible or all invisible.
+///
+/// This class is used to avoid passing lists of lists of Widgets around,
+/// to represent lists of groups.
+class _WidgetGroup {
+ _WidgetGroup({
+ required this.widgets,
+ });
+
+ final List widgets;
+}
+
+/// A button that applies a default style and applies the `MaterialState.selected`
+/// when [selected] is `true`.
+class ToolbarImageButton extends StatefulWidget {
+ const ToolbarImageButton({
+ super.key,
+ this.onPressed,
+ this.selected = false,
+ this.size,
+ required this.hint,
+ required this.child,
+ });
+
+ /// Called when the button is pressed.
+ final VoidCallback? onPressed;
+
+ /// Whether or not the internal button should contain the state
+ /// `MaterialState.selected`.
+ final bool selected;
+
+ /// The desired size for the button.
+ final Size? size;
+
+ /// Hint text displayed when hovering the button.
+ final String hint;
+
+ final Widget child;
+
+ @override
+ State createState() => _ToolbarImageButtonState();
+}
+
+class _ToolbarImageButtonState extends State {
+ final WidgetStatesController _statesController = WidgetStatesController();
+
+ @override
+ void initState() {
+ super.initState();
+ _statesController.update(WidgetState.selected, widget.selected);
+ }
+
+ @override
+ void didUpdateWidget(covariant ToolbarImageButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.selected != widget.selected) {
+ _statesController.update(WidgetState.selected, widget.selected);
+ }
+ }
+
+ @override
+ void dispose() {
+ _statesController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final size = widget.size ?? const Size(30, 30);
+ return Tooltip(
+ message: widget.hint,
+ waitDuration: _tooltipDelay,
+ child: TextButton(
+ onPressed: widget.onPressed,
+ statesController: _statesController,
+ style: defaultToolbarButtonStyle.copyWith(
+ fixedSize: WidgetStateProperty.all(size),
+ minimumSize: WidgetStateProperty.all(size),
+ maximumSize: WidgetStateProperty.all(size),
+ ),
+ child: widget.child,
+ ),
+ );
+ }
+}
+
+/// An option displayed on the "Search" button popover.
+class _ActionMenu {
+ _ActionMenu({
+ required this.icon,
+ required this.label,
+ });
+
+ final IconData icon;
+ final String label;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is _ActionMenu && runtimeType == other.runtimeType && icon == other.icon;
+
+ @override
+ int get hashCode => icon.hashCode;
+}
+
+/// The fonts displayed on the font family selector.
+final _availableFonts = [
+ 'Amatic SC',
+ 'Caveat',
+ 'Comfortaa',
+ 'Comic Neue',
+ 'Courier Prime',
+ 'EB Garamond',
+ 'Lexend',
+ 'Lobster',
+ 'Lora',
+ 'Merriweather',
+ 'Montserrat',
+ 'Nunito',
+ 'Oswald',
+ 'Pacifico',
+ 'Playfair Display',
+ 'Roboto',
+ 'Roboto Mono',
+ 'Roboto Serif',
+ 'Special Elite',
+];
+
+/// The block types displayed in the block type selector.
+const _blockTypes = [
+ TextItem(id: BlockTypes.header1, label: 'Header 1'),
+ TextItem(id: BlockTypes.header2, label: 'Header 2'),
+ TextItem(id: BlockTypes.header3, label: 'Header 3'),
+ TextItem(id: BlockTypes.paragraph, label: 'Normal Text'),
+ TextItem(id: BlockTypes.blockquote, label: 'Blockquote'),
+];
+
+const _tooltipDelay = Duration(milliseconds: 500);
+
+IconData _getTextAlignIcon(TextAlign align) {
+ switch (align) {
+ case TextAlign.left:
+ case TextAlign.start:
+ return Icons.format_align_left;
+ case TextAlign.center:
+ return Icons.format_align_center;
+ case TextAlign.right:
+ case TextAlign.end:
+ return Icons.format_align_right;
+ case TextAlign.justify:
+ return Icons.format_align_justify;
+ }
+}
+
+/// Aligns the top-left of the leader with the top-left of the follower.
+FollowerAlignment _searchPopoverAligner(
+ Rect globalLeaderRect, Size followerSize, Size screenSize, GlobalKey? boundaryKey) {
+ return const FollowerAlignment(
+ leaderAnchor: Alignment.topLeft,
+ followerAnchor: Alignment.topLeft,
+ );
+}
+
+/// Common identifiers for the block types used in the app.
+class BlockTypes {
+ static const header1 = 'header1';
+ static const header2 = 'header2';
+ static const header3 = 'header3';
+ static const blockquote = 'blockquote';
+ static const paragraph = 'paragraph';
+}
diff --git a/super_clones/google_docs/linux/.gitignore b/super_clones/google_docs/linux/.gitignore
new file mode 100644
index 0000000000..d3896c9844
--- /dev/null
+++ b/super_clones/google_docs/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/super_clones/google_docs/linux/CMakeLists.txt b/super_clones/google_docs/linux/CMakeLists.txt
new file mode 100644
index 0000000000..d352e4580e
--- /dev/null
+++ b/super_clones/google_docs/linux/CMakeLists.txt
@@ -0,0 +1,145 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "example_docs")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.example_docs")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+ set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# not the value here, or `flutter run` will no longer work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+ install(FILES "${bundled_library}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/super_clones/google_docs/linux/flutter/CMakeLists.txt b/super_clones/google_docs/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000000..d5bd01648a
--- /dev/null
+++ b/super_clones/google_docs/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/super_clones/google_docs/linux/flutter/generated_plugin_registrant.cc b/super_clones/google_docs/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000000..f6f23bfe97
--- /dev/null
+++ b/super_clones/google_docs/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+}
diff --git a/super_clones/google_docs/linux/flutter/generated_plugin_registrant.h b/super_clones/google_docs/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000000..e0f0a47bc0
--- /dev/null
+++ b/super_clones/google_docs/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/super_clones/google_docs/linux/flutter/generated_plugins.cmake b/super_clones/google_docs/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000000..f16b4c3421
--- /dev/null
+++ b/super_clones/google_docs/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/super_clones/google_docs/linux/main.cc b/super_clones/google_docs/linux/main.cc
new file mode 100644
index 0000000000..e7c5c54370
--- /dev/null
+++ b/super_clones/google_docs/linux/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/super_clones/google_docs/linux/my_application.cc b/super_clones/google_docs/linux/my_application.cc
new file mode 100644
index 0000000000..8ef27ac1fe
--- /dev/null
+++ b/super_clones/google_docs/linux/my_application.cc
@@ -0,0 +1,124 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "example_docs");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "example_docs");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+ gtk_widget_show(GTK_WIDGET(window));
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GApplication::startup.
+static void my_application_startup(GApplication* application) {
+ //MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application startup.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
+}
+
+// Implements GApplication::shutdown.
+static void my_application_shutdown(GApplication* application) {
+ //MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application shutdown.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+ G_APPLICATION_CLASS(klass)->startup = my_application_startup;
+ G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_NON_UNIQUE,
+ nullptr));
+}
diff --git a/super_clones/google_docs/linux/my_application.h b/super_clones/google_docs/linux/my_application.h
new file mode 100644
index 0000000000..72271d5e41
--- /dev/null
+++ b/super_clones/google_docs/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/super_clones/google_docs/macos/.gitignore b/super_clones/google_docs/macos/.gitignore
new file mode 100644
index 0000000000..746adbb6b9
--- /dev/null
+++ b/super_clones/google_docs/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/super_clones/google_docs/macos/Flutter/Flutter-Debug.xcconfig b/super_clones/google_docs/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000000..4b81f9b2d2
--- /dev/null
+++ b/super_clones/google_docs/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/super_clones/google_docs/macos/Flutter/Flutter-Release.xcconfig b/super_clones/google_docs/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000000..5caa9d1579
--- /dev/null
+++ b/super_clones/google_docs/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/super_clones/google_docs/macos/Flutter/GeneratedPluginRegistrant.swift b/super_clones/google_docs/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000000..a1cdfd0cd9
--- /dev/null
+++ b/super_clones/google_docs/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,14 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import path_provider_foundation
+import url_launcher_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+}
diff --git a/super_clones/google_docs/macos/Podfile b/super_clones/google_docs/macos/Podfile
new file mode 100644
index 0000000000..b52666a103
--- /dev/null
+++ b/super_clones/google_docs/macos/Podfile
@@ -0,0 +1,43 @@
+platform :osx, '10.15'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/super_clones/google_docs/macos/Podfile.lock b/super_clones/google_docs/macos/Podfile.lock
new file mode 100644
index 0000000000..529de006ae
--- /dev/null
+++ b/super_clones/google_docs/macos/Podfile.lock
@@ -0,0 +1,29 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - path_provider_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ path_provider_foundation:
+ :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+
+SPEC CHECKSUMS:
+ FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+ path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
+
+PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
+
+COCOAPODS: 1.16.2
diff --git a/super_clones/google_docs/macos/Runner.xcodeproj/project.pbxproj b/super_clones/google_docs/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..06513accfb
--- /dev/null
+++ b/super_clones/google_docs/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,791 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 5A172B85CD48C0393B809E4B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38AAD14857BB5ED261D82F81 /* Pods_Runner.framework */; };
+ C601DBF51F895CA52AABE4F5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46F822D71EC0F56AB3B751FD /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 21727F45B11C02AC16A966DE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* example_docs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example_docs.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 38AAD14857BB5ED261D82F81 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A60D6070323B014C0478622 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ 46F822D71EC0F56AB3B751FD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ A7F656F4C68E03056ADF2154 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ CCB80849E380D1FE2CFBC174 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ D1558C4424714AA123578682 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ DB3F90EAA618ACAF38603323 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C601DBF51F895CA52AABE4F5 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5A172B85CD48C0393B809E4B /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 1206F8BB4E786A36A6B9B5E3 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ CCB80849E380D1FE2CFBC174 /* Pods-Runner.debug.xcconfig */,
+ DB3F90EAA618ACAF38603323 /* Pods-Runner.release.xcconfig */,
+ 21727F45B11C02AC16A966DE /* Pods-Runner.profile.xcconfig */,
+ A7F656F4C68E03056ADF2154 /* Pods-RunnerTests.debug.xcconfig */,
+ D1558C4424714AA123578682 /* Pods-RunnerTests.release.xcconfig */,
+ 3A60D6070323B014C0478622 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "