fluffychat/lib/pangea/choreographer/igc/span_data_model.dart
ggurdin e8428783e6
Fluffychat merge 2 (#5590)
* build: Reenable shrink resources and minify in gradle

* build: (deps): bump image from 4.6.0 to 4.7.1

Bumps [image](https://github.com/brendan-duncan/image) from 4.6.0 to 4.7.1.
- [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md)
- [Commits](https://github.com/brendan-duncan/image/commits)

---
updated-dependencies:
- dependency-name: image
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build: (deps): bump file_picker from 10.3.7 to 10.3.8

Bumps [file_picker](https://github.com/miguelpruivo/flutter_file_picker) from 10.3.7 to 10.3.8.
- [Release notes](https://github.com/miguelpruivo/flutter_file_picker/releases)
- [Changelog](https://github.com/miguelpruivo/flutter_file_picker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/miguelpruivo/flutter_file_picker/compare/v10.3.7...v10.3.8)

---
updated-dependencies:
- dependency-name: file_picker
  dependency-version: 10.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: Improved search

* build: Use matrix sdk vom pub.dev again

* chore: Follow up better search

* build: (deps): bump image from 4.7.1 to 4.7.2

Bumps [image](https://github.com/brendan-duncan/image) from 4.7.1 to 4.7.2.
- [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md)
- [Commits](https://github.com/brendan-duncan/image/commits)

---
updated-dependencies:
- dependency-name: image
  dependency-version: 4.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: Make cross signing self sign mandatory for bootstrap

* chore: Update user device keys before creating bootstrap

* fix: Better wait for secrets after verification bootstrap

* refactor: Remove native imaging and enable web worker

* refactor: Remove unused html onfocus streams

* build: (deps): bump flutter_foreground_task from 9.1.0 to 9.2.0

Bumps [flutter_foreground_task](https://github.com/Dev-hwang/flutter_foreground_task) from 9.1.0 to 9.2.0.
- [Changelog](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Dev-hwang/flutter_foreground_task/commits)

---
updated-dependencies:
- dependency-name: flutter_foreground_task
  dependency-version: 9.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(translations): Translated using Weblate (Uzbek)

Currently translated at 99.7% (823 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uz/

* chore(translations): Translated using Weblate (Russian)

Currently translated at 99.8% (824 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/

* chore(translations): Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.9% (750 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/

* chore(translations): Translated using Weblate (Galician)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/

* chore(translations): Translated using Weblate (Basque)

Currently translated at 99.7% (823 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/

* chore(translations): Translated using Weblate (Ukrainian)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/

* chore(translations): Translated using Weblate (Estonian)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/

* chore(translations): Translated using Weblate (Dutch)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nl/

* chore(translations): Translated using Weblate (Russian)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/

* chore(translations): Translated using Weblate (Spanish)

Currently translated at 95.2% (788 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/

* chore(translations): Translated using Weblate (Spanish)

Currently translated at 96.3% (797 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/

* chore(translations): Translated using Weblate (Russian)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/

* chore(translations): Translated using Weblate (Russian)

Currently translated at 100.0% (825 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/

* fix: Broken ruzzian plurals

* chore(translations): Translated using Weblate (Norwegian Bokmål)

Currently translated at 91.2% (753 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/

* chore(translations): Translated using Weblate (Bengali)

Currently translated at 4.5% (38 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/bn/

* chore(translations): Translated using Weblate (French)

Currently translated at 82.3% (679 of 825 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fr/

* build: (deps): bump translations_cleaner from 0.0.5 to 0.1.0

Bumps [translations_cleaner](https://github.com/Chinmay-KB/translations_cleaner) from 0.0.5 to 0.1.0.
- [Changelog](https://github.com/Chinmay-KB/translations_cleaner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Chinmay-KB/translations_cleaner/commits)

---
updated-dependencies:
- dependency-name: translations_cleaner
  dependency-version: 0.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(translations): Translated using Weblate (German)

Currently translated at 99.2% (821 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/

* chore(translations): Translated using Weblate (Estonian)

Currently translated at 100.0% (827 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/

* build: Bump version to 2.4.0

* build: (deps): bump sqflite_common_ffi from 2.3.6 to 2.3.7+1

Bumps [sqflite_common_ffi](https://github.com/tekartik/sqflite) from 2.3.6 to 2.3.7+1.
- [Commits](https://github.com/tekartik/sqflite/compare/sqflite_common_ffi_v2.3.6...sqflite_common_ffi/v2.3.7)

---
updated-dependencies:
- dependency-name: sqflite_common_ffi
  dependency-version: 2.3.7+1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(translations): Translated using Weblate (Czech)

Currently translated at 66.1% (547 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/

* chore(translations): Translated using Weblate (Czech)

Currently translated at 72.7% (602 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/

* chore(translations): Translated using Weblate (German)

Currently translated at 99.8% (826 of 827 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/

* chore: Add security.md file

* fix: Locale unlocalized strings

* build: (deps): bump matrix from 4.1.0 to 5.0.0

Bumps [matrix](https://github.com/famedly/matrix-dart-sdk) from 4.1.0 to 5.0.0.
- [Release notes](https://github.com/famedly/matrix-dart-sdk/releases)
- [Changelog](https://github.com/famedly/matrix-dart-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/famedly/matrix-dart-sdk/compare/v4.1.0...v5.0.0)

---
updated-dependencies:
- dependency-name: matrix
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: Notifications on web correctly managed when tab not focused

* chore: Add changelog for android

* chore: Remove duplicated localization

* fix: Sign in label

* chore: Versionize fcm shared isolate

* build: Remove unused packag

* build: (deps): bump package_info_plus from 8.3.1 to 9.0.0

Bumps [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) from 8.3.1 to 9.0.0.
- [Release notes](https://github.com/fluttercommunity/plus_plugins/releases)
- [Commits](https://github.com/fluttercommunity/plus_plugins/commits/package_info_plus-v9.0.0/packages/package_info_plus)

---
updated-dependencies:
- dependency-name: package_info_plus
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: Display particle animation on login page

* chore: Use fixed version of fcm shared isolate

* fix: apk crash on some platforms due new flutter version

* chore: Correct kotlin format

* fix iOS notifications

* fluffychat merge

* fluffychat merge

* fluffychat merge

* fluffychat merge

* fluffychat merge

* fluffychat merge

* add missing type annotations

* update matrix version

* fluffychat merge

* fluffychat merge

* fix notification on click actions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Christian Kußowski <c.kussowski@famedly.com>
Co-authored-by: Krille-chan <christian-kussowski@posteo.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BeMeritus <bemerituss@gmail.com>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: josé m. <correoxm@disroot.org>
Co-authored-by: xabirequejo <xabi.rn@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Jelv <post@jelv.nl>
Co-authored-by: Дмитрий Михирев <bizdelnick@gmail.com>
Co-authored-by: Kimby <kimbyqs@gmail.com>
Co-authored-by: Christian <christian-pauly@posteo.de>
Co-authored-by: Kom nake <kominak310@svcache.com>
Co-authored-by: hugues de keyzer <komputilisto@hugues.info>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: Šebestová <ka.sebestova.cz@gmail.com>
2026-02-10 08:01:12 -05:00

337 lines
8.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/igc/text_normalization_util.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'replacement_type_enum.dart';
import 'span_choice_type_enum.dart';
class SpanData {
final String? message;
final String? shortMessage;
final List<SpanChoice>? choices;
final int offset;
final int length;
final String fullText;
final ReplacementTypeEnum type;
final Rule? rule;
SpanData({
required this.message,
required this.shortMessage,
required this.choices,
required this.offset,
required this.length,
required this.fullText,
required this.type,
required this.rule,
});
SpanData copyWith({
String? message,
String? shortMessage,
List<SpanChoice>? choices,
int? offset,
int? length,
String? fullText,
ReplacementTypeEnum? type,
Rule? rule,
}) {
return SpanData(
message: message ?? this.message,
shortMessage: shortMessage ?? this.shortMessage,
choices: choices ?? this.choices,
offset: offset ?? this.offset,
length: length ?? this.length,
fullText: fullText ?? this.fullText,
type: type ?? this.type,
rule: rule ?? this.rule,
);
}
/// Parse SpanData from JSON.
///
/// [parentFullText] is used as fallback when the span JSON doesn't contain
/// full_text (e.g., when the server omits it to reduce payload size and
/// the full text is available at the response level as original_input).
factory SpanData.fromJson(
Map<String, dynamic> json, {
String? parentFullText,
}) {
final Iterable? choices = json['choices'] ?? json['replacements'];
final dynamic rawType =
json['type'] ?? json['type_name'] ?? json['typeName'];
final String? typeString = rawType is Map<String, dynamic>
? (rawType['type_name'] ?? rawType['type'] ?? rawType['typeName'])
as String?
: rawType as String?;
// Try to get fullText from span JSON, fall back to parent's original_input
final String? spanFullText =
json['sentence'] ?? json['full_text'] ?? json['fullText'];
final String fullText = spanFullText ?? parentFullText ?? '';
return SpanData(
message: json['message'],
shortMessage: json['shortMessage'] ?? json['short_message'],
choices: choices
?.map<SpanChoice>(
(e) => SpanChoice.fromJson(e as Map<String, dynamic>),
)
.toList(),
offset: json['offset'] as int,
length: json['length'] as int,
fullText: fullText,
type:
SpanDataTypeEnumExt.fromString(typeString) ??
ReplacementTypeEnum.other,
rule: json['rule'] != null
? Rule.fromJson(json['rule'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {
'offset': offset,
'length': length,
'full_text': fullText,
'type': type.name,
};
if (message != null) {
data['message'] = message;
}
if (shortMessage != null) {
data['short_message'] = shortMessage;
}
if (choices != null) {
data['choices'] = List<dynamic>.from(choices!.map((x) => x.toJson()));
}
if (rule != null) {
data['rule'] = rule!.toJson();
}
return data;
}
bool isOffsetInMatchSpan(int offset) =>
offset >= this.offset && offset <= this.offset + length;
SpanChoice? get bestChoice {
return choices?.firstWhereOrNull((choice) => choice.isBestCorrection);
}
int get selectedChoiceIndex {
if (choices == null) {
return -1;
}
SpanChoice? mostRecent;
for (int i = 0; i < choices!.length; i++) {
final choice = choices![i];
if (choice.timestamp != null &&
(mostRecent == null ||
choice.timestamp!.isAfter(mostRecent.timestamp!))) {
mostRecent = choice;
}
}
return mostRecent != null ? choices!.indexOf(mostRecent) : -1;
}
SpanChoice? get selectedChoice {
final index = selectedChoiceIndex;
if (index == -1) {
return null;
}
return choices![index];
}
String get errorSpan =>
fullText.characters.skip(offset).take(length).toString();
/// Whether this span is a minor correction that should be auto-applied.
/// Returns true if:
/// 1. The type is explicitly marked as auto-apply (e.g., punct, spell, cap, diacritics), OR
/// 2. For backwards compatibility with old data that lacks new types:
/// the type is NOT auto-apply AND the normalized strings match.
bool isNormalizationError() {
// New data with explicit auto-apply types
if (type.isAutoApply) {
return true;
}
final correctChoice = choices
?.firstWhereOrNull((c) => c.isBestCorrection)
?.value;
final l2Code =
MatrixState.pangeaController.userController.userL2?.langCodeShort;
return correctChoice != null &&
l2Code != null &&
normalizeString(correctChoice, l2Code) ==
normalizeString(errorSpan, l2Code);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! SpanData) return false;
if (other.message != message) return false;
if (other.shortMessage != shortMessage) return false;
if (other.offset != offset) return false;
if (other.length != length) return false;
if (other.fullText != fullText) return false;
if (other.type != type) return false;
if (other.rule != rule) return false;
if (const ListEquality().equals(
other.choices?.sorted((a, b) => b.value.compareTo(a.value)),
choices?.sorted((a, b) => b.value.compareTo(a.value)),
) ==
false) {
return false;
}
return true;
}
@override
int get hashCode {
return message.hashCode ^
shortMessage.hashCode ^
Object.hashAll(
(choices ?? [])
.sorted((a, b) => b.value.compareTo(a.value))
.map((choice) => choice.hashCode),
) ^
offset.hashCode ^
length.hashCode ^
fullText.hashCode ^
type.hashCode ^
rule.hashCode;
}
}
class SpanChoice {
final String value;
final SpanChoiceTypeEnum type;
final bool selected;
final String? feedback;
final DateTime? timestamp;
SpanChoice({
required this.value,
required this.type,
this.feedback,
this.selected = false,
this.timestamp,
});
SpanChoice copyWith({
String? value,
SpanChoiceTypeEnum? type,
String? feedback,
bool? selected,
DateTime? timestamp,
}) {
return SpanChoice(
value: value ?? this.value,
type: type ?? this.type,
feedback: feedback ?? this.feedback,
selected: selected ?? this.selected,
timestamp: timestamp ?? this.timestamp,
);
}
factory SpanChoice.fromJson(Map<String, dynamic> json) {
return SpanChoice(
value: json['value'] as String,
type: json['type'] != null
? SpanChoiceTypeEnum.values.firstWhereOrNull(
(element) => element.name == json['type'],
) ??
SpanChoiceTypeEnum.suggestion
: SpanChoiceTypeEnum.suggestion,
feedback: json['feedback'],
selected: json['selected'] ?? false,
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'])
: null,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {'value': value, 'type': type.name};
// V2 format: use selected_at instead of separate selected + timestamp
if (selected && timestamp != null) {
data['selected_at'] = timestamp!.toIso8601String();
}
if (feedback != null) {
data['feedback'] = feedback;
}
return data;
}
String feedbackToDisplay(BuildContext context) {
if (feedback == null) {
return type.defaultFeedback(context);
}
return feedback!;
}
bool get isBestCorrection => type.isSuggestion;
Color get color => type.color;
// override == operator and hashcode
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SpanChoice &&
other.value == value &&
other.type.toString() == type.toString() &&
other.selected == selected &&
other.feedback == feedback &&
other.timestamp == timestamp;
}
@override
int get hashCode {
return value.hashCode ^
type.hashCode ^
selected.hashCode ^
feedback.hashCode ^
timestamp.hashCode;
}
}
class Rule {
final String id;
const Rule({required this.id});
factory Rule.fromJson(Map<String, dynamic> json) =>
Rule(id: json['id'] as String);
Map<String, dynamic> toJson() => {'id': id};
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Rule) return false;
return other.id == id;
}
@override
int get hashCode {
return id.hashCode;
}
}