Release RuleGel 1.0.1 with Realbooru

This commit is contained in:
2026-05-25 14:33:59 +03:00
parent 16e86fb9d5
commit ea39cf8567
28 changed files with 1217 additions and 263 deletions
+76 -41
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../app/motion.dart';
import '../../backend/backend.dart';
class AppSearchBar extends StatefulWidget {
@@ -48,6 +49,7 @@ class TagInputSearchBar extends StatefulWidget {
class _TagInputSearchBarState extends State<TagInputSearchBar> {
late final TextEditingController _controller;
late final FocusNode _focusNode;
late final ScrollController _tagScrollController;
Timer? _debounce;
List<String> _tags = [];
@@ -56,6 +58,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
super.initState();
_controller = TextEditingController();
_focusNode = FocusNode();
_tagScrollController = ScrollController();
_focusNode.addListener(_handleFocusChanged);
_setFromQuery(widget.initialValue ?? '');
}
@@ -72,6 +75,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
@override
void dispose() {
_debounce?.cancel();
_tagScrollController.dispose();
_focusNode.removeListener(_handleFocusChanged);
_focusNode.dispose();
_controller.dispose();
@@ -105,53 +109,64 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 7),
child: Icon(Icons.search_rounded,
color: scheme.onSurfaceVariant),
),
Icon(Icons.search_rounded, color: scheme.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final tag in _tags) _buildTagChip(context, tag),
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 120,
maxWidth: 320,
),
child: Focus(
onKeyEvent: _handleKeyEvent,
child: TextField(
controller: _controller,
focusNode: _focusNode,
autocorrect: false,
enableSuggestions: false,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _submit(),
onChanged: _handleDraftChanged,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
contentPadding:
const EdgeInsets.symmetric(vertical: 8),
hintText:
_tags.isEmpty ? widget.hintText : 'tag',
prefixIcon: null,
suffixIcon: null,
child: SizedBox(
height: 38,
child: Scrollbar(
controller: _tagScrollController,
thumbVisibility: false,
child: SingleChildScrollView(
controller: _tagScrollController,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (final tag in _tags) ...[
_buildTagChip(context, tag),
const SizedBox(width: 6),
],
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 120,
maxWidth: 300,
),
child: Focus(
onKeyEvent: _handleKeyEvent,
child: TextField(
controller: _controller,
focusNode: _focusNode,
autocorrect: false,
enableSuggestions: false,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _submit(),
onChanged: _handleDraftChanged,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
contentPadding:
const EdgeInsets.symmetric(
vertical: 8,
),
hintText: _tags.isEmpty
? widget.hintText
: 'tag',
prefixIcon: null,
suffixIcon: null,
),
),
),
),
),
],
),
),
],
),
),
),
IconButton(
@@ -238,6 +253,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
void _commitDraft(String value) {
final additions = _parseTags(value);
if (additions.isEmpty) return;
final keepInputVisible = _isInputNearEnd();
setState(() {
for (final tag in additions) {
if (!_tags.contains(tag)) _tags.add(tag);
@@ -245,6 +261,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
_controller.clear();
});
_notifyChanged();
if (keepInputVisible) _scrollTagsToEnd();
}
void _submit() {
@@ -284,6 +301,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
void _applySuggestion(String suggestion) {
final draft = _controller.text.trim();
final keepInputVisible = _isInputNearEnd();
setState(() {
if (draft.isNotEmpty) {
final draftTags = _parseTags(draft);
@@ -295,6 +313,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
_notifyChanged();
widget.onSuggestionApplied?.call(_query);
_focusNode.requestFocus();
if (keepInputVisible) _scrollTagsToEnd();
}
void _notifyChangedDebounced() {
@@ -322,6 +341,22 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
final all = [..._tags, if (draft.isNotEmpty) draft];
return all.join(' ');
}
void _scrollTagsToEnd() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_tagScrollController.hasClients) return;
_tagScrollController.animateTo(
_tagScrollController.position.maxScrollExtent,
duration: AppMotion.duration(context, 120),
curve: Curves.easeOut,
);
});
}
bool _isInputNearEnd() {
if (!_tagScrollController.hasClients) return true;
return _tagScrollController.position.extentAfter < 96;
}
}
class _TagSuggestionDropdown extends StatelessWidget {
+3 -2
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import '../../app/motion.dart';
import '../../app/responsive.dart';
class AppShell extends StatelessWidget {
@@ -93,7 +94,7 @@ class _DesktopShellState extends State<_DesktopShell> {
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
duration: AppMotion.duration(context, 180),
curve: Curves.easeOutCubic,
width: _hovered ? 256 : 72,
color: Theme.of(context).navigationRailTheme.backgroundColor,
@@ -174,7 +175,7 @@ class _RailButton extends StatelessWidget {
child: ClipRect(
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 120),
duration: AppMotion.duration(context, 120),
curve: Curves.easeOut,
child: Align(
alignment: Alignment.centerLeft,
+2 -1
View File
@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../app/motion.dart';
import '../../backend/backend.dart';
import 'blur_content.dart';
import 'loading_skeleton.dart';
@@ -188,7 +189,7 @@ class _PostCardState extends State<PostCard> {
ignoring: !_hovered || mobile,
child: AnimatedOpacity(
opacity: _hovered && !mobile ? 1 : 0,
duration: const Duration(milliseconds: 140),
duration: AppMotion.duration(context, 140),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(