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:
Nils-Johan Gynther
2026-05-02 09:19:59 +02:00
parent f67bf8baef
commit 41ae7d4d06
17 changed files with 742 additions and 124 deletions
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/global_error_handler.dart';
import '../../auth/data/auth_providers.dart';
import '../data/import_providers.dart';
@@ -23,17 +22,17 @@ class RecipeImportTab extends ConsumerStatefulWidget {
}
class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
// Shared state
bool _isLoading = false;
String? _error;
// File mode
PlatformFile? _pickedFile;
// URL mode
_Method _method = _Method.file;
final _urlCtrl = TextEditingController();
@override
void initState() {
super.initState();
_urlCtrl.addListener(() => setState(() {}));
}
@override
void dispose() {
_urlCtrl.dispose();
@@ -59,17 +58,15 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
Future<void> _submit() async {
if (_pickedFile == null && _method == _Method.file) {
setState(() => _error = 'Vänligen välj en fil först');
showGlobalErrorDialog(context, 'Vänligen välj en fil först');
return;
}
setState(() {
_isLoading = true;
_error = null;
});
setState(() => _isLoading = true);
try {
final token = await ref.read(authStateProvider.future);
final token = ref.read(authStateProvider).valueOrNull ??
await ref.read(authStateProvider.future);
final repo = ref.read(importRepositoryProvider);
final result = _method == _Method.file
? await repo.importFile(
@@ -83,7 +80,10 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
);
if (!mounted) return;
context.push('/recipes/create', extra: result);
context.push('/recipes/create', extra: {
'markdown': result.markdown,
'imageUrl': result.imageUrl,
});
} catch (e) {
showGlobalErrorDialog(context, 'Ett fel uppstod vid import: $e');
} finally {
@@ -172,7 +172,6 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
prefixIcon: Icon(Icons.link),
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
onSubmitted: (_) {
if (_canSubmit) _submit();
},
@@ -193,31 +192,6 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
const SizedBox(height: 16),
],
// ── Felmeddelande ───────────────────────────────────────────────
if (_error != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.error_outline,
color: theme.colorScheme.onErrorContainer, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onErrorContainer),
),
),
],
),
),
const SizedBox(height: 16),
],
// ── Knapp ───────────────────────────────────────────────────────
FilledButton.icon(