@jant/core 0.6.16 → 0.7.1

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.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -2,7 +2,10 @@
2
2
  * Client-side Video Processor
3
3
  *
4
4
  * Processes videos before upload using mediabunny:
5
- * - Transcodes to H.264/AAC MP4 (universal playback)
5
+ * - Remuxes to MP4, transcoding to H.264/AAC only when the source is not
6
+ * already in those codecs or does not fit the size caps. A source that
7
+ * needs nothing has its encoded packets copied straight across, so it
8
+ * reaches the server without a generation of re-encoding loss
6
9
  * - Resizes to max 1920px long edge / 1080px short edge
7
10
  * - Strips spurious rotation metadata from the output (mediabunny may
8
11
  * bake rotation into pixels AND write a display matrix, causing the
@@ -14,17 +17,8 @@
14
17
  * Requires WebCodecs API support — check `isSupported()` before use.
15
18
  */
16
19
 
17
- import {
18
- Input,
19
- Output,
20
- Mp4OutputFormat,
21
- BufferTarget,
22
- BlobSource,
23
- CanvasSink,
24
- Conversion,
25
- QUALITY_HIGH,
26
- ALL_FORMATS,
27
- } from "mediabunny";
20
+ import type { VideoCodec } from "mediabunny";
21
+ import { loadMediabunny } from "./mediabunny.js";
28
22
  import { encode } from "blurhash";
29
23
  import { normalizeDurationSeconds } from "../lib/video-playback.js";
30
24
  import { zeroTrackAlternateGroups } from "../lib/mp4-track-flags.js";
@@ -36,6 +30,66 @@ const MAX_SHORT_EDGE = 1080;
36
30
  const POSTER_WIDTH = 640;
37
31
  const BLURHASH_SIZE = 32;
38
32
 
33
+ /**
34
+ * Bitrate targeted for a 1080p H.264 re-encode; every other size scales from
35
+ * here. Around 8 Mbps — enough to hold up against the source on a phone or a
36
+ * laptop, low enough that a clip stays servable over the web.
37
+ *
38
+ * This is expressed as a bitrate on purpose. Given a quality *level*,
39
+ * mediabunny prefers quantizer-based encoding, which has no size ceiling at
40
+ * all: a frugally-encoded source gets faithfully reproduced — its own
41
+ * compression artifacts included — at several times its original size. A
42
+ * bitrate keeps the output predictable.
43
+ */
44
+ const REFERENCE_BITRATE = 7_900_000;
45
+ const REFERENCE_PIXELS = 1920 * 1080;
46
+
47
+ /** Bitrate scales slightly sub-linearly with pixel count. */
48
+ const PIXEL_SCALE_EXPONENT = 0.95;
49
+
50
+ /**
51
+ * Bits H.264 needs to carry one bit of the source codec at the same quality.
52
+ * H.264 is the baseline; HEVC and VP9 fit roughly 40% more picture into a
53
+ * bit, AV1 around 60%. Used to cap the target at what the source content
54
+ * actually carries — spending more than that reproduces the source's
55
+ * artifacts in high fidelity and buys nothing, because the detail was never
56
+ * there to begin with.
57
+ */
58
+ const CODEC_BITRATE_RATIO: Partial<Record<VideoCodec, number>> = {
59
+ avc: 1,
60
+ hevc: 1 / 0.6,
61
+ vp9: 1 / 0.6,
62
+ av1: 1 / 0.4,
63
+ vp8: 1 / 1.2,
64
+ };
65
+
66
+ /**
67
+ * Pick the bitrate for a re-encode: the size-appropriate target, but never
68
+ * more than the source content itself warrants.
69
+ */
70
+ function reencodeBitrate(
71
+ source: {
72
+ pixels: number;
73
+ bitrate: number | undefined;
74
+ codec: VideoCodec | null | undefined;
75
+ },
76
+ targetPixels: number,
77
+ ): number {
78
+ const target =
79
+ REFERENCE_BITRATE *
80
+ (targetPixels / REFERENCE_PIXELS) ** PIXEL_SCALE_EXPONENT;
81
+
82
+ if (!source.bitrate || !source.pixels) return Math.round(target);
83
+
84
+ const ratio = (source.codec && CODEC_BITRATE_RATIO[source.codec]) ?? 1;
85
+ const sourceCap =
86
+ source.bitrate *
87
+ ratio *
88
+ (targetPixels / source.pixels) ** PIXEL_SCALE_EXPONENT;
89
+
90
+ return Math.round(Math.min(target, sourceCap));
91
+ }
92
+
39
93
  export interface VideoProcessResult {
40
94
  file: File;
41
95
  width: number;
@@ -54,102 +108,281 @@ function isSupported(): boolean {
54
108
  return typeof VideoEncoder !== "undefined";
55
109
  }
56
110
 
57
- /**
58
- * Extract a poster frame, blurhash, and source dimensions from a video file.
59
- * Seeks to `min(duration × 0.1, 1s)` and captures the frame.
60
- * Also returns the original video dimensions so the caller can compute
61
- * the correct output size without opening a second Input instance.
62
- *
63
- * @param file - Source video file
64
- * @returns Poster blob (640px-wide WebP), blurhash string, and source dimensions
65
- */
66
- async function extractPoster(file: File): Promise<{
111
+ interface SourceProbe {
67
112
  poster?: Blob;
68
113
  blurhash?: string;
69
114
  sourceWidth?: number;
70
115
  sourceHeight?: number;
71
116
  rotation?: number;
72
117
  durationSeconds?: number;
73
- }> {
118
+ /** Source video codec, or `null` when it could not be determined. */
119
+ videoCodec?: VideoCodec | null;
120
+ /** Average bitrate of the source video track, in bits per second. */
121
+ videoBitrate?: number;
122
+ }
123
+
124
+ /**
125
+ * Report a probe step that failed. Every step here is optional — the upload
126
+ * proceeds without it — so the failure has to be visible somewhere, or a
127
+ * broken decoder reads as a video that merely has no thumbnail.
128
+ */
129
+ function warnProbeFailure(step: string, error: unknown): void {
130
+ // eslint-disable-next-line no-console -- an optional step failing is silent by design; say so
131
+ console.warn(`[jant] video probe: ${step} failed`, error);
132
+ }
133
+
134
+ /**
135
+ * Run one optional probe step, returning `undefined` when it fails.
136
+ *
137
+ * Each step feeds a different part of the result, and none of them is worth
138
+ * the others: an unreadable bitrate should not cost the poster, and an
139
+ * undecodable frame should not cost the codec that decides whether the file
140
+ * needs re-encoding at all.
141
+ */
142
+ async function optional<T>(
143
+ step: string,
144
+ run: () => Promise<T>,
145
+ ): Promise<T | undefined> {
146
+ try {
147
+ return await run();
148
+ } catch (error) {
149
+ warnProbeFailure(step, error);
150
+ return undefined;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Draw the captured frame into a canvas of the given size.
156
+ * Returns null when the browser refuses a 2D context.
157
+ */
158
+ function drawFrame(
159
+ source: HTMLCanvasElement,
160
+ width: number,
161
+ height: number,
162
+ ): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null {
163
+ const canvas = document.createElement("canvas");
164
+ canvas.width = width;
165
+ canvas.height = height;
166
+ const ctx = canvas.getContext("2d");
167
+ if (!ctx) return null;
168
+ ctx.drawImage(source, 0, 0, width, height);
169
+ return { canvas, ctx };
170
+ }
171
+
172
+ /**
173
+ * Probe a video file and extract its poster frame and blurhash in one pass.
174
+ * Seeks to `min(duration × 0.1, 1s)` and captures the frame.
175
+ *
176
+ * Returns the source dimensions, codec, and bitrate alongside, so the caller
177
+ * can decide whether a re-encode is needed at all — and at what bitrate —
178
+ * without opening a second Input instance.
179
+ *
180
+ * Every field is optional and probed independently. A file whose frames will
181
+ * not decode still reports its codec and duration; one whose bitrate cannot be
182
+ * read still yields a poster. Only the container being unreadable — no video
183
+ * track at all — returns nothing, because then there is nothing to report.
184
+ *
185
+ * @param file - Source video file
186
+ * @returns Poster blob (640px-wide WebP), blurhash, dimensions, codec, bitrate
187
+ */
188
+ async function probeSource(file: File): Promise<SourceProbe> {
189
+ const { Input, BlobSource, ALL_FORMATS, CanvasSink } = await loadMediabunny();
74
190
  const input = new Input({
75
191
  source: new BlobSource(file),
76
192
  formats: ALL_FORMATS,
77
193
  });
78
194
  try {
79
- const videoTrack = await input.getPrimaryVideoTrack();
195
+ const videoTrack = await optional("open video track", () =>
196
+ input.getPrimaryVideoTrack(),
197
+ );
80
198
  if (!videoTrack) return {};
81
199
 
82
- const sourceWidth = videoTrack.displayWidth;
83
- const sourceHeight = videoTrack.displayHeight;
84
- const rotation = videoTrack.rotation;
200
+ const videoCodec = await optional("read codec", () =>
201
+ videoTrack.getCodec(),
202
+ );
203
+ // Metadata-only scan of the sample table — milliseconds even on a large
204
+ // file, and exact, unlike dividing file size by duration. Only an input
205
+ // to the re-encode bitrate, so `undefined` degrades to the size-based
206
+ // target rather than failing the probe.
207
+ const videoBitrate = await optional(
208
+ "compute bitrate",
209
+ async () => (await videoTrack.computePacketStats()).averageBitrate,
210
+ );
211
+ const duration = await optional("compute duration", () =>
212
+ input.computeDuration(),
213
+ );
85
214
 
86
- const duration = await input.computeDuration();
87
- const durationSeconds = normalizeDurationSeconds(duration);
88
- const seekTime = Math.min(duration * 0.1, 1);
215
+ // Everything the caller needs to plan the conversion.
216
+ const base: SourceProbe = {
217
+ sourceWidth: videoTrack.displayWidth,
218
+ sourceHeight: videoTrack.displayHeight,
219
+ rotation: videoTrack.rotation,
220
+ durationSeconds:
221
+ duration === undefined ? undefined : normalizeDurationSeconds(duration),
222
+ videoCodec,
223
+ videoBitrate,
224
+ };
89
225
 
90
- const sink = new CanvasSink(videoTrack);
91
- const wrapped = await sink.getCanvas(seekTime);
92
- if (!wrapped) {
93
- return { sourceWidth, sourceHeight, rotation, durationSeconds };
94
- }
226
+ // A tenth of the way in, so the still is not the fade-from-black most
227
+ // clips open on. With no duration to take a tenth of, the opening frame
228
+ // is the only one we can name.
229
+ const seekTime = duration === undefined ? 0 : Math.min(duration * 0.1, 1);
230
+ const wrapped = await optional("capture frame", () =>
231
+ new CanvasSink(videoTrack).getCanvas(seekTime),
232
+ );
233
+ if (!wrapped) return base;
95
234
 
96
235
  const canvas = wrapped.canvas as HTMLCanvasElement;
97
-
98
- // Poster: 640px wide WebP
99
236
  const srcW = canvas.width;
100
237
  const srcH = canvas.height;
238
+
239
+ // Poster: 640px wide WebP
101
240
  const posterScale = Math.min(POSTER_WIDTH / srcW, 1);
102
- const pw = Math.round(srcW * posterScale);
103
- const ph = Math.round(srcH * posterScale);
104
-
105
- const posterCanvas = document.createElement("canvas");
106
- posterCanvas.width = pw;
107
- posterCanvas.height = ph;
108
- const pCtx = posterCanvas.getContext("2d");
109
- if (!pCtx) return { sourceWidth, sourceHeight };
110
- pCtx.drawImage(canvas, 0, 0, pw, ph);
111
-
112
- const poster = await new Promise<Blob | undefined>((resolve) => {
113
- posterCanvas.toBlob(
114
- (blob) => resolve(blob ?? undefined),
115
- "image/webp",
116
- 0.8,
117
- );
118
- });
241
+ const posterFrame = drawFrame(
242
+ canvas,
243
+ Math.round(srcW * posterScale),
244
+ Math.round(srcH * posterScale),
245
+ );
246
+ const poster = posterFrame
247
+ ? await optional(
248
+ "encode poster",
249
+ () =>
250
+ new Promise<Blob | undefined>((resolve) => {
251
+ posterFrame.canvas.toBlob(
252
+ (blob) => resolve(blob ?? undefined),
253
+ "image/webp",
254
+ 0.8,
255
+ );
256
+ }),
257
+ )
258
+ : undefined;
119
259
 
120
260
  // Blurhash: 32px canvas, 4×3 components
121
261
  const bhScale = Math.min(BLURHASH_SIZE / srcW, BLURHASH_SIZE / srcH, 1);
122
262
  const bw = Math.max(Math.round(srcW * bhScale), 1);
123
263
  const bh = Math.max(Math.round(srcH * bhScale), 1);
264
+ const bhFrame = drawFrame(canvas, bw, bh);
265
+ const blurhash = bhFrame
266
+ ? await optional("encode blurhash", async () =>
267
+ encode(bhFrame.ctx.getImageData(0, 0, bw, bh).data, bw, bh, 4, 3),
268
+ )
269
+ : undefined;
270
+
271
+ return { ...base, poster, blurhash };
272
+ } finally {
273
+ input.dispose();
274
+ }
275
+ }
124
276
 
125
- const bhCanvas = document.createElement("canvas");
126
- bhCanvas.width = bw;
127
- bhCanvas.height = bh;
128
- const bhCtx = bhCanvas.getContext("2d");
129
- if (!bhCtx) return { poster, sourceWidth, sourceHeight };
130
- bhCtx.drawImage(canvas, 0, 0, bw, bh);
277
+ /** What a given source needs done to it before upload. */
278
+ export interface VideoProcessPlan {
279
+ /** True when the video must be scaled down to fit the size caps. */
280
+ needsResize: boolean;
281
+ /** Target dimensions — equal to the source dimensions when not resizing. */
282
+ width: number;
283
+ height: number;
284
+ /** True when the video track must be decoded and re-encoded. */
285
+ videoNeedsReencode: boolean;
286
+ /**
287
+ * Target video bitrate in bits per second, or `undefined` when the track
288
+ * is copied rather than re-encoded.
289
+ */
290
+ videoBitrate?: number;
291
+ }
131
292
 
132
- const imageData = bhCtx.getImageData(0, 0, bw, bh);
133
- const blurhash = encode(imageData.data, bw, bh, 4, 3);
293
+ /**
294
+ * Decide what a source video needs: a resize, a re-encode, or nothing at all.
295
+ *
296
+ * Asking for a size or a quality is what disables mediabunny's copy fast
297
+ * path, so anything already within the caps and already in H.264 must come
298
+ * back with `videoNeedsReencode` false — its encoded packets are then copied
299
+ * across untouched, sparing it a generation of quality. An unknown codec
300
+ * (`null`) counts as needing a re-encode, since we can't vouch for it playing
301
+ * everywhere.
302
+ *
303
+ * When a re-encode is unavoidable, the bitrate is the smaller of what the
304
+ * output size warrants and what the source content actually carries. The
305
+ * second half matters: an efficiently-encoded source re-encoded at constant
306
+ * quality can come out several times larger than it went in, all of those
307
+ * bits spent preserving its own compression artifacts.
308
+ *
309
+ * Dimensions are the source's *display* dimensions, i.e. post-rotation, so
310
+ * the caps apply orientation-agnostically.
311
+ *
312
+ * @param source - Display dimensions, codec, and bitrate of the source
313
+ * @param options - `maxLongEdge` and `maxShortEdge` caps
314
+ * @returns The processing plan
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * // Already H.264 and within the caps — copied, not re-encoded
319
+ * planVideoProcessing(
320
+ * { width: 1280, height: 720, videoCodec: "avc" },
321
+ * { maxLongEdge: 1920, maxShortEdge: 1080 },
322
+ * );
323
+ * // { needsResize: false, width: 1280, height: 720,
324
+ * // videoNeedsReencode: false, videoBitrate: undefined }
325
+ * ```
326
+ */
327
+ export function planVideoProcessing(
328
+ source: {
329
+ width?: number;
330
+ height?: number;
331
+ videoCodec?: VideoCodec | null;
332
+ videoBitrate?: number;
333
+ },
334
+ options: { maxLongEdge: number; maxShortEdge: number },
335
+ ): VideoProcessPlan {
336
+ const { width: sourceWidth, height: sourceHeight } = source;
337
+
338
+ // Fall back to the caps when the probe could not read the dimensions;
339
+ // without a source size there is nothing to scale against.
340
+ let width = sourceWidth || options.maxLongEdge;
341
+ let height = sourceHeight || options.maxShortEdge;
342
+ let needsResize = false;
134
343
 
135
- return {
136
- poster,
137
- blurhash,
138
- sourceWidth,
139
- sourceHeight,
140
- rotation,
141
- durationSeconds,
142
- };
143
- } catch {
144
- return {};
145
- } finally {
146
- input.dispose();
344
+ if (sourceWidth && sourceHeight) {
345
+ const longSide = Math.max(sourceWidth, sourceHeight);
346
+ const shortSide = Math.min(sourceWidth, sourceHeight);
347
+ const scale = Math.min(
348
+ options.maxLongEdge / longSide,
349
+ options.maxShortEdge / shortSide,
350
+ 1,
351
+ );
352
+ needsResize = scale < 1;
353
+ if (needsResize) {
354
+ width = Math.round(sourceWidth * scale);
355
+ height = Math.round(sourceHeight * scale);
356
+ // H.264 requires even dimensions
357
+ width += width % 2;
358
+ height += height % 2;
359
+ }
147
360
  }
361
+
362
+ const videoNeedsReencode = needsResize || source.videoCodec !== "avc";
363
+
364
+ return {
365
+ needsResize,
366
+ width,
367
+ height,
368
+ videoNeedsReencode,
369
+ videoBitrate: videoNeedsReencode
370
+ ? reencodeBitrate(
371
+ {
372
+ pixels: (sourceWidth ?? 0) * (sourceHeight ?? 0),
373
+ bitrate: source.videoBitrate,
374
+ codec: source.videoCodec,
375
+ },
376
+ width * height,
377
+ )
378
+ : undefined,
379
+ };
148
380
  }
149
381
 
150
382
  /**
151
- * Process a video file: transcode to H.264/AAC MP4, resize to fit within
152
- * 1920×1080, and extract poster frame + blurhash.
383
+ * Process a video file: remux to MP4, re-encoding to H.264/AAC only when the
384
+ * source needs it, resize to fit within 1920×1080, and extract poster frame
385
+ * + blurhash.
153
386
  *
154
387
  * @param file - Source video file
155
388
  * @param onProgress - Optional callback receiving progress from 0 to 1
@@ -159,8 +392,8 @@ async function processToFile(
159
392
  file: File,
160
393
  onProgress?: (progress: number) => void,
161
394
  ): Promise<VideoProcessResult> {
162
- // Extract poster + blurhash + source dimensions (separate Input instance,
163
- // so the transcoding Input below starts with clean demuxer state).
395
+ // Probe the source and grab poster + blurhash (separate Input instance,
396
+ // so the conversion Input below starts with clean demuxer state).
164
397
  const {
165
398
  poster,
166
399
  blurhash,
@@ -168,28 +401,36 @@ async function processToFile(
168
401
  sourceHeight,
169
402
  rotation,
170
403
  durationSeconds,
171
- } = await extractPoster(file);
404
+ videoCodec,
405
+ videoBitrate,
406
+ } = await probeSource(file);
172
407
 
173
- // Compute output size from display dimensions (post-rotation).
174
- // Orientation-agnostic: long edge ≤ 1920, short edge ≤ 1080.
175
- let targetW = sourceWidth || MAX_LONG_EDGE;
176
- let targetH = sourceHeight || MAX_SHORT_EDGE;
177
- if (sourceWidth && sourceHeight) {
178
- const longSide = Math.max(sourceWidth, sourceHeight);
179
- const shortSide = Math.min(sourceWidth, sourceHeight);
180
- const scale = Math.min(
181
- MAX_LONG_EDGE / longSide,
182
- MAX_SHORT_EDGE / shortSide,
183
- 1,
184
- );
185
- targetW = Math.round(sourceWidth * scale);
186
- targetH = Math.round(sourceHeight * scale);
187
- }
188
- // H.264 requires even dimensions
189
- targetW += targetW % 2;
190
- targetH += targetH % 2;
191
-
192
- // Transcode to MP4 H.264/AAC (fresh Input — not shared with extractPoster)
408
+ const {
409
+ needsResize,
410
+ width: targetW,
411
+ height: targetH,
412
+ videoBitrate: targetBitrate,
413
+ } = planVideoProcessing(
414
+ {
415
+ width: sourceWidth,
416
+ height: sourceHeight,
417
+ videoCodec,
418
+ videoBitrate,
419
+ },
420
+ { maxLongEdge: MAX_LONG_EDGE, maxShortEdge: MAX_SHORT_EDGE },
421
+ );
422
+
423
+ // Convert to MP4 (fresh Input — not shared with probeSource)
424
+ const {
425
+ Input,
426
+ Output,
427
+ Mp4OutputFormat,
428
+ BufferTarget,
429
+ BlobSource,
430
+ Conversion,
431
+ Quality,
432
+ ALL_FORMATS,
433
+ } = await loadMediabunny();
193
434
  const input = new Input({
194
435
  source: new BlobSource(file),
195
436
  formats: ALL_FORMATS,
@@ -206,14 +447,16 @@ async function processToFile(
206
447
  output,
207
448
  video: {
208
449
  codec: "avc",
209
- width: targetW,
210
- height: targetH,
211
- fit: "contain",
212
- bitrate: QUALITY_HIGH,
213
- },
214
- audio: {
215
- codec: "aac",
450
+ ...(needsResize
451
+ ? { width: targetW, height: targetH, fit: "contain" as const }
452
+ : {}),
453
+ ...(targetBitrate !== undefined
454
+ ? { quality: new Quality({ bitrate: targetBitrate }) }
455
+ : {}),
216
456
  },
457
+ // No quality set: an AAC track is copied, and anything else falls back
458
+ // to mediabunny's default, which for AAC lands on 192 kbps either way.
459
+ audio: { codec: "aac" },
217
460
  });
218
461
 
219
462
  if (onProgress) {
@@ -237,7 +480,8 @@ async function processToFile(
237
480
  // Safari's WebCodecs does NOT bake rotation, so the matrix is needed.
238
481
  // Strategy: probe the output as-is; if the dimensions already match
239
482
  // the expected display size, leave the file alone. Otherwise strip
240
- // the matrix and re-probe.
483
+ // the matrix and re-probe. A copied (not re-encoded) track never hits
484
+ // this — no encoder touched the pixels — so it falls out via dimsMatch.
241
485
  const originalName = file.name.replace(/\.[^.]+$/, "");
242
486
  let mp4File = new File([buffer], `${originalName}.mp4`, {
243
487
  type: "video/mp4",
@@ -1,44 +1,25 @@
1
1
  /**
2
- * Authenticated client entry point.
2
+ * Authenticated client entry point: the shell every signed-in page loads.
3
3
  *
4
- * Extends the public bundle with editor, upload, and settings interactions
5
- * that should not be shipped to anonymous page views.
4
+ * Extends the public bundle with what an author uses on any page — the post
5
+ * menu, the command palette, keyboard shortcuts, the buttons that open the
6
+ * composer — and leaves the rest to `client/lazy-entries.ts`, which loads the
7
+ * editor, settings, and management entries by what the page contains.
6
8
  */
7
9
 
8
10
  import "./client.js";
9
- import "./client/image-processor.js";
10
- import "./client/avatar-upload.js";
11
11
  import { ensureConfirmDialog } from "./client/confirm.js";
12
- import "./client/components/jant-compose-dialog.js";
13
- import "./client/components/jant-compose-editor.js";
14
- import "./client/components/jant-compose-fullscreen.js";
15
- import "./client/compose-bridge.js";
16
- import "./client/compose-discovery-bridge.js";
17
- import "./client/slash-discovery-bridge.js";
18
- import "./client/compose-shortcuts.js";
19
- import "./client/components/jant-settings-general.js";
20
- import "./client/components/jant-settings-avatar.js";
21
- import "./client/components/jant-config-editor.js";
22
- import "./client/settings-bridge.js";
23
- import "./client/components/jant-repo-picker.js";
24
- import "./client/components/jant-collection-form.js";
25
- import "./client/components/jant-collection-directory.js";
26
- import "./client/collection-form-bridge.js";
27
- import "./client/components/jant-post-form.js";
28
- import "./client/post-form-bridge.js";
29
- import "./client/components/jant-nav-manager.js";
30
12
  import "./client/components/jant-post-menu.js";
31
- import "./client/collection-page-actions.js";
32
- import "./client/custom-url-menu.js";
13
+ // The post menu renders the collection form inline when a post needs a new
14
+ // collection, so the form's definition travels with the menu.
15
+ import "./client/components/jant-collection-form.js";
33
16
  import "./client/components/jant-command-palette.js";
34
17
  import "./client/palette-shortcuts.js";
35
18
  import "./client/palette-search-trigger.js";
36
-
37
- // Mount fullscreen overlay at body level to escape the dialog's containing
38
- // block (dialog animation creates a containing block that traps fixed children).
39
- if (!document.querySelector("jant-compose-fullscreen")) {
40
- document.body.appendChild(document.createElement("jant-compose-fullscreen"));
41
- }
19
+ import "./client/compose-triggers.js";
20
+ import "./client/compose-shortcuts.js";
21
+ import "./client/compose-discovery-bridge.js";
22
+ import { loadEntriesForPage } from "./client/lazy-entries.js";
42
23
 
43
24
  ensureConfirmDialog();
44
25
 
@@ -46,3 +27,5 @@ ensureConfirmDialog();
46
27
  if (!document.querySelector("jant-command-palette")) {
47
28
  document.body.appendChild(document.createElement("jant-command-palette"));
48
29
  }
30
+
31
+ loadEntriesForPage();
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Composer entry: the editor and everything that only runs while writing.
3
+ *
4
+ * Loaded by `client/lazy-entries.ts` — on first open through
5
+ * `compose-launch.ts`, at once on the compose page — and preloaded by the
6
+ * layout on every signed-in page. Never referenced from a script tag.
7
+ */
8
+
9
+ import "./client/components/jant-compose-dialog.js";
10
+ import "./client/components/jant-compose-editor.js";
11
+ import "./client/components/jant-compose-fullscreen.js";
12
+ import "./client/compose-bridge.js";
13
+ import "./client/slash-discovery-bridge.js";
14
+
15
+ // Mount the fullscreen overlay at body level to escape the dialog's containing
16
+ // block (dialog animation creates a containing block that traps fixed children).
17
+ if (!document.querySelector("jant-compose-fullscreen")) {
18
+ document.body.appendChild(document.createElement("jant-compose-fullscreen"));
19
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Management entry: navigation manager, collection directory, and the menus
3
+ * on collection and custom URL pages.
4
+ *
5
+ * Loaded by `client/lazy-entries.ts` when the page carries one of their
6
+ * elements or hooks. Never referenced from a script tag.
7
+ */
8
+
9
+ import "./client/components/jant-nav-manager.js";
10
+ import "./client/components/jant-collection-directory.js";
11
+ import "./client/collection-page-actions.js";
12
+ import "./client/smart-collection-page-actions.js";
13
+ import "./client/custom-url-menu.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Settings entry: the components behind the settings pages.
3
+ *
4
+ * Loaded by `client/lazy-entries.ts` when one of these elements is on the
5
+ * page. Never referenced from a script tag.
6
+ */
7
+
8
+ import "./client/avatar-upload.js";
9
+ import "./client/components/jant-settings-general.js";
10
+ import "./client/components/jant-settings-language.js";
11
+ import "./client/components/jant-settings-avatar.js";
12
+ import "./client/components/jant-config-editor.js";
13
+ import "./client/components/jant-repo-picker.js";
14
+ import "./client/settings-bridge.js";