Compare commits

...
2 Commits
Author SHA1 Message Date
RarDog d5916e01b5 fix(artists): resolve double keyboard inset and white square bug during search (v3.5.2)
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 13:16:00 +03:00
RarDog a535f2be39 fix(artists): refine Liquid Glass search bar, clean filter bar, and bump to v3.5.1
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 12:58:05 +03:00
6 changed files with 285 additions and 139 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.5.0';
const appBuildNumber = 76;
const appDisplayVersion = '3.5.2';
const appBuildNumber = 78;
const appReleaseChannel = 'Stable';
+18
View File
@@ -1,4 +1,22 @@
const prismaChangelog = [
PrismaChange(
version: '3.5.2',
title: 'Исправление наложения клавиатуры и поиска авторов',
bullets: [
'Устранена ошибка выдвижения белого прямоугольника: исправлен конфликт двойного учета клавиатурного отступа (double viewInsets) во вложенных скаффолдах.',
'Корректная видимость авторов: карточки артистов больше не сжимаются и остаются полностью видимыми и прокручиваемыми при вводе запроса.',
'Улучшенное поведение интерфейса: плавающая кнопка настроек и нижняя панель скрываются при открытии клавиатуры, добавлен свайп для скрытия клавиатуры при прокрутке.',
],
),
PrismaChange(
version: '3.5.1',
title: 'Улучшение панели поиска и фильтров авторов в Liquid Glass',
bullets: [
'Чистый дизайн поиска: убрана лишняя заливка поля ввода и тяжелые рамки, поиск авторов стал идеально прозрачным и монолитным.',
'Единая компактная строка фильтров: провайдеры и платформы объединены в элегантную полосу прокрутки без нагромождения строк.',
'Нейтральные бейджи и аккуратные даты: скорректированы цвета счетчиков постов и укорочен вывод времени обновления.',
],
),
PrismaChange(
version: '3.5.0',
title: 'Редизайн вкладки «Артисты» в стиле Liquid Glass',
@@ -97,7 +97,11 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
void initState() {
super.initState();
_scroll.addListener(() {
if (_scroll.position.extentAfter < 500) _loadMore();
if (_scroll.hasClients &&
_scroll.position.hasContentDimensions &&
_scroll.position.extentAfter < 500) {
_loadMore();
}
});
}
@@ -128,6 +132,7 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
return AdaptiveScaffold(
title: 'Artists',
resizeToAvoidBottomInset: false,
actions: [
IconButton(
tooltip: 'Синхронизация Pawchive',
@@ -252,6 +257,8 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
: 1;
return GridView.builder(
controller: _scroll,
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
padding:
const EdgeInsets.fromLTRB(16, 10, 16, 18),
gridDelegate:
@@ -554,28 +561,29 @@ class _ArtistsHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final scheme = theme.colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 6),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
child: _ArtistsLiquidCard(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Glass search bar
// Glass search bar (completely transparent inside, no unwanted background tint)
Container(
height: 44,
decoration: BoxDecoration(
color: isDark
? Colors.black.withValues(alpha: 0.22)
: Colors.white.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(16),
? Colors.black.withValues(alpha: 0.28)
: Colors.black.withValues(alpha: 0.045),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.10)
: scheme.outlineVariant.withValues(alpha: 0.35),
? Colors.white.withValues(alpha: 0.12)
: Colors.black.withValues(alpha: 0.08),
width: 1.0,
),
),
child: Row(
@@ -584,155 +592,245 @@ class _ArtistsHeader extends StatelessWidget {
Icon(
Icons.search_rounded,
size: 20,
color: isDark ? Colors.white70 : scheme.primary,
color: isDark
? Colors.white70
: const Color(0xFF64748B),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: searchController,
textInputAction: TextInputAction.search,
decoration: const InputDecoration(
autocorrect: false,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
decoration: InputDecoration(
hintText: 'Поиск авторов...',
hintStyle: TextStyle(
color: isDark
? Colors.white.withValues(alpha: 0.45)
: const Color(0xFF94A3B8),
fontSize: 14,
fontWeight: FontWeight.normal,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
fillColor: Colors.transparent,
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 11),
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
onSubmitted: (_) => onSearch(),
),
),
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.close_rounded, size: 18),
visualDensity: VisualDensity.compact,
onPressed: () {
searchController.clear();
onSearch();
},
),
Container(
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: isDark ? 0.25 : 0.15),
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
visualDensity: VisualDensity.compact,
tooltip: 'Найти',
onPressed: onSearch,
icon: Icon(
Icons.arrow_forward_rounded,
size: 18,
color: scheme.primary,
ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) return const SizedBox.shrink();
return IconButton(
icon: const Icon(Icons.cancel_rounded, size: 18),
color: isDark ? Colors.white60 : const Color(0xFF94A3B8),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 32, minHeight: 32),
tooltip: 'Очистить',
onPressed: () {
searchController.clear();
onSearch();
},
);
},
),
const SizedBox(width: 4),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(11),
onTap: onSearch,
child: Container(
margin: const EdgeInsets.only(right: 5),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isDark
? Colors.white.withValues(alpha: 0.10)
: Colors.black.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(11),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.12)
: Colors.black.withValues(alpha: 0.08),
width: 1.0,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Найти',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isDark
? Colors.white
: const Color(0xFF0F172A),
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_forward_rounded,
size: 14,
color: isDark
? Colors.white70
: const Color(0xFF334155),
),
],
),
),
),
),
],
),
),
const SizedBox(height: 10),
// Providers horizontal selector
if (providers.isNotEmpty)
SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: providers.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final provider = providers[index];
final isSelected = provider.id == selectedProviderId;
return InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => onProviderChanged(provider.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected
? scheme.primary.withValues(alpha: isDark ? 0.28 : 0.18)
: (isDark
? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: isSelected
? scheme.primary.withValues(alpha: 0.7)
: (isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)),
width: 1.2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.person_search_rounded,
size: 16,
color: isSelected ? scheme.primary : scheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Text(
provider.name,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: isSelected ? scheme.primary : scheme.onSurface,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 8),
// Services horizontal filter pills
// Unified single-row filter bar: providers + separator + platforms
SizedBox(
height: 34,
height: 32,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
// Providers
for (final provider in providers) ...[
Builder(
builder: (context) {
final isSelected = provider.id == selectedProviderId;
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => onProviderChanged(provider.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: isSelected
? (isDark
? Colors.white.withValues(alpha: 0.20)
: const Color(0xFF0F172A))
: (isDark
? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? (isDark
? Colors.white.withValues(alpha: 0.40)
: const Color(0xFF0F172A))
: (isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)),
width: 1.1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.dns_rounded,
size: 14,
color: isSelected
? Colors.white
: (isDark
? Colors.white70
: const Color(0xFF64748B)),
),
const SizedBox(width: 5),
Text(
provider.name,
style: TextStyle(
fontSize: 11.5,
fontWeight: isSelected
? FontWeight.w700
: FontWeight.w500,
color: isSelected
? Colors.white
: (isDark
? Colors.white
: const Color(0xFF1E293B)),
),
),
],
),
),
);
},
),
const SizedBox(width: 6),
],
// Vertical divider between provider and platforms
if (providers.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2, vertical: 4),
child: Container(
width: 1,
color: isDark
? Colors.white.withValues(alpha: 0.15)
: Colors.black.withValues(alpha: 0.12),
),
),
const SizedBox(width: 6),
// "All platforms" chip
InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onClearServices,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: selectedServices.isEmpty
? scheme.primary.withValues(alpha: isDark ? 0.28 : 0.18)
? (isDark
? Colors.white.withValues(alpha: 0.20)
: const Color(0xFF0F172A))
: (isDark
? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selectedServices.isEmpty
? scheme.primary.withValues(alpha: 0.7)
? (isDark
? Colors.white.withValues(alpha: 0.40)
: const Color(0xFF0F172A))
: (isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)),
width: 1.2,
width: 1.1,
),
),
child: Center(
child: Text(
'Все платформы',
style: TextStyle(
fontSize: 12,
fontSize: 11.5,
fontWeight: selectedServices.isEmpty
? FontWeight.w700
: FontWeight.w500,
color: selectedServices.isEmpty
? scheme.primary
: scheme.onSurface,
? Colors.white
: (isDark
? Colors.white
: const Color(0xFF1E293B)),
),
),
),
),
),
const SizedBox(width: 8),
const SizedBox(width: 6),
// Platform chips
for (final service in _availableServices) ...[
Builder(
builder: (context) {
@@ -744,51 +842,58 @@ class _ArtistsHeader extends StatelessWidget {
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
horizontal: 9, vertical: 5),
decoration: BoxDecoration(
color: isSelected
? sColor.withValues(alpha: isDark ? 0.25 : 0.16)
? sColor.withValues(
alpha: isDark ? 0.26 : 0.14)
: (isDark
? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)),
: Colors.black.withValues(alpha: 0.035)),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? sColor.withValues(alpha: 0.75)
: (isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)),
width: 1.2,
: Colors.black
.withValues(alpha: 0.06)),
width: 1.1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
width: 6,
height: 6,
decoration: BoxDecoration(
color: sColor,
shape: BoxShape.circle,
boxShadow: isSelected
? [
BoxShadow(
color: sColor.withValues(alpha: 0.6),
blurRadius: 6,
color: sColor.withValues(
alpha: 0.6),
blurRadius: 4,
),
]
: null,
),
),
const SizedBox(width: 6),
const SizedBox(width: 5),
Text(
_formatServiceName(service),
style: TextStyle(
fontSize: 12,
fontSize: 11.5,
fontWeight: isSelected
? FontWeight.w700
: FontWeight.w500,
color: isSelected ? sColor : scheme.onSurface,
color: isSelected
? sColor
: (isDark
? Colors.white70
: const Color(0xFF334155)),
),
),
],
@@ -797,7 +902,7 @@ class _ArtistsHeader extends StatelessWidget {
);
},
),
const SizedBox(width: 8),
const SizedBox(width: 6),
],
],
),
@@ -865,20 +970,32 @@ class _ArtistCard extends StatelessWidget {
if (artist.postCount != null)
_LiquidMiniBadge(
label: '${artist.postCount} постов',
color: scheme.primary,
color: isDark
? Colors.white70
: const Color(0xFF64748B),
),
],
),
if (artist.updatedAt != null) ...[
const SizedBox(height: 5),
Text(
'Обновлено: ${artist.updatedAt!.toLocal().toString().split('.').first}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant.withValues(alpha: 0.8),
fontSize: 11,
),
Builder(
builder: (_) {
final dt = artist.updatedAt!.toLocal();
final d = dt.day.toString().padLeft(2, '0');
final m = dt.month.toString().padLeft(2, '0');
final y = dt.year;
final hr = dt.hour.toString().padLeft(2, '0');
final min = dt.minute.toString().padLeft(2, '0');
return Text(
'Обновлено: $d.$m.$y $hr:$min',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant.withValues(alpha: 0.8),
fontSize: 11,
),
);
},
),
],
],
@@ -9,6 +9,7 @@ class AdaptiveScaffold extends StatelessWidget {
this.titleWidget,
this.actions = const [],
this.floatingActionButton,
this.resizeToAvoidBottomInset = false,
super.key,
});
@@ -17,11 +18,13 @@ class AdaptiveScaffold extends StatelessWidget {
final Widget body;
final List<Widget> actions;
final Widget? floatingActionButton;
final bool resizeToAvoidBottomInset;
@override
Widget build(BuildContext context) {
if (Responsive.isMobile(context)) {
return Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
appBar: AppBar(
title: titleWidget ?? Text(title),
actions: actions,
@@ -35,6 +38,7 @@ class AdaptiveScaffold extends StatelessWidget {
);
}
return Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
appBar: AppBar(title: titleWidget ?? Text(title), actions: actions),
body: body,
floatingActionButton: floatingActionButton,
+15 -8
View File
@@ -352,19 +352,23 @@ class _MobileShell extends StatelessWidget {
? location == '/'
: location.startsWith(item.location),
);
final bottomInset = 76.0 + MediaQuery.paddingOf(context).bottom;
final isKeyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
final bottomInset = isKeyboardOpen
? 0.0
: 76.0 + MediaQuery.paddingOf(context).bottom;
return Scaffold(
extendBody: true,
body: MediaQuery(
data: MediaQuery.of(context).copyWith(
viewInsets: MediaQuery.of(context).viewInsets.copyWith(bottom: 0),
padding: MediaQuery.of(context).padding.copyWith(
bottom: bottomInset,
),
),
child: child,
),
floatingActionButton: location == '/settings'
floatingActionButton: (location == '/settings' || isKeyboardOpen)
? null
: Padding(
padding: const EdgeInsets.only(bottom: 0, right: 2),
@@ -374,12 +378,15 @@ class _MobileShell extends StatelessWidget {
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
bottomNavigationBar: _LiquidGlassBottomBar(
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (index) => context.go(items[index].location),
items: items,
ru: ru,
),
bottomNavigationBar: isKeyboardOpen
? null
: _LiquidGlassBottomBar(
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (index) =>
context.go(items[index].location),
items: items,
ru: ru,
),
);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none"
version: 3.5.0+76
version: 3.5.2+78
environment:
sdk: ">=3.3.0 <4.0.0"