Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 198 lines 4.9 kB view raw
1import {type PickerImage} from './picker.shared' 2import {type Dimensions} from './types' 3import {blobToDataUri, getDataUriSize} from './util' 4import {mimeToExt} from './video/util' 5 6export async function compressIfNeeded( 7 img: PickerImage, 8 maxSize: number, 9): Promise<PickerImage> { 10 if (img.size < maxSize) { 11 return img 12 } 13 return await doResize(img.path, { 14 width: img.width, 15 height: img.height, 16 mode: 'stretch', 17 maxSize, 18 }) 19} 20 21export interface DownloadAndResizeOpts { 22 uri: string 23 width: number 24 height: number 25 mode: 'contain' | 'cover' | 'stretch' 26 maxSize: number 27 timeout: number 28} 29 30export async function downloadAndResize(opts: DownloadAndResizeOpts) { 31 const controller = new AbortController() 32 const to = setTimeout(() => controller.abort(), opts.timeout || 5e3) 33 const res = await fetch(opts.uri) 34 const resBody = await res.blob() 35 clearTimeout(to) 36 37 const dataUri = await blobToDataUri(resBody) 38 return await doResize(dataUri, opts) 39} 40 41export async function shareImageModal(_opts: {uri: string}) { 42 // TODO 43 throw new Error('TODO') 44} 45 46export async function saveImageToMediaLibrary(_opts: {uri: string}) { 47 // TODO 48 throw new Error('TODO') 49} 50 51export async function downloadVideoWeb({uri}: {uri: string}) { 52 // download the file to cache 53 const downloadResponse = await fetch(uri) 54 .then(res => res.blob()) 55 .catch(() => null) 56 if (downloadResponse == null) return false 57 const extension = mimeToExt(downloadResponse.type) 58 59 const blobUrl = URL.createObjectURL(downloadResponse) 60 const link = document.createElement('a') 61 link.setAttribute('download', uri.slice(-10) + '.' + extension) 62 link.setAttribute('href', blobUrl) 63 link.click() 64 return true 65} 66 67export async function getImageDim(path: string): Promise<Dimensions> { 68 var img = document.createElement('img') 69 const promise = new Promise((resolve, reject) => { 70 img.onload = resolve 71 img.onerror = reject 72 }) 73 img.src = path 74 await promise 75 return {width: img.width, height: img.height} 76} 77 78// internal methods 79// = 80 81interface DoResizeOpts { 82 width: number 83 height: number 84 mode: 'contain' | 'cover' | 'stretch' 85 maxSize: number 86} 87 88async function doResize( 89 dataUri: string, 90 opts: DoResizeOpts, 91): Promise<PickerImage> { 92 let newDataUri 93 94 let minQualityPercentage = 0 95 let maxQualityPercentage = 101 //exclusive 96 97 while (maxQualityPercentage - minQualityPercentage > 1) { 98 const qualityPercentage = Math.round( 99 (maxQualityPercentage + minQualityPercentage) / 2, 100 ) 101 const tempDataUri = await createResizedImage(dataUri, { 102 width: opts.width, 103 height: opts.height, 104 quality: qualityPercentage / 100, 105 mode: opts.mode, 106 }) 107 108 if (getDataUriSize(tempDataUri) < opts.maxSize) { 109 minQualityPercentage = qualityPercentage 110 newDataUri = tempDataUri 111 } else { 112 maxQualityPercentage = qualityPercentage 113 } 114 } 115 116 if (!newDataUri) { 117 throw new Error('Failed to compress image') 118 } 119 return { 120 path: newDataUri, 121 mime: 'image/webp', 122 size: getDataUriSize(newDataUri), 123 width: opts.width, 124 height: opts.height, 125 } 126} 127 128function createResizedImage( 129 dataUri: string, 130 { 131 width, 132 height, 133 quality, 134 mode, 135 }: { 136 width: number 137 height: number 138 quality: number 139 mode: 'contain' | 'cover' | 'stretch' 140 }, 141): Promise<string> { 142 return new Promise((resolve, reject) => { 143 const img = document.createElement('img') 144 img.addEventListener('load', () => { 145 const canvas = document.createElement('canvas') 146 const ctx = canvas.getContext('2d') 147 if (!ctx) { 148 return reject(new Error('Failed to resize image')) 149 } 150 151 let scale = 1 152 if (mode === 'cover') { 153 scale = img.width < img.height ? width / img.width : height / img.height 154 } else if (mode === 'contain') { 155 scale = img.width > img.height ? width / img.width : height / img.height 156 } 157 let w = img.width * scale 158 let h = img.height * scale 159 160 canvas.width = w 161 canvas.height = h 162 163 ctx.drawImage(img, 0, 0, w, h) 164 resolve(canvas.toDataURL('image/webp', quality)) 165 }) 166 img.addEventListener('error', ev => { 167 reject(ev.error) 168 }) 169 img.src = dataUri 170 }) 171} 172 173export async function saveBytesToDisk( 174 filename: string, 175 bytes: Uint8Array<ArrayBuffer>, 176 type: string, 177) { 178 const blob = new Blob([bytes], {type}) 179 const url = URL.createObjectURL(blob) 180 downloadUrl(url, filename) 181 // Firefox requires a small delay 182 setTimeout(() => URL.revokeObjectURL(url), 100) 183 return true 184} 185 186function downloadUrl(href: string, filename: string) { 187 const a = document.createElement('a') 188 a.href = href 189 a.download = filename 190 a.style.display = 'none' 191 document.body.appendChild(a) 192 a.click() 193 document.body.removeChild(a) 194} 195 196export async function safeDeleteAsync() { 197 // no-op 198}