handled image insertion in action taken
This commit is contained in:
parent
1b2b89d506
commit
8bb69a80af
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -6,6 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
|||
import '../models/task.dart';
|
||||
import '../models/task_activity_log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import '../models/task_assignment.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
|
|
@ -210,6 +213,10 @@ final tasksControllerProvider = Provider<TasksController>((ref) {
|
|||
class TasksController {
|
||||
TasksController(this._client);
|
||||
|
||||
// Supabase storage bucket for task action images. Ensure this bucket exists
|
||||
// with public read access.
|
||||
static const String _actionImageBucket = 'task_action_taken_images';
|
||||
|
||||
// _client is declared dynamic allowing test doubles that mimic only the
|
||||
// subset of methods used by this class. In production it will be a
|
||||
// SupabaseClient instance.
|
||||
|
|
@ -277,6 +284,114 @@ class TasksController {
|
|||
unawaited(_notifyCreated(taskId: taskId, actorId: actorId));
|
||||
}
|
||||
|
||||
/// Uploads an image for a task's action field and returns the public URL.
|
||||
///
|
||||
/// [bytes] should contain the file data and [extension] the file extension
|
||||
/// (e.g. 'png' or 'jpg'). The image will be stored under a path that
|
||||
/// includes the task ID and a timestamp to avoid collisions. Returns `null`
|
||||
/// if the upload fails.
|
||||
Future<String?> uploadActionImage({
|
||||
required String taskId,
|
||||
required Uint8List bytes,
|
||||
required String extension,
|
||||
}) async {
|
||||
final path =
|
||||
'tasks/$taskId/${DateTime.now().millisecondsSinceEpoch}.$extension';
|
||||
try {
|
||||
// debug: show upload path
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage uploading to path: $path');
|
||||
// perform the upload and capture whatever the SDK returns (it varies by platform)
|
||||
final dynamic res;
|
||||
if (kIsWeb) {
|
||||
// on web, upload binary data
|
||||
res = await _client.storage
|
||||
.from(_actionImageBucket)
|
||||
.uploadBinary(path, bytes);
|
||||
} else {
|
||||
// write bytes to a simple temp file (no nested folders)
|
||||
final tmpDir = Directory.systemTemp;
|
||||
final localFile = File(
|
||||
'${tmpDir.path}/${DateTime.now().millisecondsSinceEpoch}.$extension',
|
||||
);
|
||||
try {
|
||||
await localFile.create();
|
||||
await localFile.writeAsBytes(bytes);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage failed writing temp file: $e');
|
||||
return null;
|
||||
}
|
||||
res = await _client.storage
|
||||
.from(_actionImageBucket)
|
||||
.upload(path, localFile);
|
||||
try {
|
||||
await localFile.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// debug: inspect the response object/type
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage response type=${res.runtimeType} value=$res');
|
||||
|
||||
// Some SDK methods return a simple String (path) on success, others
|
||||
// return a StorageResponse with an error field. Avoid calling .error on a
|
||||
// String to prevent NoSuchMethodError as seen in logs earlier.
|
||||
if (res is String) {
|
||||
// treat as success
|
||||
} else if (res is Map && res['error'] != null) {
|
||||
// older versions might return a plain map
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage upload error: ${res['error']}');
|
||||
return null;
|
||||
} else if (res != null && res.error != null) {
|
||||
// StorageResponse case
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage upload error: ${res.error}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage failed upload: $e');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final urlRes = await _client.storage
|
||||
.from(_actionImageBucket)
|
||||
.getPublicUrl(path);
|
||||
// debug: log full response
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage getPublicUrl response: $urlRes');
|
||||
|
||||
String? url;
|
||||
if (urlRes is String) {
|
||||
url = urlRes;
|
||||
} else if (urlRes is Map && urlRes['data'] is String) {
|
||||
url = urlRes['data'] as String;
|
||||
} else if (urlRes != null) {
|
||||
try {
|
||||
url = urlRes.data as String?;
|
||||
} catch (_) {
|
||||
url = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (url != null && url.isNotEmpty) {
|
||||
// trim whitespace/newline which may be added by SDK or logging
|
||||
return url.trim();
|
||||
}
|
||||
// fallback: construct URL manually using env variable
|
||||
final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
|
||||
if (supabaseUrl.isEmpty) return null;
|
||||
return '$supabaseUrl/storage/v1/object/public/$_actionImageBucket/$path'
|
||||
.trim();
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('uploadActionImage getPublicUrl error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _notifyCreated({
|
||||
required String taskId,
|
||||
required String? actorId,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
|
|
@ -1612,25 +1613,62 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
if (bytes == null) {
|
||||
return;
|
||||
}
|
||||
final b64 =
|
||||
base64Encode(bytes);
|
||||
final ext =
|
||||
file.extension ??
|
||||
'png';
|
||||
final dataUri =
|
||||
'data:image/$ext;base64,$b64';
|
||||
final messenger =
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
);
|
||||
String? url;
|
||||
try {
|
||||
url = await ref
|
||||
.read(
|
||||
tasksControllerProvider,
|
||||
)
|
||||
.uploadActionImage(
|
||||
taskId: task.id,
|
||||
bytes: bytes,
|
||||
extension: ext,
|
||||
);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Upload error: $e',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (url == null) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Image upload failed (no URL returned)',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final trimmedUrl = url
|
||||
.trim();
|
||||
final idx =
|
||||
_actionController
|
||||
?.selection
|
||||
.baseOffset ??
|
||||
0;
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'inserting image embed idx=$idx url=$trimmedUrl',
|
||||
);
|
||||
_actionController
|
||||
?.document
|
||||
.insert(
|
||||
idx,
|
||||
quill
|
||||
.BlockEmbed.image(
|
||||
dataUri,
|
||||
trimmedUrl,
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user