feat(recipes): add recipe visibility and sharing features
- Implemented functionality to set recipe visibility (public/private) with appropriate checks for user permissions. - Added ability to share recipes with other users, including validation for existing users and permissions. - Introduced new DTOs for setting visibility and sharing recipes. - Updated RecipesController and RecipesService to handle new endpoints for visibility and sharing. - Enhanced inventory preview to consider user permissions and shared recipes. - Updated front-end to support new sharing and visibility features, including UI changes for recipe detail and admin user management.
This commit is contained in:
@@ -4,12 +4,18 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/api/api_error_mapper.dart';
|
||||
import '../../../core/api/api_exception.dart';
|
||||
import '../../../core/auth/jwt_decoder.dart';
|
||||
import '../../../core/ui/async_state_views.dart';
|
||||
import '../../auth/data/auth_providers.dart';
|
||||
import '../data/recipe_providers.dart';
|
||||
import '../domain/recipe.dart';
|
||||
import '../domain/inventory_preview.dart';
|
||||
|
||||
String _fmtQty(double v) =>
|
||||
v == v.truncateToDouble() ? v.toInt().toString() : v.toString();
|
||||
|
||||
enum _ShareAction { share, unshare }
|
||||
|
||||
class RecipeDetailScreen extends ConsumerWidget {
|
||||
final int recipeId;
|
||||
|
||||
@@ -18,18 +24,34 @@ class RecipeDetailScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final recipeAsync = ref.watch(recipeDetailProvider(recipeId));
|
||||
final token = ref.watch(authStateProvider).valueOrNull;
|
||||
final currentUserId = jwtUserId(token);
|
||||
final recipe = recipeAsync.valueOrNull;
|
||||
final isOwner = recipe != null && currentUserId != null && recipe.ownerId == currentUserId;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(recipeAsync.maybeWhen(data: (d) => d, orElse: () => null)?.title ?? 'Recept'),
|
||||
title: const SizedBox.shrink(),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/recipes'),
|
||||
tooltip: 'Tillbaka till receptlistan',
|
||||
),
|
||||
actions: recipeAsync.maybeWhen(data: (d) => d, orElse: () => null) == null
|
||||
actions: recipe == null
|
||||
? []
|
||||
: [
|
||||
if (isOwner)
|
||||
IconButton(
|
||||
tooltip: recipe.isPublic ? 'Gör privat' : 'Gör publik',
|
||||
icon: Icon(recipe.isPublic ? Icons.public : Icons.lock_outline),
|
||||
onPressed: () => _toggleVisibility(context, ref, recipe),
|
||||
),
|
||||
if (isOwner)
|
||||
IconButton(
|
||||
tooltip: 'Dela med användare',
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
onPressed: () => _shareRecipe(context, ref, recipe),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Redigera',
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
@@ -41,7 +63,7 @@ class RecipeDetailScreen extends ConsumerWidget {
|
||||
icon: const Icon(Icons.inventory_2_outlined),
|
||||
onPressed: () => context.go('/inventory'),
|
||||
),
|
||||
_DeleteButton(recipe: recipeAsync.value!),
|
||||
if (isOwner) _DeleteButton(recipe: recipe),
|
||||
],
|
||||
),
|
||||
body: recipeAsync.when(
|
||||
@@ -54,23 +76,75 @@ class RecipeDetailScreen extends ConsumerWidget {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: MediaQuery.of(context).size.height * 2 / 3,
|
||||
expandedHeight: MediaQuery.of(context).size.height * 0.42,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: recipe.imageUrl != null
|
||||
? Image.network(
|
||||
recipe.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(color: Colors.grey[200]),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
recipe.imageUrl != null
|
||||
? Image.network(
|
||||
recipe.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _ImagePlaceholder(),
|
||||
)
|
||||
: _ImagePlaceholder(),
|
||||
// Gradient + title overlay
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black87],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
recipe.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [Shadow(blurRadius: 4, color: Colors.black54)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (recipe.isPublic &&
|
||||
recipe.ownerUsername != null &&
|
||||
recipe.ownerUsername!.isNotEmpty)
|
||||
Positioned(
|
||||
right: 12,
|
||||
top: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.45),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Text(
|
||||
'@${recipe.ownerUsername}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
floating: false,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
@@ -83,6 +157,140 @@ class RecipeDetailScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleVisibility(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Recipe recipe,
|
||||
) async {
|
||||
try {
|
||||
final token = ref.read(authStateProvider).valueOrNull ??
|
||||
await ref.read(authStateProvider.future);
|
||||
await ref.read(recipeRepositoryProvider).setRecipeVisibility(
|
||||
recipe.id,
|
||||
isPublic: !recipe.isPublic,
|
||||
token: token,
|
||||
);
|
||||
ref.invalidate(recipeDetailProvider(recipe.id));
|
||||
ref.invalidate(recipesProvider);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
!recipe.isPublic
|
||||
? 'Receptet är nu publikt.'
|
||||
: 'Receptet är nu privat.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shareRecipe(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Recipe recipe,
|
||||
) async {
|
||||
final ctrl = TextEditingController();
|
||||
final result = await showDialog<(_ShareAction, String)>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Dela recept'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Användarnamn',
|
||||
hintText: 't.ex. anna',
|
||||
),
|
||||
onSubmitted: (_) => Navigator.pop(
|
||||
context,
|
||||
(_ShareAction.share, ctrl.text.trim()),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Avbryt'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
(_ShareAction.unshare, ctrl.text.trim()),
|
||||
),
|
||||
child: const Text('Ta bort delning'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
(_ShareAction.share, ctrl.text.trim()),
|
||||
),
|
||||
child: const Text('Dela'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
ctrl.dispose();
|
||||
|
||||
final action = result?.$1;
|
||||
final trimmed = result?.$2.trim() ?? '';
|
||||
if (trimmed.isEmpty) return;
|
||||
|
||||
try {
|
||||
final token = ref.read(authStateProvider).valueOrNull ??
|
||||
await ref.read(authStateProvider.future);
|
||||
if (action == _ShareAction.unshare) {
|
||||
await ref.read(recipeRepositoryProvider).unshareRecipeWithUsername(
|
||||
recipe.id,
|
||||
username: trimmed,
|
||||
token: token,
|
||||
);
|
||||
} else {
|
||||
await ref.read(recipeRepositoryProvider).shareRecipeWithUsername(
|
||||
recipe.id,
|
||||
username: trimmed,
|
||||
token: token,
|
||||
);
|
||||
}
|
||||
ref.invalidate(recipeDetailProvider(recipe.id));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
action == _ShareAction.unshare
|
||||
? 'Delning borttagen för $trimmed.'
|
||||
: 'Receptet delades med $trimmed.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ImagePlaceholder extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.restaurant,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteButton extends ConsumerWidget {
|
||||
@@ -124,7 +332,8 @@ class _DeleteButton extends ConsumerWidget {
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
final token = await ref.read(authStateProvider.future);
|
||||
final token = ref.read(authStateProvider).valueOrNull ??
|
||||
await ref.read(authStateProvider.future);
|
||||
await ref.read(recipeRepositoryProvider).deleteRecipe(recipe.id,
|
||||
token: token);
|
||||
ref.invalidate(recipesProvider);
|
||||
@@ -143,23 +352,15 @@ class _RecipeBody extends StatelessWidget {
|
||||
|
||||
const _RecipeBody({required this.recipe});
|
||||
|
||||
String _formatQty(double qty) {
|
||||
if (qty == 0) return '';
|
||||
return qty == qty.truncateToDouble()
|
||||
? qty.toInt().toString()
|
||||
: qty.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SingleChildScrollView(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Bilden visas endast som bakgrund i detaljvyn
|
||||
Text(recipe.title, style: theme.textTheme.headlineSmall),
|
||||
// Titel visas som overlay på bilden — inte upprepas här
|
||||
if (recipe.description != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(recipe.description!,
|
||||
@@ -180,22 +381,48 @@ class _RecipeBody extends StatelessWidget {
|
||||
if (recipe.ingredients.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text('Ingredienser', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 12),
|
||||
...recipe.ingredients.map((ing) {
|
||||
final qtyStr = _formatQty(ing.quantity);
|
||||
final parts = [
|
||||
final qtyStr = ing.quantity == 0 ? '' : _fmtQty(ing.quantity);
|
||||
final measureParts = [
|
||||
if (qtyStr.isNotEmpty) qtyStr,
|
||||
if (ing.unit.isNotEmpty) ing.unit,
|
||||
ing.productName,
|
||||
if (ing.note != null) '(${ing.note})',
|
||||
];
|
||||
final measure = measureParts.join(' ');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• '),
|
||||
Expanded(child: Text(parts.join(' '))),
|
||||
if (measure.isNotEmpty) ...[
|
||||
Container(
|
||||
width: 72,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
measure,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
] else
|
||||
const SizedBox(width: 82),
|
||||
Expanded(
|
||||
child: Text(
|
||||
ing.note != null
|
||||
? '${ing.productName} (${ing.note})'
|
||||
: ing.productName,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -203,12 +430,10 @@ class _RecipeBody extends StatelessWidget {
|
||||
],
|
||||
if (recipe.instructions != null &&
|
||||
recipe.instructions!.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 32),
|
||||
Text('Tillvägagångssätt', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(recipe.instructions!,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(height: 1.6)),
|
||||
const SizedBox(height: 16),
|
||||
..._buildSteps(recipe.instructions!, theme),
|
||||
],
|
||||
_InventoryPreviewSection(recipeId: recipe.id),
|
||||
const SizedBox(height: 40),
|
||||
@@ -216,6 +441,54 @@ class _RecipeBody extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSteps(String instructions, ThemeData theme) {
|
||||
final steps = instructions
|
||||
.split(RegExp(r'\n{2,}'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
return steps.asMap().entries.map((entry) {
|
||||
final index = entry.key + 1;
|
||||
final text = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
'$index',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(height: 1.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _InventoryPreviewSection extends ConsumerStatefulWidget {
|
||||
@@ -367,18 +640,15 @@ class _IngredientPreviewRow extends StatelessWidget {
|
||||
IngredientStatus.missing => (Icons.cancel_outlined, cs.error),
|
||||
};
|
||||
|
||||
String _fmt(double v) =>
|
||||
v == v.truncateToDouble() ? v.toInt().toString() : v.toString();
|
||||
|
||||
final requiredStr =
|
||||
'${_fmt(ingredient.requiredQuantity)} ${ingredient.requiredUnit}'.trim();
|
||||
'${_fmtQty(ingredient.requiredQuantity)} ${ingredient.requiredUnit}'.trim();
|
||||
final availableStr =
|
||||
'${_fmt(ingredient.availableQuantity)} ${ingredient.requiredUnit}'.trim();
|
||||
'${_fmtQty(ingredient.availableQuantity)} ${ingredient.requiredUnit}'.trim();
|
||||
|
||||
final subtitle = switch (ingredient.status) {
|
||||
IngredientStatus.enough => 'Tillgängligt: $availableStr',
|
||||
IngredientStatus.missing => ingredient.availableQuantity > 0
|
||||
? 'Saknar ${_fmt(ingredient.missingQuantity)} ${ingredient.requiredUnit} '
|
||||
? 'Saknar ${_fmtQty(ingredient.missingQuantity)} ${ingredient.requiredUnit} '
|
||||
'(har $availableStr)'
|
||||
: 'Saknas helt',
|
||||
IngredientStatus.unitMismatch =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../../core/api/api_error_mapper.dart';
|
||||
import '../../../core/ui/async_state_views.dart';
|
||||
import '../data/recipe_providers.dart';
|
||||
import '../data/recipes_grid_provider.dart';
|
||||
import '../domain/recipe.dart';
|
||||
|
||||
class RecipesScreen extends ConsumerWidget {
|
||||
const RecipesScreen({super.key});
|
||||
@@ -47,21 +48,7 @@ class RecipesScreen extends ConsumerWidget {
|
||||
final recipe = recipes[index];
|
||||
return GestureDetector(
|
||||
onTap: () => context.push('/recipes/${recipe.id}'),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Colors.grey[200],
|
||||
image: recipe.imageUrl != null
|
||||
? DecorationImage(
|
||||
image: NetworkImage(recipe.imageUrl!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: recipe.imageUrl == null
|
||||
? const Center(child: Icon(Icons.restaurant, size: 32))
|
||||
: null,
|
||||
),
|
||||
child: _RecipeImageCard(recipe: recipe),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -80,20 +67,11 @@ class RecipesScreen extends ConsumerWidget {
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: recipe.imageUrl != null
|
||||
? Image.network(
|
||||
recipe.imageUrl!,
|
||||
width: 72,
|
||||
height: 72,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.restaurant,
|
||||
size: 32),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: _RecipeImageCard(recipe: recipe, compact: true),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -126,3 +104,57 @@ class RecipesScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecipeImageCard extends StatelessWidget {
|
||||
final Recipe recipe;
|
||||
final bool compact;
|
||||
|
||||
const _RecipeImageCard({required this.recipe, this.compact = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showStamp = recipe.isPublic == true &&
|
||||
recipe.ownerUsername != null &&
|
||||
recipe.ownerUsername.toString().isNotEmpty;
|
||||
final radius = compact ? 0.0 : 8.0;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
color: Colors.grey[200],
|
||||
image: recipe.imageUrl != null
|
||||
? DecorationImage(
|
||||
image: NetworkImage(recipe.imageUrl!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (recipe.imageUrl == null)
|
||||
const Center(child: Icon(Icons.restaurant, size: 32)),
|
||||
if (showStamp)
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.45),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'@${recipe.ownerUsername}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: compact ? 8 : 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user