Skip to content

Commit

Permalink
feat: NesScaffold (#139)
Browse files Browse the repository at this point in the history
* progress

* progress
  • Loading branch information
erickzanardo authored Mar 27, 2024
1 parent fb4ecac commit 064b2f0
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- feat: adding different frames to `NesDialog`s
- feat: allow `NesFileExplorer` to have custom icons
- feat: adding `NesIcons.alien`
- feat: allow `NesScaffold` and allow snackbars to be shown on them.

# 0.19.0
- feat: adding `NesSectionHeader`
Expand Down
69 changes: 69 additions & 0 deletions example/lib/advanced/scaffold/scaffold.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:nes_ui/nes_ui.dart';

class NesScaffoldExample extends StatefulWidget {
const NesScaffoldExample({super.key});

static Route<void> route() {
return MaterialPageRoute<void>(
builder: (_) => const NesScaffoldExample(),
);
}

@override
State<NesScaffoldExample> createState() => _NesScaffoldExampleState();
}

class _NesScaffoldExampleState extends State<NesScaffoldExample> {
var _counter = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
body: NesScaffold(
body: Builder(
builder: (context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NesButton(
type: NesButtonType.primary,
onPressed: () {
NesScaffoldMessenger.of(context).showSnackBar(
NesSnackbar(
text: 'Hello, World! ${_counter++}',
type: _counter.isOdd
? NesSnackbarType.success
: NesSnackbarType.warning,
),
alignment: Alignment.topRight,
);
},
child: const Text('Show Snackbar top Right'),
),
const SizedBox(width: 8),
NesButton(
type: NesButtonType.primary,
onPressed: () {
NesScaffoldMessenger.of(context).showSnackBar(
NesSnackbar(
text: 'Hello, World! ${_counter++}',
type: _counter.isOdd
? NesSnackbarType.success
: NesSnackbarType.warning,
),
alignment: Alignment.topLeft,
);
},
child: const Text('Show Snackbar top Left'),
),
],
),
);
},
),
),
);
}
}
2 changes: 1 addition & 1 deletion example/lib/gallery/gallery_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class GalleryPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return NesScaffold(
return NesExampleScaffold(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
Expand Down
15 changes: 13 additions & 2 deletions example/lib/widgets/nes_scaffold.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import 'package:example/advanced/container_playground/container_playground.dart';
import 'package:example/advanced/fixed_viewport/page/fixed_viewport_page.dart';
import 'package:example/advanced/rpg_menu/rpg_menu.dart';
import 'package:example/advanced/scaffold/scaffold.dart';
import 'package:example/advanced/window_manager/window_manager.dart';
import 'package:example/app/app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nes_ui/nes_ui.dart';

class NesScaffold extends StatelessWidget {
const NesScaffold({
class NesExampleScaffold extends StatelessWidget {
const NesExampleScaffold({
super.key,
required this.child,
});
Expand All @@ -23,6 +24,16 @@ class NesScaffold extends StatelessWidget {
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
NesButton(
type: NesButtonType.normal,
onPressed: () {
Navigator.of(context).push(
NesScaffoldExample.route(),
);
},
child: NesIcon(iconData: NesIcons.rail),
),
const SizedBox(height: 16),
NesButton(
type: NesButtonType.normal,
onPressed: () {
Expand Down
251 changes: 251 additions & 0 deletions lib/src/widgets/nes_scaffold.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:nes_ui/nes_ui.dart';

/// {@template nes_scaffold_messenger}
/// Manages widgets displayed by a [NesScaffold].
/// {@endtemplate}
class NesScaffoldMessenger extends StatefulWidget {
/// {@macro nes_scaffold_messenger}
const NesScaffoldMessenger({
required this.body,
super.key,
});

/// The primary content of the scaffold.
final Widget body;

/// Returns the [NesScaffoldMessengerState] in the scope.
static NesScaffoldMessengerState of(BuildContext context) {
return _NesScaffoldMessengerScope.of(context);
}

@override
NesScaffoldMessengerState createState() => NesScaffoldMessengerState();
}

class _SnackbarMessage {
const _SnackbarMessage({
required this.id,
required this.snackbar,
required this.alignment,
required this.offset,
this.markForRemoval = false,
});

final int id;
final NesSnackbar snackbar;
final Alignment alignment;
final Offset offset;
final bool markForRemoval;

_SnackbarMessage copyWith({
int? id,
Alignment? alignment,
Offset? offset,
bool? markForRemoval,
}) {
return _SnackbarMessage(
id: id ?? this.id,
snackbar: snackbar,
alignment: alignment ?? this.alignment,
offset: offset ?? this.offset,
markForRemoval: markForRemoval ?? this.markForRemoval,
);
}
}

/// Handles the state of a [NesScaffoldMessenger].
///
/// This class allows the user to show snackbars and etc.
class NesScaffoldMessengerState extends State<NesScaffoldMessenger> {
late int _snackbarId = 0;
final List<Timer> _timers = [];

Future<void> _waitABit() async {
final completer = Completer<void>();

late final Timer timer;

timer = Timer(const Duration(seconds: 3), () {
completer.complete();
_timers.remove(timer);
timer.cancel();
});

_timers.add(timer);

return completer.future;
}

/// Adds a [NesSnackbar] to the [NesScaffold].
void showSnackBar(
NesSnackbar snackbar, {
Alignment alignment = Alignment.bottomCenter,
}) {
final message = _SnackbarMessage(
id: _snackbarId++,
snackbar: snackbar,
alignment: alignment,
offset: Offset(alignment.x, 0),
);
_snackbars.value = [
..._snackbars.value,
message,
];

WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_snackbars.value = _snackbars.value.map((e) {
if (e.id == message.id) {
return e.copyWith(
offset: Offset.zero,
);
}
return e;
}).toList();
});

_waitABit().then((_) {
_snackbars.value = _snackbars.value.map((e) {
if (e.id == message.id) {
return e.copyWith(
markForRemoval: true,
offset: Offset(alignment.x, 0),
);
}
return e;
}).toList();
});

_overlayController.show();
}

late final _snackbars = ValueNotifier<List<_SnackbarMessage>>([]);
late final _overlayController = OverlayPortalController();

void _checkForRemovals() {
_snackbars.value = _snackbars.value.where((element) {
return !element.markForRemoval;
}).toList();
}

@override
void dispose() {
super.dispose();

for (final timer in _timers) {
timer.cancel();
}
}

@override
Widget build(BuildContext context) {
return _NesScaffoldMessengerScope(
state: this,
child: OverlayPortal(
overlayChildBuilder: (context) {
return ValueListenableBuilder<List<_SnackbarMessage>>(
valueListenable: _snackbars,
builder: (context, snackbars, child) {
final values = List<_SnackbarMessage>.from(snackbars)
..sort((a, b) {
return a.id.compareTo(b.id);
});
return Stack(
children: [
for (var i = 0; i < values.length; i++)
Align(
alignment: values[i].alignment,
child: AnimatedSlide(
key: ValueKey('slide_${values[i].id}'),
onEnd: _checkForRemovals,
duration: const Duration(milliseconds: 300),
offset: values[i].offset,
child: AnimatedPadding(
key: ValueKey('padding_${values[i].id}'),
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.only(
top: 32 * i + 16,
left: 16,
right: 16,
),
child: NesDropshadow(
child: snackbars[i].snackbar,
),
),
),
),
],
);
},
);
},
controller: _overlayController,
child: widget.body,
),
);
}
}

class _NesScaffoldMessengerScope extends InheritedWidget {
const _NesScaffoldMessengerScope({
required super.child,
required this.state,
});

final NesScaffoldMessengerState state;

/// Returns the current message.
static NesScaffoldMessengerState of(BuildContext context) {
final result = context
.dependOnInheritedWidgetOfExactType<_NesScaffoldMessengerScope>()
?.state;

if (result == null) {
throw FlutterError(
'NesScaffoldMessenger.of() called with a context that does not contain '
'a NesScaffoldMessenger.\n'
'No NesScaffoldMessengerState ancestor could be found starting from '
'the context that was passed to NesScaffoldMessenger.of().\n'
'The context used was:\n'
' $context',
);
}

return result;
}

@override
bool updateShouldNotify(_NesScaffoldMessengerScope oldWidget) =>
state != oldWidget.state;
}

/// {@template nes_scaffold}
/// A Nes UI system scaffold widget.
///
/// Right now it doesn't replace the [Scaffold] widget, so we
/// advise to use it in combination with Material's [Scaffold].
///
/// That might change as we develop the library.
///
/// To interact with the [NesScaffold] state, use a [NesScaffoldMessenger].
///
/// {@endtemplate}
class NesScaffold extends StatelessWidget {
/// {@macro nes_scaffold}
const NesScaffold({
required this.body,
super.key,
});

/// The primary content of the scaffold.
final Widget body;

@override
Widget build(BuildContext context) {
return NesScaffoldMessenger(
body: body,
);
}
}
10 changes: 6 additions & 4 deletions lib/src/widgets/nes_snackbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ class NesSnackbar extends StatelessWidget {
SnackBar(
backgroundColor: Colors.transparent,
elevation: 0,
content: NesSnackbar(
text: text,
type: type,
content: SizedBox(
width: double.infinity,
child: NesSnackbar(
text: text,
type: type,
),
),
),
);
Expand Down Expand Up @@ -73,7 +76,6 @@ class NesSnackbar extends StatelessWidget {
}

return NesContainer(
width: double.infinity,
backgroundColor: color,
child: Text(
text,
Expand Down
Loading

0 comments on commit 064b2f0

Please sign in to comment.