@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
* - Resizes oversized images (caps the short side; the long side rides free)
|
|
6
6
|
* - Strips all metadata (privacy)
|
|
7
7
|
* - Converts to WebP format (JPEG fallback when WebP encoding is unavailable)
|
|
8
|
+
* - Samples a blurhash placeholder from the decoded pixels
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The work runs in a Web Worker (`image-worker.ts`) wherever the browser can
|
|
11
|
+
* decode and encode there — every engine with OffscreenCanvas. Decoding a
|
|
12
|
+
* photo and encoding the result are the heaviest things the composer does on
|
|
13
|
+
* the client; on the main thread they froze the page for their whole
|
|
14
|
+
* duration, and several photos at once froze it for seconds. Browsers without
|
|
15
|
+
* OffscreenCanvas, and any file the worker cannot decode, take the in-page
|
|
16
|
+
* canvas path below.
|
|
17
|
+
*
|
|
18
|
+
* EXIF orientation is handled by the browser on both paths — `<img>`, canvas
|
|
19
|
+
* `drawImage`, and `createImageBitmap` with `imageOrientation: "from-image"`
|
|
20
|
+
* all report and draw the oriented image.
|
|
12
21
|
*
|
|
13
22
|
* Long and wide screenshots (chat logs, articles, wide tables) lose their
|
|
14
23
|
* text legibility if the short side is scaled down, so the resize step caps
|
|
@@ -17,25 +26,104 @@
|
|
|
17
26
|
* untouched, so images of any length are supported.
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
import { encode } from "blurhash";
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_IMAGE_PROCESS_OPTIONS,
|
|
32
|
+
blurhashDimensions,
|
|
33
|
+
planImageProcessing,
|
|
34
|
+
resolveEncodedMimeType,
|
|
35
|
+
type ImageProcessOptions,
|
|
36
|
+
type ImageWorkerRequest,
|
|
37
|
+
type ImageWorkerResponse,
|
|
38
|
+
} from "./image-plan.js";
|
|
39
|
+
import {
|
|
40
|
+
createImageWorker,
|
|
41
|
+
supportsImageWorker,
|
|
42
|
+
} from "./image-worker-client.js";
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
type ProcessOptions = Partial<ImageProcessOptions>;
|
|
45
|
+
|
|
46
|
+
export interface ProcessResult {
|
|
47
|
+
blob: Blob;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
/** False when `blob` is the untouched original (too large to re-encode). */
|
|
51
|
+
processed: boolean;
|
|
52
|
+
/** Absent only when the pixels could not be sampled. */
|
|
53
|
+
blurhash?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ProcessToFileResult {
|
|
57
|
+
file: File;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
blurhash?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Worker path ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
interface PendingRequest {
|
|
66
|
+
resolve(result: ProcessResult): void;
|
|
67
|
+
reject(error: Error): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let worker: Worker | null = null;
|
|
71
|
+
let nextRequestId = 1;
|
|
72
|
+
const pending = new Map<number, PendingRequest>();
|
|
73
|
+
|
|
74
|
+
/** One worker for the page, started on the first image and kept warm. */
|
|
75
|
+
function getWorker(): Worker {
|
|
76
|
+
if (worker) return worker;
|
|
77
|
+
const created = createImageWorker();
|
|
78
|
+
created.addEventListener(
|
|
79
|
+
"message",
|
|
80
|
+
(event: MessageEvent<ImageWorkerResponse>) => {
|
|
81
|
+
const response = event.data;
|
|
82
|
+
const request = pending.get(response.id);
|
|
83
|
+
if (!request) return;
|
|
84
|
+
pending.delete(response.id);
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
request.resolve({
|
|
87
|
+
blob: response.blob,
|
|
88
|
+
width: response.width,
|
|
89
|
+
height: response.height,
|
|
90
|
+
processed: response.processed,
|
|
91
|
+
blurhash: response.blurhash,
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
request.reject(new Error(response.error));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
// A worker that dies takes every in-flight image with it. Fail them over to
|
|
99
|
+
// the in-page path and start clean for the next one.
|
|
100
|
+
created.addEventListener("error", (event) => {
|
|
101
|
+
if (worker !== created) return;
|
|
102
|
+
worker = null;
|
|
103
|
+
created.terminate();
|
|
104
|
+
const requests = [...pending.values()];
|
|
105
|
+
pending.clear();
|
|
106
|
+
const error = new Error(event.message || "Image worker failed");
|
|
107
|
+
for (const request of requests) request.reject(error);
|
|
108
|
+
});
|
|
109
|
+
worker = created;
|
|
110
|
+
return created;
|
|
111
|
+
}
|
|
30
112
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
function processInWorker(
|
|
114
|
+
file: File,
|
|
115
|
+
options: ImageProcessOptions,
|
|
116
|
+
): Promise<ProcessResult> {
|
|
117
|
+
const target = getWorker();
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const id = nextRequestId++;
|
|
120
|
+
pending.set(id, { resolve, reject });
|
|
121
|
+
const request: ImageWorkerRequest = { id, file, options };
|
|
122
|
+
target.postMessage(request);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
37
125
|
|
|
38
|
-
|
|
126
|
+
// ── In-page path ─────────────────────────────────────────────────────────
|
|
39
127
|
|
|
40
128
|
/**
|
|
41
129
|
* Load image from file
|
|
@@ -52,115 +140,73 @@ function loadImage(file: File): Promise<HTMLImageElement> {
|
|
|
52
140
|
});
|
|
53
141
|
}
|
|
54
142
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Decide how to handle an image given its source dimensions.
|
|
65
|
-
*
|
|
66
|
-
* - Long side over `maxLongSide` → `passthrough` (canvas can't redraw it).
|
|
67
|
-
* - Short side within `maxShortSide` → keep dimensions, just re-encode.
|
|
68
|
-
* - Otherwise → scale down so the short side hits `maxShortSide`.
|
|
69
|
-
*
|
|
70
|
-
* @param sourceWidth - Natural image width in pixels
|
|
71
|
-
* @param sourceHeight - Natural image height in pixels
|
|
72
|
-
* @param options - `maxShortSide` and `maxLongSide` caps
|
|
73
|
-
* @returns The processing plan
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* planImageProcessing(1080, 6000, { maxShortSide: 1920, maxLongSide: 8192 });
|
|
78
|
-
* // { passthrough: false, width: 1080, height: 6000 }
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
export function planImageProcessing(
|
|
82
|
-
sourceWidth: number,
|
|
83
|
-
sourceHeight: number,
|
|
84
|
-
options: { maxShortSide: number; maxLongSide: number },
|
|
85
|
-
): ImageProcessPlan {
|
|
86
|
-
const longSide = Math.max(sourceWidth, sourceHeight);
|
|
87
|
-
if (longSide > options.maxLongSide) {
|
|
88
|
-
return { passthrough: true, width: sourceWidth, height: sourceHeight };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const shortSide = Math.min(sourceWidth, sourceHeight);
|
|
92
|
-
if (shortSide <= options.maxShortSide) {
|
|
93
|
-
return { passthrough: false, width: sourceWidth, height: sourceHeight };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const scale = options.maxShortSide / shortSide;
|
|
97
|
-
return {
|
|
98
|
-
passthrough: false,
|
|
99
|
-
width: Math.round(sourceWidth * scale),
|
|
100
|
-
height: Math.round(sourceHeight * scale),
|
|
101
|
-
};
|
|
143
|
+
function createCanvas(width: number, height: number) {
|
|
144
|
+
const canvas = document.createElement("canvas");
|
|
145
|
+
canvas.width = width;
|
|
146
|
+
canvas.height = height;
|
|
147
|
+
const ctx = canvas.getContext("2d");
|
|
148
|
+
if (!ctx) throw new Error("Failed to get canvas context");
|
|
149
|
+
return { canvas, ctx };
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
* Convert canvas to Blob, falling back to JPEG when the requested format
|
|
106
|
-
* (typically WebP) is not supported by the browser (e.g. Safari).
|
|
107
|
-
*/
|
|
108
|
-
async function canvasToBlob(
|
|
152
|
+
function canvasToBlob(
|
|
109
153
|
canvas: HTMLCanvasElement,
|
|
110
|
-
|
|
154
|
+
type: string,
|
|
111
155
|
quality: number,
|
|
112
156
|
): Promise<Blob> {
|
|
113
|
-
|
|
157
|
+
return new Promise<Blob>((resolve, reject) => {
|
|
114
158
|
canvas.toBlob(
|
|
115
159
|
(b) => (b ? resolve(b) : reject(new Error("Failed to create blob"))),
|
|
116
|
-
|
|
160
|
+
type,
|
|
117
161
|
quality,
|
|
118
162
|
);
|
|
119
163
|
});
|
|
120
|
-
|
|
121
|
-
// Browser silently falls back to PNG when it can't encode the requested
|
|
122
|
-
// format. PNG ignores the quality parameter, producing oversized files.
|
|
123
|
-
// Re-encode as JPEG instead so lossy compression still applies.
|
|
124
|
-
if (mimeType !== "image/png" && blob.type === "image/png") {
|
|
125
|
-
return new Promise<Blob>((resolve, reject) => {
|
|
126
|
-
canvas.toBlob(
|
|
127
|
-
(b) => (b ? resolve(b) : reject(new Error("Failed to create blob"))),
|
|
128
|
-
"image/jpeg",
|
|
129
|
-
quality,
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return blob;
|
|
135
164
|
}
|
|
136
165
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
const pageEncodeTypeByRequest = new Map<string, Promise<string>>();
|
|
167
|
+
|
|
168
|
+
/** The format `toBlob` really produces for `requested`, probed once. */
|
|
169
|
+
function resolveEncodeTypeOnPage(
|
|
170
|
+
requested: string,
|
|
171
|
+
quality: number,
|
|
172
|
+
): Promise<string> {
|
|
173
|
+
let pending = pageEncodeTypeByRequest.get(requested);
|
|
174
|
+
if (!pending) {
|
|
175
|
+
pending = canvasToBlob(createCanvas(1, 1).canvas, requested, quality).then(
|
|
176
|
+
(probe) => resolveEncodedMimeType(requested, probe.type),
|
|
177
|
+
);
|
|
178
|
+
pageEncodeTypeByRequest.set(requested, pending);
|
|
179
|
+
}
|
|
180
|
+
return pending;
|
|
143
181
|
}
|
|
144
182
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
/** Best-effort: no blurhash beats no upload. */
|
|
184
|
+
function blurhashOf(img: HTMLImageElement): string | undefined {
|
|
185
|
+
try {
|
|
186
|
+
const { width, height } = blurhashDimensions(img.width, img.height);
|
|
187
|
+
const { ctx } = createCanvas(width, height);
|
|
188
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
189
|
+
return encode(
|
|
190
|
+
ctx.getImageData(0, 0, width, height).data,
|
|
191
|
+
width,
|
|
192
|
+
height,
|
|
193
|
+
4,
|
|
194
|
+
3,
|
|
195
|
+
);
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
|
|
151
|
-
|
|
152
|
-
* Process image file
|
|
153
|
-
*/
|
|
154
|
-
async function process(
|
|
201
|
+
async function processOnPage(
|
|
155
202
|
file: File,
|
|
156
|
-
options:
|
|
203
|
+
options: ImageProcessOptions,
|
|
157
204
|
): Promise<ProcessResult> {
|
|
158
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
159
|
-
|
|
160
205
|
const img = await loadImage(file);
|
|
161
206
|
|
|
162
207
|
// img.width / img.height already reflect EXIF orientation in modern browsers
|
|
163
|
-
const plan = planImageProcessing(img.width, img.height,
|
|
208
|
+
const plan = planImageProcessing(img.width, img.height, options);
|
|
209
|
+
const blurhash = blurhashOf(img);
|
|
164
210
|
|
|
165
211
|
// Too large to redraw on a canvas without crushing detail — keep the
|
|
166
212
|
// original bytes so images of any length upload at full quality.
|
|
@@ -170,22 +216,48 @@ async function process(
|
|
|
170
216
|
width: plan.width,
|
|
171
217
|
height: plan.height,
|
|
172
218
|
processed: false,
|
|
219
|
+
blurhash,
|
|
173
220
|
};
|
|
174
221
|
}
|
|
175
222
|
|
|
176
|
-
const canvas =
|
|
177
|
-
canvas.width = plan.width;
|
|
178
|
-
canvas.height = plan.height;
|
|
179
|
-
|
|
180
|
-
const ctx = canvas.getContext("2d");
|
|
181
|
-
if (!ctx) throw new Error("Failed to get canvas context");
|
|
182
|
-
|
|
223
|
+
const { canvas, ctx } = createCanvas(plan.width, plan.height);
|
|
183
224
|
// drawImage respects EXIF orientation — no manual rotation needed
|
|
184
225
|
ctx.drawImage(img, 0, 0, plan.width, plan.height);
|
|
185
226
|
|
|
186
|
-
const
|
|
227
|
+
const type = await resolveEncodeTypeOnPage(options.mimeType, options.quality);
|
|
228
|
+
const blob = await canvasToBlob(canvas, type, options.quality);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
blob,
|
|
232
|
+
width: plan.width,
|
|
233
|
+
height: plan.height,
|
|
234
|
+
processed: true,
|
|
235
|
+
blurhash,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Facade ───────────────────────────────────────────────────────────────
|
|
187
240
|
|
|
188
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Process image file
|
|
243
|
+
*/
|
|
244
|
+
async function process(
|
|
245
|
+
file: File,
|
|
246
|
+
options: ProcessOptions = {},
|
|
247
|
+
): Promise<ProcessResult> {
|
|
248
|
+
const opts = { ...DEFAULT_IMAGE_PROCESS_OPTIONS, ...options };
|
|
249
|
+
|
|
250
|
+
if (supportsImageWorker()) {
|
|
251
|
+
try {
|
|
252
|
+
return await processInWorker(file, opts);
|
|
253
|
+
} catch {
|
|
254
|
+
// The worker could not decode this file, or never started. `<img>`
|
|
255
|
+
// accepts a few things `createImageBitmap` does not, and if it fails
|
|
256
|
+
// too, that is the error worth reporting.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return processOnPage(file, opts);
|
|
189
261
|
}
|
|
190
262
|
|
|
191
263
|
/**
|
|
@@ -199,7 +271,12 @@ async function processToFile(
|
|
|
199
271
|
|
|
200
272
|
// Original kept untouched — upload the file as-is.
|
|
201
273
|
if (!result.processed) {
|
|
202
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
file,
|
|
276
|
+
width: result.width,
|
|
277
|
+
height: result.height,
|
|
278
|
+
blurhash: result.blurhash,
|
|
279
|
+
};
|
|
203
280
|
}
|
|
204
281
|
|
|
205
282
|
// Use actual blob type — Safari falls back to JPEG when WebP encoding isn't supported
|
|
@@ -216,6 +293,7 @@ async function processToFile(
|
|
|
216
293
|
file: new File([result.blob], newName, { type: result.blob.type }),
|
|
217
294
|
width: result.width,
|
|
218
295
|
height: result.height,
|
|
296
|
+
blurhash: result.blurhash,
|
|
219
297
|
};
|
|
220
298
|
}
|
|
221
299
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the image worker comes from.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `ImageProcessor` so tests can stand in a fake worker, and so
|
|
5
|
+
* the `?worker&inline` import has exactly one home. Inline — Vite bundles the
|
|
6
|
+
* worker into the client script and starts it from a `blob:` URL — rather than
|
|
7
|
+
* a separate file, because client assets may be served from another origin
|
|
8
|
+
* (`ASSET_BASE_URL`) and a worker script must be same-origin with the page.
|
|
9
|
+
* CSP already admits `blob:` scripts for the media workers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ImageWorker from "./image-worker.js?worker&inline";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether this browser can decode and encode images inside a worker.
|
|
16
|
+
*
|
|
17
|
+
* @returns True when `Worker`, `OffscreenCanvas`, and `createImageBitmap` exist
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* if (supportsImageWorker()) worker = createImageWorker();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function supportsImageWorker(): boolean {
|
|
25
|
+
return (
|
|
26
|
+
typeof Worker === "function" &&
|
|
27
|
+
typeof OffscreenCanvas === "function" &&
|
|
28
|
+
typeof createImageBitmap === "function"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start a fresh image worker.
|
|
34
|
+
*
|
|
35
|
+
* @returns The worker, speaking the protocol in `image-plan.ts`
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const worker = createImageWorker();
|
|
40
|
+
* worker.postMessage({ id: 1, file, options });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function createImageWorker(): Worker {
|
|
44
|
+
return new ImageWorker({ name: "jant-image" });
|
|
45
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image processing worker.
|
|
3
|
+
*
|
|
4
|
+
* Decodes, resizes, re-encodes, and blurhashes an image entirely off the main
|
|
5
|
+
* thread: `createImageBitmap` decodes without touching the DOM, and
|
|
6
|
+
* `OffscreenCanvas.convertToBlob` encodes without a synchronous readback on
|
|
7
|
+
* the UI thread. Selecting a 12-megapixel photo used to hold the composer for
|
|
8
|
+
* a couple of hundred milliseconds per image; here it costs the page a
|
|
9
|
+
* `postMessage`.
|
|
10
|
+
*
|
|
11
|
+
* Protocol in `image-plan.ts`. A failure is reported back, not retried: the
|
|
12
|
+
* caller falls back to the in-page canvas path, which is also what browsers
|
|
13
|
+
* without OffscreenCanvas use.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { encode } from "blurhash";
|
|
17
|
+
import {
|
|
18
|
+
blurhashDimensions,
|
|
19
|
+
planImageProcessing,
|
|
20
|
+
resolveEncodedMimeType,
|
|
21
|
+
type ImageProcessOptions,
|
|
22
|
+
type ImageWorkerRequest,
|
|
23
|
+
type ImageWorkerResponse,
|
|
24
|
+
type ImageWorkerSuccess,
|
|
25
|
+
} from "./image-plan.js";
|
|
26
|
+
|
|
27
|
+
// The DOM lib types `self` as a Window; the worker needs only the two members
|
|
28
|
+
// both scopes share, typed against this module's own protocol.
|
|
29
|
+
const scope = self as unknown as {
|
|
30
|
+
addEventListener(
|
|
31
|
+
type: "message",
|
|
32
|
+
listener: (event: MessageEvent<ImageWorkerRequest>) => void,
|
|
33
|
+
): void;
|
|
34
|
+
postMessage(message: ImageWorkerResponse): void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const encodeTypeByRequest = new Map<string, Promise<string>>();
|
|
38
|
+
|
|
39
|
+
/** The format `convertToBlob` really produces for `requested`, probed once. */
|
|
40
|
+
function resolveEncodeType(
|
|
41
|
+
requested: string,
|
|
42
|
+
quality: number,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
let pending = encodeTypeByRequest.get(requested);
|
|
45
|
+
if (!pending) {
|
|
46
|
+
const { canvas } = draw(null, 1, 1);
|
|
47
|
+
pending = canvas
|
|
48
|
+
.convertToBlob({ type: requested, quality })
|
|
49
|
+
.then((probe) => resolveEncodedMimeType(requested, probe.type));
|
|
50
|
+
encodeTypeByRequest.set(requested, pending);
|
|
51
|
+
}
|
|
52
|
+
return pending;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function draw(
|
|
56
|
+
source: ImageBitmap | null,
|
|
57
|
+
width: number,
|
|
58
|
+
height: number,
|
|
59
|
+
): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D } {
|
|
60
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
61
|
+
const ctx = canvas.getContext("2d");
|
|
62
|
+
if (!ctx) throw new Error("Failed to get canvas context");
|
|
63
|
+
if (source) ctx.drawImage(source, 0, 0, width, height);
|
|
64
|
+
return { canvas, ctx };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Best-effort, like the metadata pass it replaces: no blurhash beats no upload. */
|
|
68
|
+
function blurhashOf(bitmap: ImageBitmap): string | undefined {
|
|
69
|
+
try {
|
|
70
|
+
const { width, height } = blurhashDimensions(bitmap.width, bitmap.height);
|
|
71
|
+
const { ctx } = draw(bitmap, width, height);
|
|
72
|
+
return encode(
|
|
73
|
+
ctx.getImageData(0, 0, width, height).data,
|
|
74
|
+
width,
|
|
75
|
+
height,
|
|
76
|
+
4,
|
|
77
|
+
3,
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function processImage(
|
|
85
|
+
file: File,
|
|
86
|
+
options: ImageProcessOptions,
|
|
87
|
+
): Promise<Omit<ImageWorkerSuccess, "id" | "ok">> {
|
|
88
|
+
// `from-image` applies EXIF orientation, so the bitmap's dimensions are the
|
|
89
|
+
// ones the photo is meant to be seen at — the same as `<img>` reports.
|
|
90
|
+
const bitmap = await createImageBitmap(file, {
|
|
91
|
+
imageOrientation: "from-image",
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
const plan = planImageProcessing(bitmap.width, bitmap.height, options);
|
|
95
|
+
const blurhash = blurhashOf(bitmap);
|
|
96
|
+
|
|
97
|
+
// Too large to redraw on a canvas without crushing detail — keep the
|
|
98
|
+
// original bytes so images of any length upload at full quality.
|
|
99
|
+
if (plan.passthrough) {
|
|
100
|
+
return {
|
|
101
|
+
blob: file,
|
|
102
|
+
width: plan.width,
|
|
103
|
+
height: plan.height,
|
|
104
|
+
processed: false,
|
|
105
|
+
blurhash,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { canvas } = draw(bitmap, plan.width, plan.height);
|
|
110
|
+
const type = await resolveEncodeType(options.mimeType, options.quality);
|
|
111
|
+
const blob = await canvas.convertToBlob({ type, quality: options.quality });
|
|
112
|
+
return {
|
|
113
|
+
blob,
|
|
114
|
+
width: plan.width,
|
|
115
|
+
height: plan.height,
|
|
116
|
+
processed: true,
|
|
117
|
+
blurhash,
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
bitmap.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scope.addEventListener("message", async (event) => {
|
|
125
|
+
const { id, file, options } = event.data;
|
|
126
|
+
try {
|
|
127
|
+
scope.postMessage({ id, ok: true, ...(await processImage(file, options)) });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
scope.postMessage({
|
|
130
|
+
id,
|
|
131
|
+
ok: false,
|
|
132
|
+
error: error instanceof Error ? error.message : String(error),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authenticated bundle, split by what a page needs.
|
|
3
|
+
*
|
|
4
|
+
* `client-auth.ts` is the shell every signed-in page loads: the post menu,
|
|
5
|
+
* command palette, shortcuts, and the triggers that open the composer. The
|
|
6
|
+
* heavier parts ship as their own entries and load through here:
|
|
7
|
+
*
|
|
8
|
+
* - `client-compose.ts` — the editor. Loaded the first time the composer
|
|
9
|
+
* opens (`compose-launch.ts`), at once on the compose page, and preloaded
|
|
10
|
+
* by the layout on every signed-in page so that first open costs no
|
|
11
|
+
* round trip.
|
|
12
|
+
* - `client-settings.ts` — the settings pages' components.
|
|
13
|
+
* - `client-manage.ts` — navigation manager, collection directory, and the
|
|
14
|
+
* collection page menus.
|
|
15
|
+
*
|
|
16
|
+
* The page decides: {@link loadEntriesForPage} looks for the elements and
|
|
17
|
+
* hooks the server renders only where an entry is needed, and keeps looking
|
|
18
|
+
* as fragments arrive, so a component never has to be registered up front.
|
|
19
|
+
* `docs/internal/lit-guide.md` says which entry a new component belongs in.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function once(load: () => Promise<unknown>): () => Promise<void> {
|
|
23
|
+
let loading: Promise<void> | undefined;
|
|
24
|
+
return () => (loading ??= load().then(() => undefined));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Load the composer entry; resolves once its elements are defined. */
|
|
28
|
+
export const ensureCompose = once(() => import("../client-compose.js"));
|
|
29
|
+
|
|
30
|
+
/** Load the settings entry; resolves once its elements are defined. */
|
|
31
|
+
export const ensureSettings = once(() => import("../client-settings.js"));
|
|
32
|
+
|
|
33
|
+
/** Load the management entry; resolves once its elements are defined. */
|
|
34
|
+
export const ensureManage = once(() => import("../client-manage.js"));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Markup the server renders only on pages that need an entry. A selector
|
|
38
|
+
* matching means the page cannot work without that entry, so it loads at
|
|
39
|
+
* once; the composer on an ordinary page is not listed because it loads on
|
|
40
|
+
* first open instead.
|
|
41
|
+
*/
|
|
42
|
+
const PAGE_ENTRIES: ReadonlyArray<{
|
|
43
|
+
selector: string;
|
|
44
|
+
load: () => Promise<void>;
|
|
45
|
+
}> = [
|
|
46
|
+
{ selector: '[data-page="compose"]', load: ensureCompose },
|
|
47
|
+
{
|
|
48
|
+
selector: [
|
|
49
|
+
"jant-settings-general",
|
|
50
|
+
"jant-settings-language",
|
|
51
|
+
"jant-settings-avatar",
|
|
52
|
+
"jant-config-editor",
|
|
53
|
+
"jant-repo-picker",
|
|
54
|
+
].join(", "),
|
|
55
|
+
load: ensureSettings,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
selector: [
|
|
59
|
+
"jant-nav-manager",
|
|
60
|
+
"jant-collections-manager",
|
|
61
|
+
"[data-collection-page-actions]",
|
|
62
|
+
"[data-smart-collection-page-actions]",
|
|
63
|
+
"[data-custom-url-actions]",
|
|
64
|
+
].join(", "),
|
|
65
|
+
load: ensureManage,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load every entry whose markup is on the page, now and as the DOM changes.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* // client-auth.ts, once the shell is set up
|
|
75
|
+
* loadEntriesForPage();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function loadEntriesForPage(): void {
|
|
79
|
+
const pending = new Set(PAGE_ENTRIES);
|
|
80
|
+
const observer = new globalThis.MutationObserver(check);
|
|
81
|
+
|
|
82
|
+
function check(): void {
|
|
83
|
+
for (const entry of pending) {
|
|
84
|
+
if (!document.querySelector(entry.selector)) continue;
|
|
85
|
+
pending.delete(entry);
|
|
86
|
+
void entry.load();
|
|
87
|
+
}
|
|
88
|
+
if (pending.size === 0) observer.disconnect();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
check();
|
|
92
|
+
if (pending.size > 0) {
|
|
93
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lazy-loaded slug generation
|
|
3
3
|
*
|
|
4
|
-
* Wraps the `slugify` function from `
|
|
4
|
+
* Wraps the `slugify` function from `lib/slugify.ts` behind a dynamic import so
|
|
5
5
|
* `limax` (used for i18n-aware transliteration) doesn't bloat the main
|
|
6
6
|
* client bundle. Vite code-splits it into a separate chunk.
|
|
7
7
|
*
|
|
@@ -22,7 +22,7 @@ let loadingPromise: Promise<SlugifyFn> | undefined;
|
|
|
22
22
|
function load(): Promise<SlugifyFn> {
|
|
23
23
|
if (slugifyFn) return Promise.resolve(slugifyFn);
|
|
24
24
|
if (!loadingPromise) {
|
|
25
|
-
loadingPromise = import("../lib/
|
|
25
|
+
loadingPromise = import("../lib/slugify.js").then((mod) => {
|
|
26
26
|
slugifyFn = mod.slugify;
|
|
27
27
|
return mod.slugify;
|
|
28
28
|
});
|