feat: the rail opens into a sidebar with labels from expanded windows on
Decided by the developer on 9 and 10 Nov 2026: phones keep the bar, medium windows (600 to 840 dp) the rail alone, and from 840 dp a toggle at the rail's foot opens it into a sidebar 256 dp wide, as Proton Drive's: the app named beside its icon, each tab its label beside its icon, the icons where the rail had them, so nothing moves under the pointer. Its labels come in one after another, top to bottom, all at once where animations are off. Open or closed is kept per device, in halkyon_app's Look (look.json, `sidebar`), which Learn keeps in its DeviceSettings (`sidebarOpen`), as it does the appearance and language. NavigationFrame takes sidebarOpen and onSidebarChanged (no toggle without it), and sidebarSections, what the app lists below the tabs, which Todo's favourites and projects will be. The closed rail keeps Flutter's widget, its toggle pinned at the foot (trailingAtBottom); the open one is the frame's own, as the staggered labels need. Learn, Todo, Train and Meditate keep the choice. Whats-New: In wide windows, the button at the foot of the side rail opens it into a sidebar with names, and it stays open. Whats-New-De: In breiten Fenstern öffnet der Knopf unten an der Seitenleiste sie mit Namen, und sie bleibt offen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
99b75645ad
commit
3f441cbc98
7 changed files with 766 additions and 31 deletions
|
|
@ -47,6 +47,9 @@ class DeviceSettingsBloc(
|
|||
on<DeviceSettingsEventCheckGrammarChosen>(
|
||||
(event, emit) => _apply(emit, state.withCheckGrammar(event.on)),
|
||||
);
|
||||
on<DeviceSettingsEventSidebarChosen>(
|
||||
(event, emit) => _apply(emit, state.withSidebar(open: event.open)),
|
||||
);
|
||||
on<DeviceSettingsEventPdfSidebarDragged>(
|
||||
(event, emit) => _apply(emit, state.withPdfSidebarShare(event.share)),
|
||||
);
|
||||
|
|
@ -148,6 +151,13 @@ final class const DeviceSettingsEventCheckGrammarChosen(final bool on)
|
|||
List<Object?> get props => [on];
|
||||
}
|
||||
|
||||
/// The person opens the rail into the sidebar, or closes it ([open]).
|
||||
final class const DeviceSettingsEventSidebarChosen(final bool open)
|
||||
extends DeviceSettingsEvent {
|
||||
@override
|
||||
List<Object?> get props => [open];
|
||||
}
|
||||
|
||||
/// The person shows the card browser of [studysetId] one card at a time
|
||||
/// ([detailed]) or as a grid.
|
||||
final class const DeviceSettingsEventBrowserViewChosen(
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
if (value case final String name) key: ?LlmEffort.named(name),
|
||||
},
|
||||
checkGrammar: json['checkGrammar'] != false,
|
||||
sidebarOpen: json['sidebarOpen'] == true,
|
||||
);
|
||||
} on FormatException catch (error) {
|
||||
log('The device settings are unreadable', name: 'Settings', error: error);
|
||||
|
|
@ -105,6 +106,7 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
key: value.key,
|
||||
},
|
||||
'checkGrammar': settings.checkGrammar,
|
||||
if (settings.sidebarOpen) 'sidebarOpen': true,
|
||||
}),
|
||||
flush: true,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ export 'package:halkyon_app/halkyon_app.dart' show Appearance;
|
|||
/// whether studying shows the Pomodoro timer, which studysets' card
|
||||
/// browsers show one card at a time, whether and how the person searches
|
||||
/// the web from the app, who answered the field last and at which effort,
|
||||
/// and whether writing is checked as it is written.
|
||||
/// whether writing is checked as it is written, and whether the rail is
|
||||
/// open into the sidebar.
|
||||
/// Unlike the person's settings, they don't follow anyone to other devices,
|
||||
/// and they apply before anyone is signed in.
|
||||
final class const DeviceSettings({
|
||||
|
|
@ -62,6 +63,10 @@ final class const DeviceSettings({
|
|||
/// allows counts as none chosen.
|
||||
final Map<String, LlmEffort> askEfforts = const {},
|
||||
|
||||
/// Whether the rail is open into the sidebar from expanded windows on
|
||||
/// (decided 10 Nov 2026), halkyon_app's `Look.sidebarOpen` kept here.
|
||||
final bool sidebarOpen = false,
|
||||
|
||||
/// Whether the editors underline what the instance's LanguageTool finds
|
||||
/// as the person writes (ADR 0024, Settings › Writing): on unless turned
|
||||
/// off; nothing is checked where the instance checks nothing.
|
||||
|
|
@ -111,6 +116,9 @@ final class const DeviceSettings({
|
|||
DeviceSettings withAskEffort(String key, LlmEffort effort) =>
|
||||
_copy(askEfforts: {...askEfforts, key: effort});
|
||||
|
||||
/// These settings with the sidebar [open] or closed.
|
||||
DeviceSettings withSidebar({required bool open}) => _copy(sidebarOpen: open);
|
||||
|
||||
/// These settings with writing checked as it is written [on] or off.
|
||||
DeviceSettings withCheckGrammar(bool on) => _copy(checkGrammar: on);
|
||||
|
||||
|
|
@ -127,6 +135,7 @@ final class const DeviceSettings({
|
|||
Object? askWith = _keep,
|
||||
Map<String, LlmEffort>? askEfforts,
|
||||
bool? checkGrammar,
|
||||
bool? sidebarOpen,
|
||||
}) => DeviceSettings(
|
||||
appearance: appearance ?? this.appearance,
|
||||
language: identical(language, _keep) ? this.language : language as String?,
|
||||
|
|
@ -144,6 +153,7 @@ final class const DeviceSettings({
|
|||
askWith: identical(askWith, _keep) ? this.askWith : askWith as String?,
|
||||
askEfforts: askEfforts ?? this.askEfforts,
|
||||
checkGrammar: checkGrammar ?? this.checkGrammar,
|
||||
sidebarOpen: sidebarOpen ?? this.sidebarOpen,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
@ -160,6 +170,7 @@ final class const DeviceSettings({
|
|||
askWith,
|
||||
askEfforts,
|
||||
checkGrammar,
|
||||
sidebarOpen,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ class const DeviceSettingsScope({required final Widget child, super.key})
|
|||
/// language, as halkyon_app's [Look] means them for every Halkyon app.
|
||||
extension DeviceSettingsApp on DeviceSettings {
|
||||
/// The device's look of the app.
|
||||
Look get appLook => Look(appearance: appearance, language: language);
|
||||
Look get appLook => Look(
|
||||
appearance: appearance,
|
||||
language: language,
|
||||
sidebarOpen: sidebarOpen,
|
||||
);
|
||||
|
||||
/// The app's theme mode.
|
||||
ThemeMode get themeMode => appLook.themeMode;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/app/learn_frame.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/bloc/composer_cubit.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -53,6 +54,14 @@ class const AppShell({
|
|||
icon: learnIcon,
|
||||
// The bar's wave and chosen tab: Learn's purple, its icon's last colour.
|
||||
accent: PaletteColor.purple.background,
|
||||
// Open into the sidebar or not, as the device keeps it (decided
|
||||
// 10 Nov 2026).
|
||||
sidebarOpen: context.select<DeviceSettingsBloc, bool>(
|
||||
(settings) => settings.state.sidebarOpen,
|
||||
),
|
||||
onSidebarChanged: (open) => context.read<DeviceSettingsBloc>().add(
|
||||
DeviceSettingsEventSidebarChosen(open),
|
||||
),
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
onSelected: (index) {
|
||||
// Another tab keeps the assistant's field as it is (D69); the
|
||||
|
|
|
|||
|
|
@ -54,6 +54,17 @@ void main() {
|
|||
expect((await FileDeviceSettingsStore(file).read()).checkGrammar, isFalse);
|
||||
});
|
||||
|
||||
test('keeps the sidebar closed until it is opened (decided 10 Nov 2026), '
|
||||
'and the look says so', () async {
|
||||
expect((await store.read()).sidebarOpen, isFalse);
|
||||
|
||||
await store.write(const DeviceSettings(sidebarOpen: true));
|
||||
|
||||
final read = await FileDeviceSettingsStore(file).read();
|
||||
expect(read.sidebarOpen, isTrue);
|
||||
expect(read.withLanguage('de').sidebarOpen, isTrue);
|
||||
});
|
||||
|
||||
test('shows the Pomodoro timer while studying until told not to', () async {
|
||||
expect((await store.read()).pomodoroWhileStudying, isTrue);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/memory_device_settings_store.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
|
|
@ -230,6 +231,36 @@ void main() {
|
|||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('from expanded windows on, the rail opens into a sidebar '
|
||||
'kept open on this device (decided 10 Nov 2026)', (tester) async {
|
||||
final store = MemoryDeviceSettingsStore();
|
||||
tester.view.physicalSize = const Size(1200, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
deviceSettingsStore: store,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('Open sidebar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(NavigationFrame.sidebarKey), findsOneWidget);
|
||||
expect(store.writes.last.sidebarOpen, isTrue);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byKey(NavigationFrame.sidebarKey),
|
||||
matching: find.text('Library'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(LibraryScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('in wider windows, the rail stays inside a studyset', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue