feat: Feed 2.0 redesign (modern squircle cards, compact toolbar, sleek glass badges) v2.0.12

This commit is contained in:
2026-09-03 23:04:32 +03:00
parent e8448eb748
commit 4bbe04dfd8
9 changed files with 381 additions and 232 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/Version-2.0.11-blueviolet?style=for-the-badge" alt="Version 2.0.11" /> <img src="https://img.shields.io/badge/Version-2.0.12-blueviolet?style=for-the-badge" alt="Version 2.0.12" />
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" /> <img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" /> <img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" />
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" /> <img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '2.0.11'; const appDisplayVersion = '2.0.12';
const appBuildNumber = 33; const appBuildNumber = 34;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+13
View File
@@ -1,4 +1,17 @@
const lunarisChangelog = [ const lunarisChangelog = [
LunarisChange(
version: '2.0.12',
title: 'Feed 2.0: Pinterest Modern Redesign & Sleek Cards',
bullets: [
'Feed 2.0 Design: modern squircle post cards with 14dp border radius, subtle micro-borders, and depth shadows.',
'Unified Media Badges: sleek glassmorphism capsules for booru providers, ratings, video/GIF indicators, and status badges.',
'Cinematic Bottom Gradient: gentle shading on post cards for clean presentation without obscuring art.',
'Tactile Floating Favorite Button: circular glassmorphism heart button with neon glow and instant feedback.',
'Compact 2-Tier Feed Toolbar: 22dp search capsule, filter count badge, and single unified horizontal scroll for sources and time periods.',
'Quick Filter & Actions: direct access to random post, selection mode, rating picker, and active filter resets.',
'Polished Search Presets: stylish bookmark capsules for quick access to saved search queries.',
],
),
LunarisChange( LunarisChange(
version: '2.0.11', version: '2.0.11',
title: 'Mobile Video Player Redesign & Format Recognition Fix', title: 'Mobile Video Player Redesign & Format Recognition Fix',
@@ -802,15 +802,22 @@ class _SearchPresetsBar extends StatelessWidget {
); );
}, },
child: FilterChip( child: FilterChip(
avatar: Icon(
active ? Icons.bookmark_rounded : Icons.bookmark_border_rounded,
size: 15,
color: active ? scheme.primary : scheme.onSurfaceVariant,
),
label: Text(name), label: Text(name),
selected: active, selected: active,
onSelected: (_) => onApply(raw), onSelected: (_) => onApply(raw),
selectedColor: scheme.primaryContainer, selectedColor: scheme.primaryContainer.withValues(alpha: 0.8),
checkmarkColor: scheme.onPrimaryContainer, checkmarkColor: scheme.onPrimaryContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
labelStyle: TextStyle( labelStyle: TextStyle(
color: active ? scheme.onPrimaryContainer : scheme.onSurface, color: active ? scheme.onPrimaryContainer : scheme.onSurface,
fontWeight: fontWeight: active ? FontWeight.w700 : FontWeight.w500,
active ? FontWeight.w600 : FontWeight.normal,
), ),
), ),
); );
@@ -77,6 +77,8 @@ class _FeedToolbarState extends State<FeedToolbar> {
} }
Widget _buildDesktop(BuildContext context) { Widget _buildDesktop(BuildContext context) {
final hasActiveFilters = widget.selectedProviderIds.isNotEmpty || widget.rating != null;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Column( child: Column(
@@ -100,34 +102,42 @@ class _FeedToolbarState extends State<FeedToolbar> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton.filledTonal( IconButton.filledTonal(
tooltip: widget.selectionMode ? 'Exit select' : 'Select posts', tooltip: widget.selectionMode ? 'Выйти из выбора' : 'Выбрать посты',
onPressed: widget.onToggleSelectionMode, onPressed: widget.onToggleSelectionMode,
icon: Icon(widget.selectionMode icon: Icon(widget.selectionMode
? Icons.check_box_rounded ? Icons.check_box_rounded
: Icons.check_box_outline_blank_rounded), : Icons.check_box_outline_blank_rounded),
), ),
IconButton.filledTonal( const SizedBox(width: 4),
tooltip: 'Providers', Badge(
onPressed: widget.onProviderFilter, isLabelVisible: hasActiveFilters,
icon: const Icon(Icons.hub_rounded), child: IconButton.filledTonal(
tooltip: 'Провайдеры',
onPressed: widget.onProviderFilter,
icon: const Icon(Icons.hub_rounded),
),
), ),
const SizedBox(width: 4),
IconButton.filledTonal( IconButton.filledTonal(
tooltip: 'Rating', tooltip: 'Рейтинг',
onPressed: widget.onRatingFilter, onPressed: widget.onRatingFilter,
icon: const Icon(Icons.tune_rounded), icon: const Icon(Icons.tune_rounded),
), ),
const SizedBox(width: 4),
IconButton( IconButton(
tooltip: 'Clear filters', tooltip: 'Очистить фильтры',
onPressed: widget.onClearFilters, onPressed: widget.onClearFilters,
icon: const Icon(Icons.filter_alt_off_rounded), icon: const Icon(Icons.filter_alt_off_rounded),
), ),
const SizedBox(width: 4),
IconButton( IconButton(
tooltip: 'Refresh', tooltip: 'Обновить',
onPressed: widget.onRefresh, onPressed: widget.onRefresh,
icon: const Icon(Icons.refresh_rounded), icon: const Icon(Icons.refresh_rounded),
), ),
const SizedBox(width: 4),
IconButton.filledTonal( IconButton.filledTonal(
tooltip: 'Random post', tooltip: 'Случайный пост',
onPressed: widget.onRandom, onPressed: widget.onRandom,
icon: const Icon(Icons.casino_rounded), icon: const Icon(Icons.casino_rounded),
), ),
@@ -154,7 +164,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
if (widget.providers.isNotEmpty) ...[ if (widget.providers.isNotEmpty) ...[
FilterChip( FilterChip(
selected: widget.selectedProviderIds.isEmpty, selected: widget.selectedProviderIds.isEmpty,
label: const Text('All providers'), label: const Text('Все источники'),
onSelected: (_) => widget.onQuickProviderToggle('__all__'), onSelected: (_) => widget.onQuickProviderToggle('__all__'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -162,7 +172,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
FilterChip( FilterChip(
selected: selected:
widget.selectedProviderIds.contains(provider.id), widget.selectedProviderIds.contains(provider.id),
avatar: const Icon(Icons.hub_rounded, size: 16), avatar: const Icon(Icons.hub_rounded, size: 15),
label: Text(provider.name), label: Text(provider.name),
onSelected: (_) => onSelected: (_) =>
widget.onQuickProviderToggle(provider.id), widget.onQuickProviderToggle(provider.id),
@@ -188,25 +198,62 @@ class _FeedToolbarState extends State<FeedToolbar> {
} }
Widget _buildMobile(BuildContext context) { Widget _buildMobile(BuildContext context) {
final hasActiveFilters = widget.selectedProviderIds.isNotEmpty || widget.rating != null;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), padding: const EdgeInsets.fromLTRB(10, 8, 10, 6),
child: Column( child: Column(
children: [ children: [
TagInputSearchBar( Row(
initialValue: _query, children: [
suggestions: widget.tagSuggestions, Expanded(
onChanged: (value) { child: TagInputSearchBar(
_query = value; initialValue: _query,
widget.onSearchChanged(value); suggestions: widget.tagSuggestions,
}, onChanged: (value) {
onSubmitted: widget.onSearch, _query = value;
onSuggestionApplied: (value) { widget.onSearchChanged(value);
_query = value; },
widget.onSuggestionTap(value); onSubmitted: widget.onSearch,
}, onSuggestionApplied: (value) {
_query = value;
widget.onSuggestionTap(value);
},
),
),
const SizedBox(width: 6),
Badge(
isLabelVisible: hasActiveFilters,
child: IconButton.filledTonal(
visualDensity: VisualDensity.compact,
tooltip: 'Фильтры',
onPressed: widget.onProviderFilter,
icon: const Icon(Icons.tune_rounded, size: 20),
),
),
const SizedBox(width: 4),
IconButton.filledTonal(
visualDensity: VisualDensity.compact,
tooltip: 'Случайный пост',
onPressed: widget.onRandom,
icon: const Icon(Icons.casino_rounded, size: 20),
),
const SizedBox(width: 4),
IconButton.filledTonal(
visualDensity: VisualDensity.compact,
tooltip: widget.selectionMode ? 'Выйти из выбора' : 'Выбрать посты',
onPressed: widget.onToggleSelectionMode,
icon: Icon(
widget.selectionMode
? Icons.check_box_rounded
: Icons.check_box_outline_blank_rounded,
size: 20,
),
),
],
), ),
const SizedBox(height: 8),
if (widget.providerStatusMessage != null) ...[ if (widget.providerStatusMessage != null) ...[
const SizedBox(height: 6),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: InputChip( child: InputChip(
@@ -214,55 +261,50 @@ class _FeedToolbarState extends State<FeedToolbar> {
label: Text(widget.providerStatusMessage!), label: Text(widget.providerStatusMessage!),
), ),
), ),
const SizedBox(height: 6),
], ],
const SizedBox(height: 7),
_HorizontalWheelScroller( _HorizontalWheelScroller(
height: 40, height: 38,
builder: (controller) => ListView( builder: (controller) => ListView(
controller: controller, controller: controller,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
primary: false, primary: false,
children: [ children: [
IconButton.filledTonal( if (hasActiveFilters || widget.selectedTags.isNotEmpty) ...[
tooltip: ActionChip(
widget.selectionMode ? 'Exit select' : 'Select posts', visualDensity: VisualDensity.compact,
onPressed: widget.onToggleSelectionMode, avatar: const Icon(Icons.filter_alt_off_rounded, size: 15),
icon: Icon(widget.selectionMode label: const Text('Сбросить'),
? Icons.check_box_rounded onPressed: widget.onClearFilters,
: Icons.check_box_outline_blank_rounded), ),
), const SizedBox(width: 6),
const SizedBox(width: 6), ],
IconButton.filledTonal( ActionChip(
tooltip: 'Rating', visualDensity: VisualDensity.compact,
onPressed: widget.onRatingFilter, avatar: const Icon(Icons.refresh_rounded, size: 15),
icon: const Icon(Icons.tune_rounded), label: const Text('Обновить'),
),
const SizedBox(width: 6),
IconButton(
tooltip: 'Clear filters',
onPressed: widget.onClearFilters,
icon: const Icon(Icons.filter_alt_off_rounded),
),
IconButton(
tooltip: 'Refresh',
onPressed: widget.onRefresh, onPressed: widget.onRefresh,
icon: const Icon(Icons.refresh_rounded),
), ),
IconButton.filledTonal( const SizedBox(width: 6),
tooltip: 'Random post', FilterChip(
onPressed: widget.onRandom, visualDensity: VisualDensity.compact,
icon: const Icon(Icons.casino_rounded), selected: widget.rating != null,
avatar: const Icon(Icons.shield_outlined, size: 15),
label: Text(widget.rating ?? 'Рейтинг'),
onSelected: (_) => widget.onRatingFilter(),
), ),
const SizedBox(width: 8), const SizedBox(width: 6),
if (widget.providers.isNotEmpty) ...[ if (widget.providers.isNotEmpty) ...[
FilterChip( FilterChip(
visualDensity: VisualDensity.compact,
selected: widget.selectedProviderIds.isEmpty, selected: widget.selectedProviderIds.isEmpty,
label: const Text('All'), label: const Text('Все'),
onSelected: (_) => widget.onQuickProviderToggle('__all__'), onSelected: (_) => widget.onQuickProviderToggle('__all__'),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
for (final provider in widget.providers) ...[ for (final provider in widget.providers) ...[
FilterChip( FilterChip(
visualDensity: VisualDensity.compact,
selected: selected:
widget.selectedProviderIds.contains(provider.id), widget.selectedProviderIds.contains(provider.id),
label: Text(provider.name), label: Text(provider.name),
@@ -272,19 +314,9 @@ class _FeedToolbarState extends State<FeedToolbar> {
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
], ],
],
),
),
const SizedBox(height: 6),
_HorizontalWheelScroller(
height: 36,
builder: (controller) => ListView(
controller: controller,
scrollDirection: Axis.horizontal,
primary: false,
children: [
for (final period in TopPeriodFilter.values) ...[ for (final period in TopPeriodFilter.values) ...[
ChoiceChip( ChoiceChip(
visualDensity: VisualDensity.compact,
selected: widget.topPeriodFilter == period, selected: widget.topPeriodFilter == period,
label: Text(period.label), label: Text(period.label),
onSelected: (_) => widget.onTopPeriodChanged(period), onSelected: (_) => widget.onTopPeriodChanged(period),
+5 -4
View File
@@ -121,15 +121,16 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).inputDecorationTheme.fillColor ?? color: Theme.of(context).inputDecorationTheme.fillColor ??
scheme.surfaceContainerHighest.withValues(alpha: 0.38), scheme.surfaceContainerHighest.withValues(alpha: 0.38),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(22),
border: Border.all( border: Border.all(
color: _focusNode.hasFocus color: _focusNode.hasFocus
? scheme.primary.withValues(alpha: 0.45) ? scheme.primary.withValues(alpha: 0.6)
: Colors.transparent, : Colors.white.withValues(alpha: 0.08),
width: 1.0,
), ),
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(22),
onTap: _focusNode.requestFocus, onTap: _focusNode.requestFocus,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
+249 -153
View File
@@ -177,114 +177,159 @@ class _PostCardState extends ConsumerState<PostCard>
], ],
); );
}, },
child: ClipRRect( child: Container(
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
child: Stack( borderRadius: BorderRadius.circular(14),
children: [ border: Border.all(
AspectRatio( color: Colors.white.withValues(alpha: 0.09),
aspectRatio: aspect.clamp(0.28, 2.2), width: 0.8,
child: BlurContent( ),
enabled: widget.blurExplicit && sensitive, boxShadow: [
child: LayoutBuilder( BoxShadow(
builder: (context, constraints) { color: Colors.black.withValues(alpha: 0.18),
final dpr = MediaQuery.devicePixelRatioOf(context); blurRadius: 8,
final cacheWidth = offset: const Offset(0, 2),
(constraints.maxWidth * dpr).round().clamp(320, 1800); ),
final headers = _headersFor(post); ],
final videoUrls = MediaUrlSelector.video(post); ),
if (!mobile && child: ClipRRect(
_hovered && borderRadius: BorderRadius.circular(13.2),
_resolvedPost != null && child: Stack(
MediaUrlSelector.isVideo(post) && children: [
videoUrls.isNotEmpty) { AspectRatio(
return _FeedVideoPreview( aspectRatio: aspect.clamp(0.28, 2.2),
videoUrl: videoUrls.first, child: BlurContent(
headers: headers, enabled: widget.blurExplicit && sensitive,
fallbackImageUrl: imageUrl, child: LayoutBuilder(
cacheWidth: cacheWidth, builder: (context, constraints) {
); final dpr = MediaQuery.devicePixelRatioOf(context);
} final cacheWidth =
return CachedNetworkImage( (constraints.maxWidth * dpr).round().clamp(320, 1800);
imageUrl: imageUrl, final headers = _headersFor(post);
httpHeaders: headers, final videoUrls = MediaUrlSelector.video(post);
memCacheWidth: cacheWidth, if (!mobile &&
maxWidthDiskCache: cacheWidth, _hovered &&
fit: BoxFit.cover, _resolvedPost != null &&
placeholder: (context, url) => const LoadingSkeleton(), MediaUrlSelector.isVideo(post) &&
errorWidget: (context, url, error) => ColoredBox( videoUrls.isNotEmpty) {
color: Theme.of(context) return _FeedVideoPreview(
.colorScheme videoUrl: videoUrls.first,
.surfaceContainerHighest, headers: headers,
child: const Center( fallbackImageUrl: imageUrl,
child: Icon(Icons.broken_image_rounded), cacheWidth: cacheWidth,
);
}
return CachedNetworkImage(
imageUrl: imageUrl,
httpHeaders: headers,
memCacheWidth: cacheWidth,
maxWidthDiskCache: cacheWidth,
fit: BoxFit.cover,
placeholder: (context, url) => const LoadingSkeleton(),
errorWidget: (context, url, error) => ColoredBox(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.broken_image_rounded),
),
), ),
), );
); },
}, ),
), ),
), ),
), // Bottom cinematic gradient overlay for badges and favorite
if (widget.showBadges) ...[
Positioned( Positioned(
left: 8, left: 0,
top: 8, right: 0,
child: _ProviderBadge(name: post.providerName), bottom: 0,
height: 48,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.65),
Colors.transparent,
],
),
),
),
),
), ),
Positioned( if (widget.showBadges) ...[
right: 8, Positioned(
top: 8, left: 8,
child: RatingBadge(rating: post.rating), top: 8,
), child: _ProviderBadge(name: post.providerName),
Positioned( ),
left: 8, Positioned(
bottom: 8, right: 8,
child: _MediaBadge(fileType: post.fileType), top: 8,
), child: Row(
if (widget.isViewed) mainAxisSize: MainAxisSize.min,
const Positioned( children: [
if (post.fileType.toLowerCase().contains('video') ||
post.fileType.toLowerCase().contains('webm') ||
post.fileType.toLowerCase().contains('mp4') ||
post.fileType.toLowerCase().contains('gif')) ...[
_MediaBadge(fileType: post.fileType),
const SizedBox(width: 4),
],
RatingBadge(rating: post.rating),
],
),
),
Positioned(
left: 8,
bottom: 8,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.isViewed) ...[
const _SeenBadge(),
const SizedBox(width: 4),
],
if (widget.isDownloaded) const _DownloadedBadge(),
],
),
),
],
if (widget.selected)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.24),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
borderRadius: BorderRadius.circular(13),
),
child: const Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.check_circle_rounded),
),
),
),
),
if (mobile)
Positioned(
right: 8, right: 8,
bottom: 8, bottom: 8,
child: _SeenBadge(), child: _FavoriteButton(
), isFavorite: widget.isFavorite,
if (widget.isDownloaded) onPressed: widget.onFavorite,
Positioned(
right: widget.isViewed ? 72 : 8,
bottom: 8,
child: const _DownloadedBadge(),
),
],
if (widget.selected)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.24),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
borderRadius: BorderRadius.circular(8),
),
child: const Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.check_circle_rounded),
),
), ),
), ),
),
if (mobile)
Positioned(
right: 8,
bottom: 8,
child: _FavoriteButton(
isFavorite: widget.isFavorite,
onPressed: widget.onFavorite,
),
),
// Double-tap heart animation overlay // Double-tap heart animation overlay
if (_showHeart) if (_showHeart)
Positioned.fill( Positioned.fill(
@@ -369,7 +414,8 @@ class _PostCardState extends ConsumerState<PostCard>
), ),
), ),
), ),
], ],
),
), ),
), ),
), ),
@@ -635,13 +681,37 @@ class _FavoriteButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton.filledTonal( return InkResponse(
visualDensity: VisualDensity.compact, onTap: onPressed,
tooltip: isFavorite ? 'Remove favorite' : 'Favorite', radius: 20,
onPressed: onPressed, child: AnimatedContainer(
icon: Icon( duration: AppMotion.duration(context, 150),
isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, width: 34,
size: 18, height: 34,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.58),
shape: BoxShape.circle,
border: Border.all(
color: isFavorite
? Colors.redAccent.withValues(alpha: 0.6)
: Colors.white.withValues(alpha: 0.16),
width: 0.9,
),
boxShadow: [
if (isFavorite)
BoxShadow(
color: Colors.redAccent.withValues(alpha: 0.35),
blurRadius: 10,
),
],
),
child: Center(
child: Icon(
isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: isFavorite ? Colors.redAccent : Colors.white,
size: 18,
),
),
), ),
); );
} }
@@ -654,16 +724,23 @@ class _ProviderBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DecoratedBox( return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3.5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55), color: Colors.black.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.12),
width: 0.7,
),
), ),
child: Padding( child: Text(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), name,
child: Text( style: const TextStyle(
name, fontSize: 10.5,
style: const TextStyle(fontSize: 11, color: Colors.white), color: Colors.white,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
), ),
), ),
); );
@@ -678,25 +755,32 @@ class _MediaBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final type = _type(fileType); final type = _type(fileType);
return DecoratedBox( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3.5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55), color: Colors.black.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
), border: Border.all(
child: Padding( color: Colors.white.withValues(alpha: 0.12),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), width: 0.7,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_icon(type), size: 12, color: Colors.white),
const SizedBox(width: 4),
Text(
type,
style: const TextStyle(fontSize: 11, color: Colors.white),
),
],
), ),
), ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_icon(type), size: 13, color: Colors.white),
const SizedBox(width: 3.5),
Text(
type,
style: const TextStyle(
fontSize: 10.5,
color: Colors.white,
fontWeight: FontWeight.w800,
letterSpacing: 0.3,
),
),
],
),
); );
} }
@@ -723,25 +807,31 @@ class _SeenBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DecoratedBox( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3.5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55), color: Colors.black.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
), border: Border.all(
child: const Padding( color: Colors.white.withValues(alpha: 0.12),
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3), width: 0.7,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.visibility_rounded, size: 12, color: Colors.white),
SizedBox(width: 4),
Text(
'seen',
style: TextStyle(fontSize: 11, color: Colors.white),
),
],
), ),
), ),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.visibility_rounded, size: 12, color: Colors.white70),
SizedBox(width: 3.5),
Text(
'seen',
style: TextStyle(
fontSize: 10,
color: Colors.white70,
fontWeight: FontWeight.w700,
),
),
],
),
); );
} }
} }
@@ -751,14 +841,20 @@ class _DownloadedBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DecoratedBox( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3.5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55), color: Colors.black.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.12),
width: 0.7,
),
), ),
child: const Padding( child: const Icon(
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3), Icons.download_done_rounded,
child: Icon(Icons.download_done_rounded, size: 13, color: Colors.white), size: 13,
color: Colors.white,
), ),
); );
} }
+1 -1
View File
@@ -95,7 +95,7 @@ class PostMasonryGrid extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= posts.length) return const LoadingSkeleton(); if (index >= posts.length) return const LoadingSkeleton();
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(14),
child: SizedBox.expand( child: SizedBox.expand(
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
+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: 2.0.11+33 version: 2.0.12+34
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"