Compare commits

..
1 Commits
Author SHA1 Message Date
RarDog a535f2be39 fix(artists): refine Liquid Glass search bar, clean filter bar, and bump to v3.5.1
Build and Release / Build macOS (push) Has been cancelled
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 12:58:05 +03:00
4 changed files with 248 additions and 130 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.5.0'; const appDisplayVersion = '3.5.1';
const appBuildNumber = 76; const appBuildNumber = 77;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+9
View File
@@ -1,4 +1,13 @@
const prismaChangelog = [ const prismaChangelog = [
PrismaChange(
version: '3.5.1',
title: 'Улучшение панели поиска и фильтров авторов в Liquid Glass',
bullets: [
'Чистый дизайн поиска: убрана лишняя заливка поля ввода и тяжелые рамки, поиск авторов стал идеально прозрачным и монолитным.',
'Единая компактная строка фильтров: провайдеры и платформы объединены в элегантную полосу прокрутки без нагромождения строк.',
'Нейтральные бейджи и аккуратные даты: скорректированы цвета счетчиков постов и укорочен вывод времени обновления.',
],
),
PrismaChange( PrismaChange(
version: '3.5.0', version: '3.5.0',
title: 'Редизайн вкладки «Артисты» в стиле Liquid Glass', title: 'Редизайн вкладки «Артисты» в стиле Liquid Glass',
@@ -554,28 +554,29 @@ class _ArtistsHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final isDark = Theme.of(context).brightness == Brightness.dark;
final isDark = theme.brightness == Brightness.dark;
final scheme = theme.colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 6), padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
child: _ArtistsLiquidCard( child: _ArtistsLiquidCard(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
// Glass search bar // Glass search bar (completely transparent inside, no unwanted background tint)
Container( Container(
height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark color: isDark
? Colors.black.withValues(alpha: 0.22) ? Colors.black.withValues(alpha: 0.28)
: Colors.white.withValues(alpha: 0.65), : Colors.black.withValues(alpha: 0.045),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: isDark color: isDark
? Colors.white.withValues(alpha: 0.10) ? Colors.white.withValues(alpha: 0.12)
: scheme.outlineVariant.withValues(alpha: 0.35), : Colors.black.withValues(alpha: 0.08),
width: 1.0,
), ),
), ),
child: Row( child: Row(
@@ -584,155 +585,244 @@ class _ArtistsHeader extends StatelessWidget {
Icon( Icon(
Icons.search_rounded, Icons.search_rounded,
size: 20, size: 20,
color: isDark ? Colors.white70 : scheme.primary, color: isDark
? Colors.white70
: const Color(0xFF64748B),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: TextField( child: TextField(
controller: searchController, controller: searchController,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
decoration: const InputDecoration( style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
decoration: InputDecoration(
hintText: 'Поиск авторов...', hintText: 'Поиск авторов...',
hintStyle: TextStyle(
color: isDark
? Colors.white.withValues(alpha: 0.45)
: const Color(0xFF94A3B8),
fontSize: 14,
fontWeight: FontWeight.normal,
),
border: InputBorder.none, border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
fillColor: Colors.transparent,
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 11), contentPadding: const EdgeInsets.symmetric(vertical: 12),
), ),
onSubmitted: (_) => onSearch(), onSubmitted: (_) => onSearch(),
), ),
), ),
if (searchController.text.isNotEmpty) ValueListenableBuilder<TextEditingValue>(
IconButton( valueListenable: searchController,
icon: const Icon(Icons.close_rounded, size: 18), builder: (context, value, _) {
visualDensity: VisualDensity.compact, if (value.text.isEmpty) return const SizedBox.shrink();
onPressed: () { return IconButton(
searchController.clear(); icon: const Icon(Icons.cancel_rounded, size: 18),
onSearch(); color: isDark ? Colors.white60 : const Color(0xFF94A3B8),
}, visualDensity: VisualDensity.compact,
), padding: EdgeInsets.zero,
Container( constraints:
margin: const EdgeInsets.only(right: 6), const BoxConstraints(minWidth: 32, minHeight: 32),
decoration: BoxDecoration( tooltip: 'Очистить',
color: scheme.primary.withValues(alpha: isDark ? 0.25 : 0.15), onPressed: () {
borderRadius: BorderRadius.circular(12), searchController.clear();
), onSearch();
child: IconButton( },
visualDensity: VisualDensity.compact, );
tooltip: 'Найти', },
onPressed: onSearch, ),
icon: Icon( const SizedBox(width: 4),
Icons.arrow_forward_rounded, Material(
size: 18, color: Colors.transparent,
color: scheme.primary, 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), const SizedBox(height: 8),
// Services horizontal filter pills // Unified single-row filter bar: providers + separator + platforms
SizedBox( SizedBox(
height: 34, height: 32,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: [ 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( InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: onClearServices, onTap: onClearServices,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selectedServices.isEmpty color: selectedServices.isEmpty
? scheme.primary.withValues(alpha: isDark ? 0.28 : 0.18) ? (isDark
? Colors.white.withValues(alpha: 0.20)
: const Color(0xFF0F172A))
: (isDark : (isDark
? Colors.white.withValues(alpha: 0.05) ? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)), : Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: selectedServices.isEmpty color: selectedServices.isEmpty
? scheme.primary.withValues(alpha: 0.7) ? (isDark
? Colors.white.withValues(alpha: 0.40)
: const Color(0xFF0F172A))
: (isDark : (isDark
? Colors.white.withValues(alpha: 0.08) ? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)), : Colors.black.withValues(alpha: 0.06)),
width: 1.2, width: 1.1,
), ),
), ),
child: Center( child: Center(
child: Text( child: Text(
'Все платформы', 'Все платформы',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11.5,
fontWeight: selectedServices.isEmpty fontWeight: selectedServices.isEmpty
? FontWeight.w700 ? FontWeight.w700
: FontWeight.w500, : FontWeight.w500,
color: selectedServices.isEmpty color: selectedServices.isEmpty
? scheme.primary ? Colors.white
: scheme.onSurface, : (isDark
? Colors.white
: const Color(0xFF1E293B)),
), ),
), ),
), ),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 6),
// Platform chips
for (final service in _availableServices) ...[ for (final service in _availableServices) ...[
Builder( Builder(
builder: (context) { builder: (context) {
@@ -744,51 +834,58 @@ class _ArtistsHeader extends StatelessWidget {
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6), horizontal: 9, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? sColor.withValues(alpha: isDark ? 0.25 : 0.16) ? sColor.withValues(
alpha: isDark ? 0.26 : 0.14)
: (isDark : (isDark
? Colors.white.withValues(alpha: 0.05) ? Colors.white.withValues(alpha: 0.05)
: Colors.black.withValues(alpha: 0.04)), : Colors.black.withValues(alpha: 0.035)),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
? sColor.withValues(alpha: 0.75) ? sColor.withValues(alpha: 0.75)
: (isDark : (isDark
? Colors.white.withValues(alpha: 0.08) ? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.06)), : Colors.black
width: 1.2, .withValues(alpha: 0.06)),
width: 1.1,
), ),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 7, width: 6,
height: 7, height: 6,
decoration: BoxDecoration( decoration: BoxDecoration(
color: sColor, color: sColor,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: isSelected boxShadow: isSelected
? [ ? [
BoxShadow( BoxShadow(
color: sColor.withValues(alpha: 0.6), color: sColor.withValues(
blurRadius: 6, alpha: 0.6),
blurRadius: 4,
), ),
] ]
: null, : null,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 5),
Text( Text(
_formatServiceName(service), _formatServiceName(service),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11.5,
fontWeight: isSelected fontWeight: isSelected
? FontWeight.w700 ? FontWeight.w700
: FontWeight.w500, : FontWeight.w500,
color: isSelected ? sColor : scheme.onSurface, color: isSelected
? sColor
: (isDark
? Colors.white70
: const Color(0xFF334155)),
), ),
), ),
], ],
@@ -797,7 +894,7 @@ class _ArtistsHeader extends StatelessWidget {
); );
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 6),
], ],
], ],
), ),
@@ -865,20 +962,32 @@ class _ArtistCard extends StatelessWidget {
if (artist.postCount != null) if (artist.postCount != null)
_LiquidMiniBadge( _LiquidMiniBadge(
label: '${artist.postCount} постов', label: '${artist.postCount} постов',
color: scheme.primary, color: isDark
? Colors.white70
: const Color(0xFF64748B),
), ),
], ],
), ),
if (artist.updatedAt != null) ...[ if (artist.updatedAt != null) ...[
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Builder(
'Обновлено: ${artist.updatedAt!.toLocal().toString().split('.').first}', builder: (_) {
maxLines: 1, final dt = artist.updatedAt!.toLocal();
overflow: TextOverflow.ellipsis, final d = dt.day.toString().padLeft(2, '0');
style: theme.textTheme.bodySmall?.copyWith( final m = dt.month.toString().padLeft(2, '0');
color: scheme.onSurfaceVariant.withValues(alpha: 0.8), final y = dt.year;
fontSize: 11, 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,
),
);
},
), ),
], ],
], ],
+1 -1
View File
@@ -1,7 +1,7 @@
name: gel_rule_app name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app. description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none" publish_to: "none"
version: 3.5.0+76 version: 3.5.1+77
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"