yubioath-flutter/lib/app/views/app_list_item.dart

186 lines
6.3 KiB
Dart
Raw Normal View History

2023-06-15 18:39:17 +03:00
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
2023-06-15 18:39:17 +03:00
import '../../core/state.dart';
import '../models.dart';
import '../shortcuts.dart';
import 'action_popup_menu.dart';
class AppListItem<T> extends ConsumerStatefulWidget {
final T item;
2023-06-15 18:39:17 +03:00
final Widget? leading;
final String title;
final String? subtitle;
2024-01-09 16:26:52 +03:00
final String? semanticTitle;
2023-06-15 18:39:17 +03:00
final Widget? trailing;
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
2024-05-02 17:10:56 +03:00
final Widget Function(BuildContext context)? itemBuilder;
final Intent? tapIntent;
final Intent? doubleTapIntent;
final Color? tileColor;
2024-01-09 14:50:26 +03:00
final bool selected;
2023-06-15 18:39:17 +03:00
const AppListItem(
this.item, {
2023-06-15 18:39:17 +03:00
super.key,
this.leading,
required this.title,
2024-01-09 16:26:52 +03:00
this.semanticTitle,
2023-06-15 18:39:17 +03:00
this.subtitle,
this.trailing,
this.buildPopupActions,
2024-05-02 17:10:56 +03:00
this.itemBuilder,
this.tapIntent,
this.doubleTapIntent,
this.tileColor,
2024-01-09 14:50:26 +03:00
this.selected = false,
2023-06-15 18:39:17 +03:00
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _AppListItemState<T>();
2023-06-15 18:39:17 +03:00
}
class _AppListItemState<T> extends ConsumerState<AppListItem> {
2023-06-15 18:39:17 +03:00
final FocusNode _focusNode = FocusNode();
int _lastTap = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final subtitle = widget.subtitle;
final buildPopupActions = widget.buildPopupActions;
final tapIntent = widget.tapIntent;
final doubleTapIntent = widget.doubleTapIntent;
2023-06-15 18:39:17 +03:00
final trailing = widget.trailing;
final hasFeature = ref.watch(featureProvider);
final colorScheme = Theme.of(context).colorScheme;
2023-06-15 18:39:17 +03:00
2024-01-09 16:26:52 +03:00
return Semantics(
label: widget.semanticTitle ?? widget.title,
child: ItemShortcuts<T>(
item: widget.item,
2024-01-09 16:26:52 +03:00
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(16),
2024-01-09 16:26:52 +03:00
onSecondaryTapDown: buildPopupActions == null
? null
: (details) {
final menuItems = buildPopupActions(context)
.where((action) =>
action.feature == null || hasFeature(action.feature!))
.toList();
if (menuItems.isNotEmpty) {
showPopupMenu(
context,
details.globalPosition,
menuItems,
);
}
},
onTap: () {
2024-01-19 14:59:57 +03:00
_focusNode.requestFocus();
if (tapIntent != null) {
Actions.invoke(context, tapIntent);
}
if (isDesktop && doubleTapIntent != null) {
2024-01-09 16:26:52 +03:00
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastTap < 500) {
setState(() {
_lastTap = 0;
});
Actions.invoke(context, doubleTapIntent);
2024-01-09 16:26:52 +03:00
} else {
setState(() {
_lastTap = now;
});
}
2023-06-15 18:39:17 +03:00
}
2024-01-09 16:26:52 +03:00
},
onLongPress: doubleTapIntent == null
2024-01-09 16:26:52 +03:00
? null
: () {
Actions.invoke(context, doubleTapIntent);
2024-01-09 16:26:52 +03:00
},
2024-05-02 17:10:56 +03:00
child: widget.itemBuilder != null
? widget.itemBuilder!.call(context)
: Stack(
alignment: AlignmentDirectional.center,
children: [
const SizedBox(height: 64),
ListTile(
mouseCursor: widget.tapIntent != null
? SystemMouseCursors.click
: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
2024-05-02 17:10:56 +03:00
selectedTileColor: colorScheme.secondaryContainer,
selectedColor: colorScheme.onSecondaryContainer,
tileColor: widget.tileColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
2024-05-02 17:10:56 +03:00
selected: widget.selected,
leading: widget.leading,
title: subtitle == null
// We use SizedBox to fill entire space
? SizedBox(
height: 48,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
),
)
: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: trailing == null
? null
: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: trailing,
),
),
],
),
2023-06-15 18:39:17 +03:00
),
),
);
}
}