diff --git a/README.md b/README.md
index 301e2f1..65f0734 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart
index f059ecf..174819c 100644
--- a/lib/app/app_version.dart
+++ b/lib/app/app_version.dart
@@ -1,3 +1,3 @@
-const appDisplayVersion = '3.0.9';
-const appBuildNumber = 60;
+const appDisplayVersion = '3.1.0';
+const appBuildNumber = 61;
const appReleaseChannel = 'Stable';
diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart
index ab9dcba..d199127 100644
--- a/lib/app/changelog.dart
+++ b/lib/app/changelog.dart
@@ -1,4 +1,14 @@
const lunarisChangelog = [
+ LunarisChange(
+ version: '3.1.0',
+ title: 'iPhone Liquid Glass — Поиск, Избранное, История и исправление доступности',
+ bullets: [
+ 'Liquid Glass редизайн разделов «Поиск», «Избранное» и «История»: матовые стеклянные карточки (BackdropFilter blur), полупрозрачные градиенты, зеркальные контуры и сквиркл-бейджи иконок в стиле iOS.',
+ 'Исправление шпаргалки по поиску: увеличен нижний отступ прокрутки во всех списках и сетках до 140px + safe area, благодаря чему блок операторов и шпаргалка по поиску больше не перекрываются парящей навигационной панелью и легко нажимаются.',
+ 'Исправление модального окна проверки обновлений: панель с выбором источников (GitHub и Gitea) открывается через корневой навигатор поверх всей навигации с безопасным отступом снизу, обеспечивая беспрепятственный доступ к обеим кнопкам.',
+ 'Тактильный отклик и стеклянные пилюли: быстрые операторы, категории, недавние запросы, фильтры медиа и переключатели разделов получили матовое оформление и вибрационный отклик Haptic Feedback.',
+ ],
+ ),
LunarisChange(
version: '3.0.9',
title: 'Коррекция позиции кнопки настроек и отображения кнопок JSON и бэкапа',
diff --git a/lib/features/favorites/presentation/favorites_screen.dart b/lib/features/favorites/presentation/favorites_screen.dart
index de08eb2..fc7cce5 100644
--- a/lib/features/favorites/presentation/favorites_screen.dart
+++ b/lib/features/favorites/presentation/favorites_screen.dart
@@ -1,5 +1,8 @@
+import 'dart:ui';
+
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -176,82 +179,154 @@ class _FavoritesScreenState extends ConsumerState {
// Search input bar if active
if (_isSearching)
Padding(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
- child: TextField(
- controller: _searchController,
- autofocus: true,
- decoration: InputDecoration(
- hintText: isRu
- ? 'Поиск по тегам и авторам...'
- : 'Search by tags and artists...',
- prefixIcon: const Icon(Icons.search_rounded),
- suffixIcon: _searchQuery.isNotEmpty
- ? IconButton(
- icon: const Icon(Icons.clear_rounded),
- onPressed: () {
- _searchController.clear();
- setState(() => _searchQuery = '');
- },
- )
- : null,
- isDense: true,
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 10,
- ),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: BorderSide.none,
+ padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Theme.of(context).colorScheme.surfaceContainerHigh
+ .withValues(alpha: 0.55)
+ : Colors.white.withValues(alpha: 0.80),
+ borderRadius: BorderRadius.circular(18),
+ border: Border.all(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withValues(alpha: 0.14)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.1,
+ ),
+ ),
+ child: TextField(
+ controller: _searchController,
+ autofocus: true,
+ decoration: InputDecoration(
+ hintText: isRu
+ ? 'Поиск по тегам и авторам...'
+ : 'Search by tags and artists...',
+ prefixIcon: const Icon(Icons.search_rounded),
+ suffixIcon: _searchQuery.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.clear_rounded),
+ onPressed: () {
+ HapticFeedback.lightImpact();
+ _searchController.clear();
+ setState(() => _searchQuery = '');
+ },
+ )
+ : null,
+ isDense: true,
+ filled: false,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 10,
+ ),
+ border: InputBorder.none,
+ ),
+ onChanged: (val) =>
+ setState(() => _searchQuery = val.trim()),
+ ),
),
),
- onChanged: (val) => setState(() => _searchQuery = val.trim()),
),
),
- // Navigation Tabs
+ // Navigation Tabs with Liquid Glass container
Padding(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
- child: SizedBox(
- width: double.infinity,
- child: SegmentedButton(
- showSelectedIcon: false,
- segments: [
- ButtonSegment(
- value: 0,
- icon: const Icon(Icons.favorite_rounded, size: 18),
- label: Text(
- '${isRu ? 'Все' : 'All'} (${allPosts.length})',
- style: const TextStyle(fontSize: 12),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ButtonSegment(
- value: 1,
- icon: const Icon(Icons.offline_pin_rounded, size: 18),
- label: Text(
- '${isRu ? 'Офлайн' : 'Offline'} (${offlinePosts.length})',
- style: const TextStyle(fontSize: 12),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ButtonSegment(
- value: 2,
- icon: const Icon(Icons.groups_rounded, size: 18),
- label: Text(
- '${isRu ? 'Авторы' : 'Artists'} (${artistGroups.length})',
- style: const TextStyle(fontSize: 12),
- overflow: TextOverflow.ellipsis,
+ padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(
+ alpha: Theme.of(context).brightness == Brightness.dark
+ ? 0.20
+ : 0.04,
),
+ blurRadius: 10,
+ offset: const Offset(0, 2),
),
],
- selected: {_tabIndex},
- onSelectionChanged: (value) {
- setState(() {
- _tabIndex = value.first;
- _selectedArtist = null;
- });
- },
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(22),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Theme.of(context)
+ .colorScheme
+ .surfaceContainerHigh
+ .withValues(alpha: 0.50)
+ : Colors.white.withValues(alpha: 0.72),
+ border: Border.all(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withValues(alpha: 0.12)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.1,
+ ),
+ ),
+ child: SizedBox(
+ width: double.infinity,
+ child: SegmentedButton(
+ showSelectedIcon: false,
+ style: ButtonStyle(
+ visualDensity: VisualDensity.compact,
+ shape: WidgetStatePropertyAll(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ ),
+ ),
+ segments: [
+ ButtonSegment(
+ value: 0,
+ icon: const Icon(Icons.favorite_rounded, size: 18),
+ label: Text(
+ '${isRu ? 'Все' : 'All'} (${allPosts.length})',
+ style: const TextStyle(
+ fontSize: 12, fontWeight: FontWeight.w600),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ButtonSegment(
+ value: 1,
+ icon: const Icon(Icons.offline_pin_rounded,
+ size: 18),
+ label: Text(
+ '${isRu ? 'Офлайн' : 'Offline'} (${offlinePosts.length})',
+ style: const TextStyle(
+ fontSize: 12, fontWeight: FontWeight.w600),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ButtonSegment(
+ value: 2,
+ icon: const Icon(Icons.groups_rounded, size: 18),
+ label: Text(
+ '${isRu ? 'Авторы' : 'Artists'} (${artistGroups.length})',
+ style: const TextStyle(
+ fontSize: 12, fontWeight: FontWeight.w600),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ selected: {_tabIndex},
+ onSelectionChanged: (value) {
+ HapticFeedback.selectionClick();
+ setState(() {
+ _tabIndex = value.first;
+ _selectedArtist = null;
+ });
+ },
+ ),
+ ),
+ ),
+ ),
),
),
),
@@ -265,9 +340,12 @@ class _FavoritesScreenState extends ConsumerState {
FilterChip(
label: Text(isRu ? 'Все' : 'All'),
selected: _mediaFilter == FavoriteMediaType.all,
- onSelected: (_) => setState(
- () => _mediaFilter = FavoriteMediaType.all,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _mediaFilter = FavoriteMediaType.all,
+ );
+ },
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 8),
@@ -275,9 +353,12 @@ class _FavoritesScreenState extends ConsumerState {
avatar: const Icon(Icons.videocam_rounded, size: 16),
label: Text(isRu ? 'Видео' : 'Videos'),
selected: _mediaFilter == FavoriteMediaType.video,
- onSelected: (_) => setState(
- () => _mediaFilter = FavoriteMediaType.video,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _mediaFilter = FavoriteMediaType.video,
+ );
+ },
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 8),
@@ -285,9 +366,12 @@ class _FavoritesScreenState extends ConsumerState {
avatar: const Icon(Icons.image_rounded, size: 16),
label: Text(isRu ? 'Фото' : 'Images'),
selected: _mediaFilter == FavoriteMediaType.image,
- onSelected: (_) => setState(
- () => _mediaFilter = FavoriteMediaType.image,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _mediaFilter = FavoriteMediaType.image,
+ );
+ },
visualDensity: VisualDensity.compact,
),
],
@@ -490,31 +574,40 @@ class _FavoritesScreenState extends ConsumerState {
}) {
final scheme = Theme.of(context).colorScheme;
- return Container(
+ return _FavoritesLiquidCard(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
padding: const EdgeInsets.all(14),
- decoration: BoxDecoration(
- color: scheme.surfaceContainerHigh,
- borderRadius: BorderRadius.circular(18),
- border: Border.all(
- color: scheme.outlineVariant.withValues(alpha: 0.35),
- ),
- ),
+ glowColor: scheme.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
- padding: const EdgeInsets.all(10),
+ width: 40,
+ height: 40,
decoration: BoxDecoration(
- color: scheme.primaryContainer.withValues(alpha: 0.7),
+ gradient: LinearGradient(
+ colors: [
+ scheme.primary,
+ scheme.primary.withValues(alpha: 0.85),
+ ],
+ ),
borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: scheme.primary.withValues(alpha: 0.35),
+ blurRadius: 7,
+ offset: const Offset(0, 2.5),
+ ),
+ ],
),
- child: Icon(
- Icons.offline_pin_rounded,
- color: scheme.onPrimaryContainer,
- size: 22,
+ child: const Center(
+ child: Icon(
+ Icons.offline_pin_rounded,
+ color: Colors.white,
+ size: 22,
+ ),
),
),
const SizedBox(width: 12),
@@ -526,6 +619,7 @@ class _FavoritesScreenState extends ConsumerState {
'$downloadedCount ${isRu ? 'из' : 'of'} $totalCount ${isRu ? 'офлайн' : 'saved'}',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
+ letterSpacing: -0.2,
),
),
const SizedBox(height: 2),
@@ -553,21 +647,22 @@ class _FavoritesScreenState extends ConsumerState {
if (pendingCount > 0)
FilledButton.tonalIcon(
onPressed: () async {
+ HapticFeedback.mediumImpact();
final dm = ref.read(downloadManagerServiceProvider);
for (final p in pendingPosts) {
await dm.start(p);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- isRu
- ? 'В очередь загрузки добавлено: $pendingCount'
- : 'Queued $pendingCount downloads',
- ),
- behavior: SnackBarBehavior.floating,
+ SnackBar(
+ content: Text(
+ isRu
+ ? 'В очередь загрузки добавлено: $pendingCount'
+ : 'Queued $pendingCount downloads',
),
- );
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
},
icon: const Icon(Icons.download_rounded, size: 16),
label: Text(
@@ -594,7 +689,7 @@ class _FavoritesScreenState extends ConsumerState {
vertical: 6,
),
decoration: BoxDecoration(
- color: scheme.secondaryContainer.withValues(alpha: 0.5),
+ color: scheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Row(
@@ -610,8 +705,8 @@ class _FavoritesScreenState extends ConsumerState {
isRu ? 'Всё готово' : 'Synced',
style:
Theme.of(context).textTheme.labelSmall?.copyWith(
- color: scheme.onSecondaryContainer,
- fontWeight: FontWeight.w600,
+ color: scheme.primary,
+ fontWeight: FontWeight.w700,
),
),
],
@@ -666,27 +761,36 @@ class _FavoritesScreenState extends ConsumerState {
ChoiceChip(
label: Text(isRu ? 'По дате' : 'Date'),
selected: _offlineSort == OfflineSortOption.dateAddedDesc,
- onSelected: (_) => setState(
- () => _offlineSort = OfflineSortOption.dateAddedDesc,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _offlineSort = OfflineSortOption.dateAddedDesc,
+ );
+ },
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 6),
ChoiceChip(
label: Text(isRu ? 'По размеру' : 'Size'),
selected: _offlineSort == OfflineSortOption.sizeDesc,
- onSelected: (_) => setState(
- () => _offlineSort = OfflineSortOption.sizeDesc,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _offlineSort = OfflineSortOption.sizeDesc,
+ );
+ },
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 6),
ChoiceChip(
label: Text(isRu ? 'По автору' : 'Artist'),
selected: _offlineSort == OfflineSortOption.artist,
- onSelected: (_) => setState(
- () => _offlineSort = OfflineSortOption.artist,
- ),
+ onSelected: (_) {
+ HapticFeedback.selectionClick();
+ setState(
+ () => _offlineSort = OfflineSortOption.artist,
+ );
+ },
visualDensity: VisualDensity.compact,
),
],
@@ -712,38 +816,50 @@ class _FavoritesScreenState extends ConsumerState {
return Column(
children: [
- // Artist Header Banner
- Container(
+ // Artist Header Banner (Liquid Glass)
+ _FavoritesLiquidCard(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- decoration: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .surfaceContainerLow,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(
- color: Theme.of(context)
- .colorScheme
- .outlineVariant
- .withValues(alpha: 0.3),
- ),
- ),
child: Row(
children: [
IconButton(
- onPressed: () => setState(() => _selectedArtist = null),
+ onPressed: () {
+ HapticFeedback.selectionClick();
+ setState(() => _selectedArtist = null);
+ },
icon: const Icon(Icons.arrow_back_rounded),
tooltip: isRu ? 'Все авторы' : 'All artists',
),
const SizedBox(width: 8),
- CircleAvatar(
- radius: 18,
- backgroundColor: Theme.of(context).colorScheme.primaryContainer,
- child: Text(
- selected.isNotEmpty ? selected[0].toUpperCase() : '?',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: Theme.of(context).colorScheme.onPrimaryContainer,
+ Container(
+ width: 38,
+ height: 38,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.tertiary,
+ ],
+ ),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Theme.of(context)
+ .colorScheme
+ .primary
+ .withValues(alpha: 0.35),
+ blurRadius: 6,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Center(
+ child: Text(
+ selected.isNotEmpty ? selected[0].toUpperCase() : '?',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
),
),
),
@@ -756,6 +872,7 @@ class _FavoritesScreenState extends ConsumerState {
selected,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
+ letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -776,6 +893,7 @@ class _FavoritesScreenState extends ConsumerState {
visualDensity: VisualDensity.compact,
),
onPressed: () {
+ HapticFeedback.selectionClick();
context.push('/search?q=${Uri.encodeComponent('artist:$selected')}');
},
icon: const Icon(Icons.search_rounded, size: 16),
@@ -808,7 +926,9 @@ class _FavoritesScreenState extends ConsumerState {
}
return ListView.separated(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ // Extended bottom padding so the last artist cards scroll comfortably above the floating dock
+ padding: EdgeInsets.fromLTRB(
+ 16, 8, 16, 140 + MediaQuery.paddingOf(context).bottom),
itemCount: entries.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
@@ -863,6 +983,83 @@ class _FavoritesScreenState extends ConsumerState {
}
}
+class _FavoritesLiquidCard extends StatelessWidget {
+ const _FavoritesLiquidCard({
+ required this.child,
+ this.padding,
+ this.glowColor,
+ this.margin,
+ });
+
+ final Widget child;
+ final EdgeInsetsGeometry? padding;
+ final EdgeInsetsGeometry? margin;
+ final Color? glowColor;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+ final accent = glowColor ?? theme.colorScheme.primary;
+
+ return Container(
+ margin: margin,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
+ boxShadow: [
+ BoxShadow(
+ color: isDark
+ ? Colors.black.withValues(alpha: 0.30)
+ : accent.withValues(alpha: 0.07),
+ blurRadius: 16,
+ offset: const Offset(0, 5),
+ spreadRadius: -2,
+ ),
+ BoxShadow(
+ color: accent.withValues(alpha: isDark ? 0.05 : 0.02),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(22),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
+ child: Container(
+ padding: padding ?? const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: isDark
+ ? [
+ theme.colorScheme.surfaceContainerHigh
+ .withValues(alpha: 0.60),
+ theme.colorScheme.surfaceContainerLow
+ .withValues(alpha: 0.38),
+ ]
+ : [
+ Colors.white.withValues(alpha: 0.88),
+ Colors.white.withValues(alpha: 0.72),
+ ],
+ ),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.14)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.2,
+ ),
+ ),
+ child: child,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
class _ArtistCard extends StatelessWidget {
const _ArtistCard({
required this.artistName,
@@ -881,123 +1078,182 @@ class _ArtistCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
+ final isDark = Theme.of(context).brightness == Brightness.dark;
- return Card(
- elevation: 0,
- color: scheme.surfaceContainerLow,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- side: BorderSide(
- color: scheme.outlineVariant.withValues(alpha: 0.3),
- ),
+ return Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
+ boxShadow: [
+ BoxShadow(
+ color: isDark
+ ? Colors.black.withValues(alpha: 0.28)
+ : scheme.primary.withValues(alpha: 0.06),
+ blurRadius: 14,
+ offset: const Offset(0, 4),
+ spreadRadius: -2,
+ ),
+ ],
),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: onTap,
- child: Padding(
- padding: const EdgeInsets.all(12),
- child: Row(
- children: [
- // Artist Avatar
- Container(
- width: 48,
- height: 48,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(22),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(22),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ onTap();
+ },
+ child: Container(
+ padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
- colors: [
- scheme.primaryContainer,
- scheme.tertiaryContainer,
- ],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
+ colors: isDark
+ ? [
+ scheme.surfaceContainerHigh
+ .withValues(alpha: 0.58),
+ scheme.surfaceContainerLow
+ .withValues(alpha: 0.36),
+ ]
+ : [
+ Colors.white.withValues(alpha: 0.88),
+ Colors.white.withValues(alpha: 0.72),
+ ],
),
- borderRadius: BorderRadius.circular(14),
- ),
- child: Center(
- child: Text(
- artistName.isNotEmpty ? artistName[0].toUpperCase() : '?',
- style: TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- color: scheme.onPrimaryContainer,
- ),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.13)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.1,
),
),
- ),
- const SizedBox(width: 12),
- // Artist Name & Count
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ child: Row(
children: [
- Text(
- artistName,
- style: Theme.of(context).textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 2),
+ // Artist Squircle Avatar
Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 2,
- ),
+ width: 48,
+ height: 48,
decoration: BoxDecoration(
- color: scheme.secondaryContainer.withValues(alpha: 0.5),
- borderRadius: BorderRadius.circular(8),
+ gradient: LinearGradient(
+ colors: [
+ scheme.primary,
+ scheme.tertiary,
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(15),
+ boxShadow: [
+ BoxShadow(
+ color: scheme.primary.withValues(alpha: 0.35),
+ blurRadius: 8,
+ offset: const Offset(0, 2.5),
+ ),
+ ],
),
- child: Text(
- '$postsCount ${isRu ? 'работ' : 'posts'}',
- style: Theme.of(context).textTheme.labelSmall?.copyWith(
- color: scheme.onSecondaryContainer,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ],
- ),
- ),
- // Thumbnails preview
- if (previewUrls.isNotEmpty) ...[
- const SizedBox(width: 8),
- Row(
- children: [
- for (final url in previewUrls)
- Padding(
- padding: const EdgeInsets.only(left: 4),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: SizedBox(
- width: 44,
- height: 44,
- child: CachedNetworkImage(
- imageUrl: url,
- fit: BoxFit.cover,
- placeholder: (_, __) => Container(
- color: scheme.surfaceContainerHighest,
- ),
- errorWidget: (_, __, ___) => Container(
- color: scheme.surfaceContainerHighest,
- child: const Icon(
- Icons.image_not_supported_rounded,
- size: 16,
- ),
- ),
- ),
+ child: Center(
+ child: Text(
+ artistName.isNotEmpty ? artistName[0].toUpperCase() : '?',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
),
),
),
+ ),
+ const SizedBox(width: 12),
+ // Artist Name & Count
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ artistName,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ letterSpacing: -0.2,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 3),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: scheme.primary.withValues(alpha: 0.12),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ '$postsCount ${isRu ? 'работ' : 'posts'}',
+ style: Theme.of(context).textTheme.labelSmall?.copyWith(
+ color: scheme.primary,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ // Thumbnails preview
+ if (previewUrls.isNotEmpty) ...[
+ const SizedBox(width: 8),
+ Row(
+ children: [
+ for (final url in previewUrls)
+ Padding(
+ padding: const EdgeInsets.only(left: 5),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.10)
+ : scheme.outlineVariant.withValues(alpha: 0.25),
+ ),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: CachedNetworkImage(
+ imageUrl: url,
+ fit: BoxFit.cover,
+ placeholder: (_, __) => Container(
+ color: scheme.surfaceContainerHighest,
+ ),
+ errorWidget: (_, __, ___) => Container(
+ color: scheme.surfaceContainerHighest,
+ child: const Icon(
+ Icons.image_not_supported_rounded,
+ size: 16,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ const SizedBox(width: 4),
+ Icon(
+ Icons.chevron_right_rounded,
+ size: 20,
+ color: scheme.onSurfaceVariant.withValues(alpha: 0.5),
+ ),
],
),
- ],
- const SizedBox(width: 6),
- Icon(
- Icons.chevron_right_rounded,
- color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
),
- ],
+ ),
),
),
),
diff --git a/lib/features/search/presentation/search_screen.dart b/lib/features/search/presentation/search_screen.dart
index 9021df4..908e71e 100644
--- a/lib/features/search/presentation/search_screen.dart
+++ b/lib/features/search/presentation/search_screen.dart
@@ -1,4 +1,7 @@
+import 'dart:ui';
+
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -47,6 +50,7 @@ class _SearchScreenState extends ConsumerState {
final strings = AppStrings(settings.languageCode);
final isRu = strings.ru;
final theme = Theme.of(context);
+ final bottomInset = MediaQuery.paddingOf(context).bottom;
return AdaptiveScaffold(
title: strings.search,
@@ -78,19 +82,12 @@ class _SearchScreenState extends ConsumerState {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: ListView(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+ // Extended bottom padding so the cheat sheet card is never obscured by the floating dock
+ padding: EdgeInsets.fromLTRB(16, 14, 16, 140 + bottomInset),
children: [
// 1. Search Bar with Quick Operators
- Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: theme.colorScheme.surfaceContainerLow,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(
- color: theme.colorScheme.outlineVariant
- .withValues(alpha: 0.3),
- ),
- ),
+ _SearchLiquidCard(
+ glowColor: theme.colorScheme.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -171,56 +168,124 @@ class _SearchScreenState extends ConsumerState {
const SizedBox(height: 16),
// 2. Recent Searches Card
- _SearchSectionCard(
- icon: Icons.history_rounded,
- accentColor: const Color(0xFF6366F1),
- title: isRu ? 'Недавние запросы' : 'Recent searches',
- badgeCount: data.recent
- .map((e) => e.query.trim().toLowerCase())
- .where((q) => q.isNotEmpty)
- .toSet()
- .length,
- trailing: data.recent.isEmpty
- ? null
- : TextButton(
- style: TextButton.styleFrom(
- visualDensity: VisualDensity.compact,
- padding: const EdgeInsets.symmetric(
- horizontal: 10, vertical: 4),
- minimumSize: Size.zero,
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ _SearchLiquidCard(
+ glowColor: const Color(0xFF6366F1),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const _SearchIconBadge(
+ icon: Icons.history_rounded,
+ color: Color(0xFF6366F1),
),
- onPressed: () => ref
- .read(searchControllerProvider.notifier)
- .clearHistory(),
- child: Text(
- isRu ? 'Очистить' : 'Clear',
- style: TextStyle(
- color: theme.colorScheme.primary,
- fontSize: 13,
- fontWeight: FontWeight.w600,
+ const SizedBox(width: 12),
+ Expanded(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: Text(
+ isRu ? 'Недавние запросы' : 'Recent searches',
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w700,
+ fontSize: 15,
+ letterSpacing: -0.2,
+ ),
+ ),
+ ),
+ if (data.recent.isNotEmpty) ...[
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 7, vertical: 2),
+ decoration: BoxDecoration(
+ color: const Color(0xFF6366F1)
+ .withValues(alpha: 0.16),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Text(
+ '${data.recent.map((e) => e.query.trim().toLowerCase()).where((q) => q.isNotEmpty).toSet().length}',
+ style: theme.textTheme.labelSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ color: const Color(0xFF6366F1),
+ ),
+ ),
+ ),
+ ],
+ ],
),
),
- ),
- child: RecentSearches(
- items: data.recent,
- onTap: (query) =>
- context.go('/?q=${Uri.encodeQueryComponent(query)}'),
- onDelete: (id) => ref
- .read(searchControllerProvider.notifier)
- .deleteHistory(id),
+ if (data.recent.isNotEmpty)
+ TextButton(
+ style: TextButton.styleFrom(
+ visualDensity: VisualDensity.compact,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10, vertical: 4),
+ minimumSize: Size.zero,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ onPressed: () {
+ HapticFeedback.selectionClick();
+ ref
+ .read(searchControllerProvider.notifier)
+ .clearHistory();
+ },
+ child: Text(
+ isRu ? 'Очистить' : 'Clear',
+ style: TextStyle(
+ color: theme.colorScheme.primary,
+ fontSize: 13,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 14),
+ RecentSearches(
+ items: data.recent,
+ onTap: (query) =>
+ context.go('/?q=${Uri.encodeQueryComponent(query)}'),
+ onDelete: (id) => ref
+ .read(searchControllerProvider.notifier)
+ .deleteHistory(id),
+ ),
+ ],
),
),
const SizedBox(height: 16),
// 3. Explore Popular Categories & Tags
- _SearchSectionCard(
- icon: Icons.explore_rounded,
- accentColor: const Color(0xFF10B981),
- title: isRu ? 'Популярные категории' : 'Explore categories',
+ _SearchLiquidCard(
+ glowColor: const Color(0xFF10B981),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+ Row(
+ children: [
+ const _SearchIconBadge(
+ icon: Icons.explore_rounded,
+ color: Color(0xFF10B981),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ isRu ? 'Популярные категории' : 'Explore categories',
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w700,
+ fontSize: 15,
+ letterSpacing: -0.2,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
_CategoryTagRow(
categoryName: isRu ? 'Тематика' : 'Theme & Ambience',
color: const Color(0xFF0EA5E9),
@@ -234,7 +299,7 @@ class _SearchScreenState extends ConsumerState {
],
onTap: (tag) => _onTagTap(tag, data.query),
),
- const SizedBox(height: 14),
+ const SizedBox(height: 16),
_CategoryTagRow(
categoryName:
isRu ? 'Персонажи и детали' : 'Characters & Attire',
@@ -250,7 +315,7 @@ class _SearchScreenState extends ConsumerState {
],
onTap: (tag) => _onTagTap(tag, data.query),
),
- const SizedBox(height: 14),
+ const SizedBox(height: 16),
_CategoryTagRow(
categoryName: isRu ? 'Провайдеры' : 'Providers',
color: const Color(0xFF8B5CF6),
@@ -268,84 +333,78 @@ class _SearchScreenState extends ConsumerState {
),
const SizedBox(height: 16),
- // 4. Search Tips / Cheat Sheet Card
- Container(
- decoration: BoxDecoration(
- color: theme.colorScheme.surfaceContainerLow,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(
- color: theme.colorScheme.outlineVariant
- .withValues(alpha: 0.3),
- ),
- ),
- child: ExpansionTile(
- shape: const Border(),
- collapsedShape: const Border(),
- leading: Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- color: const Color(0xFFF59E0B).withValues(alpha: 0.15),
- borderRadius: BorderRadius.circular(10),
- ),
- child: const Icon(
- Icons.lightbulb_outline_rounded,
- size: 18,
+ // 4. Search Tips / Cheat Sheet Card (Full Liquid Glass Container)
+ _SearchLiquidCard(
+ padding: EdgeInsets.zero,
+ glowColor: const Color(0xFFF59E0B),
+ child: Theme(
+ data: theme.copyWith(dividerColor: Colors.transparent),
+ child: ExpansionTile(
+ shape: const Border(),
+ collapsedShape: const Border(),
+ tilePadding: const EdgeInsets.symmetric(
+ horizontal: 16, vertical: 4),
+ leading: const _SearchIconBadge(
+ icon: Icons.lightbulb_rounded,
color: Color(0xFFF59E0B),
),
- ),
- title: Text(
- isRu ? 'Шпаргалка по поиску' : 'Search tips & operators',
- style: theme.textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.w700,
- ),
- ),
- initiallyExpanded: _showTips,
- onExpansionChanged: (exp) => setState(() => _showTips = exp),
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _TipRow(
- code: 'tag_a and tag_b',
- description: isRu
- ? 'Независимый опрос обоих тегов с чередованием постов в ленте.'
- : 'Interleaves results from both queries independently.',
- ),
- const SizedBox(height: 8),
- _TipRow(
- code: 'type:video / type:gif',
- description: isRu
- ? 'Показывает только видео или анимированные GIF.'
- : 'Filters results to only videos or animated GIFs.',
- ),
- const SizedBox(height: 8),
- _TipRow(
- code: 'rating:safe / rating:explicit',
- description: isRu
- ? 'Фильтр по возрастному рейтингу медиа.'
- : 'Filter by age rating classification.',
- ),
- const SizedBox(height: 8),
- _TipRow(
- code: 'score:>50 / score:>100',
- description: isRu
- ? 'Посты с оценкой пользователей выше указанной.'
- : 'Posts with community score greater than threshold.',
- ),
- const SizedBox(height: 8),
- _TipRow(
- code: 'provider:gelbooru',
- description: isRu
- ? 'Поиск исключительно в указанном источнике.'
- : 'Search specifically on the given booru provider.',
- ),
- ],
+ title: Text(
+ isRu ? 'Шпаргалка по поиску' : 'Search tips & operators',
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w700,
+ letterSpacing: -0.2,
),
),
- ],
+ initiallyExpanded: _showTips,
+ onExpansionChanged: (exp) {
+ HapticFeedback.selectionClick();
+ setState(() => _showTips = exp);
+ },
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _TipRow(
+ code: 'tag_a and tag_b',
+ description: isRu
+ ? 'Независимый опрос обоих тегов с чередованием постов в ленте.'
+ : 'Interleaves results from both queries independently.',
+ ),
+ const SizedBox(height: 8),
+ _TipRow(
+ code: 'type:video / type:gif',
+ description: isRu
+ ? 'Показывает только видео или анимированные GIF.'
+ : 'Filters results to only videos or animated GIFs.',
+ ),
+ const SizedBox(height: 8),
+ _TipRow(
+ code: 'rating:safe / rating:explicit',
+ description: isRu
+ ? 'Фильтр по возрастному рейтингу медиа.'
+ : 'Filter by age rating classification.',
+ ),
+ const SizedBox(height: 8),
+ _TipRow(
+ code: 'score:>50 / score:>100',
+ description: isRu
+ ? 'Посты с оценкой пользователей выше указанной.'
+ : 'Posts with community score greater than threshold.',
+ ),
+ const SizedBox(height: 8),
+ _TipRow(
+ code: 'provider:gelbooru',
+ description: isRu
+ ? 'Поиск исключительно в указанном источнике.'
+ : 'Search specifically on the given booru provider.',
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
),
),
const SizedBox(height: 24),
@@ -358,93 +417,117 @@ class _SearchScreenState extends ConsumerState {
}
}
-class _SearchSectionCard extends StatelessWidget {
- const _SearchSectionCard({
- required this.icon,
- required this.accentColor,
- required this.title,
+class _SearchLiquidCard extends StatelessWidget {
+ const _SearchLiquidCard({
required this.child,
- this.badgeCount,
- this.trailing,
+ this.padding,
+ this.glowColor,
});
- final IconData icon;
- final Color accentColor;
- final String title;
final Widget child;
- final int? badgeCount;
- final Widget? trailing;
+ final EdgeInsetsGeometry? padding;
+ final Color? glowColor;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+ final accent = glowColor ?? theme.colorScheme.primary;
+
return Container(
decoration: BoxDecoration(
- color: theme.colorScheme.surfaceContainerLow,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(
- color: theme.colorScheme.outlineVariant.withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(24),
+ boxShadow: [
+ BoxShadow(
+ color: isDark
+ ? Colors.black.withValues(alpha: 0.35)
+ : accent.withValues(alpha: 0.08),
+ blurRadius: 18,
+ offset: const Offset(0, 6),
+ spreadRadius: -2,
+ ),
+ BoxShadow(
+ color: accent.withValues(alpha: isDark ? 0.06 : 0.03),
+ blurRadius: 10,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(24),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
+ child: Container(
+ padding: padding ?? const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(24),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: isDark
+ ? [
+ theme.colorScheme.surfaceContainerHigh
+ .withValues(alpha: 0.60),
+ theme.colorScheme.surfaceContainerLow
+ .withValues(alpha: 0.38),
+ ]
+ : [
+ Colors.white.withValues(alpha: 0.88),
+ Colors.white.withValues(alpha: 0.72),
+ ],
+ ),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.14)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.2,
+ ),
+ ),
+ child: child,
+ ),
),
),
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- color: accentColor.withValues(alpha: 0.14),
- borderRadius: BorderRadius.circular(10),
- ),
- child: Icon(icon, size: 18, color: accentColor),
- ),
- const SizedBox(width: 10),
- Expanded(
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Flexible(
- child: Text(
- title,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: theme.textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.w700,
- fontSize: 15,
- ),
- ),
- ),
- if (badgeCount != null && badgeCount! > 0) ...[
- const SizedBox(width: 8),
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 7, vertical: 2),
- decoration: BoxDecoration(
- color: accentColor.withValues(alpha: 0.15),
- borderRadius: BorderRadius.circular(10),
- ),
- child: Text(
- '$badgeCount',
- style: theme.textTheme.labelSmall?.copyWith(
- fontWeight: FontWeight.bold,
- color: accentColor,
- ),
- ),
- ),
- ],
- ],
- ),
- ),
- if (trailing != null) trailing!,
- ],
+ );
+ }
+}
+
+class _SearchIconBadge extends StatelessWidget {
+ const _SearchIconBadge({
+ required this.icon,
+ required this.color,
+ });
+
+ final IconData icon;
+ final Color color;
+
+ @override
+ Widget build(BuildContext context) {
+ const size = 34.0;
+ const iconSize = 18.0;
+ return Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ color,
+ color.withValues(alpha: 0.86),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(size * 0.28),
+ boxShadow: [
+ BoxShadow(
+ color: color.withValues(alpha: 0.35),
+ blurRadius: 7,
+ offset: const Offset(0, 2.5),
),
- const SizedBox(height: 14),
- child,
],
),
+ child: Center(
+ child: Icon(icon, size: iconSize, color: Colors.white),
+ ),
);
}
}
@@ -465,19 +548,33 @@ class _QuickOperatorChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
return Material(
- color: color.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(12),
+ color: Colors.transparent,
+ borderRadius: BorderRadius.circular(14),
child: InkWell(
- borderRadius: BorderRadius.circular(12),
- onTap: onTap,
+ borderRadius: BorderRadius.circular(14),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ onTap();
+ },
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
+ color: color.withValues(alpha: isDark ? 0.16 : 0.10),
+ borderRadius: BorderRadius.circular(14),
border: Border.all(
- color: color.withValues(alpha: 0.35),
+ color: color.withValues(alpha: isDark ? 0.40 : 0.32),
+ width: 1.1,
),
+ boxShadow: [
+ BoxShadow(
+ color: color.withValues(alpha: isDark ? 0.14 : 0.06),
+ blurRadius: 6,
+ offset: const Offset(0, 1.5),
+ ),
+ ],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -490,6 +587,7 @@ class _QuickOperatorChip extends StatelessWidget {
fontWeight: FontWeight.w700,
color: color,
fontFamily: 'monospace',
+ letterSpacing: -0.2,
),
),
],
@@ -516,44 +614,76 @@ class _CategoryTagRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- categoryName,
- style: theme.textTheme.labelLarge?.copyWith(
- fontWeight: FontWeight.w600,
- color: theme.colorScheme.onSurfaceVariant,
- ),
+ Row(
+ children: [
+ Container(
+ width: 7,
+ height: 7,
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: color.withValues(alpha: 0.6),
+ blurRadius: 4,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ categoryName,
+ style: theme.textTheme.labelLarge?.copyWith(
+ fontWeight: FontWeight.w700,
+ color: theme.colorScheme.onSurface,
+ letterSpacing: -0.2,
+ ),
+ ),
+ ],
),
- const SizedBox(height: 8),
+ const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final tag in tags)
Material(
- color: theme.colorScheme.surfaceContainer,
- borderRadius: BorderRadius.circular(12),
+ color: Colors.transparent,
+ borderRadius: BorderRadius.circular(14),
child: InkWell(
- borderRadius: BorderRadius.circular(12),
- onTap: () => onTap(tag),
+ borderRadius: BorderRadius.circular(14),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ onTap(tag);
+ },
child: Container(
padding: const EdgeInsets.symmetric(
- horizontal: 10, vertical: 5),
+ horizontal: 11, vertical: 6),
decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
+ color: isDark
+ ? theme.colorScheme.surfaceContainerHighest
+ .withValues(alpha: 0.45)
+ : Colors.white.withValues(alpha: 0.60),
+ borderRadius: BorderRadius.circular(14),
border: Border.all(
- color: theme.colorScheme.outlineVariant
- .withValues(alpha: 0.3),
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.10)
+ : theme.colorScheme.outlineVariant
+ .withValues(alpha: 0.35),
+ width: 1.0,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
- width: 6,
- height: 6,
+ width: 5,
+ height: 5,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
@@ -563,7 +693,8 @@ class _CategoryTagRow extends StatelessWidget {
Text(
tag,
style: theme.textTheme.bodySmall?.copyWith(
- fontWeight: FontWeight.w500,
+ fontWeight: FontWeight.w600,
+ letterSpacing: -0.1,
),
),
],
@@ -587,14 +718,23 @@ class _TipRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
- color: theme.colorScheme.surfaceContainerHighest,
- borderRadius: BorderRadius.circular(6),
+ color: isDark
+ ? theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.6)
+ : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.8),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.08)
+ : theme.colorScheme.outlineVariant.withValues(alpha: 0.25),
+ ),
),
child: Text(
code,
@@ -608,10 +748,14 @@ class _TipRow extends StatelessWidget {
),
const SizedBox(width: 10),
Expanded(
- child: Text(
- description,
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 2),
+ child: Text(
+ description,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ height: 1.35,
+ ),
),
),
),
diff --git a/lib/features/search/presentation/widgets/recent_searches.dart b/lib/features/search/presentation/widgets/recent_searches.dart
index 91f6302..abd98e0 100644
--- a/lib/features/search/presentation/widgets/recent_searches.dart
+++ b/lib/features/search/presentation/widgets/recent_searches.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import '../../../../backend/backend.dart';
@@ -95,20 +96,40 @@ class _RecentSearchesState extends State {
children: [
for (final item in visibleItems)
Material(
- color: theme.colorScheme.surfaceContainer,
+ color: Colors.transparent,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
- onTap: () => widget.onTap(item.query),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ widget.onTap(item.query);
+ },
child: Container(
padding:
const EdgeInsets.only(left: 10, right: 6, top: 6, bottom: 6),
decoration: BoxDecoration(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? theme.colorScheme.surfaceContainerHighest
+ .withValues(alpha: 0.45)
+ : Colors.white.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(16),
border: Border.all(
- color: theme.colorScheme.outlineVariant
- .withValues(alpha: 0.35),
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withValues(alpha: 0.12)
+ : theme.colorScheme.outlineVariant
+ .withValues(alpha: 0.35),
+ width: 1.0,
),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(
+ alpha: Theme.of(context).brightness == Brightness.dark
+ ? 0.15
+ : 0.04),
+ blurRadius: 6,
+ offset: const Offset(0, 1.5),
+ ),
+ ],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -140,7 +161,7 @@ class _RecentSearchesState extends State {
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
- color: theme.colorScheme.primary.withValues(alpha: 0.12),
+ color: theme.colorScheme.primary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(8),
),
child: Text(
@@ -161,7 +182,10 @@ class _RecentSearchesState extends State {
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: const CircleBorder(),
- onTap: () => widget.onDelete!(item.id),
+ onTap: () {
+ HapticFeedback.lightImpact();
+ widget.onDelete!(item.id);
+ },
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart
index f769148..9b88500 100644
--- a/lib/features/settings/presentation/settings_screen.dart
+++ b/lib/features/settings/presentation/settings_screen.dart
@@ -976,191 +976,275 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
final isRu = settings.languageCode == 'ru';
final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
final selectedSource = await showModalBottomSheet(
context: context,
+ useRootNavigator: true,
+ isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
- return Container(
- decoration: BoxDecoration(
- color: theme.colorScheme.surface,
- borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
- ),
- padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Center(
- child: Container(
- width: 36,
- height: 4,
- margin: const EdgeInsets.only(bottom: 16),
- decoration: BoxDecoration(
- color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
- borderRadius: BorderRadius.circular(2),
+ return ClipRRect(
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24),
+ child: Container(
+ decoration: BoxDecoration(
+ color: isDark
+ ? theme.colorScheme.surface.withValues(alpha: 0.88)
+ : Colors.white.withValues(alpha: 0.94),
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
+ border: Border(
+ top: BorderSide(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.18)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.2,
),
),
),
- Text(
- isRu ? 'Источник обновлений' : 'Update Source',
- style: theme.textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 6),
- Text(
- isRu
- ? 'Выберите сервер для загрузки обновлений'
- : 'Choose a server to download updates from',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 20),
-
- // Option 1: GitHub (Recommended for speed)
- Material(
- color: theme.colorScheme.primaryContainer.withValues(alpha: 0.35),
- borderRadius: BorderRadius.circular(16),
- child: InkWell(
- borderRadius: BorderRadius.circular(16),
- onTap: () => Navigator.pop(sheetContext, UpdateSource.github),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Container(
- width: 44,
- height: 44,
+ child: SafeArea(
+ top: false,
+ child: Padding(
+ padding: EdgeInsets.fromLTRB(
+ 20,
+ 14,
+ 20,
+ 24 + MediaQuery.paddingOf(sheetContext).bottom,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Center(
+ child: Container(
+ width: 38,
+ height: 4.5,
+ margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
- color: theme.colorScheme.primary.withValues(alpha: 0.15),
- borderRadius: BorderRadius.circular(12),
- ),
- child: Icon(
- Icons.bolt_rounded,
- color: theme.colorScheme.primary,
- size: 24,
+ color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.35),
+ borderRadius: BorderRadius.circular(3),
),
),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Text(
- 'GitHub',
- style: theme.textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(width: 8),
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 6,
- vertical: 2,
- ),
- decoration: BoxDecoration(
- color: Colors.green.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- isRu ? 'Рекомендуется для скорости' : 'Recommended for speed',
- style: const TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.bold,
- color: Colors.green,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 3),
- Text(
- isRu
- ? 'Прямая быстрая загрузка без ограничений'
- : 'Direct fast download without limits',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- ],
- ),
+ ),
+ Text(
+ isRu ? 'Источник обновлений' : 'Update Source',
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w800,
+ letterSpacing: -0.3,
),
- Icon(
- Icons.arrow_forward_ios_rounded,
- size: 14,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ isRu
+ ? 'Выберите сервер для быстрой загрузки новой версии'
+ : 'Choose a server to download updates from',
+ style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
- ],
- ),
- ),
- ),
- ),
- const SizedBox(height: 12),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 20),
- // Option 2: Gitea (Muted / neutral grey)
- Material(
- color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.25),
- borderRadius: BorderRadius.circular(16),
- child: InkWell(
- borderRadius: BorderRadius.circular(16),
- onTap: () => Navigator.pop(sheetContext, UpdateSource.gitea),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Container(
- width: 44,
- height: 44,
- decoration: BoxDecoration(
- color: theme.colorScheme.onSurface.withValues(alpha: 0.08),
- borderRadius: BorderRadius.circular(12),
- ),
- child: Icon(
- Icons.dns_rounded,
- color: theme.colorScheme.onSurfaceVariant,
- size: 22,
- ),
- ),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Gitea',
- style: theme.textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.w600,
+ // Option 1: GitHub (Recommended for speed)
+ Material(
+ color: isDark
+ ? theme.colorScheme.primaryContainer.withValues(alpha: 0.35)
+ : theme.colorScheme.primary.withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(18),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(18),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ Navigator.pop(sheetContext, UpdateSource.github);
+ },
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(18),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.15)
+ : theme.colorScheme.primary.withValues(alpha: 0.35),
+ width: 1.2,
+ ),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFF6366F1), Color(0xFF4F46E5)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(13),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF6366F1).withValues(alpha: 0.40),
+ blurRadius: 10,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: const Center(
+ child: Icon(
+ Icons.bolt_rounded,
+ color: Colors.white,
+ size: 24,
+ ),
+ ),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ 'GitHub',
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 7,
+ vertical: 2.5,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.20),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ isRu ? 'Рекомендуется' : 'Recommended',
+ style: const TextStyle(
+ fontSize: 10.5,
+ fontWeight: FontWeight.w700,
+ color: Colors.green,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 3),
+ Text(
+ isRu
+ ? 'Прямая быстрая загрузка релизов без задержек'
+ : 'Direct fast download without limits',
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Icon(
+ Icons.arrow_forward_ios_rounded,
+ size: 15,
color: theme.colorScheme.onSurfaceVariant,
),
- ),
- const SizedBox(height: 3),
- Text(
- isRu
- ? 'Сервер релизов'
- : 'Release server',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
- ),
- ),
- ],
+ ],
+ ),
),
),
- Icon(
- Icons.arrow_forward_ios_rounded,
- size: 14,
- color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
+ ),
+ const SizedBox(height: 12),
+
+ // Option 2: Gitea
+ Material(
+ color: isDark
+ ? theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35)
+ : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.40),
+ borderRadius: BorderRadius.circular(18),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(18),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ Navigator.pop(sheetContext, UpdateSource.gitea);
+ },
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(18),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.12)
+ : theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
+ width: 1.2,
+ ),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFF0EA5E9), Color(0xFF0284C7)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(13),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF0EA5E9).withValues(alpha: 0.35),
+ blurRadius: 10,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: const Center(
+ child: Icon(
+ Icons.dns_rounded,
+ color: Colors.white,
+ size: 22,
+ ),
+ ),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Gitea (Synapse)',
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 3),
+ Text(
+ isRu
+ ? 'Альтернативный независимый сервер релизов'
+ : 'Alternative independent release mirror',
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Icon(
+ Icons.arrow_forward_ios_rounded,
+ size: 15,
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ],
+ ),
+ ),
),
- ],
- ),
+ ),
+ ],
),
),
),
- ],
+ ),
),
);
},
diff --git a/lib/features/viewed/presentation/viewed_screen.dart b/lib/features/viewed/presentation/viewed_screen.dart
index 0e52cfc..55ddc00 100644
--- a/lib/features/viewed/presentation/viewed_screen.dart
+++ b/lib/features/viewed/presentation/viewed_screen.dart
@@ -1,5 +1,8 @@
+import 'dart:ui';
+
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -156,28 +159,56 @@ class _ViewedScreenState extends ConsumerState {
);
}
+ final bottomInset = MediaQuery.paddingOf(context).bottom;
+
return RefreshIndicator(
onRefresh: () =>
ref.read(viewedControllerProvider.notifier).refresh(),
child: ListView(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ padding: EdgeInsets.fromLTRB(16, 8, 16, 140 + bottomInset),
children: [
for (final group in groups) ...[
Padding(
- padding: const EdgeInsets.fromLTRB(4, 16, 4, 10),
+ padding: const EdgeInsets.fromLTRB(2, 16, 2, 10),
child: Row(
children: [
- Icon(
- Icons.calendar_today_rounded,
- size: 16,
- color: Theme.of(context).colorScheme.primary,
+ Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.primary.withValues(alpha: 0.82),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(9),
+ boxShadow: [
+ BoxShadow(
+ color: Theme.of(context)
+ .colorScheme
+ .primary
+ .withValues(alpha: 0.32),
+ blurRadius: 6,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: const Center(
+ child: Icon(
+ Icons.calendar_today_rounded,
+ size: 14,
+ color: Colors.white,
+ ),
+ ),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 10),
Text(
_localizedGroupLabel(group.label, isRu),
style:
Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
+ letterSpacing: -0.2,
),
),
const SizedBox(width: 8),
@@ -187,10 +218,24 @@ class _ViewedScreenState extends ConsumerState {
vertical: 2,
),
decoration: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .surfaceContainerHighest,
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Theme.of(context)
+ .colorScheme
+ .surfaceContainerHighest
+ .withValues(alpha: 0.5)
+ : Theme.of(context)
+ .colorScheme
+ .primary
+ .withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
+ border: Border.all(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withValues(alpha: 0.08)
+ : Theme.of(context)
+ .colorScheme
+ .primary
+ .withValues(alpha: 0.20),
+ ),
),
child: Text(
'${group.items.length}',
@@ -198,7 +243,10 @@ class _ViewedScreenState extends ConsumerState {
.textTheme
.labelSmall
?.copyWith(
- fontWeight: FontWeight.w600,
+ fontWeight: FontWeight.w700,
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Theme.of(context).colorScheme.onSurfaceVariant
+ : Theme.of(context).colorScheme.primary,
),
),
),
@@ -299,184 +347,251 @@ class _ViewedPostTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
+ final isDark = Theme.of(context).brightness == Brightness.dark;
final image = MediaUrlSelector.preview(post).firstOrNull;
final timeStr =
'${viewedAt.hour.toString().padLeft(2, '0')}:${viewedAt.minute.toString().padLeft(2, '0')}';
- return Card(
- elevation: 0,
- color: scheme.surfaceContainerLow,
- margin: const EdgeInsets.only(bottom: 8),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- side: BorderSide(
- color: scheme.outlineVariant.withValues(alpha: 0.3),
- ),
+ return Container(
+ margin: const EdgeInsets.only(bottom: 10),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: isDark ? 0.28 : 0.05),
+ blurRadius: 14,
+ offset: const Offset(0, 4),
+ spreadRadius: -2,
+ ),
+ ],
),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: onOpen,
- child: Padding(
- padding: const EdgeInsets.all(10),
- child: Row(
- children: [
- // Squircle Thumbnail
- ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: SizedBox(
- width: 68,
- height: 68,
- child: Stack(
- fit: StackFit.expand,
- children: [
- image == null
- ? Container(
- color: scheme.surfaceContainerHighest,
- child: const Icon(Icons.image_not_supported_rounded),
- )
- : CachedNetworkImage(
- imageUrl: image,
- fit: BoxFit.cover,
- placeholder: (_, __) => Container(
- color: scheme.surfaceContainerHighest,
- ),
- errorWidget: (_, __, ___) => Container(
- color: scheme.surfaceContainerHighest,
- child: const Icon(
- Icons.broken_image_rounded,
- size: 20,
- ),
- ),
- ),
- if (_isVideo)
- Positioned(
- right: 4,
- bottom: 4,
- child: Container(
- padding: const EdgeInsets.all(3),
- decoration: BoxDecoration(
- color: Colors.black.withValues(alpha: 0.65),
- borderRadius: BorderRadius.circular(4),
- ),
- child: const Icon(
- Icons.play_arrow_rounded,
- size: 14,
- color: Colors.white,
- ),
- ),
- ),
- ],
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(20),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ onOpen();
+ },
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: isDark
+ ? [
+ scheme.surfaceContainerHigh
+ .withValues(alpha: 0.58),
+ scheme.surfaceContainerLow
+ .withValues(alpha: 0.36),
+ ]
+ : [
+ Colors.white.withValues(alpha: 0.88),
+ Colors.white.withValues(alpha: 0.72),
+ ],
+ ),
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.13)
+ : Colors.white.withValues(alpha: 0.85),
+ width: 1.1,
),
),
- ),
- const SizedBox(width: 12),
- // Post details
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ child: Row(
children: [
- Row(
- children: [
- Flexible(
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 6,
- vertical: 2,
- ),
- decoration: BoxDecoration(
- color:
- scheme.primaryContainer.withValues(alpha: 0.5),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- post.providerName,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: Theme.of(context)
- .textTheme
- .labelSmall
- ?.copyWith(
- fontWeight: FontWeight.w600,
- color: scheme.onPrimaryContainer,
+ // Squircle Thumbnail
+ ClipRRect(
+ borderRadius: BorderRadius.circular(13),
+ child: Container(
+ width: 68,
+ height: 68,
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: isDark
+ ? Colors.white.withValues(alpha: 0.10)
+ : scheme.outlineVariant.withValues(alpha: 0.30),
+ ),
+ borderRadius: BorderRadius.circular(13),
+ ),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ image == null
+ ? Container(
+ color: scheme.surfaceContainerHighest,
+ child: const Icon(
+ Icons.image_not_supported_rounded),
+ )
+ : CachedNetworkImage(
+ imageUrl: image,
+ fit: BoxFit.cover,
+ placeholder: (_, __) => Container(
+ color: scheme.surfaceContainerHighest,
+ ),
+ errorWidget: (_, __, ___) => Container(
+ color: scheme.surfaceContainerHighest,
+ child: const Icon(
+ Icons.broken_image_rounded,
+ size: 20,
+ ),
+ ),
),
- ),
- ),
- ),
- const SizedBox(width: 6),
- Text(
- timeStr,
- style: Theme.of(context)
- .textTheme
- .labelSmall
- ?.copyWith(
- color: scheme.onSurfaceVariant,
+ if (_isVideo)
+ Positioned(
+ right: 4,
+ bottom: 4,
+ child: Container(
+ padding: const EdgeInsets.all(3),
+ decoration: BoxDecoration(
+ color: Colors.black.withValues(alpha: 0.65),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: const Icon(
+ Icons.play_arrow_rounded,
+ size: 14,
+ color: Colors.white,
+ ),
+ ),
),
+ ],
),
- ],
+ ),
),
- const SizedBox(height: 4),
- Text(
- post.tags.isEmpty ? '#${post.id}' : post.tags.take(4).join(' '),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- fontWeight: FontWeight.w500,
- ),
- ),
- if (post.score != 0) ...[
- const SizedBox(height: 2),
- Row(
+ const SizedBox(width: 12),
+ // Post details
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Icon(
- Icons.thumb_up_rounded,
- size: 12,
- color: scheme.outline,
+ Row(
+ children: [
+ Flexible(
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 7,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: scheme.primary
+ .withValues(alpha: isDark ? 0.20 : 0.12),
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: scheme.primary.withValues(
+ alpha: isDark ? 0.30 : 0.20),
+ ),
+ ),
+ child: Text(
+ post.providerName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall
+ ?.copyWith(
+ fontWeight: FontWeight.w700,
+ color: scheme.primary,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ timeStr,
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall
+ ?.copyWith(
+ color: scheme.onSurfaceVariant,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
),
- const SizedBox(width: 4),
+ const SizedBox(height: 4),
Text(
- '${post.score}',
- style: Theme.of(context)
- .textTheme
- .labelSmall
- ?.copyWith(
- color: scheme.outline,
+ post.tags.isEmpty ? '#${post.id}' : post.tags.take(4).join(' '),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ letterSpacing: -0.1,
),
),
+ if (post.score != 0) ...[
+ const SizedBox(height: 2),
+ Row(
+ children: [
+ Icon(
+ Icons.thumb_up_rounded,
+ size: 12,
+ color: scheme.outline,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '${post.score}',
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall
+ ?.copyWith(
+ color: scheme.outline,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ],
],
),
- ],
+ ),
+ // Action buttons
+ IconButton(
+ visualDensity: VisualDensity.compact,
+ padding: EdgeInsets.zero,
+ constraints:
+ const BoxConstraints(minWidth: 36, minHeight: 36),
+ tooltip: isFavorite
+ ? (isRu ? 'В избранном' : 'In favorites')
+ : (isRu ? 'В избранное' : 'Add to favorites'),
+ onPressed: () {
+ HapticFeedback.selectionClick();
+ onFavorite();
+ },
+ icon: Icon(
+ isFavorite
+ ? Icons.favorite_rounded
+ : Icons.favorite_border_rounded,
+ color: isFavorite
+ ? Colors.redAccent
+ : scheme.onSurfaceVariant,
+ ),
+ ),
+ IconButton(
+ visualDensity: VisualDensity.compact,
+ padding: EdgeInsets.zero,
+ constraints:
+ const BoxConstraints(minWidth: 36, minHeight: 36),
+ tooltip:
+ isRu ? 'Удалить из истории' : 'Delete from history',
+ onPressed: () {
+ HapticFeedback.lightImpact();
+ onDelete();
+ },
+ icon: Icon(
+ Icons.close_rounded,
+ size: 20,
+ color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
+ ),
+ ),
],
),
),
- // Action buttons
- IconButton(
- visualDensity: VisualDensity.compact,
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
- tooltip: isFavorite
- ? (isRu ? 'В избранном' : 'In favorites')
- : (isRu ? 'В избранное' : 'Add to favorites'),
- onPressed: onFavorite,
- icon: Icon(
- isFavorite
- ? Icons.favorite_rounded
- : Icons.favorite_border_rounded,
- color: isFavorite ? Colors.redAccent : scheme.onSurfaceVariant,
- ),
- ),
- IconButton(
- visualDensity: VisualDensity.compact,
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
- tooltip: isRu ? 'Удалить из истории' : 'Delete from history',
- onPressed: onDelete,
- icon: Icon(
- Icons.close_rounded,
- size: 20,
- color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
- ),
- ),
- ],
+ ),
),
),
),
diff --git a/lib/shared/widgets/post_masonry_grid.dart b/lib/shared/widgets/post_masonry_grid.dart
index 81ff957..3f140e3 100644
--- a/lib/shared/widgets/post_masonry_grid.dart
+++ b/lib/shared/widgets/post_masonry_grid.dart
@@ -28,6 +28,7 @@ class PostMasonryGrid extends StatelessWidget {
this.loading = false,
this.controller,
this.gridMode = 'masonry',
+ this.padding,
super.key,
});
@@ -51,11 +52,19 @@ class PostMasonryGrid extends StatelessWidget {
final bool loading;
final ScrollController? controller;
final String gridMode;
+ final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
final mobile = Responsive.isMobile(context);
- final pad = EdgeInsets.all(mobile ? 8 : 16);
+ final bottomInset = MediaQuery.paddingOf(context).bottom;
+ final pad = padding ??
+ EdgeInsets.fromLTRB(
+ mobile ? 8 : 16,
+ mobile ? 8 : 16,
+ mobile ? 8 : 16,
+ (mobile ? 8 : 16) + (mobile ? (120.0 + bottomInset) : 0.0),
+ );
final spacing = mobile ? 8.0 : 12.0;
Widget buildCard(Post post) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 600608d..23be272 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none"
-version: 3.0.9+60
+version: 3.1.0+61
environment:
sdk: ">=3.3.0 <4.0.0"