mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-08 18:28:53 +01:00
(web) Shift+click to select range
This commit is contained in:
parent
17964ed695
commit
4ad0b673fc
4 changed files with 129 additions and 10 deletions
|
@ -356,6 +356,10 @@
|
||||||
"@previousTooltip": {
|
"@previousTooltip": {
|
||||||
"description": "Tooltip of the previous button"
|
"description": "Tooltip of the previous button"
|
||||||
},
|
},
|
||||||
|
"webSelectRangeNotification": "Hold shift + click to select all in between",
|
||||||
|
"@webSelectRangeNotification": {
|
||||||
|
"description": "Inform web user how to select items in range"
|
||||||
|
},
|
||||||
"changelogTitle": "Changelog",
|
"changelogTitle": "Changelog",
|
||||||
"@changelogTitle": {
|
"@changelogTitle": {
|
||||||
"description": "Title of the changelog dialog"
|
"description": "Title of the changelog dialog"
|
||||||
|
|
13
lib/session_storage.dart
Normal file
13
lib/session_storage.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/// Hold non-persisted global variables
|
||||||
|
class SessionStorage {
|
||||||
|
factory SessionStorage() {
|
||||||
|
return _inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionStorage._();
|
||||||
|
|
||||||
|
/// Whether the range select notification has been shown to user
|
||||||
|
bool hasShowRangeSelectNotification = false;
|
||||||
|
|
||||||
|
static SessionStorage _inst = SessionStorage._();
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
@ -15,6 +18,7 @@ import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/list_extension.dart';
|
import 'package:nc_photos/list_extension.dart';
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/session_storage.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
|
@ -61,13 +65,16 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
_transformItems();
|
_transformItems();
|
||||||
_initCover();
|
_initCover();
|
||||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
||||||
|
_keyboardFocus.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return AppTheme(
|
return AppTheme(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Builder(builder: (context) => _buildContent(context)),
|
body: Builder(
|
||||||
|
builder: (context) =>
|
||||||
|
kIsWeb ? _buildWebContent(context) : _buildContent(context)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -84,6 +91,18 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWebContent(BuildContext context) {
|
||||||
|
assert(kIsWeb);
|
||||||
|
// support switching pages with keyboard on web
|
||||||
|
return RawKeyboardListener(
|
||||||
|
onKey: (ev) {
|
||||||
|
_isRangeSelectionMode = ev.isShiftPressed;
|
||||||
|
},
|
||||||
|
focusNode: _keyboardFocus,
|
||||||
|
child: _buildContent(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
|
@ -231,12 +250,23 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
// unselect
|
// unselect
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedItems.remove(item);
|
_selectedItems.remove(item);
|
||||||
|
_lastSelectPosition = null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// select
|
// select
|
||||||
setState(() {
|
if (_isRangeSelectionMode && _lastSelectPosition != null) {
|
||||||
_selectedItems.add(item);
|
setState(() {
|
||||||
});
|
_selectedItems.addAll(_items.sublist(
|
||||||
|
math.min(_lastSelectPosition, index),
|
||||||
|
math.max(_lastSelectPosition, index) + 1));
|
||||||
|
_lastSelectPosition = index;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_lastSelectPosition = index;
|
||||||
|
_selectedItems.add(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator.pushNamed(context, Viewer.routeName,
|
Navigator.pushNamed(context, Viewer.routeName,
|
||||||
|
@ -246,8 +276,17 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
|
|
||||||
void _onItemLongPress(_GridItem item, int index) {
|
void _onItemLongPress(_GridItem item, int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_lastSelectPosition = index;
|
||||||
_selectedItems.add(item);
|
_selectedItems.add(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (kIsWeb && !SessionStorage().hasShowRangeSelectNotification) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).webSelectRangeNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
SessionStorage().hasShowRangeSelectNotification = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionAppBarRemovePressed() {
|
void _onSelectionAppBarRemovePressed() {
|
||||||
|
@ -329,6 +368,8 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
||||||
|
int _lastSelectPosition;
|
||||||
|
bool _isRangeSelectionMode = false;
|
||||||
|
|
||||||
Album _album;
|
Album _album;
|
||||||
final _items = <_GridItem>[];
|
final _items = <_GridItem>[];
|
||||||
|
@ -337,7 +378,10 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
String _coverPreviewUrl;
|
String _coverPreviewUrl;
|
||||||
var _thumbZoomLevel = 0;
|
var _thumbZoomLevel = 0;
|
||||||
|
|
||||||
final _selectedItems = <_GridItem>[];
|
final _selectedItems = <_GridItem>{};
|
||||||
|
|
||||||
|
/// used to gain focus on web for keyboard support
|
||||||
|
final _keyboardFocus = FocusNode();
|
||||||
|
|
||||||
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -18,6 +20,7 @@ import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/metadata_task_manager.dart';
|
import 'package:nc_photos/metadata_task_manager.dart';
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/session_storage.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/use_case/remove.dart';
|
import 'package:nc_photos/use_case/remove.dart';
|
||||||
|
@ -46,6 +49,7 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initBloc();
|
_initBloc();
|
||||||
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevel(0);
|
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevel(0);
|
||||||
|
_keyboardFocus.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -55,7 +59,9 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
listener: (context, state) => _onStateChange(context, state),
|
listener: (context, state) => _onStateChange(context, state),
|
||||||
child: BlocBuilder<ScanDirBloc, ScanDirBlocState>(
|
child: BlocBuilder<ScanDirBloc, ScanDirBlocState>(
|
||||||
bloc: _bloc,
|
bloc: _bloc,
|
||||||
builder: (context, state) => _buildContent(context, state),
|
builder: (context, state) => kIsWeb
|
||||||
|
? _buildWebContent(context, state)
|
||||||
|
: _buildContent(context, state),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -71,6 +77,18 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWebContent(BuildContext context, ScanDirBlocState state) {
|
||||||
|
assert(kIsWeb);
|
||||||
|
// support switching pages with keyboard on web
|
||||||
|
return RawKeyboardListener(
|
||||||
|
onKey: (ev) {
|
||||||
|
_isRangeSelectionMode = ev.isShiftPressed;
|
||||||
|
},
|
||||||
|
focusNode: _keyboardFocus,
|
||||||
|
child: _buildContent(context, state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context, ScanDirBlocState state) {
|
Widget _buildContent(BuildContext context, ScanDirBlocState state) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -254,12 +272,24 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
// unselect
|
// unselect
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedItems.remove(item);
|
_selectedItems.remove(item);
|
||||||
|
_lastSelectPosition = null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// select
|
// select
|
||||||
setState(() {
|
if (_isRangeSelectionMode && _lastSelectPosition != null) {
|
||||||
_selectedItems.add(item);
|
setState(() {
|
||||||
});
|
_selectedItems.addAll(_items
|
||||||
|
.sublist(math.min(_lastSelectPosition, index),
|
||||||
|
math.max(_lastSelectPosition, index) + 1)
|
||||||
|
.whereType<_GridFileItem>());
|
||||||
|
_lastSelectPosition = index;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_lastSelectPosition = index;
|
||||||
|
_selectedItems.add(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final fileIndex = _itemIndexToFileIndex(index);
|
final fileIndex = _itemIndexToFileIndex(index);
|
||||||
|
@ -275,8 +305,17 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_lastSelectPosition = index;
|
||||||
_selectedItems.add(item);
|
_selectedItems.add(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (kIsWeb && !SessionStorage().hasShowRangeSelectNotification) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).webSelectRangeNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
SessionStorage().hasShowRangeSelectNotification = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionAppBarAddToAlbumPressed(BuildContext context) {
|
void _onSelectionAppBarAddToAlbumPressed(BuildContext context) {
|
||||||
|
@ -404,6 +443,8 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
.where((element) => file_util.isSupportedFormat(element))
|
.where((element) => file_util.isSupportedFormat(element))
|
||||||
.sorted(compareFileDateTimeDescending);
|
.sorted(compareFileDateTimeDescending);
|
||||||
|
|
||||||
|
final lastSelectedItem =
|
||||||
|
_lastSelectPosition != null ? _items[_lastSelectPosition] : null;
|
||||||
_items.clear();
|
_items.clear();
|
||||||
String currentDateStr;
|
String currentDateStr;
|
||||||
for (final f in _backingFiles) {
|
for (final f in _backingFiles) {
|
||||||
|
@ -424,6 +465,18 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_transformSelectedItems();
|
_transformSelectedItems();
|
||||||
|
|
||||||
|
// Keep _lastSelectPosition if no changes, drop otherwise
|
||||||
|
int newLastSelectPosition;
|
||||||
|
try {
|
||||||
|
if (lastSelectedItem != null &&
|
||||||
|
lastSelectedItem is _GridFileItem &&
|
||||||
|
(_items[_lastSelectPosition] as _GridFileItem).file.path ==
|
||||||
|
lastSelectedItem.file.path) {
|
||||||
|
newLastSelectPosition = _lastSelectPosition;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
_lastSelectPosition = newLastSelectPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map selected items to the new item list
|
/// Map selected items to the new item list
|
||||||
|
@ -488,6 +541,8 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
||||||
|
int _lastSelectPosition;
|
||||||
|
bool _isRangeSelectionMode = false;
|
||||||
|
|
||||||
ScanDirBloc _bloc;
|
ScanDirBloc _bloc;
|
||||||
|
|
||||||
|
@ -496,7 +551,10 @@ class _HomePhotosState extends State<HomePhotos> {
|
||||||
|
|
||||||
var _thumbZoomLevel = 0;
|
var _thumbZoomLevel = 0;
|
||||||
|
|
||||||
final _selectedItems = <_GridFileItem>[];
|
final _selectedItems = <_GridFileItem>{};
|
||||||
|
|
||||||
|
/// used to gain focus on web for keyboard support
|
||||||
|
final _keyboardFocus = FocusNode();
|
||||||
|
|
||||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||||
static const _menuValueRefresh = 0;
|
static const _menuValueRefresh = 0;
|
||||||
|
|
Loading…
Reference in a new issue