Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}