diff --git a/Dockerfile b/Dockerfile index 2aeb415f..dd619f14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ COPY apps/web apps/web COPY packages packages COPY docs docs COPY release-summary.json release-summary.json -COPY scripts/self-hosted-config.mjs scripts/self-hosted-secrets.mjs scripts/self-hosted-server.mjs scripts/ +COPY scripts/self-hosted-config.mjs scripts/self-hosted-secrets.mjs scripts/self-hosted-server.mjs scripts/prepare-poster-thumbnails.mjs scripts/ +COPY tests/browser/poster-thumbnail-generator.html tests/browser/poster-thumbnail-generator.ts tests/browser/ COPY tsconfig.json ./ RUN bun run build:web && bun run build:self-hosted diff --git a/apps/web/src/assets/poster-template-previews/acid.en.webp b/apps/web/src/assets/poster-template-previews/acid.en.webp new file mode 100644 index 00000000..d00db57b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/acid.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/acid.zh.webp b/apps/web/src/assets/poster-template-previews/acid.zh.webp new file mode 100644 index 00000000..2dfd764a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/acid.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bento.en.webp b/apps/web/src/assets/poster-template-previews/bento.en.webp new file mode 100644 index 00000000..307d425a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bento.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bento.zh.webp b/apps/web/src/assets/poster-template-previews/bento.zh.webp new file mode 100644 index 00000000..bfec9bc8 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bento.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bili.en.webp b/apps/web/src/assets/poster-template-previews/bili.en.webp new file mode 100644 index 00000000..3c384b01 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bili.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bili.zh.webp b/apps/web/src/assets/poster-template-previews/bili.zh.webp new file mode 100644 index 00000000..a2ef7a88 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bili.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bold.en.webp b/apps/web/src/assets/poster-template-previews/bold.en.webp new file mode 100644 index 00000000..5519477b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bold.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/bold.zh.webp b/apps/web/src/assets/poster-template-previews/bold.zh.webp new file mode 100644 index 00000000..33a92837 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/bold.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/brush.en.webp b/apps/web/src/assets/poster-template-previews/brush.en.webp new file mode 100644 index 00000000..fb7d5d62 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/brush.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/brush.zh.webp b/apps/web/src/assets/poster-template-previews/brush.zh.webp new file mode 100644 index 00000000..f4973015 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/brush.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/calendar.en.webp b/apps/web/src/assets/poster-template-previews/calendar.en.webp new file mode 100644 index 00000000..187be11d Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/calendar.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/calendar.zh.webp b/apps/web/src/assets/poster-template-previews/calendar.zh.webp new file mode 100644 index 00000000..d290b1b4 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/calendar.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/calm.en.webp b/apps/web/src/assets/poster-template-previews/calm.en.webp new file mode 100644 index 00000000..cec5a120 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/calm.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/calm.zh.webp b/apps/web/src/assets/poster-template-previews/calm.zh.webp new file mode 100644 index 00000000..40ef67ab Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/calm.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/chat.en.webp b/apps/web/src/assets/poster-template-previews/chat.en.webp new file mode 100644 index 00000000..9df8ed6a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/chat.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/chat.zh.webp b/apps/web/src/assets/poster-template-previews/chat.zh.webp new file mode 100644 index 00000000..39d33fba Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/chat.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/cinema.en.webp b/apps/web/src/assets/poster-template-previews/cinema.en.webp new file mode 100644 index 00000000..47630a35 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/cinema.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/cinema.zh.webp b/apps/web/src/assets/poster-template-previews/cinema.zh.webp new file mode 100644 index 00000000..fcecc8b4 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/cinema.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/collage.en.webp b/apps/web/src/assets/poster-template-previews/collage.en.webp new file mode 100644 index 00000000..d828e982 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/collage.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/collage.zh.webp b/apps/web/src/assets/poster-template-previews/collage.zh.webp new file mode 100644 index 00000000..7e2a5072 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/collage.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/compare.en.webp b/apps/web/src/assets/poster-template-previews/compare.en.webp new file mode 100644 index 00000000..d957dc5f Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/compare.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/compare.zh.webp b/apps/web/src/assets/poster-template-previews/compare.zh.webp new file mode 100644 index 00000000..9bb1c4b7 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/compare.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/corner.en.webp b/apps/web/src/assets/poster-template-previews/corner.en.webp new file mode 100644 index 00000000..0c2dd191 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/corner.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/corner.zh.webp b/apps/web/src/assets/poster-template-previews/corner.zh.webp new file mode 100644 index 00000000..a61baa57 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/corner.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/folio.en.webp b/apps/web/src/assets/poster-template-previews/folio.en.webp new file mode 100644 index 00000000..47192069 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/folio.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/folio.zh.webp b/apps/web/src/assets/poster-template-previews/folio.zh.webp new file mode 100644 index 00000000..ef993e2d Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/folio.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/frame.en.webp b/apps/web/src/assets/poster-template-previews/frame.en.webp new file mode 100644 index 00000000..ea7367c8 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/frame.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/frame.zh.webp b/apps/web/src/assets/poster-template-previews/frame.zh.webp new file mode 100644 index 00000000..59a5c9bd Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/frame.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/glass.en.webp b/apps/web/src/assets/poster-template-previews/glass.en.webp new file mode 100644 index 00000000..3cf8b731 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/glass.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/glass.zh.webp b/apps/web/src/assets/poster-template-previews/glass.zh.webp new file mode 100644 index 00000000..9d236e2a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/glass.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/highlight.en.webp b/apps/web/src/assets/poster-template-previews/highlight.en.webp new file mode 100644 index 00000000..1c5d0fe4 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/highlight.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/highlight.zh.webp b/apps/web/src/assets/poster-template-previews/highlight.zh.webp new file mode 100644 index 00000000..abf1739a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/highlight.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/impact.en.webp b/apps/web/src/assets/poster-template-previews/impact.en.webp new file mode 100644 index 00000000..de68fccd Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/impact.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/impact.zh.webp b/apps/web/src/assets/poster-template-previews/impact.zh.webp new file mode 100644 index 00000000..dcb0e4d6 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/impact.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/interview.en.webp b/apps/web/src/assets/poster-template-previews/interview.en.webp new file mode 100644 index 00000000..4af18103 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/interview.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/interview.zh.webp b/apps/web/src/assets/poster-template-previews/interview.zh.webp new file mode 100644 index 00000000..c104c590 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/interview.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/keyword.en.webp b/apps/web/src/assets/poster-template-previews/keyword.en.webp new file mode 100644 index 00000000..aae3d646 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/keyword.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/keyword.zh.webp b/apps/web/src/assets/poster-template-previews/keyword.zh.webp new file mode 100644 index 00000000..3cc1d478 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/keyword.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/kicker.en.webp b/apps/web/src/assets/poster-template-previews/kicker.en.webp new file mode 100644 index 00000000..8bc21ab5 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/kicker.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/kicker.zh.webp b/apps/web/src/assets/poster-template-previews/kicker.zh.webp new file mode 100644 index 00000000..a51edace Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/kicker.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/mag.en.webp b/apps/web/src/assets/poster-template-previews/mag.en.webp new file mode 100644 index 00000000..88d1ce0b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/mag.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/mag.zh.webp b/apps/web/src/assets/poster-template-previews/mag.zh.webp new file mode 100644 index 00000000..88b7fcc1 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/mag.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/manifest.json b/apps/web/src/assets/poster-template-previews/manifest.json new file mode 100644 index 00000000..f1193a44 --- /dev/null +++ b/apps/web/src/assets/poster-template-previews/manifest.json @@ -0,0 +1,559 @@ +{ + "sourceHash": "0b31e86ab8bca59fbf258f62af4513b48b2dcdca9abff8963b6c6de0c5d7e8bd", + "width": 480, + "height": 640, + "templates": [ + { + "id": "folio", + "recommended": true, + "zh": { + "file": "folio.zh.webp", + "sha256": "32b38ef41e79e08768a287dd8669124a0a8c7716eefca8ce8b00d625f1522b2a" + }, + "en": { + "file": "folio.en.webp", + "sha256": "b37a410d6e1ca601d308574e7c174a79ad05e28db190fc009e796e3890d11b5b" + } + }, + { + "id": "brush", + "recommended": true, + "zh": { + "file": "brush.zh.webp", + "sha256": "43b5358725d5ce23ba0cc53e59ac890d795adbba9f1368aba4889dbd8aa35deb" + }, + "en": { + "file": "brush.en.webp", + "sha256": "d28770577aecdeb894881c77c401c7969342257bc154935a668bb9ae9b105002" + } + }, + { + "id": "keyword", + "recommended": true, + "zh": { + "file": "keyword.zh.webp", + "sha256": "8f19a768a34b19fc61165760c80e796a4921cbc1e92f6c07f21235cc52a8dcb1" + }, + "en": { + "file": "keyword.en.webp", + "sha256": "d87c4a20b6616d18983ffa01d13b25a304f6e703d3f5eaac91bf407430abdfb0" + } + }, + { + "id": "regeng", + "recommended": true, + "zh": { + "file": "regeng.zh.webp", + "sha256": "daaee9fc1c7d2ed41219df130024a0d3080b52c09165178834e15eb4a1c568cb" + }, + "en": { + "file": "regeng.en.webp", + "sha256": "ae977f20e487e29e1e9d144497493ac8e2e10918b4a64bc8cf73310bb8c49b96" + } + }, + { + "id": "highlight", + "recommended": true, + "zh": { + "file": "highlight.zh.webp", + "sha256": "cb2dbbf33dce22e37d15351d54d58367e8862ded39c3d01371390abf0a3a23b9" + }, + "en": { + "file": "highlight.en.webp", + "sha256": "5445184ce5e6103731ee64e9de5e901122ad4849f1fa59ccd0f13818035da2c6" + } + }, + { + "id": "frame", + "recommended": true, + "zh": { + "file": "frame.zh.webp", + "sha256": "96881d595e8505a0a5ed2744a950184e89af448746bbb13c1141323ec60526ef" + }, + "en": { + "file": "frame.en.webp", + "sha256": "b8036e11930c45696bd8f78fba6771c3704a99e7135645680085010e17ca2271" + } + }, + { + "id": "sage", + "recommended": true, + "zh": { + "file": "sage.zh.webp", + "sha256": "ccf35a6062ac4dacd593d35c0d2ea65f5c53898f010b1735d5189e67be4e96f0" + }, + "en": { + "file": "sage.en.webp", + "sha256": "8b79de2ce1ca418e08e53aca5e1e8df749ea788bd17d4f8b4755c174f5af9711" + } + }, + { + "id": "notes", + "recommended": true, + "zh": { + "file": "notes.zh.webp", + "sha256": "87716a4e8107b11e1fdaf03d9acd2fc0a0e899f81a59e0d97e48ded131d22451" + }, + "en": { + "file": "notes.en.webp", + "sha256": "84152613d837c426473c14afe90b1419c74855070a9760115062ae1da4a9a75d" + } + }, + { + "id": "interview", + "recommended": true, + "zh": { + "file": "interview.zh.webp", + "sha256": "d732120902782f62ea0b72f8c806ff18b41f94234323b300d2e1f54fd3140b11" + }, + "en": { + "file": "interview.en.webp", + "sha256": "f0ac0a8c21abc35de5882f0fbc67169650f50335ad66afd99eb0350eb7014adb" + } + }, + { + "id": "riso", + "recommended": true, + "zh": { + "file": "riso.zh.webp", + "sha256": "f02df81ab9ea91af89fea1111a74c40287cddfc6e727b093924cdf3699ae0db7" + }, + "en": { + "file": "riso.en.webp", + "sha256": "4c30c9548cfb48583fcc26ef3c9e96457527fdb769ed736143e0d16b0e9b03b7" + } + }, + { + "id": "kicker", + "recommended": true, + "zh": { + "file": "kicker.zh.webp", + "sha256": "4a3f513e703d2f88dcc09ea43de9ceac4c07e9df353804581a361d8d26136961" + }, + "en": { + "file": "kicker.en.webp", + "sha256": "884bdd17db2fce142f021664fb763c7d10bcbdc534b1e336e30dc21fe0443c97" + } + }, + { + "id": "numeral", + "recommended": true, + "zh": { + "file": "numeral.zh.webp", + "sha256": "51746285ea9946f2dc97448678c27d2410bc913c0a003d65fc74b74f73cc65e3" + }, + "en": { + "file": "numeral.en.webp", + "sha256": "9847d7ab120588f53b75030de967d868c4f9458755248a7a546b8f018a98fe9a" + } + }, + { + "id": "calendar", + "recommended": true, + "zh": { + "file": "calendar.zh.webp", + "sha256": "ee28322ed9ac8cd150561515fc8991bac38b90798c9804b8cde13b1c4449aff1" + }, + "en": { + "file": "calendar.en.webp", + "sha256": "4ae26864c6f18c4831c2a969e8834838c7c2a3da9c154e76a8573bfce753b65a" + } + }, + { + "id": "ticket", + "recommended": true, + "zh": { + "file": "ticket.zh.webp", + "sha256": "db0be6d02049ee12424a697b22c35cb9b02fa171abfa9f670d5170ea098c5567" + }, + "en": { + "file": "ticket.en.webp", + "sha256": "38807f3f68d2ec215826b133c8890698f02575aa2a0d665b1084c4f60e4a832e" + } + }, + { + "id": "corner", + "recommended": true, + "zh": { + "file": "corner.zh.webp", + "sha256": "a669b91e27294814496b2f56f9657b7026efbe5f5a05c0b9dd98f7f0273b30e9" + }, + "en": { + "file": "corner.en.webp", + "sha256": "32a1effc38ae32290b81614d9557addb82db9acc58d0d7e48cd4a975cea8665a" + } + }, + { + "id": "polaroid", + "recommended": true, + "zh": { + "file": "polaroid.zh.webp", + "sha256": "58286dec0d3e8f4b12e6948fd20766eceabb64cf5f67d8b3cedb082dcd2a6460" + }, + "en": { + "file": "polaroid.en.webp", + "sha256": "a23c4892ece241e283856cd0d6ba805866bdf087b26b0bcc81a4ef4d14094548" + } + }, + { + "id": "serial", + "recommended": true, + "zh": { + "file": "serial.zh.webp", + "sha256": "aeb766583c4fb435fb02cbd1c2b93979252732a2ffd26b67cd327d59e11e30c5" + }, + "en": { + "file": "serial.en.webp", + "sha256": "83c7418bff171df1b67a3969f1b0f6853e6d1e0979cee6bae0bfcc3aa233be06" + } + }, + { + "id": "chat", + "recommended": true, + "zh": { + "file": "chat.zh.webp", + "sha256": "107d0c5bacc6802120ea5fe7555f97de4f7f081bb0b300851c0f2bf92929e4e1" + }, + "en": { + "file": "chat.en.webp", + "sha256": "4ce88c95fcb6bec83c4bcf546fc826364bee4774bfeec45ffa0c16a8e21cb3f0" + } + }, + { + "id": "window", + "recommended": true, + "zh": { + "file": "window.zh.webp", + "sha256": "2141cac66c25c6a23d5ffc17e4bcb3a32d080f418328b6a9c12b59ed829b3ded" + }, + "en": { + "file": "window.en.webp", + "sha256": "36c3ceb0696cdaf9e06fda1dfcb4ef18d77082697dea50ff5d1f7ed373b4e997" + } + }, + { + "id": "newspaper", + "recommended": true, + "zh": { + "file": "newspaper.zh.webp", + "sha256": "9063ac7fd5e9a80fc01caeae88687e1914a31e3b8b6f44f98e9dbe8def30df8c" + }, + "en": { + "file": "newspaper.en.webp", + "sha256": "d26179d919b64d89c61b7563aef11373b9d36a76cc67caf1a065331cf5c25e89" + } + }, + { + "id": "stack", + "recommended": true, + "zh": { + "file": "stack.zh.webp", + "sha256": "ebd06cb881a03d3725c5ba5c665b09e336480a7ad87ab0f6137759b8be91c807" + }, + "en": { + "file": "stack.en.webp", + "sha256": "429d2f40aa4cdb1fb3f33825f98f89cfc64184b117cdc6424931f838b14642c2" + } + }, + { + "id": "bili", + "recommended": true, + "zh": { + "file": "bili.zh.webp", + "sha256": "8908433e6443f0ac8ecc582e10f47f52b36f22612fbabc37e1c83965cd388527" + }, + "en": { + "file": "bili.en.webp", + "sha256": "ea4dd7a056f92d1883fcecc0eba4e28bf6a94252bd5ca9be955e4760abe44d44" + } + }, + { + "id": "pop", + "recommended": true, + "zh": { + "file": "pop.zh.webp", + "sha256": "463b6b02c6d891d01df1ab36bc8aaae04c381a642dbfef286be2a80c99e0c93d" + }, + "en": { + "file": "pop.en.webp", + "sha256": "9511381af1fae4351c83f925673be87cb32f5282a37739dabc96550f5801e1c0" + } + }, + { + "id": "swiss", + "recommended": true, + "zh": { + "file": "swiss.zh.webp", + "sha256": "cdb608fbc018e4e9f7902003eaecff3ce2722e06ca20a22981922710a1085596" + }, + "en": { + "file": "swiss.en.webp", + "sha256": "6aad979d9fa1a55ca55e095a0de3c3142690440401dd534ba6d16c7bcef1a7a8" + } + }, + { + "id": "collage", + "recommended": true, + "zh": { + "file": "collage.zh.webp", + "sha256": "65d01a66322186759e679d8e5051a37b5bbcdbd124c3b7b980ba80027d3576b8" + }, + "en": { + "file": "collage.en.webp", + "sha256": "9374552931a4202cb94cb9ae69576e469ece63394617c8c97adf349b30d88b20" + } + }, + { + "id": "mega", + "recommended": true, + "zh": { + "file": "mega.zh.webp", + "sha256": "4cfee18e1fa4bbacfd78e8e939a9cd625bc707a5def23e0353b389efc073cf76" + }, + "en": { + "file": "mega.en.webp", + "sha256": "390c11daf3d2b3afefd8cc83d99f99c3d1cbdd2e0c30714e115700c4c5054938" + } + }, + { + "id": "ticker", + "recommended": false, + "zh": { + "file": "ticker.zh.webp", + "sha256": "f2924666a8539fde3a925e9a7c2e3eb4b43dbd973200edb624cbb48aafcc5f7c" + }, + "en": { + "file": "ticker.en.webp", + "sha256": "e23d9f0d050349c8982e542f9c4c75adca0c32cdc745f0b59ca9a171113e8aee" + } + }, + { + "id": "memo", + "recommended": true, + "zh": { + "file": "memo.zh.webp", + "sha256": "a3726568766d29270a4bfb240e1a7939910e9cbbbb30006e51dc052b8a535fd7" + }, + "en": { + "file": "memo.en.webp", + "sha256": "c6f3b8febda7ad09dbea16bb9bfe422802a805228c630ef8209157d342a0a88b" + } + }, + { + "id": "mag", + "recommended": true, + "zh": { + "file": "mag.zh.webp", + "sha256": "0c4148d9ec399b8a5c397948d5ce13cc20ec3e0803971398ebf3b5437cb07709" + }, + "en": { + "file": "mag.en.webp", + "sha256": "39a4f459a14aef9c31fb99a00405b16258306d83c9d699b80b42784af5138138" + } + }, + { + "id": "print", + "recommended": true, + "zh": { + "file": "print.zh.webp", + "sha256": "2674d517bb291f9ff3f8bde7a3843b3a7eeb567f10aecb830257a04f4e9cb5a4" + }, + "en": { + "file": "print.en.webp", + "sha256": "703d5d57cdbe4cffe376c5e05f49c84e0029398ccd281415c143175d05a39678" + } + }, + { + "id": "bento", + "recommended": true, + "zh": { + "file": "bento.zh.webp", + "sha256": "8aa0ef1cbc77766e25e18c070afa720d55848765a2830b6f450bf584fd646a86" + }, + "en": { + "file": "bento.en.webp", + "sha256": "70fbe39e71fe44f9577165412d2f5b55bfefa2ad8ef3d2a04783e5763f3a45c8" + } + }, + { + "id": "calm", + "recommended": true, + "zh": { + "file": "calm.zh.webp", + "sha256": "879085cec407691ca92bc51b98499453ba710aa2d504059ffd33eea473d6eb8a" + }, + "en": { + "file": "calm.en.webp", + "sha256": "33964170a0f01bc654ab972484b426e16dbd77a68715119e464b227e45652581" + } + }, + { + "id": "seal", + "recommended": true, + "zh": { + "file": "seal.zh.webp", + "sha256": "c11e302e9de4e448ff4f55d0e0e5b4a8e4b87d2eb2fda64caa9c5db8ac2666fd" + }, + "en": { + "file": "seal.en.webp", + "sha256": "ff605edb68522a9fff9dd2ad2f9f51f8af28ff0e17cc8599955b10a5bcf3f2ff" + } + }, + { + "id": "compare", + "recommended": false, + "zh": { + "file": "compare.zh.webp", + "sha256": "630e5c80f96fcefeba4876b063a1c47db8f9091a38ee2adba4013eec7d00af44" + }, + "en": { + "file": "compare.en.webp", + "sha256": "66f12396df6e55d0ea8e9789203516c9d7caab6d93d2cbf6f80841baa0d02647" + } + }, + { + "id": "split", + "recommended": true, + "zh": { + "file": "split.zh.webp", + "sha256": "ff2ce3496cceb84788bc07c48a3c8cc98e11ba407a5aca661a01b092eb3dc2e9" + }, + "en": { + "file": "split.en.webp", + "sha256": "57c690da365592096184b0d504ab7f553f1e427e61f8bd058a027826aba48cf3" + } + }, + { + "id": "impact", + "recommended": false, + "zh": { + "file": "impact.zh.webp", + "sha256": "69e10213d8398ca7e36ca5db5f02e5b9ab3d3d0a519c8b5d598cf62c213ef65c" + }, + "en": { + "file": "impact.en.webp", + "sha256": "c624c21845b4345ede894c4abba0c7e2898719bfa3a27698e719119bb941b884" + } + }, + { + "id": "neon", + "recommended": false, + "zh": { + "file": "neon.zh.webp", + "sha256": "3bde7cb3a3f47621431e2bc8840324bc5f79fbe4e9d6c6a59d97a9d9fff46a9b" + }, + "en": { + "file": "neon.en.webp", + "sha256": "4cadca26bf18c5d6bb77baac5362df382be7beaadc8c944b585356038ebdd329" + } + }, + { + "id": "quote", + "recommended": false, + "zh": { + "file": "quote.zh.webp", + "sha256": "a430e4f382a46d2769ca10aef3461f533a32a3d33824cfa71c62b6f1d30e4536" + }, + "en": { + "file": "quote.en.webp", + "sha256": "94a1197ceab40636add5e358bb64eeb3b384f42d54f555ff10103540f3578d6c" + } + }, + { + "id": "cinema", + "recommended": false, + "zh": { + "file": "cinema.zh.webp", + "sha256": "5bc3588b3bd5795d440dcfd9ca4b7a4f766084379990ef8e218f3f12e7dd22d6" + }, + "en": { + "file": "cinema.en.webp", + "sha256": "213483077ff505a89d333b197d9b595e88ca44dc9ebd380c04c9101a1bd607f3" + } + }, + { + "id": "minimal", + "recommended": true, + "zh": { + "file": "minimal.zh.webp", + "sha256": "433a43877deec1b8b85fa4e09d5dce1e80087afecaed5e4c48ddd4992e04e1d1" + }, + "en": { + "file": "minimal.en.webp", + "sha256": "c1146a1d5a6c04f67a98e457b2ea03859af4d0d4b162b40d074354336cf09796" + } + }, + { + "id": "bold", + "recommended": true, + "zh": { + "file": "bold.zh.webp", + "sha256": "f419f907b1aca1c85755435df009aac866cdcc8ceb67415897c153e07b75896f" + }, + "en": { + "file": "bold.en.webp", + "sha256": "9ba9cffdf756c4d6e94c8644f477357e3ec8e20ce255a2a4759a81922a14d44f" + } + }, + { + "id": "poster", + "recommended": true, + "zh": { + "file": "poster.zh.webp", + "sha256": "6582770fbc1b566e3a2a095560c8093da8e95b0fafbaced8ac557a0db8fa9f02" + }, + "en": { + "file": "poster.en.webp", + "sha256": "e94fc2fc048726f76980b295f7389d41bee0f0184d6dff859068bef5821f1eb2" + } + }, + { + "id": "number", + "recommended": true, + "zh": { + "file": "number.zh.webp", + "sha256": "ae2f508e780334ec10e4afdb75a87940f8614225546cf9de4df52a60dc1f62ee" + }, + "en": { + "file": "number.en.webp", + "sha256": "8985392e12b99082c7679e65527ef76f2400f1d2ec359839d7245d8a0c7e5e33" + } + }, + { + "id": "neo", + "recommended": true, + "zh": { + "file": "neo.zh.webp", + "sha256": "5c8f4902c7ee615aeefee91041dca3df66926c7245753ce8d61f183590dd8df1" + }, + "en": { + "file": "neo.en.webp", + "sha256": "3d84c9fb1fbebc44d72b15b1251b9361c254027722e430bc0e4c2bd935698d39" + } + }, + { + "id": "acid", + "recommended": true, + "zh": { + "file": "acid.zh.webp", + "sha256": "2293f87d3c7408120446ee5558a56a0c5f6a679d68fc5c8b26620fcba05b304f" + }, + "en": { + "file": "acid.en.webp", + "sha256": "5ff599752562296bd1d2ba85c1feb0795fefb7d9a69d7e6d93ebcf10b3e664bf" + } + }, + { + "id": "glass", + "recommended": true, + "zh": { + "file": "glass.zh.webp", + "sha256": "015b0e194824edd73f7ba21adecb5ca9cfbcbeb0dd5fc1cde0c91e78113a5870" + }, + "en": { + "file": "glass.en.webp", + "sha256": "47214b2ef818530d75bf6a44b9732773edb9e3cee9db9a30068da105489c0eb3" + } + } + ] +} diff --git a/apps/web/src/assets/poster-template-previews/mega.en.webp b/apps/web/src/assets/poster-template-previews/mega.en.webp new file mode 100644 index 00000000..b5cf49db Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/mega.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/mega.zh.webp b/apps/web/src/assets/poster-template-previews/mega.zh.webp new file mode 100644 index 00000000..2298d231 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/mega.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/memo.en.webp b/apps/web/src/assets/poster-template-previews/memo.en.webp new file mode 100644 index 00000000..f002cf21 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/memo.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/memo.zh.webp b/apps/web/src/assets/poster-template-previews/memo.zh.webp new file mode 100644 index 00000000..e089704f Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/memo.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/minimal.en.webp b/apps/web/src/assets/poster-template-previews/minimal.en.webp new file mode 100644 index 00000000..c10abe76 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/minimal.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/minimal.zh.webp b/apps/web/src/assets/poster-template-previews/minimal.zh.webp new file mode 100644 index 00000000..889a9291 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/minimal.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/neo.en.webp b/apps/web/src/assets/poster-template-previews/neo.en.webp new file mode 100644 index 00000000..7c86f58d Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/neo.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/neo.zh.webp b/apps/web/src/assets/poster-template-previews/neo.zh.webp new file mode 100644 index 00000000..b6872132 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/neo.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/neon.en.webp b/apps/web/src/assets/poster-template-previews/neon.en.webp new file mode 100644 index 00000000..f22efed8 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/neon.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/neon.zh.webp b/apps/web/src/assets/poster-template-previews/neon.zh.webp new file mode 100644 index 00000000..dcc4980c Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/neon.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/newspaper.en.webp b/apps/web/src/assets/poster-template-previews/newspaper.en.webp new file mode 100644 index 00000000..e2a7da6f Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/newspaper.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/newspaper.zh.webp b/apps/web/src/assets/poster-template-previews/newspaper.zh.webp new file mode 100644 index 00000000..cab9bc59 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/newspaper.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/notes.en.webp b/apps/web/src/assets/poster-template-previews/notes.en.webp new file mode 100644 index 00000000..7e5890c3 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/notes.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/notes.zh.webp b/apps/web/src/assets/poster-template-previews/notes.zh.webp new file mode 100644 index 00000000..fede6c6b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/notes.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/number.en.webp b/apps/web/src/assets/poster-template-previews/number.en.webp new file mode 100644 index 00000000..2a3a5772 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/number.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/number.zh.webp b/apps/web/src/assets/poster-template-previews/number.zh.webp new file mode 100644 index 00000000..4d9d7d85 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/number.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/numeral.en.webp b/apps/web/src/assets/poster-template-previews/numeral.en.webp new file mode 100644 index 00000000..caafa175 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/numeral.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/numeral.zh.webp b/apps/web/src/assets/poster-template-previews/numeral.zh.webp new file mode 100644 index 00000000..5168c26a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/numeral.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/polaroid.en.webp b/apps/web/src/assets/poster-template-previews/polaroid.en.webp new file mode 100644 index 00000000..c4824499 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/polaroid.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/polaroid.zh.webp b/apps/web/src/assets/poster-template-previews/polaroid.zh.webp new file mode 100644 index 00000000..f444a09c Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/polaroid.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/pop.en.webp b/apps/web/src/assets/poster-template-previews/pop.en.webp new file mode 100644 index 00000000..7b10af38 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/pop.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/pop.zh.webp b/apps/web/src/assets/poster-template-previews/pop.zh.webp new file mode 100644 index 00000000..3069e96a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/pop.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/poster.en.webp b/apps/web/src/assets/poster-template-previews/poster.en.webp new file mode 100644 index 00000000..ce542796 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/poster.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/poster.zh.webp b/apps/web/src/assets/poster-template-previews/poster.zh.webp new file mode 100644 index 00000000..ef53002e Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/poster.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/print.en.webp b/apps/web/src/assets/poster-template-previews/print.en.webp new file mode 100644 index 00000000..29d9b7a4 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/print.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/print.zh.webp b/apps/web/src/assets/poster-template-previews/print.zh.webp new file mode 100644 index 00000000..3d19b7ed Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/print.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/quote.en.webp b/apps/web/src/assets/poster-template-previews/quote.en.webp new file mode 100644 index 00000000..59fda560 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/quote.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/quote.zh.webp b/apps/web/src/assets/poster-template-previews/quote.zh.webp new file mode 100644 index 00000000..905faac9 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/quote.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/regeng.en.webp b/apps/web/src/assets/poster-template-previews/regeng.en.webp new file mode 100644 index 00000000..96c3a95b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/regeng.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/regeng.zh.webp b/apps/web/src/assets/poster-template-previews/regeng.zh.webp new file mode 100644 index 00000000..0578f03b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/regeng.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/riso.en.webp b/apps/web/src/assets/poster-template-previews/riso.en.webp new file mode 100644 index 00000000..63846219 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/riso.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/riso.zh.webp b/apps/web/src/assets/poster-template-previews/riso.zh.webp new file mode 100644 index 00000000..19c5ab15 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/riso.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/sage.en.webp b/apps/web/src/assets/poster-template-previews/sage.en.webp new file mode 100644 index 00000000..be813ea2 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/sage.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/sage.zh.webp b/apps/web/src/assets/poster-template-previews/sage.zh.webp new file mode 100644 index 00000000..a807f073 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/sage.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/seal.en.webp b/apps/web/src/assets/poster-template-previews/seal.en.webp new file mode 100644 index 00000000..b80c5da8 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/seal.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/seal.zh.webp b/apps/web/src/assets/poster-template-previews/seal.zh.webp new file mode 100644 index 00000000..c45b2c67 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/seal.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/serial.en.webp b/apps/web/src/assets/poster-template-previews/serial.en.webp new file mode 100644 index 00000000..892a64eb Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/serial.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/serial.zh.webp b/apps/web/src/assets/poster-template-previews/serial.zh.webp new file mode 100644 index 00000000..d158124a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/serial.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/split.en.webp b/apps/web/src/assets/poster-template-previews/split.en.webp new file mode 100644 index 00000000..8d8c6f31 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/split.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/split.zh.webp b/apps/web/src/assets/poster-template-previews/split.zh.webp new file mode 100644 index 00000000..235ff4f7 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/split.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/stack.en.webp b/apps/web/src/assets/poster-template-previews/stack.en.webp new file mode 100644 index 00000000..18b70b18 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/stack.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/stack.zh.webp b/apps/web/src/assets/poster-template-previews/stack.zh.webp new file mode 100644 index 00000000..0bb9e807 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/stack.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/swiss.en.webp b/apps/web/src/assets/poster-template-previews/swiss.en.webp new file mode 100644 index 00000000..a96b7147 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/swiss.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/swiss.zh.webp b/apps/web/src/assets/poster-template-previews/swiss.zh.webp new file mode 100644 index 00000000..7e709444 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/swiss.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/ticker.en.webp b/apps/web/src/assets/poster-template-previews/ticker.en.webp new file mode 100644 index 00000000..c81d7de3 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/ticker.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/ticker.zh.webp b/apps/web/src/assets/poster-template-previews/ticker.zh.webp new file mode 100644 index 00000000..0966140b Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/ticker.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/ticket.en.webp b/apps/web/src/assets/poster-template-previews/ticket.en.webp new file mode 100644 index 00000000..b6dfb171 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/ticket.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/ticket.zh.webp b/apps/web/src/assets/poster-template-previews/ticket.zh.webp new file mode 100644 index 00000000..7e66a77a Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/ticket.zh.webp differ diff --git a/apps/web/src/assets/poster-template-previews/window.en.webp b/apps/web/src/assets/poster-template-previews/window.en.webp new file mode 100644 index 00000000..e4ef2932 Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/window.en.webp differ diff --git a/apps/web/src/assets/poster-template-previews/window.zh.webp b/apps/web/src/assets/poster-template-previews/window.zh.webp new file mode 100644 index 00000000..948aef4e Binary files /dev/null and b/apps/web/src/assets/poster-template-previews/window.zh.webp differ diff --git a/apps/web/src/components/PosterEditorPane.tsx b/apps/web/src/components/PosterEditorPane.tsx index c65ee548..e54d14df 100644 --- a/apps/web/src/components/PosterEditorPane.tsx +++ b/apps/web/src/components/PosterEditorPane.tsx @@ -1848,8 +1848,6 @@ export default function PosterEditorPane({
{leftTab === "templates" ? ( void; disabled: boolean; }) { - const host = useRef(null); - const [visible, setVisible] = useState(false), - [url, setUrl] = useState(), - [status, setStatus] = useState<"loading" | "ready" | "error">("loading"), - [attempt, setAttempt] = useState(0); - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry?.isIntersecting) { - setVisible(true); - observer.disconnect(); - } - }, - { rootMargin: "120px" }, - ); - if (host.current) observer.observe(host.current); - return () => observer.disconnect(); - }, []); - useEffect(() => { - if (!visible) return; - let cancelled = false, expired = false, preview: string | undefined; - setUrl(undefined); - setStatus("loading"); - const timer = setTimeout(() => { - expired = true; - setStatus("error"); - console.warn("Poster template preview timed out", template.id); - }, 15_000); - void (async () => { - try { - const document = await buildPosterDesign({ - id: template.id, - title: zh ? "把想法\n变成作品" : "MAKE IDEAS\nWORTH SHARING", - subtitle: zh ? "一个好想法,值得被看见。" : "Ideas deserve to be seen.", - badge: zh ? "创作笔记" : "CREATIVE NOTES", - width, - height, - zh, - }); - if (cancelled || expired) return; - const next = await renderPosterThumbnail(document); - if (cancelled || expired) URL.revokeObjectURL(next); - else { - preview = next; - setUrl(next); - setStatus("ready"); - } - } catch (error) { - if (!cancelled && !expired) { - setStatus("error"); - console.warn("Poster template preview failed", template.id, error); - } - } finally { - clearTimeout(timer); - } - })(); - return () => { - cancelled = true; - clearTimeout(timer); - if (preview) URL.revokeObjectURL(preview); - }; - }, [visible, width, height, zh, template.id, attempt]); + const url = zh ? template.thumbnails.zh : template.thumbnails.en; return ( -
- - {status === "error" && ( - - )} -
+ ); }); export function PosterTemplatePanel({ - width, - height, platformId, templateId, zh, disabled, onApply, }: { - width: number; - height: number; platformId?: string; templateId?: string; zh: boolean; @@ -156,7 +58,7 @@ export function PosterTemplatePanel({ const [search, setSearch] = useState(""), [recommended, setRecommended] = useState(true); const templates = ( - recommended ? templatesFor(platformId) : DESIGN_TEMPLATES + recommended ? posterGalleryTemplatesFor(platformId) : POSTER_GALLERY_TEMPLATES ).filter((template) => `${template.zh} ${template.en} ${template.zhUse} ${template.enUse}` .toLowerCase() @@ -199,8 +101,6 @@ export function PosterTemplatePanel({ onApply(template.id)} diff --git a/apps/web/src/lib/poster-template-previews.ts b/apps/web/src/lib/poster-template-previews.ts new file mode 100644 index 00000000..ce2ceb3b --- /dev/null +++ b/apps/web/src/lib/poster-template-previews.ts @@ -0,0 +1,24 @@ +import { POSTER_DESIGN_TEMPLATES } from "@edgeever/shared"; +import manifest from "@/assets/poster-template-previews/manifest.json"; + +const images = import.meta.glob("../assets/poster-template-previews/*.webp", { + eager: true, + query: "?url&no-inline", + import: "default", +}); +export const POSTER_GALLERY_TEMPLATES = manifest.templates.map(entry => { + const descriptor = POSTER_DESIGN_TEMPLATES.find(template => template.id === entry.id); + if (!descriptor) throw new Error(`Unknown poster preview template: ${entry.id}`); + return { + ...descriptor, + recommended: entry.recommended, + thumbnails: { + zh: images[`../assets/poster-template-previews/${entry.zh.file}`], + en: images[`../assets/poster-template-previews/${entry.en.file}`], + }, + }; +}); +export const posterGalleryTemplatesFor = (platformId?: string) => + POSTER_GALLERY_TEMPLATES.filter(template => template.recommended).sort((a, b) => + Number(b.fit.includes(platformId ?? "")) - Number(a.fit.includes(platformId ?? "")), + ); diff --git a/package.json b/package.json index c0c6e0d5..ca18b022 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "test:e2e": "playwright test", "test:e2e:install": "playwright install chromium", "dev:extension": "vite --config apps/extension/vite.config.ts", - "build:web": "vite build --config apps/web/vite.config.ts", + "prepare:poster:thumbnails": "bun scripts/prepare-poster-thumbnails.mjs", + "check:poster:thumbnails": "bun scripts/prepare-poster-thumbnails.mjs --check", + "build:web": "bun run check:poster:thumbnails && vite build --config apps/web/vite.config.ts", "build:desktop:sidecar": "cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release", "build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist", "benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs", diff --git a/scripts/docker-release.test.mjs b/scripts/docker-release.test.mjs index 79b94881..448eda48 100644 --- a/scripts/docker-release.test.mjs +++ b/scripts/docker-release.test.mjs @@ -79,6 +79,8 @@ describe("Docker release contract", () => { "bun build scripts/self-hosted-server.mjs --target=bun --format=esm --outdir=dist/self-hosted --sourcemap=none", ); expect(dockerfile).toContain("RUN bun run build:web && bun run build:self-hosted"); + expect(dockerfile).toContain("scripts/prepare-poster-thumbnails.mjs scripts/"); + expect(dockerfile).toContain("COPY tests/browser/poster-thumbnail-generator.html tests/browser/poster-thumbnail-generator.ts tests/browser/"); expect(runtimeStage).toContain( "COPY --from=build /app/dist/self-hosted/self-hosted-server.js ./scripts/self-hosted-server.js", ); diff --git a/scripts/prepare-poster-thumbnails.mjs b/scripts/prepare-poster-thumbnails.mjs new file mode 100644 index 00000000..55c6229c --- /dev/null +++ b/scripts/prepare-poster-thumbnails.mjs @@ -0,0 +1,67 @@ +// Generate with `bun run dev` already running; --check needs no browser/server. +import { createHash } from "node:crypto"; +import { readFile, writeFile, mkdir, readdir, mkdtemp, cp, rm } from "node:fs/promises"; +import path from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const output = path.join(root, "apps/web/src/assets/poster-template-previews"); +const digest = data => createHash("sha256").update(data).digest("hex"); +const sources = [ + "apps/web/src/lib/poster-design", "apps/web/src/lib/poster-canvas.ts", + "packages/shared/src/poster.ts", "packages/shared/src/poster-design-catalog.ts", + "packages/shared/src/poster-fonts.ts", "packages/shared/src/font-catalog.ts", + "packages/shared/src/font-catalog-data.ts", "packages/shared/src/font-loader.ts", + "packages/shared/src/font-defaults.ts", "packages/shared/src/font-resources.ts", + "packages/shared/src/font-assets-registry.ts", + "scripts/prepare-poster-thumbnails.mjs", + "tests/browser/poster-thumbnail-generator.html", "tests/browser/poster-thumbnail-generator.ts", +]; +const sourceFiles = async (relative) => { + const entries = await readdir(path.join(root, relative), { withFileTypes: true }).catch(error => { + if (error.code === "ENOTDIR") return undefined; + throw error; + }); + if (!entries) return [relative]; + return (await Promise.all(entries.map(entry => sourceFiles(`${relative}/${entry.name}`)))).flat(); +}; +const files = (await Promise.all(sources.map(sourceFiles))).flat().sort(); +const sourceHash = digest(await Promise.all(files.map(async file => `${file}\0${digest((await readFile(path.join(root, file), "utf8")).replace(/\r\n/g, "\n"))}`)).then(parts => parts.join("\n"))); +if (process.argv.includes("--check")) { + const manifest = JSON.parse(await readFile(path.join(output, "manifest.json"))); + if (manifest.sourceHash !== sourceHash) throw new Error("Poster thumbnails are stale. Run bun run prepare:poster:thumbnails against bun run dev, then commit the generated assets."); + if (manifest.templates.length === 0) throw new Error("No poster thumbnails"); + for (const template of manifest.templates) for (const language of ["zh", "en"]) { + const asset = template[language]; + if (!/^[a-z-]+\.(zh|en)\.webp$/.test(asset.file) || digest(await readFile(path.join(output, asset.file))) !== asset.sha256) throw new Error(`Invalid poster thumbnail: ${template.id}/${language}`); + } + console.log(`Poster thumbnails current: ${manifest.templates.length} templates, ${manifest.templates.length * 2} images`); +} else { + const { chromium } = await import("@playwright/test"); + const browser = await chromium.launch({ channel: "chrome", headless: true }); + const temporary = await mkdtemp(path.join(tmpdir(), "edgeever-poster-thumbnails-")); + try { + const page = await browser.newPage(); + const origin = process.env.EDGE_EVER_BROWSER_TEST_URL ?? "http://127.0.0.1:5173"; + await page.goto(`${origin}/@fs${root}tests/browser/poster-thumbnail-generator.html`); + await page.waitForFunction(() => window.posterThumbnailGenerator); + const templates = await page.evaluate(() => window.posterThumbnailGenerator.templates); + for (const template of templates) for (const language of ["zh", "en"]) { + const base64 = await page.evaluate(({ id, language }) => window.posterThumbnailGenerator.render(id, language === "zh"), { id: template.id, language }); + const bytes = Buffer.from(base64, "base64"); + if (bytes.toString("ascii", 0, 4) !== "RIFF" || bytes.toString("ascii", 8, 12) !== "WEBP") throw new Error(`Invalid WebP: ${template.id}`); + const file = `${template.id}.${language}.webp`; + await writeFile(path.join(temporary, file), bytes); + template[language] = { file, sha256: digest(bytes) }; + console.log(`Generated ${file}: ${bytes.length} bytes`); + } + await writeFile(path.join(temporary, "manifest.json"), `${JSON.stringify({ sourceHash, width: 480, height: 640, templates }, null, 2)}\n`); + await mkdir(output, { recursive: true }); + await cp(temporary, output, { recursive: true }); + console.log(`Generated ${templates.length * 2} static poster thumbnails`); + } finally { + await browser.close(); + await rm(temporary, { recursive: true, force: true }); + } +} diff --git a/scripts/verify-poster-previews.mjs b/scripts/verify-poster-previews.mjs index 4bf17da9..b7b5ebea 100644 --- a/scripts/verify-poster-previews.mjs +++ b/scripts/verify-poster-previews.mjs @@ -1,79 +1,82 @@ -// Run against `bun run dev`. Uses installed Google Chrome and isolated fixture -// pages, without opening a user profile or touching workspace notes. +// Run against `bun run dev`. Installed Chrome, isolated fixture, no note APIs. import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; import { chromium } from "@playwright/test"; const origin = process.env.EDGE_EVER_BROWSER_TEST_URL ?? "http://127.0.0.1:5173"; const fixture = `${origin}/@fs${process.cwd()}/tests/browser/poster-previews.html`; +const manifest = JSON.parse(await readFile(new URL("../apps/web/src/assets/poster-template-previews/manifest.json", import.meta.url))); const browser = await chromium.launch({ channel: "chrome", headless: true }); -const open = async (setup) => { - const page = await browser.newPage(); - if (setup) await page.addInitScript(setup); - await page.goto(fixture); - await page.waitForFunction(() => window.posterPreviewTest); - return page; -}; -const retryFirst = page => page.locator("[data-poster-template]").first().locator("..").getByRole("button", { name: "重试预览", exact: true }); const firstReady = page => page.waitForFunction(() => { const cards = [...document.querySelectorAll("[data-poster-template]")].slice(0, 6); return cards.length === 6 && cards.every(card => card.querySelector("img")?.naturalWidth > 0); -}, null, { timeout: 14_000 }); +}, null, { timeout: 10_000 }); try { - const page = await open(); - const results = await page.evaluate(() => Promise.all(window.posterPreviewTest.templateIds.map(window.posterPreviewTest.checkPoster))); - assert.deepEqual(results.filter(result => !result.ok), [], "All template thumbnails must render"); + const page = await browser.newPage(); + const errors = []; + page.on("pageerror", error => errors.push(String(error))); + await page.addInitScript(() => { + window.canvasAttempts = 0; + const forbidden = () => { window.canvasAttempts++; throw new Error("Gallery tried to render a canvas"); }; + HTMLCanvasElement.prototype.getContext = forbidden; + HTMLCanvasElement.prototype.toBlob = forbidden; + URL.createObjectURL = forbidden; + }); + const fontRequests = []; + await page.route("**/*", route => { + if (["font", "xhr"].includes(route.request().resourceType())) { + fontRequests.push(route.request().url()); + return route.abort(); + } + return route.continue(); + }); + await page.goto(fixture); + await page.waitForFunction(() => window.posterPreviewTest); await firstReady(page); - assert.equal(await page.locator('[role="tooltip"]').count(), 0); - console.log(`PASS: all ${results.length} templates render; visible cards show previews`); + const firstSources = await page.locator('[data-poster-template] img').evaluateAll(images => images.map(image => image.getAttribute('src'))); + assert(firstSources.every(source => source.includes('.zh.webp') && !source.startsWith('blob:'))); + assert.equal(await page.getByText('载入版式…', { exact: true }).count(), 0); + await page.screenshot({ path: "/tmp/edgeever-static-poster-previews.png" }); + console.log("PASS: visible static previews load with canvas disabled and fonts blocked"); - await page.screenshot({ path: "/tmp/edgeever-poster-previews-fixed.png" }); - await page.close(); + await page.getByRole('button', { name: 'All templates', exact: true }).click(); + assert.equal(await page.locator('[data-poster-template]').count(), manifest.templates.length); + // Scroll each real card into view so lazy image loading is exercised too. + for (const template of manifest.templates) { + const image = page.locator(`[data-poster-template="${template.id}"] img`); + await image.scrollIntoViewIfNeeded(); + await image.evaluate(image => image.decode()); + } + console.log(`PASS: all ${manifest.templates.length} Chinese thumbnails decode`); - // Use fresh pages so a previous render cannot satisfy an error assertion. - const failed = await open(() => { - window.originalToBlob = HTMLCanvasElement.prototype.toBlob; - HTMLCanvasElement.prototype.toBlob = function(callback) { callback(null); }; - }); - await retryFirst(failed).waitFor({ timeout: 14_000 }); - assert.equal(await failed.locator('[data-poster-template]').first().getAttribute("aria-busy"), "false"); - await failed.evaluate(() => { HTMLCanvasElement.prototype.toBlob = window.originalToBlob; }); - await retryFirst(failed).focus(); - await failed.keyboard.press("Enter"); - await failed.locator('[data-poster-template]').first().locator('img').waitFor(); - console.log("PASS: canvas export failure ends loading; retry recovers"); - await failed.close(); + await page.evaluate(() => window.posterPreviewTest.mount(false)); + await firstReady(page); + for (const template of manifest.templates) { + const image = page.locator(`[data-poster-template="${template.id}"] img`); + await image.scrollIntoViewIfNeeded(); + await image.evaluate(image => image.decode()); + assert((await image.getAttribute('src')).includes('.en.webp')); + } + console.log(`PASS: all ${manifest.templates.length} English thumbnails decode`); - // A canvas callback that never arrives must also leave loading within 15s. - const stalled = await open(() => { - window.originalToBlob = HTMLCanvasElement.prototype.toBlob; - window.lateExports = []; - HTMLCanvasElement.prototype.toBlob = function(callback) { window.lateExports.push([this, callback]); }; - }); - await retryFirst(stalled).waitFor({ timeout: 18_000 }); - await stalled.evaluate(() => { HTMLCanvasElement.prototype.toBlob = window.originalToBlob; }); - await retryFirst(stalled).click(); - await stalled.locator('[data-poster-template]').first().locator('img').waitFor(); - // Deliver the previous attempt's result late: it must be released, not replace - // the successful retry or switch the card back to a failed/loading state. - await stalled.evaluate(async () => { - const before = document.querySelector('[data-poster-template] img')?.getAttribute('src'); - await Promise.all(window.lateExports.map(([canvas, callback]) => new Promise(resolve => { - window.originalToBlob.call(canvas, blob => { callback(blob); resolve(); }); - }))); - await new Promise(resolve => setTimeout(resolve, 100)); - if (document.querySelector('[data-poster-template] img')?.getAttribute('src') !== before) throw new Error('Late preview replaced retry'); - }); - console.log("PASS: stalled export times out; retry succeeds; late results are ignored"); - await stalled.close(); + const selected = page.locator('[data-poster-template="folio"]'); + await selected.focus(); + await page.keyboard.press('Enter'); + assert.deepEqual(await page.evaluate(() => window.posterPreviewTest.applied), ['folio']); + await page.getByRole('textbox').fill('Minimal'); + assert.equal(await page.locator('[data-poster-template]').count(), 1); + assert.equal(await page.locator('[data-poster-template]').getAttribute('data-poster-template'), 'minimal'); + console.log("PASS: search and keyboard template selection still work"); - const slow = await browser.newPage(); - // Leave both local font assets and remote font requests unanswered. This - // reproduced blank previews before the whole font preparation was bounded. - await slow.route("**/*", route => ["xhr", "font"].includes(route.request().resourceType()) ? undefined : route.continue()); - await slow.goto(fixture); - await slow.waitForFunction(() => window.posterPreviewTest); - await firstReady(slow); - console.log("PASS: stalled font requests still produce fallback thumbnails"); - await slow.close(); + await page.evaluate(() => window.posterPreviewTest.mount(true, undefined, false)); + await page.locator('[data-poster-template]').first().waitFor({ state: 'detached' }); + await page.evaluate(() => window.posterPreviewTest.mount(true, 'wechat')); + await firstReady(page); + assert.equal(await page.locator('[data-poster-template]').count(), manifest.templates.filter(template => template.recommended).length); + assert.equal(await page.evaluate(() => window.canvasAttempts), 0); + // CSS may request UI text fonts; none of the gallery's design fonts are loaded. + assert.equal(fontRequests.filter(url => /poster-fonts|fontsource|jsdelivr|fonts\.googleapis/.test(url)).length, 0); + assert.deepEqual(errors, []); + console.log("PASS: reopening/platform changes use static images; zero canvas renders or design-font requests"); } finally { await browser.close(); } diff --git a/tests/browser/poster-previews.browser.tsx b/tests/browser/poster-previews.browser.tsx index 4120ad4b..70ee4a42 100644 --- a/tests/browser/poster-previews.browser.tsx +++ b/tests/browser/poster-previews.browser.tsx @@ -1,36 +1,22 @@ -// Local Chrome fixture: real template builders, fonts, Fabric canvas and React -// panel; never reads or changes notes. Driven by verify-poster-previews.mjs. +// Local fixture for the static gallery: no renderer, fonts or note APIs. import React from "react"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import i18next from "i18next"; import { enUS } from "../../packages/shared/src/i18n/en-US"; import { PosterTemplatePanel } from "../../apps/web/src/components/poster/PosterTemplatePanel"; -import { buildPosterDesign, DESIGN_TEMPLATES } from "../../apps/web/src/lib/poster-design/templates"; -import { renderPosterThumbnail } from "../../apps/web/src/lib/poster-canvas"; -import { installEditorBodyFontFaces } from "../../apps/web/src/lib/editor-body-font-faces"; import "../../apps/web/src/styles/globals.css"; -installEditorBodyFontFaces(); const i18n = i18next.createInstance(); await i18n.init({ lng: "en", resources: { en: { translation: enUS } }, initAsync: false }); const root = createRoot(document.getElementById("fixture")!); -const mount = (width = 1080, zh = true) => root.render( - +const applied: string[] = []; +const mount = (zh = true, platformId?: string, show = true) => root.render( + show ?
- {}} /> + applied.push(id)} />
-
, +
: null, ); -const checkPoster = async (id: string) => { - try { - const doc = await buildPosterDesign({ id, title: "把想法\n变成作品", subtitle: "一个好想法,值得被看见。", badge: "创作笔记", width: 1080, height: 1440, zh: true }); - const url = await renderPosterThumbnail(doc); - URL.revokeObjectURL(url); - return { id, ok: true }; - } catch (error) { - return { id, ok: false, error: String(error) }; - } -}; -Object.assign(window, { posterPreviewTest: { mount, checkPoster, templateIds: DESIGN_TEMPLATES.map(template => template.id) } }); +Object.assign(window, { posterPreviewTest: { mount, applied } }); mount(); diff --git a/tests/browser/poster-thumbnail-generator.html b/tests/browser/poster-thumbnail-generator.html new file mode 100644 index 00000000..d8c6dd02 --- /dev/null +++ b/tests/browser/poster-thumbnail-generator.html @@ -0,0 +1,3 @@ + +Poster thumbnail generator + diff --git a/tests/browser/poster-thumbnail-generator.ts b/tests/browser/poster-thumbnail-generator.ts new file mode 100644 index 00000000..5ef6667e --- /dev/null +++ b/tests/browser/poster-thumbnail-generator.ts @@ -0,0 +1,35 @@ +// Build-time only. The gallery never imports this renderer. +import { buildPosterDesign, DESIGN_TEMPLATES, templatesFor } from "../../apps/web/src/lib/poster-design/templates"; +import { renderPosterBlob } from "../../apps/web/src/lib/poster-canvas"; +import { loadPosterFont } from "../../apps/web/src/lib/poster-design/fonts"; +import { installEditorBodyFontFaces } from "../../apps/web/src/lib/editor-body-font-faces"; +installEditorBodyFontFaces(); +const recommended = new Set(templatesFor(undefined).map(template => template.id)); +Object.assign(window, { + posterThumbnailGenerator: { + templates: DESIGN_TEMPLATES.map(template => ({ id: template.id, recommended: recommended.has(template.id) })), + async render(id: string, zh: boolean) { + const input = { + id, zh, width: 1080, height: 1440, + title: zh ? "把想法\n变成作品" : "MAKE IDEAS\nWORTH SHARING", + subtitle: zh ? "一个好想法,值得被看见。" : "Ideas deserve to be seen.", + badge: zh ? "创作笔记" : "CREATIVE NOTES", + }; + const draft = await buildPosterDesign(input); + const texts = draft.elements.filter(element => element.type === "text"); + // Generation must wait for the actual typefaces; never freeze a temporary + // system-font fallback into a released thumbnail. + await Promise.all([...new Set(texts.map(element => element.fontFamily))].map(family => + loadPosterFont(family, texts.map(element => element.text).join(" "), { deadline: false }), + )); + const document = await buildPosterDesign(input); + const blob = await renderPosterBlob(document, async () => { throw new Error("Thumbnail cannot read note resources"); }, "webp", true, 1, 0.85); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result).split(",")[1]); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); + }, + }, +}); diff --git a/tests/browser/tsconfig.json b/tests/browser/tsconfig.json index 4a654e1c..aac3e348 100644 --- a/tests/browser/tsconfig.json +++ b/tests/browser/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["./*.browser.tsx", "../../apps/web/src/vite-env.d.ts"] + "include": ["./*.browser.tsx", "./*.ts", "../../apps/web/src/vite-env.d.ts"] }