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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ After audits, investigations, or substantial testing runs, read [answers.md](ans
Before changing view cones, map blockers, or elevation handling, read [docs/vision-model.md](docs/vision-model.md). It defines which data controls wall placement and height.

## The library is sacred
Corrupted or dropped library data is unrecoverable. In local mode nothing here changes: a change to the Hive models means source models, generated adapters, and a migration (`lib/migrations/`) so that data written by any past version loads in this one. In cloud mode the op queue holds work the user believes is saved. Every op either lands or the user is told, on screen, before they walk away. When a write path is uncertain, fail loudly without saving rather than save something wrong.
Corrupted or dropped library data is unrecoverable. In local mode nothing here changes: a change to the Hive models means source models and generated adapters, plus a migration (`lib/migrations/`) whenever data written by a past version would not load correctly in this one. A new field whose default reproduces the old behavior needs no migration; a test that reads a record written without the field proves it. In cloud mode the op queue holds work the user believes is saved. Every op either lands or the user is told, on screen, before they walk away. When a write path is uncertain, fail loudly without saving rather than save something wrong.

## Sync status is a promise
The chip that says synced is the app promising the work is on the server. A conflict resolved in silence makes the app lie. So does an op dropped after retries, and so do offline edits with no badge. A user who catches the app lying once stops trusting it with their library. Every state the user's work can be in has a face on screen, and when the true state is uncertain, show uncertainty.
Expand Down
8 changes: 6 additions & 2 deletions lib/hive/hive_adapters.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion lib/hive/hive_adapters.g.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ types:
index: 4
AppPreferences:
typeId: 28
nextIndex: 19
nextIndex: 20
fields:
defaultThemeProfileIdForNewStrategies:
index: 0
Expand Down Expand Up @@ -409,6 +409,8 @@ types:
index: 17
videoExportStepDurationSeconds:
index: 18
backgroundDotOpacity:
index: 19
PlacedViewConeAgent:
typeId: 29
nextIndex: 11
Expand Down
15 changes: 15 additions & 0 deletions lib/providers/user_preferences_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class AppPreferences extends HiveObject {
final bool discordPresenceEnabled;
final double videoExportStepDurationSeconds;

/// How strongly the dot grid behind maps and the library draws, from 0
/// (hidden) to 1 (the original look).
final double backgroundDotOpacity;

Comment thread
greptile-apps[bot] marked this conversation as resolved.
AppPreferences({
required this.defaultThemeProfileIdForNewStrategies,
this.autosaveEnabled = true,
Expand All @@ -148,6 +152,7 @@ class AppPreferences extends HiveObject {
this.drawingThickness = Settings.defaultStrokeThickness,
this.discordPresenceEnabled = true,
this.videoExportStepDurationSeconds = 3.0,
this.backgroundDotOpacity = 1.0,
}) : customColorValues = List.unmodifiable(customColorValues ?? const []),
customShortcutBindings =
Map.unmodifiable(customShortcutBindings ?? const {});
Expand All @@ -171,6 +176,7 @@ class AppPreferences extends HiveObject {
double? drawingThickness,
bool? discordPresenceEnabled,
double? videoExportStepDurationSeconds,
double? backgroundDotOpacity,
}) {
return AppPreferences(
defaultThemeProfileIdForNewStrategies:
Expand Down Expand Up @@ -201,6 +207,7 @@ class AppPreferences extends HiveObject {
discordPresenceEnabled ?? this.discordPresenceEnabled,
videoExportStepDurationSeconds:
videoExportStepDurationSeconds ?? this.videoExportStepDurationSeconds,
backgroundDotOpacity: backgroundDotOpacity ?? this.backgroundDotOpacity,
);
}
}
Expand Down Expand Up @@ -656,6 +663,14 @@ class AppPreferencesNotifier extends Notifier<AppPreferences> {
);
}

Future<void> setBackgroundDotOpacity(double opacity) {
return _updatePreferences(
(current) => current.copyWith(
backgroundDotOpacity: opacity.clamp(0.0, 1.0),
),
);
}

Future<void> setDrawingDefaults({
int? colorValue,
double? thickness,
Expand Down
1 change: 1 addition & 0 deletions lib/screenshot/page_screenshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Future<Uint8List> captureEditorPage(WidgetRef ref) async {
showSpawnBarrier: mapState.showSpawnBarrier,
showRegionNames: mapState.showRegionNames,
showUltOrbs: mapState.showUltOrbs,
backgroundDotOpacity: ref.read(appPreferencesProvider).backgroundDotOpacity,
agents: page.agentData,
abilities: page.abilityData,
text: page.textData,
Expand Down
8 changes: 5 additions & 3 deletions lib/screenshot/screenshot_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ScreenshotView extends ConsumerWidget {
required this.showSpawnBarrier,
required this.showRegionNames,
required this.showUltOrbs,
required this.backgroundDotOpacity,
required this.agents,
required this.abilities,
required this.text,
Expand All @@ -52,6 +53,7 @@ class ScreenshotView extends ConsumerWidget {
final bool showSpawnBarrier;
final bool showRegionNames;
final bool showUltOrbs;
final double backgroundDotOpacity;
final List<PlacedAgentNode> agents;
final List<PlacedAbility> abilities;
final List<PlacedText> text;
Expand Down Expand Up @@ -134,10 +136,10 @@ class ScreenshotView extends ConsumerWidget {
width: CoordinateSystem.screenShotSize.width,
child: Stack(
children: [
const Positioned.fill(
Positioned.fill(
child: Padding(
padding: EdgeInsets.all(4.0),
child: DotGrid(isScreenshot: true),
padding: const EdgeInsets.all(4.0),
child: DotGrid(isScreenshot: true, opacity: backgroundDotOpacity),
)),
Positioned(
left: mapLeft,
Expand Down
17 changes: 17 additions & 0 deletions lib/services/archive_manifest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ class ArchiveGlobals {
required this.showSpawnBarrier,
required this.showUltOrbs,
required this.showRegionNames,
required this.backgroundDotOpacity,
required this.customColorValues,
this.hasCustomColorValues = true,
required this.favoriteAgents,
Expand All @@ -219,6 +220,7 @@ class ArchiveGlobals {
final bool? showSpawnBarrier;
final bool? showUltOrbs;
final bool? showRegionNames;
final double? backgroundDotOpacity;
final List<int> customColorValues;
final bool hasCustomColorValues;
final List<String> favoriteAgents;
Expand All @@ -233,6 +235,7 @@ class ArchiveGlobals {
'showSpawnBarrier': showSpawnBarrier,
'showUltOrbs': showUltOrbs,
'showRegionNames': showRegionNames,
'backgroundDotOpacity': backgroundDotOpacity,
'customColorValues': customColorValues,
},
'favoriteAgents': favoriteAgents,
Expand Down Expand Up @@ -267,6 +270,9 @@ class ArchiveGlobals {
showRegionNames: appPreferencesMap == null
? null
: _readNullableBool(appPreferencesMap, 'showRegionNames'),
backgroundDotOpacity: appPreferencesMap == null
? null
: _readNullableDouble(appPreferencesMap, 'backgroundDotOpacity'),
customColorValues: appPreferencesMap == null
? const []
: _readOptionalList(appPreferencesMap, 'customColorValues')
Expand Down Expand Up @@ -442,6 +448,17 @@ bool? _readNullableBool(Map<String, dynamic> json, String key) {
return _readRequiredBool(json, key);
}

double? _readNullableDouble(Map<String, dynamic> json, String key) {
final value = json[key];
if (value == null) return null;
if (value is num) return value.toDouble();
if (value is String) {
final parsed = double.tryParse(value);
if (parsed != null) return parsed;
}
throw FormatException('Expected number or null for $key');
}

bool _readRequiredBool(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is bool) {
Expand Down
3 changes: 3 additions & 0 deletions lib/services/video_export/video_exporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class VideoExporter {
required this.strategy,
required this.strategyState,
required this.mapState,
required this.backgroundDotOpacity,
required this.geometry,
this.imageSources = const {},
this.navigation,
Expand All @@ -54,6 +55,7 @@ class VideoExporter {
final StrategyData strategy;
final StrategyState strategyState;
final MapState mapState;
final double backgroundDotOpacity;
final VisionGeometryMap? geometry;

/// What each image on the exported pages paints, by image id; see
Expand Down Expand Up @@ -305,6 +307,7 @@ class VideoExporter {
showSpawnBarrier: mapState.showSpawnBarrier,
showRegionNames: mapState.showRegionNames,
showUltOrbs: mapState.showUltOrbs,
backgroundDotOpacity: backgroundDotOpacity,
agents: page.agentData,
abilities: page.abilityData,
text: page.textData,
Expand Down
3 changes: 3 additions & 0 deletions lib/strategy/strategy_import_export.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,8 @@ class StrategyImportExportService {
showUltOrbs: globals.showUltOrbs ?? currentPreferences.showUltOrbs,
showRegionNames:
globals.showRegionNames ?? currentPreferences.showRegionNames,
backgroundDotOpacity: globals.backgroundDotOpacity?.clamp(0.0, 1.0) ??
currentPreferences.backgroundDotOpacity,
customColorValues: globals.hasCustomColorValues
? globals.customColorValues
: currentPreferences.customColorValues,
Expand Down Expand Up @@ -2262,6 +2264,7 @@ class StrategyImportExportService {
showSpawnBarrier: appPreferences?.showSpawnBarrier,
showUltOrbs: appPreferences?.showUltOrbs,
showRegionNames: appPreferences?.showRegionNames,
backgroundDotOpacity: appPreferences?.backgroundDotOpacity,
customColorValues: appPreferences?.customColorValues ?? const [],
favoriteAgents: favoriteAgents,
);
Expand Down
2 changes: 2 additions & 0 deletions lib/widgets/dialogs/export_video_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ class _ExportVideoDialogState extends ConsumerState<ExportVideoDialog> {
strategy: source.strategy,
strategyState: ref.read(strategyProvider),
mapState: mapState,
backgroundDotOpacity:
ref.read(appPreferencesProvider).backgroundDotOpacity,
geometry: geometry,
imageSources: images.sources,
navigation: navigation,
Expand Down
31 changes: 25 additions & 6 deletions lib/widgets/dot_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,39 @@
import 'dart:ui' show PointMode;

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/providers/user_preferences_provider.dart';

class DotGrid extends StatelessWidget {
class DotGrid extends ConsumerWidget {
const DotGrid({
super.key,
this.isScreenshot = false,
this.opacity,
});
final bool isScreenshot;

/// Offscreen captures pass the opacity in because their isolated provider
/// container doesn't read Hive. Everywhere else the user's setting applies.
final double? opacity;

@override
Widget build(BuildContext context) {
return CustomPaint(painter: DotPainter(isScreenshot: isScreenshot));
Widget build(BuildContext context, WidgetRef ref) {
final double opacity = this.opacity ??
ref.watch(
appPreferencesProvider.select((prefs) => prefs.backgroundDotOpacity),
);
return CustomPaint(
painter: DotPainter(isScreenshot: isScreenshot, opacity: opacity),
);
}
}

class DotPainter extends CustomPainter {
DotPainter({required this.isScreenshot});
DotPainter({required this.isScreenshot, required this.opacity});

final bool isScreenshot;
final double opacity;
Size? _cachedSize;
List<Offset> _cachedPoints = const [];

Expand All @@ -77,8 +92,11 @@ class DotPainter extends CustomPainter {

@override
void paint(Canvas canvas, Size size) {
if (opacity <= 0) return;

final paint = Paint()
..color = Settings.tacticalVioletTheme.border.withValues(alpha: 0.7)
..color =
Settings.tacticalVioletTheme.border.withValues(alpha: 0.7 * opacity)
..strokeWidth = dotSize
..strokeCap = StrokeCap.round;

Expand Down Expand Up @@ -122,6 +140,7 @@ class DotPainter extends CustomPainter {

@override
bool shouldRepaint(DotPainter oldDelegate) {
return oldDelegate.isScreenshot != isScreenshot;
return oldDelegate.opacity != opacity ||
oldDelegate.isScreenshot != isScreenshot;
}
}
33 changes: 24 additions & 9 deletions lib/widgets/hover_dot_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/providers/user_preferences_provider.dart';

/// A shader-driven replacement for [DotGrid] that reacts to the cursor:
/// dots near the mouse brighten and swell while the rest of the field dims.
/// Falls back to nothing while the shader is loading (a single frame).
class HoverDotGrid extends StatefulWidget {
/// Both colors scale with the user's background dot opacity.
class HoverDotGrid extends ConsumerStatefulWidget {
const HoverDotGrid({
super.key,
this.spacing = 9.5,
Expand Down Expand Up @@ -39,10 +42,10 @@ class HoverDotGrid extends StatefulWidget {
}

@override
State<HoverDotGrid> createState() => _HoverDotGridState();
ConsumerState<HoverDotGrid> createState() => _HoverDotGridState();
}

class _HoverDotGridState extends State<HoverDotGrid>
class _HoverDotGridState extends ConsumerState<HoverDotGrid>
with SingleTickerProviderStateMixin {
ui.FragmentShader? _shader;
late final Ticker _ticker;
Expand Down Expand Up @@ -80,6 +83,13 @@ class _HoverDotGridState extends State<HoverDotGrid>

void _handleGlobalPointer(PointerEvent event) {
if (event.kind != PointerDeviceKind.mouse) return;
// A hidden grid has no glow to animate. Drop any glow at once, so
// turning the dots back on doesn't show it at a stale spot.
if (ref.read(appPreferencesProvider).backgroundDotOpacity <= 0) {
_hover = 0;
_hoverTarget = 0;
return;
}
if (event is PointerRemovedEvent) {
_onExit();
return;
Expand Down Expand Up @@ -158,9 +168,16 @@ class _HoverDotGridState extends State<HoverDotGrid>
@override
Widget build(BuildContext context) {
final shader = _shader;
if (shader == null) {
final opacity = ref.watch(
appPreferencesProvider.select((prefs) => prefs.backgroundDotOpacity),
);
if (shader == null || opacity <= 0) {
return const SizedBox.expand();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
final baseColor = widget.baseColor ??
Settings.tacticalVioletTheme.border.withValues(alpha: 0.7);
final glowColor = widget.glowColor ??
Settings.tacticalVioletTheme.foreground.withValues(alpha: 0.45);

return CustomPaint(
size: Size.infinite,
Expand All @@ -171,10 +188,8 @@ class _HoverDotGridState extends State<HoverDotGrid>
spacing: widget.spacing,
dotRadius: widget.dotRadius,
glowRadius: widget.glowRadius,
baseColor: widget.baseColor ??
Settings.tacticalVioletTheme.border.withValues(alpha: 0.7),
glowColor: widget.glowColor ??
Settings.tacticalVioletTheme.foreground.withValues(alpha: 0.45),
baseColor: baseColor.withValues(alpha: baseColor.a * opacity),
glowColor: glowColor.withValues(alpha: glowColor.a * opacity),
),
);
}
Expand Down Expand Up @@ -237,4 +252,4 @@ class _HoverDotGridPainter extends CustomPainter {
oldDelegate.baseColor != baseColor ||
oldDelegate.glowColor != glowColor;
}
}
}
Loading
Loading