Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ windows/.ffmpeg-cache/

# Local investigation/benchmark evidence (may contain user strategy renders)
/artifacts/

# Built from lib/l10n/*.arb by flutter gen-l10n (pubspec `generate: true`).
lib/l10n/generated/
9 changes: 9 additions & 0 deletions l10n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Every word the app shows lives in lib/l10n: app_en.arb is the source, and
# each other app_<language>.arb holds the same keys translated. Flutter turns
# them into AppLocalizations (lib/l10n/generated, never committed).
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-dir: lib/l10n/generated
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
136 changes: 9 additions & 127 deletions lib/collab/cloud_sync_error_message.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:icarus/collab/collab_models.dart';
import 'package:icarus/collab/generated/convex_error_codes.dart';
import 'package:icarus/collab/page_changes.dart';
Expand Down Expand Up @@ -68,6 +67,15 @@ String incompletePageCopyMessage(String pageName) =>

const _incompletePageCopyMark = "didn't finish saving on this computer";

/// The page name in [reason], when it starts as an
/// [incompletePageCopyMessage] does; null otherwise.
String? incompletePageCopyPageName(String reason) {
const start = 'The copy of ';
final end = reason.lastIndexOf(' $_incompletePageCopyMark');
if (!reason.startsWith(start) || end < start.length) return null;
return reason.substring(start.length, end);
}

/// Whether [reason] is an [incompletePageCopyMessage].
bool isIncompletePageCopyReason(String? reason) =>
reason != null && reason.contains(_incompletePageCopyMark);
Expand Down Expand Up @@ -108,131 +116,5 @@ bool isClientUpgradeRequiredReason(String? reason) =>
const _keptRefusal =
'ConvexFunctionException($clientUpgradeRequiredCode, Client upgrade required)';

/// What the user is told when the server needs a newer Icarus, with the way
/// out their platform has. Desktop says what it can only once it knows
/// whether an update is there to install.
String clientUpgradeRequiredMessage({
bool isWeb = kIsWeb,
ClientUpdateAvailability update = ClientUpdateAvailability.available,
}) {
if (isWeb) return 'Icarus was updated. Reload to keep syncing.';
return switch (update) {
ClientUpdateAvailability.available =>
'Icarus was updated. Install the update to keep syncing.',
ClientUpdateAvailability.checking =>
'Icarus was updated. Looking for the update this app needs to keep '
'syncing.',
ClientUpdateAvailability.unavailable =>
"Icarus was updated, but the update isn't available for this app yet. "
'Your saved work stays on this device and syncs once the update is '
'installed.',
};
}

/// Whether the update a refused desktop build needs can be installed yet.
enum ClientUpdateAvailability { available, checking, unavailable }

/// What the user is told about a page delete held for them (see
/// heldPageDelete). A page this device could not check is never said to
/// have changed: nothing says it did.
String heldPageDeleteMessage(({String page, bool checked}) held) => held.checked
? '${held.page} $pageChangedSinceDeleteReason. Review before deleting.'
: "Icarus couldn't check ${held.page} for a teammate's changes. "
'Review it before deleting.';

String friendlyCloudSyncError(String raw) {
if (isClientUpgradeRequiredReason(raw)) {
return clientUpgradeRequiredMessage();
}
if (heldPageDelete(raw) case final held?) {
return heldPageDeleteMessage(held);
}
final lower = raw.toLowerCase();
if (lower.contains(_incompletePageCopyMark)) return raw;
if (lower.contains('strategy was deleted')) {
return 'This strategy was deleted, so its unsent changes cannot be '
'saved. Discard them from the library.';
}
if (lower.contains('unreadable saved work')) {
return 'A saved cloud change could not be read. It remains on this '
'device; keep this strategy open and recover the outbox before '
'continuing.';
}
if (lower.contains('could not be verified in the durable outbox') ||
lower.contains('could not be saved to the durable outbox')) {
return unverifiedCloudWorkMessage;
}
if (lower.contains(editAccessRemovedMessage.toLowerCase())) {
return editAccessRemovedMessage;
}
if (lower.contains('forbidden')) {
return 'This account does not have permission to save these changes. '
'They remain on this device. Ask the owner for edit access, then '
'retry.';
}
if (lower.contains('retry paused')) {
return 'A saved cloud change is paused after repeated failures. Retry '
'when the connection and account are healthy.';
}
if (lower.contains(pageTooLargeToCopyMessage.toLowerCase())) {
return 'A new page was too large for the cloud to copy, so it was not '
'added. Keep mine tries again; Use cloud drops it.';
}
if (lower.contains('too large for cloud sync')) {
return 'A saved change is too large for cloud sync. It remains saved on '
'this device. Reduce it, then choose Keep mine to retry, or Use '
'cloud to drop it.';
}
if (lower.contains(lineupOverlapMessage.toLowerCase())) {
return 'This lineup shares a spot with lineups saved separately, so it '
'was not saved to the cloud. It remains on this device. Keep mine '
'sends it again, and is refused while the spot is still shared; Use '
'cloud removes it here.';
}
if (lower.contains(lineupPageMismatchMessage.toLowerCase())) {
return 'This lineup clashes with one on another page, so it was not '
'saved to the cloud. It remains on this device; Use cloud removes '
'it here.';
}
if (lower.contains(teammateDeletedCannotRestoreMessage.toLowerCase())) {
return 'A teammate deleted what this change edits, and this change '
'cannot bring it back. Use cloud to let it go.';
}
if (lower.contains(teammateDeletedMessage.toLowerCase())) {
return 'A teammate deleted what you changed. Keep mine brings it back '
'as you have it; Use cloud lets it go.';
}
if (lower.contains(retiredLineupOpMessage.toLowerCase())) {
return 'A lineup change saved by an older version of Icarus cannot be '
'sent to the cloud, so it is not in this strategy. Redo it, then '
'choose Use cloud to clear this.';
}
if (lower.contains(undrawableWorkMessage.toLowerCase())) {
return 'A saved change no longer fits this page as the cloud has it now, '
'so it was not sent. It remains on this device. Keep mine replaces '
"the cloud's version with yours, whole; Use cloud drops yours.";
}
if (lower.contains(pageDeletedMessage.toLowerCase())) {
return 'The page these changes were on was deleted, so they were not '
'saved. They remain on this device; Use cloud drops them.';
}
if (lower.contains('needs attention')) {
return 'Another edit reached the cloud first. Your version remains '
'saved on this device.';
}
if (lower.contains('cannot be retried automatically')) {
return 'The server cannot match this retained edit to a current cloud '
'revision. It remains saved on this device.';
}
if (lower.contains('auth')) {
return 'Your cloud session needs to be refreshed — retry, or sign in '
'again from the library.';
}
if (lower.contains('offline') || lower.contains('connection')) {
return 'The cloud could not be reached.';
}
if (lower.contains('setup is not ready')) {
return 'Cloud sync is still starting up.';
}
return "Some changes haven't reached the cloud yet.";
}
36 changes: 22 additions & 14 deletions lib/config/platform_policy.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/l10n/l10n.dart';

