Skip to content
Open
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
8 changes: 8 additions & 0 deletions lib/screens/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ class HomePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Title(
title: 'Telepathy',
color: const Color(0xFF000000),
child: _buildPage(context),
);
}

Widget _buildPage(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/settings/sections/audio_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class _AudioSettingsState extends State<AudioSettings> {
return const SizedBox.shrink();
},
),
const SizedBox(height: 20),
if (!kIsWeb) const SizedBox(height: 20),
Row(children: [
Selector<StateController, (bool, bool)>(
selector: (context, controller) =>
Expand Down
56 changes: 29 additions & 27 deletions lib/screens/settings/sections/networking.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,34 +212,36 @@ class NetworkSettingsState extends State<NetworkSettings> {
],
),
),
Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: [
SizedBox(
width: width,
child: TextInput(
labelText: 'Bind Addresses',
hintText: '0.0.0.0, ::, 127.0.0.1',
controller: _bindAddressesInput,
enabled: !isRestartSafe,
onChanged: (_) => _updateUnsavedChanges(),
errorText: _bindAddressesError,
)),
SizedBox(
width: width,
child: TextInput(
labelText: 'Listen Port',
controller: _listenPortInput,
enabled: !isRestartSafe,
onChanged: (_) => _updateUnsavedChanges(),
errorText: _listenPortError,
)),
],
if (!kIsWeb) ...[
Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: [
SizedBox(
width: width,
child: TextInput(
labelText: 'Bind Addresses',
hintText: '0.0.0.0, ::, 127.0.0.1',
controller: _bindAddressesInput,
enabled: !isRestartSafe,
onChanged: (_) => _updateUnsavedChanges(),
errorText: _bindAddressesError,
)),
SizedBox(
width: width,
child: TextInput(
labelText: 'Listen Port',
controller: _listenPortInput,
enabled: !isRestartSafe,
onChanged: (_) => _updateUnsavedChanges(),
errorText: _listenPortError,
)),
],
),
),
),
const SizedBox(height: 8),
const SizedBox(height: 8),
],
_buildRelaysSection(isRestartSafe),
const SizedBox(height: 8),
if (!kIsWeb) _buildDnsSection(width, isRestartSafe),
Expand Down
123 changes: 82 additions & 41 deletions lib/screens/settings/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class SettingsPageState extends State<SettingsPage>

late AnimationController _animationController;
late Animation<Offset> _menuSlideAnimation;
final FocusNode _menuFocusNode = FocusNode();
final Object _menuTapRegionGroup = Object();

@override
void initState() {
Expand All @@ -66,6 +68,7 @@ class SettingsPageState extends State<SettingsPage>

@override
void dispose() {
_menuFocusNode.dispose();
_animationController.dispose();
super.dispose();
}
Expand All @@ -78,6 +81,32 @@ class SettingsPageState extends State<SettingsPage>
}
}

void _setMenuVisibility(bool visible) {
if (showMenu == visible) return;

setState(() {
if (visible) {
_animationController.reverse();
} else {
_animationController.forward();
}
showMenu = visible;
});

if (visible) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && showMenu == true && widget.constraints.maxWidth < 600) {
_menuFocusNode.requestFocus();
}
});
}
}

void _dismissNarrowMenu() {
if (widget.constraints.maxWidth >= 600 || showMenu != true) return;
_setMenuVisibility(false);
}

