154154 </div >
155155 </div >
156156
157+ <div class =" form-check mb-3 mt-3" >
158+ <input
159+ class =" form-check-input"
160+ type =" checkbox"
161+ id =" embedMedia"
162+ v-model =" embedMedia"
163+ />
164+ <label class =" form-check-label" for =" embedMedia" >
165+ Embed media files as data URIs
166+ </label >
167+ <div class =" form-text text-muted" >
168+ Converts images, audio, and video files to embedded data URIs for better
169+ portability.
170+ </div >
171+ </div >
172+
157173 <div class =" nostr-actions" >
158174 <button class =" btn btn-outline-primary me-2" @click =" step = 'customize'" >
159175 <i class =" bi bi-pencil-square me-1" ></i > Customize Post
@@ -310,6 +326,9 @@ export default {
310326 // Relay management
311327 relays: [" wss://relay.damus.io" , " wss://relay.nostr.band" , " wss://nos.lol" ],
312328 storeRelays: true ,
329+
330+ // Embed media option
331+ embedMedia: true , // Default to true for better portability
313332 };
314333 },
315334
@@ -431,6 +450,141 @@ export default {
431450 this .tags .splice (index , 1 );
432451 },
433452
453+ async embedMediaAsDataURIs(content ) {
454+ try {
455+ // Get all blob references from the editor
456+ const blobs = this .$parent .$refs .editor .getAllBlobs ();
457+
458+ if (! blobs || blobs .length === 0 ) {
459+ return content ; // No media to embed
460+ }
461+
462+ let modifiedContent = content ;
463+
464+ // Process each blob and replace references in the content
465+ for (const blob of blobs ) {
466+ const fileName = blob .name ;
467+ const fileData = blob .data ;
468+
469+ // Only process media files
470+ if (! this .isMediaFile (fileName )) {
471+ continue ;
472+ }
473+
474+ try {
475+ // Determine the MIME type based on file extension
476+ const mimeType = this .getMimeType (fileName );
477+
478+ // Convert the blob data to a base64 data URI
479+ const dataUri = await this .blobToDataUri (fileData , mimeType );
480+
481+ // Replace only the file references with data URIs
482+ // Escape special characters in the file name for regex
483+ const fileNameEscaped = fileName .replace (/ [. *+?^${}()|[\]\\ ] / g , " \\ $&" );
484+
485+ // Match URLs in markdown patterns
486+ // Image syntax: 
487+ const imgRegex = new RegExp (
488+ ` (\\ !\\ [[^\\ ]]*\\ ]\\ ()${fileNameEscaped }(\\ )) ` ,
489+ " g"
490+ );
491+ modifiedContent = modifiedContent .replace (imgRegex , ` $1${dataUri }$2 ` );
492+
493+ // Link syntax: [text](fileName)
494+ const linkRegex = new RegExp (
495+ ` (\\ [[^\\ ]]*\\ ]\\ ()${fileNameEscaped }(\\ )) ` ,
496+ " g"
497+ );
498+ modifiedContent = modifiedContent .replace (linkRegex , ` $1${dataUri }$2 ` );
499+
500+ // URL references
501+ const urlRegex = new RegExp (
502+ ` (src="|href="|url\\ (|@import ['"]?)${fileNameEscaped }(['"]?\\ )|["']) ` ,
503+ " g"
504+ );
505+ modifiedContent = modifiedContent .replace (urlRegex , ` $1${dataUri }$2 ` );
506+ } catch (err ) {
507+ console .error (` Error converting ${fileName } to data URI: ` , err );
508+ }
509+ }
510+
511+ return modifiedContent ;
512+ } catch (error ) {
513+ console .error (" Error embedding media:" , error );
514+ return content ; // Return original content if there was an error
515+ }
516+ },
517+
518+ isMediaFile(fileName ) {
519+ const mediaExtensions = [
520+ // Images
521+ " .jpg" ,
522+ " .jpeg" ,
523+ " .png" ,
524+ " .gif" ,
525+ " .svg" ,
526+ " .webp" ,
527+ " .bmp" ,
528+ // Audio
529+ " .mp3" ,
530+ " .wav" ,
531+ " .ogg" ,
532+ " .m4a" ,
533+ // Video
534+ " .mp4" ,
535+ " .webm" ,
536+ " .ogv" ,
537+ " .mov" ,
538+ ];
539+
540+ const extension = " ." + fileName .split (" ." ).pop ().toLowerCase ();
541+ return mediaExtensions .includes (extension );
542+ },
543+
544+ getMimeType(fileName ) {
545+ const extension = fileName .split (" ." ).pop ().toLowerCase ();
546+
547+ const mimeTypes = {
548+ // Images
549+ jpg: " image/jpeg" ,
550+ jpeg: " image/jpeg" ,
551+ png: " image/png" ,
552+ gif: " image/gif" ,
553+ svg: " image/svg+xml" ,
554+ webp: " image/webp" ,
555+ bmp: " image/bmp" ,
556+ // Audio
557+ mp3: " audio/mpeg" ,
558+ wav: " audio/wav" ,
559+ ogg: " audio/ogg" ,
560+ m4a: " audio/mp4" ,
561+ // Video
562+ mp4: " video/mp4" ,
563+ webm: " video/webm" ,
564+ ogv: " video/ogg" ,
565+ mov: " video/quicktime" ,
566+ };
567+
568+ return mimeTypes [extension ] || " application/octet-stream" ;
569+ },
570+
571+ blobToDataUri(blob , mimeType ) {
572+ return new Promise ((resolve , reject ) => {
573+ // Use FileReader's built-in base64 encoding
574+ const reader = new FileReader ();
575+
576+ reader .onload = function () {
577+ resolve (reader .result );
578+ };
579+
580+ reader .onerror = function () {
581+ reject (new Error (" Failed to convert file to data URI" ));
582+ };
583+
584+ reader .readAsDataURL (new Blob ([blob ], { type: mimeType }));
585+ });
586+ },
587+
434588 publishToNostr() {
435589 if (! this .publicKey || ! this .privateKey ) {
436590 alert (" Please enter both your public and private keys before publishing." );
@@ -457,7 +611,12 @@ export default {
457611 provider .on (" synced" , async (_ : any ) => {
458612 try {
459613 const metaData = await meta ;
460- const contentData = yDoc .getText (this .storageId ).toJSON ();
614+ let contentData = yDoc .getText (this .storageId ).toJSON ();
615+
616+ // Embed media if option is checked
617+ if (this .embedMedia ) {
618+ contentData = await this .embedMediaAsDataURIs (contentData );
619+ }
461620
462621 const pool = new SimplePool ();
463622 const relays = [... this .relays ];
0 commit comments