feat(notes): Add audio recording and playback features
This commit is contained in:
@@ -5,6 +5,22 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'note.freezed.dart';
|
||||
part 'note.g.dart';
|
||||
|
||||
/// Helper to extract user_id from JSON, falling back to user.id if not present.
|
||||
/// OpenWebUI's NoteItemResponse (list endpoint) doesn't include user_id directly
|
||||
/// but does include the user object with an id field.
|
||||
Object? _readUserId(Map<dynamic, dynamic> json, String key) {
|
||||
// First try the direct user_id field
|
||||
if (json['user_id'] != null) {
|
||||
return json['user_id'];
|
||||
}
|
||||
// Fall back to extracting from user object
|
||||
final user = json['user'];
|
||||
if (user is Map && user['id'] != null) {
|
||||
return user['id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Content structure for a note, supporting multiple formats.
|
||||
@freezed
|
||||
sealed class NoteContent with _$NoteContent {
|
||||
@@ -80,7 +96,11 @@ sealed class Note with _$Note {
|
||||
|
||||
const factory Note({
|
||||
required String id,
|
||||
@JsonKey(name: 'user_id') required String userId,
|
||||
|
||||
/// User ID - may be null in list responses (NoteItemResponse)
|
||||
/// Can be extracted from user.id if present
|
||||
@JsonKey(name: 'user_id', readValue: _readUserId) String? userId,
|
||||
|
||||
required String title,
|
||||
|
||||
/// Note content and associated data
|
||||
|
||||
@@ -33,6 +33,28 @@ void _traceApi(String message) {
|
||||
DebugLogger.log(message, scope: 'api/trace');
|
||||
}
|
||||
|
||||
/// Get MIME type from file extension.
|
||||
String? _getMimeType(String fileName) {
|
||||
final ext = fileName.toLowerCase().split('.').last;
|
||||
return switch (ext) {
|
||||
'm4a' => 'audio/mp4',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'wav' => 'audio/wav',
|
||||
'aac' => 'audio/aac',
|
||||
'ogg' => 'audio/ogg',
|
||||
'webm' => 'audio/webm',
|
||||
'mp4' => 'video/mp4',
|
||||
'jpg' || 'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'pdf' => 'application/pdf',
|
||||
'txt' => 'text/plain',
|
||||
'json' => 'application/json',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Result of a health check with proxy detection.
|
||||
///
|
||||
/// This enum distinguishes between different failure modes:
|
||||
@@ -1831,6 +1853,12 @@ class ApiService {
|
||||
return response.data as String;
|
||||
}
|
||||
|
||||
/// Get the URL for a file's content (for direct access/playback).
|
||||
/// This URL can be used directly by audio/video players.
|
||||
String getFileContentUrl(String fileId) {
|
||||
return '$baseUrl/api/v1/files/$fileId/content';
|
||||
}
|
||||
|
||||
Future<void> deleteFile(String fileId) async {
|
||||
_traceApi('Deleting file: $fileId');
|
||||
await _dio.delete('/api/v1/files/$fileId');
|
||||
@@ -3434,7 +3462,7 @@ class ApiService {
|
||||
}
|
||||
|
||||
// File upload for RAG
|
||||
Future<String> uploadFile(String filePath, String fileName) async {
|
||||
Future<String> uploadFile(String filePath, String fileName, {String? contentType}) async {
|
||||
_traceApi('Starting file upload: $fileName from $filePath');
|
||||
|
||||
try {
|
||||
@@ -3444,8 +3472,15 @@ class ApiService {
|
||||
throw Exception('File does not exist: $filePath');
|
||||
}
|
||||
|
||||
// Determine content type from file extension if not provided
|
||||
final mimeType = contentType ?? _getMimeType(fileName);
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||
'file': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: fileName,
|
||||
contentType: mimeType != null ? DioMediaType.parse(mimeType) : null,
|
||||
),
|
||||
});
|
||||
|
||||
_traceApi('Uploading to /api/v1/files/');
|
||||
@@ -4020,12 +4055,14 @@ class ApiService {
|
||||
return (const <Map<String, dynamic>>[], true);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// 403 indicates notes feature is disabled server-side
|
||||
if (e.response?.statusCode == 403) {
|
||||
// 401/403 indicates notes feature is disabled server-side or user lacks permission
|
||||
// OpenWebUI returns 401 when user doesn't have "features.notes" permission
|
||||
final statusCode = e.response?.statusCode;
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
DebugLogger.log(
|
||||
'feature-disabled',
|
||||
scope: 'api/notes',
|
||||
data: {'status': 403},
|
||||
data: {'status': statusCode},
|
||||
);
|
||||
return (const <Map<String, dynamic>>[], false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user