Compare commits
12 Commits
6c5c1fea27
...
v1.20.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
1ba4afdf08
|
|||
|
d764134513
|
|||
|
e38f540c79
|
|||
|
73ad5b4eb1
|
|||
|
b4a70233cf
|
|||
|
cb4b9c6b40
|
|||
|
98dcb4f25b
|
|||
|
7709634a9a
|
|||
|
3ddc85669f
|
|||
|
95961e680f
|
|||
|
9468a6a0cc
|
|||
|
c9465c8fa8
|
@@ -12,6 +12,7 @@ const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
|
||||
export default class AppMenuSettingsNostr extends Component {
|
||||
@service settings;
|
||||
@service nostrData;
|
||||
@service toast;
|
||||
|
||||
@tracked newReadRelay = '';
|
||||
@tracked newWriteRelay = '';
|
||||
@@ -90,6 +91,16 @@ export default class AppMenuSettingsNostr extends Component {
|
||||
this.settings.update('nostrWriteRelays', null);
|
||||
}
|
||||
|
||||
@action
|
||||
async clearCache() {
|
||||
try {
|
||||
await this.nostrData.clearCache();
|
||||
this.toast.show('Nostr cache cleared');
|
||||
} catch (e) {
|
||||
this.toast.show(`Failed to clear Nostr cache: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! template-lint-disable no-nested-interactive }}
|
||||
<details>
|
||||
@@ -213,6 +224,18 @@ export default class AppMenuSettingsNostr extends Component {
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Cached data</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-full"
|
||||
{{on "click" this.clearCache}}
|
||||
>
|
||||
<Icon @name="database" @size={{18}} @color="var(--danger-color)" />
|
||||
Clear profiles, photos, and reviews
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@ export default class Modal extends Component {
|
||||
|
||||
@action
|
||||
close() {
|
||||
if (this.args.disableClose) return;
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
@@ -31,10 +32,11 @@ export default class Modal extends Component {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="close-modal-btn btn-text"
|
||||
class="close-modal-btn btn-text {{if @disableClose 'disabled'}}"
|
||||
disabled={{@disableClose}}
|
||||
{{on "click" this.close}}
|
||||
>
|
||||
<Icon @name="x" @size={{24}} />
|
||||
<Icon @name="x" @size={{24}} @color="currentColor" />
|
||||
</button>
|
||||
{{yield}}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,12 @@ export default class PlaceDetails extends Component {
|
||||
@tracked isPhotoUploadModalOpen = false;
|
||||
@tracked isNostrConnectModalOpen = false;
|
||||
@tracked newlyUploadedPhotoId = null;
|
||||
@tracked isPhotoUploadActive = false;
|
||||
|
||||
@action
|
||||
handleUploadStateChange(isActive) {
|
||||
this.isPhotoUploadActive = isActive;
|
||||
}
|
||||
|
||||
@action
|
||||
openPhotoUploadModal(e) {
|
||||
@@ -42,6 +48,7 @@ export default class PlaceDetails extends Component {
|
||||
|
||||
@action
|
||||
closePhotoUploadModal(eventId) {
|
||||
if (this.isPhotoUploadActive) return;
|
||||
this.isPhotoUploadModalOpen = false;
|
||||
if (typeof eventId === 'string') {
|
||||
this.newlyUploadedPhotoId = eventId;
|
||||
@@ -585,10 +592,14 @@ export default class PlaceDetails extends Component {
|
||||
</div>
|
||||
|
||||
{{#if this.isPhotoUploadModalOpen}}
|
||||
<Modal @onClose={{this.closePhotoUploadModal}}>
|
||||
<Modal
|
||||
@onClose={{this.closePhotoUploadModal}}
|
||||
@disableClose={{this.isPhotoUploadActive}}
|
||||
>
|
||||
<PlacePhotoUpload
|
||||
@place={{this.saveablePlace}}
|
||||
@onClose={{this.closePhotoUploadModal}}
|
||||
@onUploadStateChange={{this.handleUploadStateChange}}
|
||||
/>
|
||||
</Modal>
|
||||
{{/if}}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default class PlacePhotoUploadItem extends Component {
|
||||
@tracked thumbnailUrl = '';
|
||||
@tracked blurhash = '';
|
||||
@tracked error = '';
|
||||
@tracked statusText = '';
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -47,6 +48,7 @@ export default class PlacePhotoUploadItem extends Component {
|
||||
|
||||
uploadTask = task(async (file) => {
|
||||
this.error = '';
|
||||
this.statusText = 'Processing';
|
||||
try {
|
||||
// 1. Process main image and generate blurhash in worker
|
||||
const mainData = await this.imageProcessor.process(
|
||||
@@ -71,18 +73,34 @@ export default class PlacePhotoUploadItem extends Component {
|
||||
let mainResult, thumbResult;
|
||||
const isMobileDevice = isMobile();
|
||||
|
||||
const mainProgress = (status) => {
|
||||
if (status === 'signing') this.statusText = 'Signing photo upload';
|
||||
if (status === 'uploading') this.statusText = 'Uploading photo';
|
||||
};
|
||||
|
||||
const thumbProgress = (status) => {
|
||||
if (status === 'signing') this.statusText = 'Signing thumbnail upload';
|
||||
if (status === 'uploading') this.statusText = 'Uploading thumbnail';
|
||||
};
|
||||
|
||||
if (isMobileDevice) {
|
||||
// Mobile: sequential uploads to preserve bandwidth and memory
|
||||
mainResult = await this.blossom.upload(mainData.blob, {
|
||||
sequential: true,
|
||||
onProgress: mainProgress,
|
||||
});
|
||||
thumbResult = await this.blossom.upload(thumbData.blob, {
|
||||
sequential: true,
|
||||
onProgress: thumbProgress,
|
||||
});
|
||||
} else {
|
||||
// Desktop: concurrent uploads
|
||||
const mainUploadPromise = this.blossom.upload(mainData.blob);
|
||||
const thumbUploadPromise = this.blossom.upload(thumbData.blob);
|
||||
const mainUploadPromise = this.blossom.upload(mainData.blob, {
|
||||
onProgress: mainProgress,
|
||||
});
|
||||
const thumbUploadPromise = this.blossom.upload(thumbData.blob, {
|
||||
onProgress: thumbProgress,
|
||||
});
|
||||
|
||||
[mainResult, thumbResult] = await Promise.all([
|
||||
mainUploadPromise,
|
||||
@@ -127,6 +145,9 @@ export default class PlacePhotoUploadItem extends Component {
|
||||
@color="white"
|
||||
class="spin-animation"
|
||||
/>
|
||||
{{#if this.statusText}}
|
||||
<span class="upload-status-text">{{this.statusText}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class PlacePhotoUpload extends Component {
|
||||
|
||||
@tracked file = null;
|
||||
@tracked uploadedPhoto = null;
|
||||
@tracked status = '';
|
||||
@tracked error = '';
|
||||
@tracked isPublishing = false;
|
||||
@tracked isDragging = false;
|
||||
@@ -77,6 +76,9 @@ export default class PlacePhotoUpload extends Component {
|
||||
}
|
||||
this.file = file;
|
||||
this.uploadedPhoto = null;
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -91,6 +93,9 @@ export default class PlacePhotoUpload extends Component {
|
||||
}
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
deletePhotoTask = task(async (photoData) => {
|
||||
@@ -126,7 +131,6 @@ export default class PlacePhotoUpload extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = 'Publishing event...';
|
||||
this.error = '';
|
||||
this.isPublishing = true;
|
||||
|
||||
@@ -185,18 +189,20 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.nostrData.store.add(event);
|
||||
|
||||
this.toast.show('Photo published successfully');
|
||||
this.status = '';
|
||||
|
||||
// Clear out the file so user can upload more or be done
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
}
|
||||
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose(event.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = 'Failed to publish: ' + e.message;
|
||||
this.status = '';
|
||||
} finally {
|
||||
this.isPublishing = false;
|
||||
}
|
||||
@@ -212,12 +218,6 @@ export default class PlacePhotoUpload extends Component {
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.status}}
|
||||
<div class="alert alert-info">
|
||||
{{this.status}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.file}}
|
||||
<div class="photo-grid">
|
||||
<PlacePhotoUploadItem
|
||||
|
||||
@@ -116,7 +116,8 @@ export default class PlacePhotosCarousel extends Component {
|
||||
{{#each this.photos as |photo|}}
|
||||
{{! template-lint-disable no-inline-styles }}
|
||||
<div
|
||||
class="carousel-slide"
|
||||
class="carousel-slide
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
style={{photo.style}}
|
||||
data-event-id={{photo.eventId}}
|
||||
>
|
||||
|
||||
@@ -60,10 +60,13 @@ export default class BlossomService extends Service {
|
||||
return `Nostr ${base64url}`;
|
||||
}
|
||||
|
||||
async _uploadToServer(file, hash, serverUrl) {
|
||||
async _uploadToServer(file, hash, serverUrl, onProgress) {
|
||||
const uploadUrl = getBlossomUrl(serverUrl, 'upload');
|
||||
|
||||
if (onProgress) onProgress('signing');
|
||||
const authHeader = await this._getAuthHeader('upload', hash, serverUrl);
|
||||
|
||||
if (onProgress) onProgress('uploading');
|
||||
// eslint-disable-next-line warp-drive/no-external-request-patterns
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
@@ -109,14 +112,20 @@ export default class BlossomService extends Service {
|
||||
|
||||
if (options.sequential) {
|
||||
// Sequential upload logic
|
||||
mainResult = await this._uploadToServer(file, payloadHash, mainServer);
|
||||
mainResult = await this._uploadToServer(
|
||||
file,
|
||||
payloadHash,
|
||||
mainServer,
|
||||
options.onProgress
|
||||
);
|
||||
|
||||
for (const serverUrl of fallbackServers) {
|
||||
try {
|
||||
const result = await this._uploadToServer(
|
||||
file,
|
||||
payloadHash,
|
||||
serverUrl
|
||||
serverUrl,
|
||||
options.onProgress
|
||||
);
|
||||
fallbackUrls.push(result.url);
|
||||
} catch (error) {
|
||||
@@ -125,9 +134,14 @@ export default class BlossomService extends Service {
|
||||
}
|
||||
} else {
|
||||
// Concurrent upload logic
|
||||
const mainPromise = this._uploadToServer(file, payloadHash, mainServer);
|
||||
const mainPromise = this._uploadToServer(
|
||||
file,
|
||||
payloadHash,
|
||||
mainServer,
|
||||
options.onProgress
|
||||
);
|
||||
const fallbackPromises = fallbackServers.map((serverUrl) =>
|
||||
this._uploadToServer(file, payloadHash, serverUrl)
|
||||
this._uploadToServer(file, payloadHash, serverUrl, options.onProgress)
|
||||
);
|
||||
|
||||
// Main server MUST succeed
|
||||
|
||||
@@ -356,6 +356,13 @@ export default class NostrDataService extends Service {
|
||||
return 'Not connected';
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this._cachePromise;
|
||||
if (this.cache) {
|
||||
await this.cache.deleteAllEvents();
|
||||
}
|
||||
}
|
||||
|
||||
_cleanupSubscriptions() {
|
||||
if (this._requestSub) {
|
||||
this._requestSub.unsubscribe();
|
||||
|
||||
@@ -285,10 +285,20 @@ body {
|
||||
inset: 0;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-status-text {
|
||||
color: white;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
text-shadow: 0 1px 3px rgb(0 0 0 / 80%);
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.photo-upload-item .error-overlay {
|
||||
background: rgb(224 108 117 / 80%);
|
||||
cursor: pointer;
|
||||
@@ -1006,10 +1016,17 @@ abbr[title] {
|
||||
flex: 0 0 auto;
|
||||
height: 100px;
|
||||
width: auto;
|
||||
aspect-ratio: var(--slide-ratio, 16 / 9);
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
.carousel-slide.landscape {
|
||||
aspect-ratio: var(--slide-ratio, 16 / 9);
|
||||
}
|
||||
|
||||
.carousel-slide.portrait {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.carousel-placeholder {
|
||||
display: block;
|
||||
background-color: var(--hover-bg);
|
||||
@@ -1798,6 +1815,12 @@ button.create-place {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
cursor: pointer;
|
||||
color: #898989;
|
||||
}
|
||||
|
||||
.close-modal-btn.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.place-photo-upload h2 {
|
||||
@@ -1816,11 +1839,6 @@ button.create-place {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #eef;
|
||||
color: #00c;
|
||||
}
|
||||
|
||||
.preview-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
||||
import chevronLeft from 'feather-icons/dist/icons/chevron-left.svg?raw';
|
||||
import chevronRight from 'feather-icons/dist/icons/chevron-right.svg?raw';
|
||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||
import database from 'feather-icons/dist/icons/database.svg?raw';
|
||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
||||
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
||||
@@ -153,6 +154,7 @@ const ICONS = {
|
||||
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
||||
croissant,
|
||||
'cup-and-saucer': cupAndSaucer,
|
||||
database,
|
||||
donut,
|
||||
edit,
|
||||
eyeglasses,
|
||||
|
||||
@@ -14,7 +14,7 @@ While NIP-68 (Picture-first feeds) caters to general visual feeds, this NIP spec
|
||||
|
||||
## Content
|
||||
|
||||
The `.content` of the event SHOULD generally be empty. If a user wishes to provide a detailed description, summary, or caption for a place, clients SHOULD encourage them to create a Place Review event (`kind: 30360`) instead.
|
||||
The `.content` of the event SHOULD generally be empty. If a user wishes to provide a detailed description for a place, clients SHOULD encourage them to create a Place Review event (`kind: 30360`) instead.
|
||||
|
||||
## Tags
|
||||
|
||||
@@ -45,17 +45,19 @@ Used for spatial indexing and discovery. Events MUST include at least one high-p
|
||||
|
||||
#### 3. `imeta` — Inline Media Metadata
|
||||
|
||||
An event MUST contain exactly one `imeta` tag representing a single media item. The primary `url` SHOULD also be appended to the event's `.content` for backwards compatibility with clients that do not parse `imeta` tags.
|
||||
An event MUST contain exactly one `imeta` tag representing a single media item. The primary `url` MAY also be appended to the event's `.content` for backwards compatibility with clients that do not parse `imeta` tags.
|
||||
|
||||
Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `m` (MIME type), and `blurhash` where possible.
|
||||
Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `m` (MIME type), `thumb` (URL to a smaller thumbnail image), and `blurhash` where possible. Clients MAY also include `fallback` URLs if the media is hosted on multiple servers.
|
||||
|
||||
```json
|
||||
[
|
||||
"imeta",
|
||||
"url https://example.com/photo.jpg",
|
||||
"url https://blossom.example.com/8e2e28a503fa37482de5b0959ee38b2bb4de4e0a752db24c568981c2ab410260.jpg",
|
||||
"m image/jpeg",
|
||||
"dim 3024x4032",
|
||||
"dim 1440x1920",
|
||||
"alt A steaming bowl of ramen on a wooden table at the restaurant.",
|
||||
"fallback https://mirror.example.com/8e2e28a503fa37482de5b0959ee38b2bb4de4e0a752db24c568981c2ab410260.jpg",
|
||||
"thumb https://example.com/7a1f592f6ea8e932b1de9568285b01851e4cf708466b0a03010b91e92c6c8135.jpg",
|
||||
"blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$"
|
||||
]
|
||||
```
|
||||
@@ -83,10 +85,12 @@ Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `
|
||||
|
||||
[
|
||||
"imeta",
|
||||
"url https://example.com/ramen.jpg",
|
||||
"url https://blossom.example.com/a9c84e183789a74288b8e05d04cc61230e74f386925a953e6b29f957e8cc3a61.jpg",
|
||||
"m image/jpeg",
|
||||
"dim 1080x1080",
|
||||
"dim 1920x1920",
|
||||
"alt A close-up of spicy miso ramen with chashu pork, soft boiled egg, and scallions.",
|
||||
"fallback https://mirror.example.com/a9c84e183789a74288b8e05d04cc61230e74f386925a953e6b29f957e8cc3a61.jpg",
|
||||
"thumb https://example.com/c5a528e20235e16cc1c18090b8f04179de76288ea4e410b0fcb8d1487e416a2d.jpg",
|
||||
"blurhash UHI=0o~q4T-o~q%MozM{x]t7RjRPt7oKkCWB"
|
||||
],
|
||||
|
||||
@@ -98,6 +102,10 @@ Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `
|
||||
|
||||
## Rationale
|
||||
|
||||
### Kind 360
|
||||
|
||||
Easy to remember as a 360-degree view of places.
|
||||
|
||||
### Why not use NIP-68 (Picture-first feeds)?
|
||||
|
||||
NIP-68 is designed for general-purpose social feeds (like Instagram). Place photos require strict guarantees about what entity is being depicted to be useful for map clients, directories, and review aggregators. By mandating the `i` tag for POI linking and the `g` tag for spatial querying, this kind ensures interoperability for geo-spatial applications without cluttering general picture feeds with mundane POI images (like photos of storefronts or menus).
|
||||
|
||||
@@ -276,6 +276,10 @@ Content payloads SHOULD NOT include place identifiers.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Kind 30360
|
||||
|
||||
Pairs with kind 360 (Place Photos). Easy to remember as a 360-degree review of all aspects of a place.
|
||||
|
||||
### No Place Field in Content
|
||||
|
||||
Avoids duplication and inconsistency with tags.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.19.1",
|
||||
"version": "1.20.3",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
|
||||
1
release/assets/image-processor-Dj3-kZwI.js
Normal file
1
release/assets/image-processor-Dj3-kZwI.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var t=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","#","$","%","*","+",",","-",".",":",";","=","?","@","[","]","^","_","{","|","}","~"],e=(e,a)=>{var o="";for(let r=1;r<=a;r++){let h=Math.floor(e)/Math.pow(83,a-r)%83;o+=t[Math.floor(h)]}return o},a=t=>{let e=t/255;return e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)},o=t=>{let e=Math.max(0,Math.min(1,t));return e<=.0031308?Math.trunc(12.92*e*255+.5):Math.trunc(255*(1.055*Math.pow(e,.4166666666666667)-.055)+.5)},r=(t,e)=>(t=>t<0?-1:1)(t)*Math.pow(Math.abs(t),e),h=class extends Error{constructor(t){super(t),this.name="ValidationError",this.message=t}},i=(t,e,o,r)=>{let h=0,i=0,n=0,s=4*e;for(let g=0;g<e;g++){let e=4*g;for(let l=0;l<o;l++){let o=e+l*s,c=r(g,l);h+=c*a(t[o]),i+=c*a(t[o+1]),n+=c*a(t[o+2])}}let l=1/(e*o);return[h*l,i*l,n*l]};self.onmessage=async t=>{if("PROCESS_IMAGE"!==t.data?.type)return;const{id:a,file:n,targetWidth:s,targetHeight:l,quality:g,computeBlurhash:c}=t.data;try{let t,M;try{const e=await createImageBitmap(n,{resizeWidth:s,resizeHeight:l,resizeQuality:"high"});if(t=new OffscreenCanvas(s,l),M=t.getContext("2d"),!M)throw new Error("Failed to get 2d context from OffscreenCanvas");M.drawImage(e,0,0,s,l),e.close()}catch(f){console.warn("Hardware resize failed, falling back to stepped software scaling:",f);const e=await n.arrayBuffer(),a=new Blob([e],{type:n.type}),o=await createImageBitmap(a);let r=o.width,h=o.height,i=new OffscreenCanvas(r,h),g=i.getContext("2d");for(g.imageSmoothingEnabled=!0,g.imageSmoothingQuality="high",g.drawImage(o,0,0);.5*i.width>s&&.5*i.height>l;){const t=new OffscreenCanvas(Math.floor(.5*i.width),Math.floor(.5*i.height)),e=t.getContext("2d");e.imageSmoothingEnabled=!0,e.imageSmoothingQuality="high",e.drawImage(i,0,0,t.width,t.height),i=t}t=new OffscreenCanvas(s,l),M=t.getContext("2d"),M.imageSmoothingEnabled=!0,M.imageSmoothingQuality="high",M.drawImage(i,0,0,s,l),o.close()}let d=null;if(c)try{d=((t,a,n)=>{if(a*n*4!==t.length)throw new h("Width and height must match the pixels array");let s=[];for(let e=0;e<3;e++)for(let o=0;o<4;o++){let r=0==o&&0==e?1:2,h=i(t,a,n,(t,h)=>r*Math.cos(Math.PI*o*t/a)*Math.cos(Math.PI*e*h/n));s.push(h)}let l,g=s[0],c=s.slice(1),f="";if(f+=e(21,1),c.length>0){let t=Math.max(...c.map(t=>Math.max(...t))),a=Math.floor(Math.max(0,Math.min(82,Math.floor(166*t-.5))));l=(a+1)/166,f+=e(a,1)}else l=1,f+=e(0,1);return f+=e((t=>(o(t[0])<<16)+(o(t[1])<<8)+o(t[2]))(g),4),c.forEach(t=>{f+=e(((t,e)=>19*Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[0]/e,.5)+9.5))))*19+19*Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[1]/e,.5)+9.5))))+Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[2]/e,.5)+9.5)))))(t,l),2)}),f})(M.getImageData(0,0,s,l).data,s,l)}catch(m){console.warn("Could not generate blurhash (possible canvas fingerprinting protection):",m)}const u=await t.convertToBlob({type:"image/jpeg",quality:g}),w=`${s}x${l}`;self.postMessage({id:a,success:!0,blob:u,dim:w,blurhash:d})}catch(M){self.postMessage({id:a,success:!1,error:M.message})}}}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
release/assets/main-CHuW_yI-.css
Normal file
1
release/assets/main-CHuW_yI-.css
Normal file
File diff suppressed because one or more lines are too long
16
release/assets/main-CfJ9up1Y.js
Normal file
16
release/assets/main-CfJ9up1Y.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
release/assets/negentropy-Dz25kdHq.js
Normal file
2
release/assets/negentropy-Dz25kdHq.js
Normal file
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-BVEi_-zb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BF2Ls-fG.css">
|
||||
<script type="module" crossorigin src="/assets/main-CfJ9up1Y.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CHuW_yI-.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user