/// A feature one platform may not offer. [label] names it in user-facing
/// copy, so it reads as the subject of a sentence.
enum PlatformFeature {
exportFiles('Export'),
importFiles('Import'),
screenshot('Screenshot'),
videoExport('Video export'),
fileDrop('Drag and drop'),
addImages('Adding images'),
addLineups('Adding lineups'),
replays('Replays');
exportFiles,
importFiles,
screenshot,
videoExport,
fileDrop,
addImages,
addLineups,
replays;

const PlatformFeature(this.label);

final String label;
String label(AppLocalizations l10n) => switch (this) {
exportFiles => l10n.serviceMsgFeatureExport,
importFiles => l10n.serviceMsgFeatureImport,
screenshot => l10n.serviceMsgFeatureScreenshot,
videoExport => l10n.serviceMsgFeatureVideoExport,
fileDrop => l10n.serviceMsgFeatureFileDrop,
addImages => l10n.serviceMsgFeatureAddImages,
addLineups => l10n.serviceMsgFeatureAddLineups,
replays => l10n.serviceMsgFeatureReplays,
};
}

/// What this build of Icarus offers. The web beta is one value of this
Expand Down Expand Up @@ -77,12 +85,12 @@ class PlatformPolicy {
!desktopOnly.contains(feature) && !comingToWebBeta.contains(feature);

/// Why [feature] is not here, in one short sentence, or null when it is.
String? unavailableMessage(PlatformFeature feature) {
String? unavailableMessage(AppLocalizations l10n, PlatformFeature feature) {
if (desktopOnly.contains(feature)) {
return '${feature.label} is desktop-only for now.';
return l10n.serviceMsgFeatureDesktopOnly(feature.label(l10n));
}
if (comingToWebBeta.contains(feature)) {
return '${feature.label} is coming to the web beta.';
return l10n.serviceMsgFeatureComingToWeb(feature.label(l10n));
}
return null;
}
Expand Down
19 changes: 11 additions & 8 deletions lib/const/line_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:ui';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/l10n/l10n.dart';
import 'package:icarus/providers/collab/strategy_capabilities_provider.dart';
import 'package:hive_ce_flutter/adapters.dart';
import 'package:icarus/const/agents.dart';
Expand Down Expand Up @@ -1029,16 +1030,19 @@ class LineUpProvider extends Notifier<LineUpState> {
if (placement == null) return;
if (placement.pinnedOriginId != null) {
Settings.showToast(
message: "The origin is pinned. Drag an ability instead.",
message: ref.read(l10nProvider).lineupOriginPinned,
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
return;
}
final pinnedType = placement.pinnedAgentType;
if (pinnedType != null && agent.type != pinnedType) {
final l10n = ref.read(l10nProvider);
Settings.showToast(
message: "Drag ${AgentData.agents[pinnedType]?.name ?? 'the agent'} "
"for this landing spot.",
message: switch (AgentData.agents[pinnedType]?.name) {
final name? => l10n.lineupDragPinnedAgent(l10n.gameName(name)),
null => l10n.lineupDragPinnedAgentUnknown,
},
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
return;
Expand All @@ -1059,22 +1063,22 @@ class LineUpProvider extends Notifier<LineUpState> {
if (placement == null) return;
if (placement.pinnedLandingId != null) {
Settings.showToast(
message: "The landing spot is pinned. Drag an agent instead.",
message: ref.read(l10nProvider).lineupLandingPinned,
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
return;
}
final agentType = placement.lockedAgentType;
if (agentType == null) {
Settings.showToast(
message: "Please select an agent first.",
message: ref.read(l10nProvider).lineupSelectAgentFirst,
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
return;
}
if (ability.data.type != agentType) {
Settings.showToast(
message: "Ability does not match the selected agent.",
message: ref.read(l10nProvider).lineupAbilityAgentMismatch,
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
return;
Expand Down Expand Up @@ -1791,8 +1795,7 @@ class LineUpProvider extends Notifier<LineUpState> {

void _toastDroppedEdit() {
Settings.showToast(
message: 'The page changed, so the placement edit closed without '
'saving.',
message: ref.read(l10nProvider).lineupPlacementEditClosed,
backgroundColor: Settings.tacticalVioletTheme.destructive,
);
}
Expand Down
Loading
Loading