mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
Enhance category management with animation
This commit is contained in:
parent
db24951673
commit
701a696820
1 changed files with 237 additions and 182 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
|
@ -91,27 +93,75 @@ class CategoriesTab extends ConsumerStatefulWidget {
|
|||
ConsumerState<CategoriesTab> createState() => _CategoriesTabState();
|
||||
}
|
||||
|
||||
class _CategoriesTabState extends ConsumerState<CategoriesTab> {
|
||||
class _CategoriesTabState extends ConsumerState<CategoriesTab>
|
||||
with SingleTickerProviderStateMixin {
|
||||
List<Category> _entries = [];
|
||||
late AnimationController _swapAnimationController;
|
||||
int? _animatingFromIndex;
|
||||
int? _animatingToIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_swapAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_swapAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
return Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
}
|
||||
|
||||
/// Moves a category from `index` to `newIndex` in the list,
|
||||
/// swaps their positions in memory, and persists the change in Isar.
|
||||
Future<void> _moveCategory(int index, int newIndex) async {
|
||||
// Prevent invalid moves (out of bounds)
|
||||
if (newIndex < 0 || newIndex >= _entries.length) return;
|
||||
// Grab the two category objects involved in the swap
|
||||
final a = _entries[index];
|
||||
final b = _entries[newIndex];
|
||||
// Swap their positions inside the in‑memory list
|
||||
_entries[newIndex] = a;
|
||||
_entries[index] = b;
|
||||
// Swap their persisted `pos` values so ordering is saved correctly
|
||||
final temp = a.pos;
|
||||
a.pos = b.pos;
|
||||
b.pos = temp;
|
||||
// Persist both updated objects in a single Isar transaction
|
||||
await isar.writeTxn(() async => isar.categorys.putAll([a, b]));
|
||||
setState(() {}); // Trigger a UI rebuild to reflect the updated order
|
||||
|
||||
if (_isDesktop && mounted) {
|
||||
setState(() {
|
||||
_animatingFromIndex = index;
|
||||
_animatingToIndex = newIndex;
|
||||
});
|
||||
|
||||
await _swapAnimationController.forward(from: 0.0);
|
||||
|
||||
// Grab the two category objects involved in the swap
|
||||
final a = _entries[index];
|
||||
final b = _entries[newIndex];
|
||||
// Swap their positions inside the in‑memory list
|
||||
_entries[newIndex] = a;
|
||||
_entries[index] = b;
|
||||
// Swap their persisted `pos` values so ordering is saved correctly
|
||||
final temp = a.pos;
|
||||
a.pos = b.pos;
|
||||
b.pos = temp;
|
||||
// Persist both updated objects in a single Isar transaction
|
||||
await isar.writeTxn(() async => isar.categorys.putAll([a, b]));
|
||||
|
||||
setState(() {
|
||||
_animatingFromIndex = null;
|
||||
_animatingToIndex = null;
|
||||
});
|
||||
} else {
|
||||
final a = _entries[index];
|
||||
final b = _entries[newIndex];
|
||||
_entries[newIndex] = a;
|
||||
_entries[index] = b;
|
||||
final temp = a.pos;
|
||||
a.pos = b.pos;
|
||||
b.pos = temp;
|
||||
await isar.writeTxn(() async => isar.categorys.putAll([a, b]));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -143,176 +193,37 @@ class _CategoriesTabState extends ConsumerState<CategoriesTab> {
|
|||
padding: const EdgeInsets.only(bottom: 100),
|
||||
itemBuilder: (context, index) {
|
||||
final category = _entries[index];
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 900),
|
||||
child: Padding(
|
||||
key: Key('category_${category.id}'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(0),
|
||||
bottomRight: Radius.circular(0),
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
_renameCategory(category);
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(Icons.label_outline_rounded),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(category.name!)),
|
||||
],
|
||||
),
|
||||
|
||||
Widget itemWidget = _buildCategoryCard(context, category, index);
|
||||
|
||||
if (_isDesktop &&
|
||||
_animatingFromIndex != null &&
|
||||
_animatingToIndex != null) {
|
||||
if (index == _animatingFromIndex ||
|
||||
index == _animatingToIndex) {
|
||||
final isMovingDown =
|
||||
_animatingFromIndex! < _animatingToIndex!;
|
||||
final offset = index == _animatingFromIndex
|
||||
? (isMovingDown ? 1.0 : -1.0)
|
||||
: (isMovingDown ? -1.0 : 1.0);
|
||||
|
||||
itemWidget = AnimatedBuilder(
|
||||
animation: _swapAnimationController,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
offset * (1 - _swapAnimationController.value) * 80,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_up_outlined,
|
||||
),
|
||||
onPressed: index > 0
|
||||
? () {
|
||||
_moveCategory(index, index - 1);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_down_outlined,
|
||||
),
|
||||
onPressed: index < _entries.length - 1
|
||||
? () {
|
||||
_moveCategory(index, index + 1);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_renameCategory(category);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.mode_edit_outline_outlined,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await isar.writeTxn(() async {
|
||||
category.hide = !(category.hide ?? false);
|
||||
category.updatedAt =
|
||||
DateTime.now().millisecondsSinceEpoch;
|
||||
isar.categorys.put(category);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
!(category.hide ?? false)
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(l10n.delete_category),
|
||||
content: Text(
|
||||
l10n.delete_category_msg(
|
||||
category.name!,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _removeCategory(
|
||||
category,
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SlideTransition(
|
||||
position:
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0, 1),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
),
|
||||
axisAlignment: 0.5,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: itemWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return itemWidget;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -428,6 +339,150 @@ class _CategoriesTabState extends ConsumerState<CategoriesTab> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryCard(
|
||||
BuildContext context,
|
||||
Category category,
|
||||
int index,
|
||||
) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
return Padding(
|
||||
key: Key('category_${category.id}'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(0),
|
||||
bottomRight: Radius.circular(0),
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
_renameCategory(category);
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(Icons.label_outline_rounded),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(category.name!)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_up_outlined),
|
||||
onPressed: index > 0
|
||||
? () {
|
||||
_moveCategory(index, index - 1);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down_outlined),
|
||||
onPressed: index < _entries.length - 1
|
||||
? () {
|
||||
_moveCategory(index, index + 1);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_renameCategory(category);
|
||||
},
|
||||
icon: const Icon(Icons.mode_edit_outline_outlined),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await isar.writeTxn(() async {
|
||||
category.hide = !(category.hide ?? false);
|
||||
category.updatedAt =
|
||||
DateTime.now().millisecondsSinceEpoch;
|
||||
isar.categorys.put(category);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
!(category.hide ?? false)
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(l10n.delete_category),
|
||||
content: Text(
|
||||
l10n.delete_category_msg(category.name!),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _removeCategory(
|
||||
category,
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _removeCategory(Category category, BuildContext context) async {
|
||||
await isar.writeTxn(() async {
|
||||
// All Items with this category
|
||||
|
|
|
|||
Loading…
Reference in a new issue