@override
Widget build(BuildContext context) {
BoxConstraints constraints = widget.constraints;
Expand Down Expand Up @@ -145,50 +174,62 @@ class SettingsPageState extends State<SettingsPage>
),
)),
),
if (constraints.maxWidth > 600 || (showMenu ?? true))
SlideTransition(
position: _menuSlideAnimation,
child: Container(
width: 200,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceDim,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
bottomRight: Radius.circular(8),
Positioned.fill(
child: Focus(
focusNode: _menuFocusNode,
onFocusChange: (hasFocus) {
if (!hasFocus) _dismissNarrowMenu();
},
child: Stack(
children: [
if (constraints.maxWidth > 600 || (showMenu ?? true))
TapRegion(
groupId: _menuTapRegionGroup,
onTapOutside: (_) => _dismissNarrowMenu(),
child: SlideTransition(
position: _menuSlideAnimation,
child: Container(
width: 200,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceDim,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
padding: const EdgeInsets.only(top: 60),
child: SettingsMenu(
selected: _section,
onSectionSelected: (section) =>
tapHandler(section),
showOverlayItem: !kIsWeb && Platform.isWindows,
),
),
),
),
TapRegion(
groupId: _menuTapRegionGroup,
child: SettingsHeader(
isNarrow: constraints.maxWidth < 600,
showMenu: showMenu ?? true,
onBack: () async {
if (_section == SettingsSection.networking &&
(_key.currentState?.unsavedChanges ?? false)) {
bool leave = await unsavedConfirmation(context);
if (!leave) return;
}

if (context.mounted) {
Navigator.of(context).pop();
}
},
onToggleMenu: () =>
_setMenuVisibility(!(showMenu ?? true)),
),
),
),
padding: const EdgeInsets.only(top: 60),
child: SettingsMenu(
selected: _section,
onSectionSelected: (section) => tapHandler(section),
showOverlayItem: !kIsWeb && Platform.isWindows,
),
],
),
),
SettingsHeader(
isNarrow: constraints.maxWidth < 600,
showMenu: showMenu ?? true,
onBack: () async {
if (_section == SettingsSection.networking &&
(_key.currentState?.unsavedChanges ?? false)) {
bool leave = await unsavedConfirmation(context);
if (!leave) return;
}

if (context.mounted) {
Navigator.of(context).pop();
}
},
onToggleMenu: () {
setState(() {
if (showMenu ?? true) {
_animationController.forward();
} else {
_animationController.reverse();
}
showMenu = !(showMenu ?? true);
});
},
),
],
));
Expand Down
28 changes: 27 additions & 1 deletion test/screens/settings/sections/audio_settings_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -170,6 +170,32 @@ void main() {
);
expect(settings.inputDeviceId, 'input-1');
});

testWidgets(
'Web hides audio device selectors and their following spacer',
(WidgetTester tester) async {
final audioDevices = _FakeAudioDevices(telepathy: _FakeTelepathy());
await _pumpAudioSettings(
tester,
audioDevices,
_FakeAudioSettingsController(),
);

expect(find.text('Input Device'), findsNothing);
expect(find.text('Output Device'), findsNothing);

final audioOptionsBottom =
tester.getBottomLeft(find.text('Audio Options')).dy;
final soundTestTop =
tester.getTopLeft(find.widgetWithText(Button, 'Sound Test')).dy;
expect(
soundTestTop - audioOptionsBottom,
lessThan(30),
reason: 'Web must not retain the hidden selector spacer',
);
},
skip: !kIsWeb,
);
}

Future<void> _pumpAudioSettings(
Expand Down
25 changes: 23 additions & 2 deletions test/screens/settings/sections/networking_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -329,6 +328,28 @@ void main() {
'Save button must be re-enabled after a backend error so the user can retry',
);
});

testWidgets(
'Web hides unsupported network fields and retains supported controls',
(WidgetTester tester) async {
final recorder = _NetworkConfigRecorder(
listenPort: 40142,
bindAddresses: const ['0.0.0.0', '::'],
);
await tester.pumpNetworkSettings(
controller: _FakeNetworkSettingsController(recorder),
stateController: StateController(),
telepathy: _FakeTelepathy(),
);

expect(find.text('Bind Addresses'), findsNothing);
expect(find.text('Listen Port'), findsNothing);
expect(find.text('Use Custom DNS'), findsNothing);
expect(find.text('Use Custom Relays'), findsOneWidget);
expect(find.text('Use Custom Pkarr Relay'), findsOneWidget);
},
skip: !kIsWeb,
);
}

extension on WidgetTester {
Expand Down
Loading
Loading