@jant/core 0.3.35 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side Video Processor
|
|
3
|
+
*
|
|
4
|
+
* Processes videos before upload using mediabunny:
|
|
5
|
+
* - Transcodes to H.264/AAC MP4 (universal playback)
|
|
6
|
+
* - Resizes to max 1920×1080
|
|
7
|
+
* - Extracts poster frame + blurhash during processing
|
|
8
|
+
*
|
|
9
|
+
* Requires WebCodecs API support — check `isSupported()` before use.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Input,
|
|
14
|
+
Output,
|
|
15
|
+
Mp4OutputFormat,
|
|
16
|
+
BufferTarget,
|
|
17
|
+
BlobSource,
|
|
18
|
+
CanvasSink,
|
|
19
|
+
Conversion,
|
|
20
|
+
QUALITY_HIGH,
|
|
21
|
+
ALL_FORMATS,
|
|
22
|
+
} from "mediabunny";
|
|
23
|
+
import { encode } from "blurhash";
|
|
24
|
+
|
|
25
|
+
const MAX_WIDTH = 1920;
|
|
26
|
+
const MAX_HEIGHT = 1080;
|
|
27
|
+
const POSTER_WIDTH = 640;
|
|
28
|
+
const BLURHASH_SIZE = 32;
|
|
29
|
+
|
|
30
|
+
export interface VideoProcessResult {
|
|
31
|
+
file: File;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
poster?: Blob;
|
|
35
|
+
blurhash?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the browser supports WebCodecs-based video processing.
|
|
40
|
+
*
|
|
41
|
+
* @returns `true` if `VideoEncoder` is available in the current environment
|
|
42
|
+
*/
|
|
43
|
+
function isSupported(): boolean {
|
|
44
|
+
return typeof VideoEncoder !== "undefined";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a poster frame, blurhash, and source dimensions from a video file.
|
|
49
|
+
* Seeks to `min(duration × 0.1, 3s)` and captures the frame.
|
|
50
|
+
* Also returns the original video dimensions so the caller can compute
|
|
51
|
+
* the correct output size without opening a second Input instance.
|
|
52
|
+
*
|
|
53
|
+
* @param file - Source video file
|
|
54
|
+
* @returns Poster blob (640px-wide WebP), blurhash string, and source dimensions
|
|
55
|
+
*/
|
|
56
|
+
async function extractPoster(file: File): Promise<{
|
|
57
|
+
poster?: Blob;
|
|
58
|
+
blurhash?: string;
|
|
59
|
+
sourceWidth?: number;
|
|
60
|
+
sourceHeight?: number;
|
|
61
|
+
}> {
|
|
62
|
+
const input = new Input({
|
|
63
|
+
source: new BlobSource(file),
|
|
64
|
+
formats: ALL_FORMATS,
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
const videoTrack = await input.getPrimaryVideoTrack();
|
|
68
|
+
if (!videoTrack) return {};
|
|
69
|
+
|
|
70
|
+
const sourceWidth = videoTrack.displayWidth;
|
|
71
|
+
const sourceHeight = videoTrack.displayHeight;
|
|
72
|
+
|
|
73
|
+
const duration = await input.computeDuration();
|
|
74
|
+
const seekTime = Math.min(duration * 0.1, 3);
|
|
75
|
+
|
|
76
|
+
const sink = new CanvasSink(videoTrack);
|
|
77
|
+
const wrapped = await sink.getCanvas(seekTime);
|
|
78
|
+
if (!wrapped) return { sourceWidth, sourceHeight };
|
|
79
|
+
|
|
80
|
+
const canvas = wrapped.canvas as HTMLCanvasElement;
|
|
81
|
+
|
|
82
|
+
// Poster: 640px wide WebP
|
|
83
|
+
const srcW = canvas.width;
|
|
84
|
+
const srcH = canvas.height;
|
|
85
|
+
const posterScale = Math.min(POSTER_WIDTH / srcW, 1);
|
|
86
|
+
const pw = Math.round(srcW * posterScale);
|
|
87
|
+
const ph = Math.round(srcH * posterScale);
|
|
88
|
+
|
|
89
|
+
const posterCanvas = document.createElement("canvas");
|
|
90
|
+
posterCanvas.width = pw;
|
|
91
|
+
posterCanvas.height = ph;
|
|
92
|
+
const pCtx = posterCanvas.getContext("2d");
|
|
93
|
+
if (!pCtx) return { sourceWidth, sourceHeight };
|
|
94
|
+
pCtx.drawImage(canvas, 0, 0, pw, ph);
|
|
95
|
+
|
|
96
|
+
const poster = await new Promise<Blob | undefined>((resolve) => {
|
|
97
|
+
posterCanvas.toBlob(
|
|
98
|
+
(blob) => resolve(blob ?? undefined),
|
|
99
|
+
"image/webp",
|
|
100
|
+
0.8,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Blurhash: 32px canvas, 4×3 components
|
|
105
|
+
const bhScale = Math.min(BLURHASH_SIZE / srcW, BLURHASH_SIZE / srcH, 1);
|
|
106
|
+
const bw = Math.max(Math.round(srcW * bhScale), 1);
|
|
107
|
+
const bh = Math.max(Math.round(srcH * bhScale), 1);
|
|
108
|
+
|
|
109
|
+
const bhCanvas = document.createElement("canvas");
|
|
110
|
+
bhCanvas.width = bw;
|
|
111
|
+
bhCanvas.height = bh;
|
|
112
|
+
const bhCtx = bhCanvas.getContext("2d");
|
|
113
|
+
if (!bhCtx) return { poster, sourceWidth, sourceHeight };
|
|
114
|
+
bhCtx.drawImage(canvas, 0, 0, bw, bh);
|
|
115
|
+
|
|
116
|
+
const imageData = bhCtx.getImageData(0, 0, bw, bh);
|
|
117
|
+
const blurhash = encode(imageData.data, bw, bh, 4, 3);
|
|
118
|
+
|
|
119
|
+
return { poster, blurhash, sourceWidth, sourceHeight };
|
|
120
|
+
} catch {
|
|
121
|
+
return {};
|
|
122
|
+
} finally {
|
|
123
|
+
input.dispose();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Process a video file: transcode to H.264/AAC MP4, resize to fit within
|
|
129
|
+
* 1920×1080, and extract poster frame + blurhash.
|
|
130
|
+
*
|
|
131
|
+
* @param file - Source video file
|
|
132
|
+
* @param onProgress - Optional callback receiving progress from 0 to 1
|
|
133
|
+
* @returns Processed MP4 file with dimensions, poster, and blurhash
|
|
134
|
+
*/
|
|
135
|
+
async function processToFile(
|
|
136
|
+
file: File,
|
|
137
|
+
onProgress?: (progress: number) => void,
|
|
138
|
+
): Promise<VideoProcessResult> {
|
|
139
|
+
// Extract poster + blurhash + source dimensions (separate Input instance,
|
|
140
|
+
// so the transcoding Input below starts with clean demuxer state).
|
|
141
|
+
const { poster, blurhash, sourceWidth, sourceHeight } =
|
|
142
|
+
await extractPoster(file);
|
|
143
|
+
|
|
144
|
+
// Compute output size preserving the original aspect ratio
|
|
145
|
+
let width = MAX_WIDTH;
|
|
146
|
+
let height = MAX_HEIGHT;
|
|
147
|
+
if (sourceWidth && sourceHeight) {
|
|
148
|
+
const scale = Math.min(
|
|
149
|
+
MAX_WIDTH / sourceWidth,
|
|
150
|
+
MAX_HEIGHT / sourceHeight,
|
|
151
|
+
1,
|
|
152
|
+
);
|
|
153
|
+
width = Math.round(sourceWidth * scale);
|
|
154
|
+
height = Math.round(sourceHeight * scale);
|
|
155
|
+
}
|
|
156
|
+
// H.264 requires even dimensions
|
|
157
|
+
width += width % 2;
|
|
158
|
+
height += height % 2;
|
|
159
|
+
|
|
160
|
+
// Transcode to MP4 H.264/AAC (fresh Input — not shared with extractPoster)
|
|
161
|
+
const input = new Input({
|
|
162
|
+
source: new BlobSource(file),
|
|
163
|
+
formats: ALL_FORMATS,
|
|
164
|
+
});
|
|
165
|
+
const target = new BufferTarget();
|
|
166
|
+
const output = new Output({
|
|
167
|
+
format: new Mp4OutputFormat({ fastStart: "in-memory" }),
|
|
168
|
+
target,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const conversion = await Conversion.init({
|
|
173
|
+
input,
|
|
174
|
+
output,
|
|
175
|
+
video: {
|
|
176
|
+
codec: "avc",
|
|
177
|
+
width,
|
|
178
|
+
height,
|
|
179
|
+
fit: "contain",
|
|
180
|
+
bitrate: QUALITY_HIGH,
|
|
181
|
+
},
|
|
182
|
+
audio: {
|
|
183
|
+
codec: "aac",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (onProgress) {
|
|
188
|
+
conversion.onProgress = onProgress;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await conversion.execute();
|
|
192
|
+
|
|
193
|
+
const buffer = target.buffer;
|
|
194
|
+
if (!buffer) throw new Error("Video processing produced no output");
|
|
195
|
+
|
|
196
|
+
const originalName = file.name.replace(/\.[^.]+$/, "");
|
|
197
|
+
const mp4File = new File([buffer], `${originalName}.mp4`, {
|
|
198
|
+
type: "video/mp4",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return { file: mp4File, width, height, poster, blurhash };
|
|
202
|
+
} finally {
|
|
203
|
+
input.dispose();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const VideoProcessor = { isSupported, processToFile };
|
package/src/client.ts
CHANGED
|
@@ -9,21 +9,31 @@
|
|
|
9
9
|
|
|
10
10
|
import "./vendor/datastar.js";
|
|
11
11
|
import "basecoat-css/all";
|
|
12
|
-
import "./
|
|
13
|
-
import "./
|
|
14
|
-
import "./lib/avatar-upload.js";
|
|
15
|
-
import "./lib/collections-reorder.js";
|
|
12
|
+
import "./client/image-processor.js";
|
|
13
|
+
import "./client/avatar-upload.js";
|
|
16
14
|
|
|
17
|
-
// Lit Web Components
|
|
18
|
-
import "./
|
|
19
|
-
import "./
|
|
20
|
-
import "./
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import "./
|
|
26
|
-
import "./
|
|
27
|
-
import "./
|
|
28
|
-
import "./
|
|
29
|
-
import "./
|
|
15
|
+
// Lit Web Components (and their bridge modules)
|
|
16
|
+
import "./client/components/jant-compose-dialog.js";
|
|
17
|
+
import "./client/components/jant-compose-editor.js";
|
|
18
|
+
import "./client/components/jant-compose-fullscreen.js";
|
|
19
|
+
|
|
20
|
+
// Mount fullscreen overlay at body level to escape the dialog's containing block
|
|
21
|
+
// (dialog animation creates a containing block that traps position:fixed descendants)
|
|
22
|
+
document.body.appendChild(document.createElement("jant-compose-fullscreen"));
|
|
23
|
+
import "./client/compose-bridge.js";
|
|
24
|
+
import "./client/components/jant-settings-general.js";
|
|
25
|
+
import "./client/components/jant-settings-avatar.js";
|
|
26
|
+
import "./client/settings-bridge.js";
|
|
27
|
+
import "./client/components/jant-collection-form.js";
|
|
28
|
+
import "./client/components/jant-collection-sidebar.js";
|
|
29
|
+
import "./client/collection-form-bridge.js";
|
|
30
|
+
import "./client/components/jant-post-form.js";
|
|
31
|
+
import "./client/post-form-bridge.js";
|
|
32
|
+
import "./client/components/jant-nav-manager.js";
|
|
33
|
+
import "./client/nav-manager-bridge.js";
|
|
34
|
+
import "./client/audio-player.js";
|
|
35
|
+
import "./client/components/jant-media-lightbox.js";
|
|
36
|
+
import "./client/components/jant-text-preview.js";
|
|
37
|
+
import "./client/components/jant-post-menu.js";
|
|
38
|
+
import "./client/thread-context.js";
|
|
39
|
+
import "./client/archive-nav.js";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Integrity Tests
|
|
3
|
+
*
|
|
4
|
+
* Ensures every migration SQL file is tracked in the Drizzle journal.
|
|
5
|
+
* Hand-written migrations bypass drizzle-kit and won't have journal entries,
|
|
6
|
+
* which breaks `drizzle-kit generate` for future schema changes.
|
|
7
|
+
*
|
|
8
|
+
* Fix: always run `mise run db-generate` instead of writing SQL by hand.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { readdirSync, readFileSync } from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
|
|
15
|
+
const MIGRATIONS_DIR = resolve(import.meta.dirname, "../migrations");
|
|
16
|
+
const JOURNAL_PATH = resolve(MIGRATIONS_DIR, "meta/_journal.json");
|
|
17
|
+
|
|
18
|
+
interface JournalEntry {
|
|
19
|
+
idx: number;
|
|
20
|
+
version: string;
|
|
21
|
+
when: number;
|
|
22
|
+
tag: string;
|
|
23
|
+
breakpoints: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Journal {
|
|
27
|
+
version: string;
|
|
28
|
+
dialect: string;
|
|
29
|
+
entries: JournalEntry[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJournal(): Journal {
|
|
33
|
+
return JSON.parse(readFileSync(JOURNAL_PATH, "utf-8"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listMigrationFiles(): string[] {
|
|
37
|
+
return readdirSync(MIGRATIONS_DIR)
|
|
38
|
+
.filter((f) => f.endsWith(".sql"))
|
|
39
|
+
.sort();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("migration integrity", () => {
|
|
43
|
+
it("every SQL file has a corresponding journal entry", () => {
|
|
44
|
+
const journal = readJournal();
|
|
45
|
+
const tags = new Set(journal.entries.map((e) => e.tag));
|
|
46
|
+
const sqlFiles = listMigrationFiles();
|
|
47
|
+
|
|
48
|
+
const untracked = sqlFiles
|
|
49
|
+
.map((f) => f.replace(".sql", ""))
|
|
50
|
+
.filter((tag) => !tags.has(tag));
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
untracked,
|
|
54
|
+
[
|
|
55
|
+
"These migration files are not tracked in meta/_journal.json.",
|
|
56
|
+
"This usually means they were hand-written instead of generated with `mise run db-generate`.",
|
|
57
|
+
"Fix: update src/db/schema.ts first, then run `mise run db-generate`.",
|
|
58
|
+
`Untracked files: ${untracked.map((t) => `${t}.sql`).join(", ")}`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("every journal entry has a corresponding SQL file", () => {
|
|
64
|
+
const journal = readJournal();
|
|
65
|
+
const sqlFiles = new Set(
|
|
66
|
+
listMigrationFiles().map((f) => f.replace(".sql", "")),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const missing = journal.entries
|
|
70
|
+
.map((e) => e.tag)
|
|
71
|
+
.filter((tag) => !sqlFiles.has(tag));
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
missing,
|
|
75
|
+
[
|
|
76
|
+
"These journal entries have no matching SQL file.",
|
|
77
|
+
`Missing files: ${missing.map((t) => `${t}.sql`).join(", ")}`,
|
|
78
|
+
].join("\n"),
|
|
79
|
+
).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("journal entries have sequential idx values", () => {
|
|
83
|
+
const journal = readJournal();
|
|
84
|
+
for (let i = 0; i < journal.entries.length; i++) {
|
|
85
|
+
const entry = journal.entries[i];
|
|
86
|
+
if (entry) expect(entry.idx).toBe(i);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("latest migration has a snapshot file", () => {
|
|
91
|
+
const journal = readJournal();
|
|
92
|
+
const lastEntry = journal.entries[journal.entries.length - 1];
|
|
93
|
+
if (!lastEntry) return;
|
|
94
|
+
|
|
95
|
+
const prefix = lastEntry.tag.split("_")[0];
|
|
96
|
+
const snapshotPath = resolve(
|
|
97
|
+
MIGRATIONS_DIR,
|
|
98
|
+
`meta/${prefix}_snapshot.json`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
let exists = false;
|
|
102
|
+
try {
|
|
103
|
+
readFileSync(snapshotPath);
|
|
104
|
+
exists = true;
|
|
105
|
+
} catch {
|
|
106
|
+
// file doesn't exist
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
expect(
|
|
110
|
+
exists,
|
|
111
|
+
[
|
|
112
|
+
`Missing snapshot for latest migration: meta/${prefix}_snapshot.json`,
|
|
113
|
+
"This means the migration was not generated by drizzle-kit.",
|
|
114
|
+
"Fix: run `mise run db-generate` to regenerate it properly.",
|
|
115
|
+
].join("\n"),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/db/index.ts
CHANGED
|
@@ -12,3 +12,55 @@ export function createDatabase(d1: D1Database) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export { schema };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* D1 enforces a lower SQL variable limit than standard SQLite (~999).
|
|
18
|
+
* Keep batch size well under the limit to leave room for other
|
|
19
|
+
* query parameters besides the IN-list.
|
|
20
|
+
*/
|
|
21
|
+
const BATCH_SIZE = 50;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run a query function in batches to avoid SQLite's variable limit.
|
|
25
|
+
* Splits `items` into chunks, calls `fn` for each chunk, and merges
|
|
26
|
+
* the resulting Maps.
|
|
27
|
+
*
|
|
28
|
+
* @param items - Array of IDs to batch
|
|
29
|
+
* @param fn - Async function that takes a chunk and returns a Map
|
|
30
|
+
* @returns Merged Map from all batches
|
|
31
|
+
*/
|
|
32
|
+
export async function batchQuery<K, V>(
|
|
33
|
+
items: K[],
|
|
34
|
+
fn: (chunk: K[]) => Promise<Map<K, V>>,
|
|
35
|
+
): Promise<Map<K, V>> {
|
|
36
|
+
if (items.length <= BATCH_SIZE) return fn(items);
|
|
37
|
+
|
|
38
|
+
const result = new Map<K, V>();
|
|
39
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
40
|
+
const chunk = items.slice(i, i + BATCH_SIZE);
|
|
41
|
+
const partial = await fn(chunk);
|
|
42
|
+
for (const [k, v] of partial) {
|
|
43
|
+
result.set(k, v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Like `batchQuery` but for functions that return an array of rows
|
|
51
|
+
* rather than a Map.
|
|
52
|
+
*/
|
|
53
|
+
export async function batchQueryRows<K, R>(
|
|
54
|
+
items: K[],
|
|
55
|
+
fn: (chunk: K[]) => Promise<R[]>,
|
|
56
|
+
): Promise<R[]> {
|
|
57
|
+
if (items.length <= BATCH_SIZE) return fn(items);
|
|
58
|
+
|
|
59
|
+
const result: R[] = [];
|
|
60
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
61
|
+
const chunk = items.slice(i, i + BATCH_SIZE);
|
|
62
|
+
const partial = await fn(chunk);
|
|
63
|
+
result.push(...partial);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|