this repo has no description
0
fork

Configure Feed

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

Migrate to rolldown

uwx 4349040a 963b683c

+280 -815
+2 -1
.gitignore
··· 1 - node_modules 1 + node_modules 2 + /public/kinklist.js
+1
package.json
··· 22 22 "lz-string": "^1.5.0", 23 23 "masonic": "^4.1.0", 24 24 "preact": "^10.28.3", 25 + "qfs-compression": "^0.2.3", 25 26 "react-masonry-css": "^1.0.16", 26 27 "react-responsive-masonry": "^2.7.1", 27 28 "tippy.js": "^6.3.7"
+8
pnpm-lock.yaml
··· 20 20 preact: 21 21 specifier: ^10.28.3 22 22 version: 10.28.3 23 + qfs-compression: 24 + specifier: ^0.2.3 25 + version: 0.2.3 23 26 react-masonry-css: 24 27 specifier: ^1.0.16 25 28 version: 1.0.16(react@19.2.4) ··· 236 239 preact@10.28.3: 237 240 resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} 238 241 242 + qfs-compression@0.2.3: 243 + resolution: {integrity: sha512-9jS4HdvjUuLGt6nW5ISojhwI4YPgJlojrj7OPRPqMUzSlMu7gUF4m1iPPjuNJQZwTO7i0buAK6kG+0oSFNwXeQ==} 244 + 239 245 raf-schd@4.0.3: 240 246 resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} 241 247 ··· 445 451 preact: 10.28.3 446 452 447 453 preact@10.28.3: {} 454 + 455 + qfs-compression@0.2.3: {} 448 456 449 457 raf-schd@4.0.3: {} 450 458
+12
public/base.ts
··· 1 + 2 + /** @type {[id: string, name: string, color: string][]} */ 3 + export const choiceOptions: [id: string, name: string, color: string][] = [ 4 + ['not-entered', 'Not Entered', '#FFFFFF'], 5 + ['favorite', 'Favorite', '#6DB5FE'], 6 + ['like', 'Like', '#23FD22'], 7 + ['okay', 'Okay', '#FDFD6B'], 8 + ['maybe', 'Maybe', '#DB6C00'], 9 + ['no', 'No', '#920000'], 10 + ['try', 'Want To Try', 'pattern'], 11 + ]; 12 + export const choiceOptionIndices = Object.fromEntries(choiceOptions.map(([id], i) => [id, i]));
+38 -103
public/exporter.js public/exporter.ts
··· 1 - // @ts-check 2 - 3 - /* eslint-disable indent */ 4 - /* globals choiceOptions, kinkCategories, kinksById, getSelectedKinkOrDefault, ClipboardItem */ 1 + import { choiceOptions } from "./base"; 2 + import { KinkCategory, Kink } from "./main"; 5 3 6 4 const IMGUR_CLIENT_ID = '9db53e5936cd02f'; 7 5 8 6 const drawCallHandlers = { 9 - /** 10 - * @param {CanvasRenderingContext2D} context 11 - * @param {CanvasPattern} pattern 12 - * @param {SimpleTitleDrawCall} drawCall 13 - */ 14 - simpleTitle(context, pattern, drawCall) { 7 + simpleTitle(context: CanvasRenderingContext2D, pattern: CanvasPattern, drawCall: SimpleTitleDrawCall) { 15 8 context.fillStyle = '#000000'; 16 9 context.font = 'bold 18px Arial'; 17 10 context.fillText(drawCall.data, drawCall.x, drawCall.y + 5); 18 11 }, 19 - /** 20 - * @param {CanvasRenderingContext2D} context 21 - * @param {CanvasPattern} pattern 22 - * @param {TitleSubtitleDrawCall} drawCall 23 - */ 24 - titleSubtitle(context, pattern, drawCall) { 12 + titleSubtitle(context: CanvasRenderingContext2D, pattern: CanvasPattern, drawCall: TitleSubtitleDrawCall) { 25 13 context.fillStyle = '#000000'; 26 14 context.font = 'bold 18px Arial'; 27 15 context.fillText(drawCall.data.category, drawCall.x, drawCall.y + 5); ··· 30 18 context.font = 'italic 12px Arial'; 31 19 context.fillText(fieldsString, drawCall.x, drawCall.y + 20); 32 20 }, 33 - /** 34 - * @param {CanvasRenderingContext2D} context 35 - * @param {CanvasPattern} pattern 36 - * @param {KinkRowDrawCall} drawCall 37 - */ 38 - kinkRow(context, pattern, drawCall) { 21 + kinkRow(context: CanvasRenderingContext2D, pattern: CanvasPattern, drawCall: KinkRowDrawCall) { 39 22 context.fillStyle = '#000000'; 40 23 context.font = '12px Arial'; 41 24 ··· 78 61 * @param {number} width 79 62 * @param {number} height 80 63 */ 81 - function setupCanvas(width, height) { 64 + function setupCanvas(width: number, height: number) { 82 65 const canvas = document.createElement('canvas'); 83 66 canvas.setAttribute('id', 'mainCanvas'); 84 67 canvas.width = width; ··· 102 85 * @param {CanvasRenderingContext2D} context 103 86 * @param {CanvasPattern} pattern 104 87 */ 105 - function drawLegend(context, pattern) { 88 + function drawLegend(context: CanvasRenderingContext2D, pattern: CanvasPattern) { 106 89 context.font = 'bold 13px Arial'; 107 90 context.fillStyle = '#000000'; 108 91 ··· 125 108 } 126 109 } 127 110 128 - /** 129 - * @typedef {object} SimpleTitleDrawCall 130 - * @property {'simpleTitle'} type 131 - * @property {string} data 132 - * @property {number} [x] 133 - * @property {number} y 134 - */ 111 + interface SimpleTitleDrawCall { 112 + type: 'simpleTitle'; 113 + data: string; 114 + x: number; 115 + y: number; 116 + } 135 117 136 - /** 137 - * @typedef {object} TitleSubtitleDrawCall 138 - * @property {'titleSubtitle'} type 139 - * @property {object} data 140 - * @property {string} data.category 141 - * @property {string[]} data.fields 142 - * @property {number} [x] 143 - * @property {number} y 144 - */ 118 + interface TitleSubtitleDrawCall { 119 + type: 'titleSubtitle'; 120 + data: { 121 + category: string; 122 + fields: string[]; 123 + }; 124 + x: number; 125 + y: number; 126 + } 145 127 146 - /** 147 - * @typedef {object} KinkRowDrawCall 148 - * @property {'kinkRow'} type 149 - * @property {object} data 150 - * @property {string[]} data.choices 151 - * @property {string} data.text 152 - * @property {number} [x] 153 - * @property {number} y 154 - */ 128 + interface KinkRowDrawCall { 129 + type: 'kinkRow'; 130 + data: { 131 + choices: string[]; 132 + text: string; 133 + }; 134 + x: number; 135 + y: number; 136 + } 155 137 156 - /** 157 - * @typedef {SimpleTitleDrawCall | TitleSubtitleDrawCall | KinkRowDrawCall} DrawCall 158 - */ 138 + type DrawCall = SimpleTitleDrawCall | TitleSubtitleDrawCall | KinkRowDrawCall; 159 139 160 140 /** 161 141 * @returns {HTMLCanvasElement} 162 142 */ 163 - function exportImage() { 143 + export function exportImage(kinkCategories: KinkCategory[], kinksById: Kink[], getSelectedKinkOrDefault: (kink: Kink, participant: string) => string): HTMLCanvasElement { 164 144 // Constants 165 145 const numberCols = 6; 166 146 const columnWidth = 250; ··· 189 169 190 170 // Initialize columns and drawStacks 191 171 /** @type {Array<{ height: number, drawStack: DrawCall[] }>} */ 192 - const columns = []; 172 + const columns: Array<{ height: number; drawStack: DrawCall[]; }> = []; 193 173 for (let i = 0; i < numberCols; i++) { 194 174 columns.push({ height: 0, drawStack: [] }); 195 175 } ··· 216 196 if (fields.length < 2) { 217 197 column.height += simpleTitleHeight; 218 198 /** @type {SimpleTitleDrawCall} */ 219 - const drawCall = { y: column.height, type: 'simpleTitle', data: catName }; 199 + const drawCall: SimpleTitleDrawCall = { x: NaN, y: column.height, type: 'simpleTitle', data: catName }; 220 200 column.drawStack.push(drawCall); 221 201 } else { 222 202 column.height += titleSubtitleHeight; 223 203 /** @type {TitleSubtitleDrawCall} */ 224 - const drawCall = { y: column.height, type: 'titleSubtitle', data: { category: catName, fields: fields } }; 204 + const drawCall: TitleSubtitleDrawCall = { x: NaN, y: column.height, type: 'titleSubtitle', data: { category: catName, fields: fields } }; 225 205 column.drawStack.push(drawCall); 226 206 } 227 207 228 208 // Drawcalls for kinks 229 209 for (const kink of category.kinks) { 230 210 /** @type {KinkRowDrawCall} */ 231 - const drawCall = { 211 + const drawCall: KinkRowDrawCall = { 212 + x: NaN, 232 213 y: column.height, 233 214 type: 'kinkRow', 234 215 data: { ··· 270 251 271 252 return canvas; 272 253 } 273 - 274 - const modalContainer = document.querySelector('#export-modal-container'); 275 - const modalContent = document.querySelector('#export-modal-content'); 276 - 277 - document.querySelector('#export-image').addEventListener('click', () => { 278 - const canvas = exportImage(); 279 - 280 - // Try 1: https://stackoverflow.com/a/57546936 281 - 282 - if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write !== undefined) { 283 - canvas.toBlob(blob => { 284 - // @ts-ignore 285 - navigator.permissions.query({name: 'clipboard-write'}) 286 - .then(status => { 287 - if (status.state !== 'granted') { 288 - // FAIL SCENARIO 289 - noCopyFallback(); 290 - return; 291 - } 292 - 293 - const item = new ClipboardItem({ 'image/png': blob }); 294 - navigator.clipboard.write([item]); 295 - 296 - alert('Copied to clipboard!'); 297 - }) 298 - .catch(err => { 299 - // FAIL SCENARIO 300 - console.error(err); 301 - noCopyFallback(); 302 - }); 303 - }); 304 - return; 305 - } 306 - 307 - noCopyFallback(); 308 - 309 - function noCopyFallback() { 310 - const image = document.createElement('img'); 311 - image.src = canvas.toDataURL(); 312 - 313 - while (modalContent.firstChild) modalContent.firstChild.remove(); 314 - modalContent.append(image); 315 - 316 - modalContainer.classList.add('is-active'); 317 - } 318 - });
-7
public/helpers.d.ts
··· 1 - declare const tippy: typeof import('tippy.js')['default']; 2 - declare const preact: typeof import('preact'); 3 - declare const preactHooks: typeof import('preact/hooks'); 4 - declare const preactCompat: typeof import('preact/compat'); 5 - declare const Masonic: typeof import('masonic'); 6 - declare const preactSignals: typeof import('@preact/signals'); 7 - declare const preactPortal: typeof import('preact-portal');
+4 -19
public/index.html
··· 39 39 40 40 // dark theme preferred, set document with a `data-theme` attribute 41 41 if (theme === 'dark') { 42 - document.documentElement.classList.add('dark-theme'); 42 + document.documentElement.classList.add('theme-dark'); 43 43 } 44 44 } 45 45 detectColorScheme(); ··· 52 52 /* border-width: 0 0 1px; */ 53 53 /* } */ 54 54 /* } */ 55 - html.dark-theme .table td, html.dark-theme .table th { 55 + html.theme-dark .table td, html.theme-dark .table th { 56 56 border-width: 0 0 1px; 57 57 } 58 58 ··· 217 217 } 218 218 219 219 /* Use white-colored border for choice buttons when dark theme is enabled */ 220 - html.dark-theme .choice { 220 + html.theme-dark .choice { 221 221 border-color: white; 222 222 } 223 223 </style> ··· 450 450 <body> 451 451 <div id="root"></div> 452 452 453 - <script src="https://unpkg.com/@popperjs/core@2" defer></script> 454 - <script src="https://unpkg.com/tippy.js@6" defer></script> 455 - <script src="https://unpkg.com/preact@10.28.3/dist/preact.min.js" defer></script> 456 - <script src="https://unpkg.com/preact@10.28.3/hooks/dist/hooks.umd.js" defer></script> 457 - <script src="https://unpkg.com/preact@10.28.3/compat/dist/compat.umd.js" defer></script> 458 - <script src="https://unpkg.com/@preact/signals-core@1.13.0/dist/signals-core.min.js" defer></script> 459 - <script src="https://unpkg.com/@preact/signals@2.8.0/dist/signals.min.js" defer></script> 460 - <script src="https://unpkg.com/preact-portal@1.1.3/dist/preact-portal.js" defer></script> 461 - <script src="./polyfill-react.js" defer></script> 462 - <script src="https://unpkg.com/masonic@4.1.0/dist/umd/masonic.js" defer></script> 463 - 464 453 <!-- defer is used here so that DOMTools is loaded in time as it's async --> 465 - <script src="pageinfo.js" async></script> 466 - <script src="qfs.js" defer></script> 467 - <script src="preact-tippy.js" defer></script> 468 - <script src="kinklist.js" defer></script> 469 - <script src="exporter.js" defer></script> 454 + <script src="kinklist.js"></script> 470 455 </body> 471 456 472 457 </html>
+175 -230
public/main.tsx
··· 1 1 import { signal, computed } from '@preact/signals'; 2 2 import { h, Fragment, render } from 'preact'; 3 - import { useReducer, useState } from 'preact/hooks'; 3 + import { useEffect, useReducer, useState } from 'preact/hooks'; 4 4 import Portal from 'preact-portal'; 5 5 import { Masonry } from 'masonic'; 6 + import Tippy from '@tippyjs/react'; 7 + import { choiceOptions, choiceOptionIndices } from './base'; 8 + import { exportImage } from './exporter'; 9 + import { compress, decompress } from 'qfs-compression'; 6 10 7 11 const root = document.querySelector('#root'); 8 12 9 - interface Kink { 13 + export interface Kink { 10 14 name: string; 11 - description: string | null; 15 + description: string | undefined; 12 16 } 13 17 14 - interface KinkCategory { 18 + export interface KinkCategory { 15 19 name: string; 16 20 description: string; 17 21 kinks: Kink[]; ··· 32 36 /** @type {Kink[]} */ 33 37 const kinksById: Kink[] = []; 34 38 35 - /** 36 - * @type {Partial<KinkCategory>} 37 - */ 38 - let curKinkCategory: Partial<KinkCategory>; 39 + let curKinkCategory: Partial<KinkCategory> | undefined; 39 40 let curKinkId = 0; 40 41 for (const line of kinkCode) { 41 42 if (line.startsWith('#')) { ··· 55 56 } 56 57 curKinkCategory.participants = removeSymbols(line, '(', ')').split(',').map(e => e.trim()); 57 58 } else if (line.startsWith('*')) { 58 - if (curKinkCategory === undefined) { 59 + if (curKinkCategory?.kinks === undefined) { 59 60 throw new Error('Encountered a kink definition before a kink type declaration'); 60 61 } 61 62 ··· 72 73 const kinkText = signal([...document.querySelectorAll('kinks')].map(e => e.textContent).join('\n')); 73 74 const kinkData = computed(() => parseKinks(kinkText.value)); 74 75 75 - /** @type {[id: string, name: string, color: string][]} */ 76 - const choiceOptions: [id: string, name: string, color: string][] = [ 77 - ['not-entered', 'Not Entered', '#FFFFFF'], 78 - ['favorite', 'Favorite', '#6DB5FE'], 79 - ['like', 'Like', '#23FD22'], 80 - ['okay', 'Okay', '#FDFD6B'], 81 - ['maybe', 'Maybe', '#DB6C00'], 82 - ['no', 'No', '#920000'], 83 - ['try', 'Want To Try', 'pattern'], 84 - ]; 85 - const choiceOptionIndices = Object.fromEntries(choiceOptions.map(([id], i) => [id, i])); 86 - 87 76 /** 88 77 * Maps kink name -> participant -> choice (id string) 89 78 * Entries may be undefined! ··· 122 111 * @returns 123 112 */ 124 113 function LegendChoice({type, typeDescription}: LegendChoiceProps) { 125 - return h('div', { 126 - class: 'level-item is-justify-content-flex-start' 127 - }, [ 128 - h('button', { 129 - class: 'choice ' + type, 130 - title: typeDescription, 131 - disabled: true 132 - }), 133 - h('span', {}, typeDescription) 134 - ]); 114 + return <div class="level-item is-justify-content-flex-start"> 115 + <button class={'choice ' + type} title={typeDescription} disabled /> 116 + <span>{typeDescription}</span> 117 + </div>; 135 118 } 136 119 137 120 function Legend() { 138 - return h(Fragment, null, choiceOptions.map(e => h(LegendChoice, { type: e[0], typeDescription: e[1] }))); 121 + return <>{choiceOptions.map(e => <LegendChoice type={e[0]} typeDescription={e[1]} />)}</>; 139 122 } 140 - 141 - render(h(Legend, null), legend); 142 123 143 124 /** 144 125 * @param {string} str ··· 154 135 return [leftHalf, rightHalf]; 155 136 } 156 137 157 - return [str.trim(), null]; 138 + return [str.trim(), undefined]; 158 139 } 159 140 160 141 /** ··· 189 170 * @returns {string} 190 171 */ 191 172 function bytesArrToBase64(arr: number[] | Uint8Array): string { 173 + // @ts-expect-error 192 174 if (Uint8Array.prototype.toBase64) { 193 175 return Uint8Array.from(arr).toBase64(); 194 176 } ··· 200 182 const c1 = i * 3 + 1 >= l; // case when "=" is on end 201 183 const c2 = i * 3 + 2 >= l; // case when "=" is on end 202 184 const chunk = toBinary(arr[3 * i]) + toBinary(c1 ? 0 : arr[3 * i + 1]) + toBinary(c2 ? 0 : arr[3 * i + 2]); 203 - const r = chunk.match(/.{1,6}/g).map((x, j) => j == 3 && c2 ? '=' : (j == 2 && c1 ? '=' : base64Alphabet[+('0b' + x)])); 185 + const r = chunk.match(/.{1,6}/g)!.map((x, j) => j == 3 && c2 ? '=' : (j == 2 && c1 ? '=' : base64Alphabet[+('0b' + x)])); 204 186 result += r.join(''); 205 187 } 206 188 ··· 222 204 for (let i = 0; i < str.length / 4; i++) { 223 205 const chunk = [...str.slice(4 * i, 4 * i + 4)]; 224 206 const bin = chunk.map(x => base64Alphabet.indexOf(x).toString(2).padStart(6, '0')).join(''); 225 - const bytes = bin.match(/.{1,8}/g).map(x => +('0b' + x)); 207 + const bytes = bin.match(/.{1,8}/g)!.map(x => +('0b' + x)); 226 208 result.push(...bytes.slice(0, 3 - (str[4 * i + 2] == '=' ? 1 : 0) - (str[4 * i + 3] == '=' ? 1 : 0))); 227 209 } 228 210 return new Uint8Array(result); ··· 234 216 * @returns {string} 235 217 */ 236 218 function getSelectedKinkOrDefault(kink: Kink, participant: string): string { 237 - return kinkSelections.has(kink) && kinkSelections.get(kink).get(participant) || 'not-entered'; 219 + return kinkSelections.has(kink) && kinkSelections.get(kink)?.get(participant) || 'not-entered'; 238 220 } 239 221 240 222 /** ··· 245 227 function setKinkSelection(kink: Kink, participant: string, toChoiceId: string) { 246 228 if (!kinkSelections.has(kink)) kinkSelections.set(kink, new Map()); 247 229 248 - kinkSelections.get(kink).set(participant, toChoiceId); 230 + kinkSelections.get(kink)!.set(participant, toChoiceId); 249 231 } 250 232 251 233 /** 252 234 * 4-bit padding reference 253 235 */ 254 236 const PADDING_MARKER = 0xF; 255 - /** 256 - * @returns {string} 257 - */ 258 237 function serializeChoices(): string { 259 - /** 260 - * @type {number[]} 261 - */ 262 238 const bytes: number[] = []; 263 239 264 240 let i = -1; 265 - 266 - /** 267 - * @param {number} num 268 - */ 269 241 function pushNumber(num: number) { 270 242 if (i !== -1) { 271 243 i |= num << 4; ··· 278 250 279 251 for (const kink of kinkData.value.kinksById) { 280 252 const category = findKinkCategory(kink); 281 - for (const participant of category.participants) { 282 - pushNumber(choiceOptionIndices[getSelectedKinkOrDefault(kink, participant)]); 253 + if (category) { 254 + for (const participant of category.participants) { 255 + pushNumber(choiceOptionIndices[getSelectedKinkOrDefault(kink, participant)]); 256 + } 283 257 } 284 258 } 285 259 ··· 289 263 290 264 // return bytesArrToBase64(bytes); 291 265 292 - return '!' + bytesArrToBase64(qfs.compress(new Uint8Array(bytes))); 266 + return '!' + bytesArrToBase64(compress(new Uint8Array(bytes))); 293 267 } 294 268 295 - /** 296 - * @param {string} base64 297 - */ 298 269 function deserializeChoices(base64: string) { 299 270 const bytes = base64.startsWith('!') 300 - ? qfs.decompress(base64ToBytesArr(base64.slice(1))) 271 + ? decompress(base64ToBytesArr(base64.slice(1))) 301 272 : base64ToBytesArr(base64); 302 273 303 274 let isUpperHalf = false; 304 275 let index = 0; 305 276 for (const kink of kinkData.value.kinksById) { 306 277 const category = findKinkCategory(kink); 278 + if (!category) { 279 + continue; 280 + } 281 + 307 282 for (const participant of category.participants) { 308 283 const byte = bytes[index]; 309 284 let choice; ··· 354 329 * @returns 355 330 */ 356 331 function KinkChoiceButton({kink, participant, choiceId, choiceDescription}: KinkChoiceButtonProps) { 357 - // <div class="column"><button class="choice notEntered" title="Not Entered"></button></div> 358 - 359 332 /** 360 333 * @param {boolean} state 361 334 * @param {'select' | 'deselect'} action ··· 367 340 } else if (action === 'deselect') { 368 341 return false; 369 342 } 343 + return state; 370 344 } 371 345 372 346 const [selected, update] = useReducer(reducer, false); ··· 383 357 384 358 choices.set(choiceId, update); 385 359 386 - // kink -> participant -> choice option -> button element 387 - 388 - return h('div', { class: 'column' }, [ 389 - h('button', { 390 - class: `choice ${choiceId}${selected ? ' selected' : ''}`, 391 - title: choiceDescription, 392 - onClick: () => { 360 + return <div class="column"> 361 + <button 362 + class={`choice ${choiceId}${selected ? ' selected' : ''}`} 363 + title={choiceDescription} 364 + onClick={() => { 393 365 setKinkSelection(kink, participant, choiceId); 394 366 395 - // Unselect all other buttons for this kink+participant combo, and select the applicable one. 396 367 for (const [thatChoiceId, thatButton] of getKinkButtonStates(kink, participant).entries()) { 397 368 if (thatChoiceId === choiceId) { 398 369 thatButton('select'); ··· 402 373 } 403 374 404 375 window.location.hash = serializeChoices(); 405 - } 406 - }) 407 - ]); 376 + }} 377 + /> 378 + </div>; 408 379 } 409 380 410 381 /** ··· 414 385 * @returns {Map<string, (action: "select" | "deselect") => void>} 415 386 */ 416 387 function getKinkButtonStates(kink: Kink, participant: string): Map<string, (action: "select" | "deselect") => void> { 417 - return kinkButtons.get(kink).get(participant); 388 + // console.log(kinkButtons, kink, participant); 389 + return kinkButtons.get(kink)!.get(participant)!; 418 390 } 419 391 420 - /** 421 - * @param {{kinkCategory: KinkCategory, kink: Kink}} props 422 - */ 423 392 function TheKink({kinkCategory, kink}: { kinkCategory: KinkCategory; kink: Kink; }) { 424 - /* 425 - <tr class="kinkRow kink-skinny"> 426 - <td> 393 + return <tr class="kinks-row"> 394 + {kinkCategory.participants.map(participant => <td data-choice-type={participant}> 427 395 <div class="choices choice-general"> 428 396 <div class="columns is-mobile is-gapless"> 429 - <div class="column"><button class="choice not-entered" title="Not Entered"></button></div> 430 - <div class="column"><button class="choice favorite" title="Favorite"></button></div> 431 - <div class="column"><button class="choice like" title="Like"></button></div> 432 - <div class="column"><button class="choice okay" title="Okay"></button></div> 433 - <div class="column"><button class="choice maybe" title="Maybe"></button></div> 434 - <div class="column"><button class="choice no" title="No"></button></div> 435 - <div class="column"><button class="choice try" title="Want To Try"></button></div> 397 + {choiceOptions.map(([id, name]) => <KinkChoiceButton 398 + key={kink + '_' + participant + '_' + id} 399 + kink={kink} 400 + participant={participant} 401 + choiceId={id} 402 + choiceDescription={name} 403 + />)} 436 404 </div> 437 405 </div> 406 + </td>)} 407 + <td> 408 + {kink.description != null 409 + ? <Tippy content={kink.description}> 410 + <span class="has-description" title={kink.description}> 411 + {kink.name} 412 + </span> 413 + </Tippy> 414 + : <span>{kink.name}</span> 415 + } 438 416 </td> 439 - <td>Skinny</td> 440 - </tr> 441 - */ 442 - 443 - return h('tr', { class: 'kinks-row' }, [ 444 - kinkCategory.participants.map(participant => h('td', { 'data-choice-type': participant }, [ 445 - h('div', { class: 'choices choice-general' }, [ 446 - h('div', { class: 'columns is-mobile is-gapless' }, [ 447 - choiceOptions.map(e => h(KinkChoiceButton, { 448 - key: kink + '_' + participant + '_' + e[0], 449 - kink, 450 - participant, 451 - choiceId: e[0], 452 - choiceDescription: e[1] 453 - })) 454 - ]) 455 - ]) 456 - ])), 457 - h('td', null, [ 458 - kink.description != null 459 - ? h(Tippy, { content: kink.description }, [ 460 - h('span', { 461 - class: 'has-description', 462 - title: kink.description 463 - }, kink.name) 464 - ]) 465 - : h('span', null, kink.name), 466 - ]) 467 - ]); 417 + </tr>; 468 418 } 469 419 470 420 interface TheKinkCategoryProps { 471 421 kinkCategory: KinkCategory; 472 422 } 473 423 474 - /** 475 - * @param {TheKinkCategoryProps} kinkCategory 476 - */ 477 424 function TheKinkCategory({kinkCategory}: TheKinkCategoryProps) { 478 - /* 479 - <div class="column is-narrow"> 480 - <!--<p class="notification is-primary"> 481 - <code class="html">is-narrow</code><br> 482 - First Column 483 - </p>--> 484 - <table class="table is-striped is-narrow is-hoverable"> 425 + return <div class="masonry-inner" data-num-participants={String(kinkCategory.participants.length)}> 426 + <h1 class="subtitle kinks-subtitle"> 427 + {kinkCategory.name} 428 + {kinkCategory.description != null && <> 429 + {' '} 430 + <Tippy content={kinkCategory.description}> 431 + <span class="has-subtitle-description" aria-description={kinkCategory.description}> 432 + (?) 433 + </span> 434 + </Tippy> 435 + </>} 436 + </h1> 437 + <table class="table kinks-table is-striped is-narrow is-hoverable"> 485 438 <thead> 486 - <th class="choicesCol">General</th> 439 + {kinkCategory.participants.map(participant => <th class="kinks-header">{participant}</th>)} 487 440 </thead> 488 441 <tbody> 489 - <tr class="kinkRow kink-skinny"> 490 - <td> 491 - <div class="choices choice-general"> 492 - <div class="columns is-mobile is-gapless"> 493 - <div class="column"><button class="choice notEntered" title="Not Entered"></button></div> 494 - <div class="column"><button class="choice favorite" title="Favorite"></button></div> 495 - <div class="column"><button class="choice like" title="Like"></button></div> 496 - <div class="column"><button class="choice okay" title="Okay"></button></div> 497 - <div class="column"><button class="choice maybe" title="Maybe"></button></div> 498 - <div class="column"><button class="choice no" title="No"></button></div> 499 - <div class="column"><button class="choice try" title="Want To Try"></button></div> 500 - </div> 501 - </div> 502 - </td> 503 - <td>Skinny</td> 504 - </tr> 505 - */ 506 - 507 - return h( 508 - 'div', 509 - { class: 'masonry-inner', 'data-num-participants': String(kinkCategory.participants.length) }, 510 - [ 511 - h( 512 - 'h1', 513 - { class: 'subtitle kinks-subtitle' }, 514 - [ 515 - kinkCategory.name, 516 - ...( 517 - kinkCategory.description != null 518 - ? [ 519 - ' ', 520 - h(Tippy, { 521 - content: kinkCategory.description, 522 - }, [ 523 - h('span', { 524 - class: 'has-subtitle-description', 525 - ariaDescription: kinkCategory.description 526 - }, '(?)') 527 - ]) 528 - ] 529 - : [] 530 - ) 531 - ] 532 - ), 533 - h('table', { class: 'table kinks-table is-striped is-narrow is-hoverable' }, [ 534 - h('thead', null, [ 535 - ...kinkCategory.participants.map(e => h('th', { class: 'kinks-header' }, e)) 536 - ]), 537 - h('tbody', null, [ 538 - ...kinkCategory.kinks.map(kink => h(TheKink, { kinkCategory, kink }, null)) 539 - ]) 540 - ]) 541 - ] 542 - ); 442 + {kinkCategory.kinks.map(kink => <TheKink kinkCategory={kinkCategory} kink={kink} />)} 443 + </tbody> 444 + </table> 445 + </div>; 543 446 } 544 447 545 448 interface MasonryItemProps { ··· 548 451 width: number; 549 452 } 550 453 551 - /** 552 - * @param {MasonryItemProps} props 553 - */ 554 454 function MasonryItem({ index, data: kinkCategory, width }: MasonryItemProps) { 555 - return h(TheKinkCategory, { kinkCategory }, null); 455 + return <TheKinkCategory kinkCategory={kinkCategory} />; 556 456 } 557 457 558 - function Root() { 559 - return h(Masonry, { 560 - items: kinkData.value.kinkCategories, 561 - render: MasonryItem, 562 - columnWidth: 460, 563 - maxColumnCount: 8, 564 - }); 458 + function KinksSection() { 459 + return <Masonry 460 + items={kinkData.value.kinkCategories} 461 + render={MasonryItem} 462 + columnWidth={460} 463 + maxColumnCount={8} 464 + overscanBy={10} 465 + />; 565 466 } 566 467 567 468 function Modal({ children, open, onClose }: { children: import('preact').ComponentChildren; open: boolean; onClose: () => void; }) { 568 - return h( 569 - Portal, 570 - { into: 'body' }, 571 - h( 572 - 'div', 573 - { class: `modal${open ? ' is-active' : ''}` }, 574 - [ 575 - h('div', { class: 'modal-background' }), 576 - h('div', { class: 'modal-content' }, children), 577 - h('button', { class: 'modal-close is-large', ariaLabel: 'close' }) 578 - ] 579 - ) 580 - ); 469 + return <Portal into="body"> 470 + <div class={`modal${open ? ' is-active' : ''}`}> 471 + <div class="modal-background" /> 472 + <div class="modal-content">{children}</div> 473 + <button class="modal-close is-large" aria-label="close" /> 474 + </div> 475 + </Portal>; 581 476 } 582 477 583 478 const changelog = [ ··· 623 518 { version: '6 - October 9th 2020', changes: [ 624 519 'Rewrite the entire list, using Bulma, in order to completely eliminate scrolling issues', 625 520 ]}, 626 - { version: '5 - September 21st 2020', changes: [ 521 + { version: '5 - September 21th 2020', changes: [ 627 522 'Add Clown, Hate sex and Choking to list', 628 523 'Remove "Handholding" meme entry from list', 629 524 ]}, ··· 645 540 646 541 function RealRoot() { 647 542 const [changelogOpen, setChangelogOpen] = useState(false); 543 + const [exportImageOpen, setExportImageOpen] = useState(false); 544 + const [exportModalContent, setExportModalContent] = useState<HTMLElement | null>(null); 545 + 546 + function exportImageCallback() { 547 + const canvas = exportImage(kinkData.value.kinkCategories, kinkData.value.kinksById, getSelectedKinkOrDefault); 548 + 549 + // Try 1: https://stackoverflow.com/a/57546936 550 + 551 + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write !== undefined) { 552 + canvas.toBlob(blob => { 553 + if (blob === null) { 554 + noCopyFallback(); 555 + return; 556 + } 557 + 558 + // @ts-ignore 559 + navigator.permissions.query({name: 'clipboard-write'}) 560 + .then(status => { 561 + if (status.state !== 'granted') { 562 + // FAIL SCENARIO 563 + noCopyFallback(); 564 + return; 565 + } 566 + 567 + const item = new ClipboardItem({ 'image/png': blob }); 568 + navigator.clipboard.write([item]); 569 + 570 + alert('Copied to clipboard!'); 571 + }) 572 + .catch(err => { 573 + // FAIL SCENARIO 574 + console.error(err); 575 + noCopyFallback(); 576 + }); 577 + }); 578 + return; 579 + } 580 + 581 + noCopyFallback(); 582 + 583 + function noCopyFallback() { 584 + const image = document.createElement('img'); 585 + image.src = canvas.toDataURL(); 586 + 587 + setExportModalContent(image); 588 + setExportImageOpen(true); 589 + } 590 + } 591 + 592 + useEffect(() => { 593 + if (window.location.hash) { 594 + try { 595 + deserializeChoices(window.location.hash.slice(1)); 596 + } catch (err) { 597 + console.error('Failed to load saved kinks:', err); 598 + } 599 + } 600 + }); 648 601 649 602 return <div id="root"> 650 603 <nav class="navbar px-5 mt-5 mb-2" role="navigation" aria-label="main navigation"> ··· 653 606 <h1 class="title">Kink list</h1> 654 607 </div> 655 608 <div class="navbar-item"> 656 - <button id="export-image" class="button is-primary">Export</button> 609 + <button id="export-image" class="button is-primary" onClick={exportImageCallback}>Export</button> 657 610 </div> 658 611 <div class="navbar-item"> 659 - <button id="view-changelog" class="button is-primary">Changelog</button> 612 + <button id="view-changelog" class="button is-primary" onClick={() => setChangelogOpen(true)}>Changelog</button> 660 613 </div> 661 614 <div class="navbar-item"> 662 615 <label class="checkbox"> 663 - <input id="dark-theme" type="checkbox" /> 664 - Dark theme 616 + <input id="dark-theme" type="checkbox" onChange={event => { 617 + if (event.currentTarget.checked) { 618 + localStorage.setItem('theme', 'dark'); 619 + document.documentElement.classList.add('theme-dark'); 620 + } else { 621 + localStorage.setItem('theme', 'light'); 622 + document.documentElement.classList.remove('theme-dark'); 623 + } 624 + }} checked={document.documentElement.classList.contains('theme-dark')} /> 625 + {' '}Dark theme 665 626 </label> 666 627 </div> 667 628 </div> ··· 669 630 670 631 <nav class="level px-5"> 671 632 <div id="legend" class="kinks-legend level-left"> 633 + <Legend /> 672 634 </div> 673 635 </nav> 674 636 675 637 <section class="section kinks-section"> 676 - <div id="root" class="masonry"> 677 - </div> 638 + <KinksSection /> 678 639 </section> 679 640 680 - <div id="export-modal-container" class="modal"> 681 - <div class="modal-background"></div> 682 - <div class="modal-content"> 683 - 684 - <div class="box"> 685 - <h1 class="title">Exported! Copy the image to your clipboard or save it now.</h1> 686 - <div id="export-modal-content"></div> 687 - </div> 688 - 641 + <Modal open={exportImageOpen} onClose={() => setExportImageOpen(false)}> 642 + <div class="box"> 643 + <h1 class="title">Exported! Copy the image to your clipboard or save it now.</h1> 644 + <div id="export-modal-content">{exportModalContent}</div> 689 645 </div> 690 - <button class="modal-close is-large" aria-label="close"></button> 691 - </div> 646 + </Modal> 692 647 693 648 <Modal open={changelogOpen} onClose={() => setChangelogOpen(false)}> 694 649 <div class="box content"> ··· 706 661 </div>; 707 662 } 708 663 709 - render(h(Root, null), root); 710 - 711 - setTimeout(() => { 712 - if (window.location.hash) { 713 - try { 714 - deserializeChoices(window.location.hash.slice(1)); 715 - } catch (err) { 716 - console.error('Failed to load saved kinks:', err); 717 - } 718 - } 719 - }, 0); 664 + render(<RealRoot />, root!);
-28
public/pageinfo.js
··· 1 - /* eslint-disable unicorn/no-useless-undefined */ 2 - /* eslint-disable unicorn/prevent-abbreviations */ 3 - /* eslint-disable indent */ 4 - 5 - // Theme switcher 6 - const darkThemeSwitch = document.querySelector('#dark-theme'); 7 - 8 - darkThemeSwitch.addEventListener('change', event => { 9 - if (event.target.checked) { 10 - localStorage.setItem('theme', 'dark'); 11 - document.documentElement.classList.add('theme-dark'); 12 - darkThemeSwitch.checked = true; 13 - } else { 14 - localStorage.setItem('theme', 'light'); 15 - document.documentElement.classList.remove('theme-dark'); 16 - darkThemeSwitch.checked = false; 17 - } 18 - }, false); 19 - 20 - darkThemeSwitch.checked = document.documentElement.classList.contains('theme-dark'); 21 - 22 - const viewChangelog = document.querySelector('#view-changelog'); 23 - 24 - const changelogModal = document.querySelector('#changelog-modal-container'); 25 - 26 - viewChangelog.addEventListener('click', () => { 27 - changelogModal.classList.add('is-active'); 28 - });
-4
public/polyfill-react.js
··· 1 - // Alias React UMD globals to Preact globals 2 - window.React = window.preactCompat; 3 - window.react = window.preactCompat; 4 - window.ReactDOM = window.preactCompat;
+8
public/preact-portal.d.ts
··· 1 + declare module 'preact-portal' { 2 + import { ComponentChildren, JSX } from 'preact'; 3 + export interface PortalProps { 4 + children: ComponentChildren; 5 + into: string | Element; 6 + } 7 + export default function Portal(props: PortalProps): JSX.Element; 8 + }
-28
public/preact-tippy.js
··· 1 - // @ts-check 2 - 3 - /** 4 - * @typedef {object} TippyProps 5 - * @property {import("tippy.js").Props} [config] 6 - * @property {string} content 7 - * @property {import("preact").ComponentChildren} [children] 8 - * @property {string} [class] 9 - */ 10 - 11 - /** 12 - * @param {TippyProps} props 13 - */ 14 - function Tippy({ config, content, children, class: className }) { 15 - const ref = preactHooks.useRef(null); 16 - 17 - preactHooks.useEffect(() => { 18 - const element = ref.current; 19 - if (element === null) return; 20 - 21 - const instance = tippy(element, {content, ...config}); 22 - return () => { 23 - instance.destroy(); 24 - }; 25 - }, []); 26 - 27 - return h('span', { ref, className }, children); 28 - }
-390
public/qfs.js
··· 1 - // @ts-check 2 - 3 - /*! 4 - MIT License 5 - 6 - Copyright (c) 2021 Sebastiaan Marynissen 7 - 8 - Permission is hereby granted, free of charge, to any person obtaining a copy 9 - of this software and associated documentation files (the "Software"), to deal 10 - in the Software without restriction, including without limitation the rights 11 - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 - copies of the Software, and to permit persons to whom the Software is 13 - furnished to do so, subject to the following conditions: 14 - 15 - The above copyright notice and this permission notice shall be included in all 16 - copies or substantial portions of the Software. 17 - 18 - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 - SOFTWARE. 25 - */ 26 - 27 - const qfs = (() => { 28 - 29 - // # index.js 30 - // A JavaScript implementation of the QFS compression and decompression 31 - // algorithms. Based on wouanagaine's C library found here: https://github.com/ 32 - // wouanagaine/SC4Mapper-2013/blob/master/Modules/qfs.c 33 - 34 - // # decompress(input) 35 - // JavaScript implementation of the QFS decompression algorithm. 36 - // IMPORTANT! In some cases, the first 4 bytes indicate the size of the input 37 - // buffer. We **don't** detect this automatically, you need to discard those 4 38 - // bytes yourself! 39 - 40 - /** 41 - * @param {Uint8Array} input 42 - * @returns {Uint8Array} 43 - */ 44 - function decompress(input) { 45 - 46 - // Check magic number. 47 - let [a, b] = input; 48 - if (!((a === 0x10 || a === 0x11) && b === 0xfb)) { 49 - throw new Error( 50 - 'Input is not a valid QFS compressed buffer! Did you forget to truncate the size bytes?' 51 - ); 52 - } 53 - 54 - // Create an malloc function based on the input buffer class we received. 55 - const malloc = createMalloc(input); 56 - 57 - // First two bytes are 0x10fb (QFS id), then follows the *uncompressed* 58 - // size, which allows us to prepare a buffer for it. 59 - const size = 0x10000*input[2] + 0x100*input[3] + input[4]; 60 - const out = malloc(size); 61 - 62 - // Start decoding now. Note that trailing bytes are handled separately, 63 - // indicated by a control character >= 0xfc. 64 - let inpos = input[0] & 0x01 ? 8 : 5; 65 - let outpos = 0; 66 - while (inpos < input.length && input[inpos] < 0xfc) { 67 - let code = input[inpos]; 68 - let a = input[inpos+1]; 69 - let b = input[inpos+2]; 70 - if (!(code & 0x80)) { 71 - let length = code & 3; 72 - memcpy(out, outpos, input, inpos+2, length); 73 - inpos += length+2; 74 - outpos += length; 75 - 76 - // Repeat data that is already in the output. This is the essence 77 - // of the compression algorithm. 78 - length = ((code & 0x1c) >> 2) + 3; 79 - let offset = ((code >> 5) << 8) + a + 1; 80 - memcpy(out, outpos, out, outpos-offset, length); 81 - outpos += length; 82 - 83 - } else if (!(code & 0x40)) { 84 - let length = (a >> 6) & 3; 85 - memcpy(out, outpos, input, inpos+3, length); 86 - inpos += length+3; 87 - outpos += length; 88 - 89 - // Repeat data already in the outpot. 90 - length = (code & 0x3f) + 4; 91 - let offset = (a & 0x3f)*256 + b + 1; 92 - memcpy(out, outpos, out, outpos-offset, length); 93 - outpos += length; 94 - 95 - } else if (!(code & 0x20)) { 96 - let c = input[inpos+3]; 97 - let length = code & 3; 98 - memcpy(out, outpos, input, inpos+4, length); 99 - inpos += length+4; 100 - outpos += length; 101 - 102 - // Repeat data that is already in the output. 103 - length = ((code>>2) & 3)*256 + c + 5; 104 - let offset = ((code & 0x10)<<12)+256*a + b + 1; 105 - memcpy(out, outpos, out, outpos-offset, length); 106 - outpos += length; 107 - 108 - } else { 109 - 110 - // The last case means there's no compression really, we just copy 111 - // as is. 112 - let length = (code & 0x1f)*4 + 4; 113 - memcpy(out, outpos, input, inpos+1, length); 114 - inpos += length+1; 115 - outpos += length; 116 - 117 - } 118 - 119 - } 120 - 121 - // Trailing bytes. This is indicated by the control character being 122 - // greater than 0xfc. 123 - if (inpos < input.length && outpos < out.length) { 124 - let length = input[inpos] & 3; 125 - memcpy(out, outpos, input, inpos+1, length); 126 - outpos += length; 127 - } 128 - 129 - // Check if everything is correct. 130 - if (outpos !== out.length) { 131 - throw new Error('Error when decompressing!'); 132 - } 133 - 134 - // We're done! 135 - return out; 136 - 137 - } 138 - 139 - // # createMalloc(buffer) 140 - // Returns an `malloc()` function which reuses the constructor of the given 141 - // buffer. That way, if we receive an Uint8Array, we output one as well and 142 - // vice versa: if we receive a Node.js buffer - even in the browser - we return 143 - // one. 144 - /** 145 - * @param {Uint8Array} buffer 146 - * @returns {(size: number) => Uint8Array} 147 - */ 148 - function createMalloc(buffer) { 149 - const Ctor = buffer.constructor; 150 - // @ts-expect-error 151 - const Constructor = Ctor[Symbol.species] || Ctor; 152 - return size => new Constructor(size); 153 - } 154 - 155 - // # memcpy(out, outpos, input, inpos, length) 156 - // LZ-compatible memcopy function. We don't use buffer.copy here because we 157 - // might be copying from ourselves as well! 158 - /** 159 - * @param {Uint8Array} out 160 - * @param {number} outpos 161 - * @param {Uint8Array} input 162 - * @param {number} inpos 163 - * @param {number} length 164 - */ 165 - function memcpy(out, outpos, input, inpos, length) { 166 - let i = length; 167 - while (i--) { 168 - out[outpos++] = input[inpos++]; 169 - } 170 - } 171 - 172 - // # SmartBuffer 173 - // Tiny implementation of a smart buffer that only supports writing raw 174 - // *bytes*. 175 - const DEFAULT_SIZE = 4096; 176 - const MAX_SIZE = 32*1024*1024; 177 - class SmartBuffer { 178 - /** 179 - * @param {{ (size: number): Uint8Array; }} malloc 180 - */ 181 - constructor(malloc) { 182 - this.length = 0; 183 - this.buffer = malloc(DEFAULT_SIZE); 184 - this.malloc = malloc; 185 - } 186 - /** 187 - * @param {number} byte 188 - */ 189 - push(byte) { 190 - let { buffer } = this; 191 - if (buffer.length < this.length+1) { 192 - let newLength = Math.min(MAX_SIZE, 2*buffer.length); 193 - let newBuffer = this.malloc(newLength); 194 - newBuffer.set(buffer); 195 - this.buffer = newBuffer; 196 - } 197 - this.buffer[this.length++] = byte; 198 - } 199 - toBuffer() { 200 - return this.buffer.subarray(0, this.length); 201 - } 202 - } 203 - 204 - // Performance calibration constants for compression. 205 - const QFS_MAXITER = 50; 206 - 207 - // # compress(input, opts) 208 - // A JavaScript implementation of QFS compression. We use a smart buffer here 209 - // so that we don't have to manage the output size manually. 210 - /** 211 - * @param {Uint8Array} input 212 - * @param {{ windowBits?: number, includeSize?: boolean }} [opts] 213 - * @returns {Uint8Array} 214 - */ 215 - function compress(input, opts = {}) { 216 - 217 - // Important! If the input buffer is larger than 16MB, we can't compress 218 - // because that would cause a bit overflow and the size to be stored as 0! 219 - const inlen = input.length; 220 - if (inlen > 0xffffff) { 221 - throw new Error(`Input size cannot be larger than ${0xffffff} bytes!`); 222 - } 223 - 224 - // Constants for tuning performance. 225 - const { windowBits = 17, includeSize = false } = opts; 226 - const WINDOW_LEN = 2**windowBits; 227 - const WINDOW_MASK = WINDOW_LEN-1; 228 - 229 - // Prepare our buffer to which we'll write the output. 230 - const malloc = createMalloc(input); 231 - const out = new SmartBuffer(malloc); 232 - const push = out.push.bind(out); 233 - 234 - // Initialize our occurence tables. The C++ code is rather difficult to 235 - // understand here as there is a lot of pointer magic involved.Anyway, 236 - // `rev_similar` is an array where we store the offsets that we calculated 237 - // every input position. 238 - let rev_similar = new Int32Array(WINDOW_LEN).fill(-1); 239 - 240 - // The `rev_last` code is a lot more difficult to understand though. In 241 - // C++ it's a data structure that can hold 256 x 256 integer pointers. 242 - // This is actually a table for tracking the *offset* at which the last 243 - // [a, b] byte 244 - // sequence was found! We implement this table simply as a flat array. of 245 - // 256*256 size, which means our indices have to be calculated as 256*a + 246 - // b. 247 - let rev_last = new Int32Array(256*256).fill(-1); 248 - 249 - // The "fill" method simply writes uncompressed data to the output stream. 250 - // We always do this right before writing away a "best length" match. 251 - let inpos = 0; 252 - let lastwrot = 0; 253 - const fill = () => { 254 - while (inpos - lastwrot >= 4) { 255 - let length = Math.floor((inpos - lastwrot)/4) - 1; 256 - if (length > 0x1b) length = 0x1b; 257 - push(0xe0 + length); 258 - length = 4*length + 4; 259 - while (length--) push(input[lastwrot++]); 260 - } 261 - }; 262 - 263 - // If we have to include the size of the compressed buffer as well, we'll 264 - // reserve 4 bytes to write this away once we know the size. 265 - if (includeSize) { 266 - for (let i = 0; i < 4; i++) push(0); 267 - } 268 - 269 - // Write the header to the output. 270 - push(0x10); 271 - push(0xfb); 272 - push(inlen >> 16); 273 - push((inlen >> 8) & 0xff); 274 - push(inlen & 0xff); 275 - 276 - // Main encoding loop. 277 - const max = inlen-1; 278 - for (; inpos < max; inpos++) { 279 - 280 - // Update the occurence tables. The C++ code uses some pointer magic 281 - // for this, but we will do it in a more modern way. We simply update 282 - // the last time this combination was found. 283 - let index = 256*input[inpos] + input[inpos+1]; 284 - let offs = rev_similar[inpos & WINDOW_MASK] = rev_last[index]; 285 - rev_last[index] = inpos; 286 - 287 - // If this part has already been compressed, skip ahead. 288 - if (inpos < lastwrot) continue; 289 - 290 - // Look for a redundancy now. 291 - let bestlen = 0; 292 - let bestoffs = 0; 293 - let i = 0; 294 - while (offs >= 0 && inpos-offs < WINDOW_LEN && i++ < QFS_MAXITER) { 295 - let length = 2; 296 - let incmp = inpos + 2; 297 - let inref = offs + 2; 298 - while ( 299 - incmp < inlen && 300 - inref < inlen && 301 - input[incmp++] === input[inref++] && 302 - length < 1028 303 - ) { 304 - length++; 305 - } 306 - if (length > bestlen) { 307 - bestlen = length; 308 - bestoffs = inpos-offs; 309 - } 310 - offs = rev_similar[offs & WINDOW_MASK]; 311 - } 312 - 313 - // Check if redundancy is good enough. 314 - if (bestlen > inlen-inpos) { 315 - bestlen = inpos-inlen; 316 - } else if ( 317 - bestlen <= 2 || 318 - (bestlen === 3 && bestoffs > 1024) || 319 - (bestlen === 4 && bestoffs > 16384) 320 - ) { 321 - continue; 322 - } 323 - 324 - // If we did not find a suitable redundancy length by now, continue. 325 - // We do this to avoid additional nesting. 326 - if (!bestlen) continue; 327 - 328 - // Cool, we found a good redundancy. Now write away. 329 - fill(); 330 - let length = inpos-lastwrot; 331 - if (bestlen <= 10 && bestoffs <= 1024) { 332 - 333 - // 2-byte control character. 334 - let d = bestoffs-1; 335 - push(((d>>8)<<5) + ((bestlen-3)<<2) + length); 336 - push(d & 0xff); 337 - while (length--) push(input[lastwrot++]); 338 - lastwrot += bestlen; 339 - 340 - } else if (bestlen <= 67 && bestoffs <= 16384) { 341 - 342 - // 3-byte control character. 343 - let d = bestoffs-1; 344 - push(0x80 + (bestlen-4)); 345 - push((length<<6) + (d>>8)); 346 - push(d & 0xff); 347 - while (length--) push(input[lastwrot++]); 348 - lastwrot += bestlen; 349 - 350 - } else if (bestlen <= 1028 && bestoffs < WINDOW_LEN) { 351 - 352 - // 4-byte control character. 353 - let d = bestoffs-1; 354 - push(0xC0 + ((d>>16)<<4) + (((bestlen-5)>>8)<<2) + length); 355 - push((d>>8) & 0xff); 356 - push(d & 0xff); 357 - push((bestlen-5) & 0xff); 358 - while (length--) push(input[lastwrot++]); 359 - lastwrot += bestlen; 360 - 361 - } 362 - 363 - } 364 - 365 - // Grab the length of what still needs to be processed and write it away 366 - // as a control character. Then, write the raw contents. 367 - inpos = inlen; 368 - fill(); 369 - let length = inpos - lastwrot; 370 - push(0xfc + length); 371 - while (length--) push(input[lastwrot++]); 372 - 373 - // If we have to include the size, of the *compressed* buffer, do that as 374 - // well. 375 - let buffer = out.toBuffer(); 376 - if (includeSize) { 377 - let size = out.length - 4; 378 - buffer[0] = size & 0xff; 379 - buffer[1] = (size >> 8) & 0xff; 380 - buffer[2] = (size >> 16) & 0xff; 381 - buffer[3] = (size >> 24) & 0xff; 382 - } 383 - 384 - // We're done! 385 - return buffer; 386 - 387 - } 388 - 389 - return { decompress, compress }; 390 - })();
+10 -4
public/tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 3 "strict": true, 4 - "strictNullChecks": false, 5 - "module": "commonjs", 4 + "module": "esnext", 6 5 "target": "es6", 7 6 "lib": ["ESNext", "dom", "dom.iterable"], 8 - "jsx": "preserve" 7 + "jsx": "react-jsx", 8 + "jsxImportSource": "preact", 9 + "paths": { 10 + "react": ["../node_modules/preact/compat/"], 11 + "react/jsx-runtime": ["../node_modules/preact/jsx-runtime"], 12 + "react-dom": ["../node_modules/preact/compat/"], 13 + "react-dom/*": ["../node_modules/preact/compat/*"] 14 + } 9 15 }, 10 - "exclude": ["node_modules", "**/node_modules/*", "kinklist.js"], 16 + "exclude": ["kinklist.js"], 11 17 "include": ["*"] 12 18 }
+22 -1
rolldown.config.js
··· 1 + // @ts-check 1 2 import { defineConfig } from 'rolldown'; 2 3 3 4 export default defineConfig({ 4 - input: 'public/main.ts', 5 + input: 'public/main.tsx', 6 + tsconfig: 'public/tsconfig.json', 5 7 output: { 6 8 file: 'public/kinklist.js', 9 + format: 'iife', 10 + generatedCode: { 11 + preset: 'es5' 12 + } 7 13 }, 14 + platform: 'browser', 15 + transform: { 16 + jsx: { 17 + runtime: 'automatic', 18 + importSource: 'preact' 19 + } 20 + }, 21 + resolve: { 22 + alias: { 23 + "react": "preact/compat", 24 + "react-dom/test-utils": "preact/test-utils", 25 + "react-dom": "preact/compat", 26 + "react/jsx-runtime": "preact/jsx-runtime" 27 + } 28 + } 8 29 });