add reader amoled mode; improve reader mode ui;

This commit is contained in:
Fabian Freund
2026-06-21 10:28:38 +02:00
parent 2060275c0e
commit 3f6f20d61d
18 changed files with 1265 additions and 38 deletions
@@ -108,6 +108,23 @@ class EngineSettingsReplicationService
},
);
ref.listen(
fireImmediately: true,
generalSettingsWithDefaultsProvider.select(
(settings) => settings.pureBlack,
),
(previous, next) async {
await _service.setReaderViewPureBlack(next);
},
onError: (error, stackTrace) {
logger.e(
'Error listening to pureBlack',
error: error,
stackTrace: stackTrace,
);
},
);
ref.listen(
fireImmediately: true,
generalSettingsWithDefaultsProvider.select(
@@ -48,9 +48,38 @@ class BrowserFab extends HookConsumerWidget {
),
);
final Widget child;
if (readerabilityState.active && appearanceButtonVisible) {
child = FloatingActionButton(
final showAppearance = readerabilityState.active && appearanceButtonVisible;
final showDock = toolbarState == ToolbarVisibility.dismissed;
void forceShowToolbar() {
ref
.read(toolbarVisibilityControllerProvider(selectedTabId).notifier)
.forceShow();
}
// Re-show the hidden tab bar / toolbar. Kept available even while reading,
// where the reader appearance button would otherwise take the FAB's slot
// and leave no way to bring the toolbar back. Rendered smaller when paired
// with the appearance button to mark it as the secondary action.
Widget buildDockFab({required bool small}) {
const icon = Icon(MdiIcons.dockBottom);
return small
? FloatingActionButton.small(
key: const ValueKey('dock_fab'),
heroTag: 'dock_fab',
onPressed: forceShowToolbar,
child: icon,
)
: FloatingActionButton(
key: const ValueKey('dock_fab'),
heroTag: 'dock_fab',
onPressed: forceShowToolbar,
child: icon,
);
}
Widget buildAppearanceFab() {
return FloatingActionButton(
key: const ValueKey('appearance_fab'),
heroTag: 'appearance_fab',
onPressed: () async {
@@ -58,21 +87,24 @@ class BrowserFab extends HookConsumerWidget {
},
child: const Icon(MdiIcons.formatFont),
);
} else if (toolbarState == ToolbarVisibility.dismissed) {
child = FloatingActionButton(
key: const ValueKey('dock_fab'),
heroTag: 'dock_fab',
onPressed: () {
ref
.read(toolbarVisibilityControllerProvider(selectedTabId).notifier)
.forceShow();
},
child: const Icon(MdiIcons.dockBottom),
);
} else {
child = const SizedBox.shrink(key: ValueKey('no_fab'));
}
return child;
if (showAppearance && showDock) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
buildDockFab(small: true),
const SizedBox(height: 12),
buildAppearanceFab(),
],
);
} else if (showAppearance) {
return buildAppearanceFab();
} else if (showDock) {
return buildDockFab(small: false);
} else {
return const SizedBox.shrink(key: ValueKey('no_fab'));
}
}
}
@@ -50,30 +50,52 @@ class DraggableFab extends HookConsumerWidget {
final disableAnimations = mediaQuery.disableAnimations;
final isDragging = useState(false);
// Stored as distances from the bottom-right corner (dx = from right edge,
// dy = from bottom edge) rather than a top-left position, so the FAB is
// anchored by its bottom-right corner. This keeps the primary (bottom) FAB
// in a stable slot when extra FABs are stacked above it (e.g. the re-dock
// button while reading), independent of the child's height.
final customOffset = useState<Offset?>(null);
final dragStartOffset = useRef<Offset?>(null);
final dragStartPosition = useRef<Offset?>(null);
// Actual rendered size of the (possibly stacked) FAB child. Used for drag
// clamping so a taller stacked column (e.g. dock + appearance FABs) can't be
// dragged off-screen. Falls back to [fabSize] until first measured.
final fabKey = useMemoized(GlobalKey.new);
final fabRenderSize = useState<Size?>(null);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final size = fabKey.currentContext?.size;
if (size != null && size != fabRenderSize.value) {
fabRenderSize.value = size;
}
});
return null;
});
final fabWidth = fabRenderSize.value?.width ?? fabSize;
final fabHeight = fabRenderSize.value?.height ?? fabSize;
// Calculate default position (bottom-right, respecting toolbar)
final defaultBottom = bottomToolbarVisible
? bottomAppBarHeight + _edgePadding
: _edgePadding + bottomSafeArea;
const defaultRight = _edgePadding;
final defaultLeft = screenSize.width - fabSize - defaultRight;
final defaultTop = screenSize.height - fabSize - defaultBottom;
// Current position: custom if set, otherwise default
final currentLeft = customOffset.value?.dx ?? defaultLeft;
final currentTop = customOffset.value?.dy ?? defaultTop;
final currentRight = customOffset.value?.dx ?? defaultRight;
final currentBottom = customOffset.value?.dy ?? defaultBottom;
return Positioned(
left: currentLeft,
top: currentTop,
right: currentRight,
bottom: currentBottom,
child: GestureDetector(
onLongPressStart: (details) {
isDragging.value = true;
unawaited(HapticFeedback.mediumImpact());
dragStartOffset.value = Offset(currentLeft, currentTop);
dragStartOffset.value = Offset(currentRight, currentBottom);
dragStartPosition.value = details.globalPosition;
},
onLongPressMoveUpdate: (details) {
@@ -84,13 +106,19 @@ class DraggableFab extends HookConsumerWidget {
}
final delta = details.globalPosition - dragStartPosition.value!;
final newOffset = dragStartOffset.value! + delta;
// Dragging right/down reduces the distance from the right/bottom edge.
final newOffset = Offset(
dragStartOffset.value!.dx - delta.dx,
dragStartOffset.value!.dy - delta.dy,
);
// Clamp to screen bounds
customOffset.value = _clampToBounds(
newOffset,
screenSize: screenSize,
padding: padding,
fabWidth: fabWidth,
fabHeight: fabHeight,
);
},
onLongPressEnd: (details) {
@@ -103,7 +131,7 @@ class DraggableFab extends HookConsumerWidget {
? Duration.zero
: const Duration(milliseconds: 150),
scale: isDragging.value ? 1.1 : 1.0,
child: child,
child: KeyedSubtree(key: fabKey, child: child),
),
),
);
@@ -113,17 +141,22 @@ class DraggableFab extends HookConsumerWidget {
Offset offset, {
required Size screenSize,
required EdgeInsets padding,
required double fabWidth,
required double fabHeight,
}) {
const minEdgePadding = 8.0;
// Distances from the bottom-right corner: keep at least the safe-area inset
// plus a margin on the near edge, and leave room for the (possibly stacked)
// FAB on the far edge so it can't be dragged off-screen.
return Offset(
offset.dx.clamp(
padding.left + minEdgePadding,
screenSize.width - fabSize - padding.right - minEdgePadding,
padding.right + minEdgePadding,
screenSize.width - fabWidth - padding.left - minEdgePadding,
),
offset.dy.clamp(
padding.top + minEdgePadding,
screenSize.height - fabSize - padding.bottom - minEdgePadding,
padding.bottom + minEdgePadding,
screenSize.height - fabHeight - padding.top - minEdgePadding,
),
);
}