cuju-render 0.1.0__py3-none-any.whl
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.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The learning part of the Help page and the ways into it. Help gains a part to learn by doing at
|
|
5
|
+
// its top, with the guided tour, the hands-on lessons and their progress, the shipped samples with
|
|
6
|
+
// one action to add or restore each, the diagnostic report and the installable page. The tour also
|
|
7
|
+
// starts from the first-run tip and from the command palette, which lists the lessons, the samples
|
|
8
|
+
// and the report as well. Scripts reach the parts as window.cuju.tour, cuju.tutorial,
|
|
9
|
+
// cuju.samples, cuju.doctor and cuju.install. Each part loads on its own, so a failure of one
|
|
10
|
+
// leaves the others and the page working.
|
|
11
|
+
|
|
12
|
+
import { GuidedTour } from './tour.js';
|
|
13
|
+
import { Lessons, loadCatalog } from './lessons.js';
|
|
14
|
+
import { doneCount, lessonAskNodes, lessonTitle } from './lessons-core.js';
|
|
15
|
+
import { HELP_TOPICS, paletteAction, searchWords } from './palette.js';
|
|
16
|
+
import { serverText, t, tOr, tn } from '../core/i18n.js';
|
|
17
|
+
import { icon, labelText, relabel, stateBadge, symbolOnly } from '../core/icons.js';
|
|
18
|
+
|
|
19
|
+
// The title and the description of a sample from the server, as the messages
|
|
20
|
+
// helpcenter.sample.<id>.title and .about, with the server's English for a sample without them.
|
|
21
|
+
function sampleTitle(entry) {
|
|
22
|
+
return tOr(`helpcenter.sample.${entry.id}.title`, entry.title);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sampleAbout(entry) {
|
|
26
|
+
return tOr(`helpcenter.sample.${entry.id}.about`, entry.about);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A team sample lives in the registry of the Teams page, not in the library of recordings.
|
|
30
|
+
function sampleState(entry) {
|
|
31
|
+
const kind = entry.kind === 'team' ? 'teamState' : 'state';
|
|
32
|
+
return tOr(`helpcenter.sample.${kind}.${entry.state}`, entry.state);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function h(tag, attributes = {}, ...children) {
|
|
36
|
+
const node = document.createElement(tag);
|
|
37
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
38
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
39
|
+
node.addEventListener(key.slice(2), value);
|
|
40
|
+
} else if (value !== null && value !== undefined && value !== false) {
|
|
41
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
node.append(...children.flat().filter((child) => child !== null && child !== undefined));
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function call(method, path, json) {
|
|
49
|
+
const init = { method, cache: 'no-store', headers: {} };
|
|
50
|
+
if (json !== undefined) {
|
|
51
|
+
init.body = JSON.stringify(json);
|
|
52
|
+
init.headers['Content-Type'] = 'application/json';
|
|
53
|
+
}
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(path, init);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(t('helpcenter.error.unreachable', { origin: location.origin }));
|
|
59
|
+
}
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
let body = null;
|
|
62
|
+
try {
|
|
63
|
+
body = text ? JSON.parse(text) : null;
|
|
64
|
+
} catch {
|
|
65
|
+
body = null;
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
throw new Error(body && body.error ? serverText(body) : t('helpcenter.error.status', { status: res.status }));
|
|
69
|
+
}
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The samples of the server, for window.cuju.samples and the Help page.
|
|
74
|
+
export const samplesApi = {
|
|
75
|
+
list: () => call('GET', '/api/help/samples'),
|
|
76
|
+
add: (names) => call('POST', '/api/help/samples', names ? { names } : {}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function injectStyle() {
|
|
80
|
+
if (document.getElementById('cuju-help-style')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const style = document.createElement('style');
|
|
84
|
+
style.id = 'cuju-help-style';
|
|
85
|
+
style.textContent = `
|
|
86
|
+
#help-learn, #help-samples { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
|
|
87
|
+
.help-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
88
|
+
.lesson-list, .sample-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px;
|
|
89
|
+
grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); }
|
|
90
|
+
.lesson-list li, .sample-list li { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px;
|
|
91
|
+
border: 1px solid var(--border); border-radius: 6px; }
|
|
92
|
+
.lesson-list li.done { border-style: dashed; }
|
|
93
|
+
.lesson-list .lesson-mark, .sample-list .sample-state { font-size: 12px; }
|
|
94
|
+
.lesson-list h3, .sample-list h3 { margin: 0; color: var(--text); font-size: 15px; font-variant: normal; letter-spacing: normal; }
|
|
95
|
+
.lesson-list p, .sample-list p { margin: 0; }
|
|
96
|
+
/* Above the palette's scrim, 80, so a palette that Try it opens leaves the card and its ring
|
|
97
|
+
undimmed, while the card's placement keeps it clear of the palette box. */
|
|
98
|
+
.tour { position: fixed; z-index: 82; width: min(380px, calc(100vw - 24px)); padding: 14px 16px;
|
|
99
|
+
border: 1px solid var(--border); border-radius: 10px; background: var(--surface); color: var(--text);
|
|
100
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); transition: left 0.25s ease, top 0.25s ease; }
|
|
101
|
+
.tour[hidden], .tour-ring[hidden], .lesson-panel[hidden] { display: none; }
|
|
102
|
+
.tour h2 { margin: 2px 0 6px; font-size: 18px; }
|
|
103
|
+
.tour h2:focus { outline: none; }
|
|
104
|
+
.tour h2:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
105
|
+
.tour-count, .lesson-count { margin: 0; font-size: 12px; opacity: 0.8; }
|
|
106
|
+
.tour-text { margin: 0 0 8px; }
|
|
107
|
+
.tour-offer { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
|
|
108
|
+
.tour-offer[hidden], .lesson-offer[hidden] { display: none; }
|
|
109
|
+
.tour-actions, .lesson-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
110
|
+
.tour-keys { margin: 8px 0 0; }
|
|
111
|
+
.tour-ring { position: fixed; z-index: 81; pointer-events: none; border-radius: 8px;
|
|
112
|
+
border: 3px solid var(--accent); box-shadow: 0 0 0 4px var(--scrim);
|
|
113
|
+
transition: left 0.25s ease, top 0.25s ease, width 0.25s ease, height 0.25s ease; }
|
|
114
|
+
.tour.still, .tour-ring.still, :root.reduced-motion .tour, :root.reduced-motion .tour-ring { transition: none; }
|
|
115
|
+
.lesson-panel { position: fixed; z-index: 58; right: 12px; bottom: 12px; width: min(360px, calc(100vw - 24px));
|
|
116
|
+
padding: 12px 14px; border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 10px;
|
|
117
|
+
background: var(--surface); color: var(--text);
|
|
118
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; gap: 6px; }
|
|
119
|
+
.lesson-panel h2 { margin: 0; font-size: 16px; }
|
|
120
|
+
.lesson-panel h2:focus { outline: none; }
|
|
121
|
+
.lesson-panel h2:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
122
|
+
.lesson-panel p { margin: 0; }
|
|
123
|
+
.lesson-offer { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
124
|
+
.lesson-status { font-size: 13px; }
|
|
125
|
+
.lesson-panel.lesson-done { border-color: var(--accent); }
|
|
126
|
+
.lesson-panel.lesson-compact .lesson-ask, .lesson-panel.lesson-compact .lesson-offer { display: none; }
|
|
127
|
+
#app[data-page="watch"] .lesson-panel { bottom: 120px; }
|
|
128
|
+
#app.tv .lesson-panel { display: none; }
|
|
129
|
+
`;
|
|
130
|
+
document.head.appendChild(style);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class HelpCenter {
|
|
134
|
+
constructor(app) {
|
|
135
|
+
this.app = app;
|
|
136
|
+
injectStyle();
|
|
137
|
+
this.tour = new GuidedTour(app, { facts: () => this.facts(), ensure: (need) => this.ensure(need) });
|
|
138
|
+
this.samples = [];
|
|
139
|
+
this.lessons = null;
|
|
140
|
+
this.buildHelp();
|
|
141
|
+
this.bindTip();
|
|
142
|
+
this.bindPalette();
|
|
143
|
+
app.tour = this.tour.script();
|
|
144
|
+
app.samples = {
|
|
145
|
+
list: async () => (this.samples = await samplesApi.list()),
|
|
146
|
+
add: async (names) => {
|
|
147
|
+
const result = await samplesApi.add(names);
|
|
148
|
+
this.samples = result.samples;
|
|
149
|
+
this.renderSamples();
|
|
150
|
+
return result;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
this.ready = Promise.allSettled([this.loadLessons(), this.refreshSamples(), this.loadParts()]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// What the steps of the tour and the lessons need, as the page holds it now.
|
|
157
|
+
facts() {
|
|
158
|
+
const teams = this.app.library && Array.isArray(this.app.library.teams) ? this.app.library.teams : [];
|
|
159
|
+
const listed = this.samples.find((entry) => entry.kind === 'team');
|
|
160
|
+
return {
|
|
161
|
+
recording: !!(this.app.source && !this.app.source.game),
|
|
162
|
+
team: teams.some((team) => team.kind !== 'builtin') || !!(listed && listed.state === 'library'),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Adds the sample that a need asks for and shows it, a recording on Watch or a team on Teams.
|
|
167
|
+
async ensure(need) {
|
|
168
|
+
if (need === 'recording') {
|
|
169
|
+
const result = await samplesApi.add(['sample-5v5']);
|
|
170
|
+
this.samples = result.samples;
|
|
171
|
+
this.renderSamples();
|
|
172
|
+
await this.app.openMatch('sample-5v5');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (need === 'team') {
|
|
176
|
+
const result = await samplesApi.add(['starter-team']);
|
|
177
|
+
this.samples = result.samples;
|
|
178
|
+
this.renderSamples();
|
|
179
|
+
if (this.app.library && typeof this.app.library.refresh === 'function') {
|
|
180
|
+
await this.app.library.refresh();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
buildHelp() {
|
|
186
|
+
const card = document.querySelector('#help .help-card');
|
|
187
|
+
if (!card) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.tourButton = h('button', { type: 'button', class: 'btn primary', onclick: () => this.tour.start() }, icon('play'), labelText(''));
|
|
191
|
+
this.tourAgain = h('button', { type: 'button', class: 'btn', ...symbolOnly(t('helpcenter.tour.again')), onclick: () => this.tour.start(0) }, icon('replay'));
|
|
192
|
+
this.lessonSummary = h('p', { class: 'muted', 'aria-live': 'polite' }, t('helpcenter.lessons.loading'));
|
|
193
|
+
this.lessonList = h('ol', { class: 'lesson-list', 'aria-label': t('helpcenter.lessons.list') });
|
|
194
|
+
this.continueButton = h('button', { type: 'button', class: 'btn primary', onclick: () => this.lessons && this.lessons.start() }, icon('play'), labelText(t('helpcenter.lessons.start')));
|
|
195
|
+
this.resetButton = h('button', { type: 'button', class: 'btn', ...symbolOnly(t('helpcenter.lessons.reset')), onclick: () => this.resetLessons() }, icon('reset'));
|
|
196
|
+
const learn = h('section', { id: 'help-learn', 'aria-labelledby': 'help-learn-title', tabindex: '-1' },
|
|
197
|
+
h('h2', { id: 'help-learn-title' }, t('helpcenter.learn.title')),
|
|
198
|
+
h('p', {}, tn('helpcenter.learn.text')),
|
|
199
|
+
h('div', { class: 'help-row' }, this.tourButton, this.tourAgain),
|
|
200
|
+
h('h2', { id: 'help-lessons' }, t('helpcenter.lessons.title')),
|
|
201
|
+
this.lessonSummary,
|
|
202
|
+
this.lessonList,
|
|
203
|
+
h('div', { class: 'help-row' }, this.continueButton, this.resetButton),
|
|
204
|
+
h('p', { class: 'muted small' }, tn('helpcenter.lessons.agent', {
|
|
205
|
+
command: h('code', {}, 'cuju render tutorial'), script: h('code', {}, 'window.cuju.tutorial'),
|
|
206
|
+
})));
|
|
207
|
+
this.sampleList = h('ul', { class: 'sample-list', 'aria-label': t('helpcenter.samples.list') });
|
|
208
|
+
this.sampleNote = h('p', { class: 'muted small', 'aria-live': 'polite' });
|
|
209
|
+
this.allButton = h('button', { type: 'button', class: 'btn', onclick: () => this.addSamples(null) }, icon('plus'), t('helpcenter.samples.all'));
|
|
210
|
+
const samples = h('section', { id: 'help-samples', 'aria-labelledby': 'help-samples-title' },
|
|
211
|
+
h('h2', { id: 'help-samples-title' }, t('helpcenter.samples.title')),
|
|
212
|
+
h('p', {}, tn('helpcenter.samples.text')),
|
|
213
|
+
this.sampleList,
|
|
214
|
+
h('div', { class: 'help-row' }, this.allButton),
|
|
215
|
+
this.sampleNote);
|
|
216
|
+
this.doctorHost = h('section', { id: 'help-doctor', 'aria-labelledby': 'help-doctor-title' });
|
|
217
|
+
this.installHost = h('section', { id: 'help-install', 'aria-labelledby': 'help-install-title' });
|
|
218
|
+
const header = card.querySelector('header');
|
|
219
|
+
(header || card.firstChild).after(learn, samples, this.doctorHost, this.installHost);
|
|
220
|
+
// Each topic's title and words are the messages helpcenter.topic.<key>.title and .words.
|
|
221
|
+
for (const topic of [
|
|
222
|
+
['help-learn-title', 'tour', 'tour walkthrough start learn tutorial'],
|
|
223
|
+
['help-lessons', 'lessons', 'tutorial hands on learn practice'],
|
|
224
|
+
['help-samples-title', 'samples', 'sample recording starter team restore'],
|
|
225
|
+
['help-doctor-title', 'doctor', 'doctor report diagnostics webgl versions problem'],
|
|
226
|
+
['help-install-title', 'install', 'install app pwa offline'],
|
|
227
|
+
].map(([id, key, words]) => [id, t(`helpcenter.topic.${key}.title`), searchWords(`helpcenter.topic.${key}.words`, words)])) {
|
|
228
|
+
if (!HELP_TOPICS.some((entry) => entry[0] === topic[0])) {
|
|
229
|
+
HELP_TOPICS.push(topic);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.renderTour();
|
|
233
|
+
const page = document.getElementById('page-help');
|
|
234
|
+
if (page) {
|
|
235
|
+
new MutationObserver(() => {
|
|
236
|
+
if (!page.hidden) {
|
|
237
|
+
this.renderTour();
|
|
238
|
+
this.refreshSamples();
|
|
239
|
+
// A lesson that the command line or another page finished while Help was
|
|
240
|
+
// away shows as done each time Help shows.
|
|
241
|
+
this.lessons?.sync().then(() => this.renderLessons());
|
|
242
|
+
}
|
|
243
|
+
}).observe(page, { attributes: true, attributeFilter: ['hidden'] });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
renderTour() {
|
|
248
|
+
if (!this.tourButton) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const at = this.tour.resumeAt;
|
|
252
|
+
relabel(this.tourButton, at > 0 ? t('helpcenter.tour.resume', { index: at + 1 }) : t('helpcenter.tour.take'));
|
|
253
|
+
this.tourAgain.hidden = at === 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async loadLessons() {
|
|
257
|
+
const catalog = await loadCatalog();
|
|
258
|
+
this.lessons = new Lessons(this.app, catalog, {
|
|
259
|
+
ensure: (need) => this.ensure(need),
|
|
260
|
+
onChange: () => this.renderLessons(),
|
|
261
|
+
});
|
|
262
|
+
this.app.tutorial = this.lessons.script();
|
|
263
|
+
this.renderLessons();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderLessons() {
|
|
267
|
+
const lessons = this.lessons;
|
|
268
|
+
if (!lessons || !this.lessonList) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const done = doneCount(lessons.progress, lessons.ids);
|
|
272
|
+
this.lessonSummary.textContent = t('helpcenter.lessons.summary', { count: done, total: lessons.ids.length });
|
|
273
|
+
relabel(this.continueButton, done === 0 ? t('helpcenter.lessons.start') : (done === lessons.ids.length ? t('helpcenter.lessons.again') : t('helpcenter.lessons.continue')));
|
|
274
|
+
this.resetButton.disabled = done === 0;
|
|
275
|
+
this.lessonList.replaceChildren(...lessons.lessons.map((lesson) => {
|
|
276
|
+
const finished = lesson.id in lessons.progress.done;
|
|
277
|
+
return h('li', { class: finished ? 'done' : null, 'data-lesson': lesson.id },
|
|
278
|
+
stateBadge(finished ? 'done' : 'todo', finished ? t('helpcenter.lesson.done') : t('helpcenter.lesson.todo'), 'lesson-mark'),
|
|
279
|
+
h('h3', {}, lessonTitle(lesson)),
|
|
280
|
+
h('p', { class: 'muted small' }, ...lessonAskNodes(lesson)),
|
|
281
|
+
h('div', {}, h('button', { type: 'button', class: 'btn small', ...symbolOnly(t('helpcenter.lesson.start.label', { title: lessonTitle(lesson) })), onclick: () => lessons.start(lesson.id) }, icon(finished ? 'replay' : 'play'))));
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async resetLessons() {
|
|
286
|
+
if (this.lessons) {
|
|
287
|
+
await this.lessons.reset();
|
|
288
|
+
this.app.hud?.toast(t('helpcenter.lessons.resetDone'), 'info', 2500);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async refreshSamples() {
|
|
293
|
+
try {
|
|
294
|
+
this.samples = await samplesApi.list();
|
|
295
|
+
this.sampleError = null;
|
|
296
|
+
} catch (err) {
|
|
297
|
+
this.sampleError = err.message;
|
|
298
|
+
}
|
|
299
|
+
this.renderSamples();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
renderSamples() {
|
|
303
|
+
if (!this.sampleList) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (this.sampleError && !this.samples.length) {
|
|
307
|
+
this.sampleList.replaceChildren();
|
|
308
|
+
this.sampleNote.replaceChildren(...tn('helpcenter.samples.offline', { error: this.sampleError }));
|
|
309
|
+
this.allButton.disabled = true;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.allButton.disabled = this.samples.every((entry) => entry.state === 'library');
|
|
313
|
+
this.sampleList.replaceChildren(...this.samples.map((entry) => {
|
|
314
|
+
const action = entry.state === 'library'
|
|
315
|
+
? h('button', { type: 'button', class: 'btn small', ...symbolOnly(t('helpcenter.samples.open.label', { title: sampleTitle(entry) })), onclick: () => this.openSample(entry) }, icon('play'))
|
|
316
|
+
: h('button', {
|
|
317
|
+
type: 'button', class: 'btn small',
|
|
318
|
+
...symbolOnly(t(entry.state === 'trash' ? 'helpcenter.samples.restore.label' : 'helpcenter.samples.add.label', { title: sampleTitle(entry) })),
|
|
319
|
+
onclick: () => this.addSamples([entry.id]),
|
|
320
|
+
}, icon(entry.state === 'trash' ? 'reset' : 'plus'));
|
|
321
|
+
return h('li', { 'data-sample': entry.id },
|
|
322
|
+
h('span', { class: 'sample-state' }, sampleState(entry)),
|
|
323
|
+
h('h3', {}, sampleTitle(entry)),
|
|
324
|
+
h('p', { class: 'muted small' }, sampleAbout(entry)),
|
|
325
|
+
h('div', {}, action));
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async addSamples(names) {
|
|
330
|
+
this.sampleNote.textContent = t('helpcenter.samples.adding');
|
|
331
|
+
try {
|
|
332
|
+
const result = await samplesApi.add(names);
|
|
333
|
+
this.samples = result.samples;
|
|
334
|
+
this.sampleError = null;
|
|
335
|
+
const words = result.changes.map((change) => {
|
|
336
|
+
const entry = this.samples.find((item) => item.id === change.id);
|
|
337
|
+
const title = entry ? sampleTitle(entry) : change.id;
|
|
338
|
+
return tOr(`helpcenter.samples.change.${change.change}`, `${change.change} {title}`, { title });
|
|
339
|
+
});
|
|
340
|
+
this.sampleNote.textContent = words.length
|
|
341
|
+
? t('helpcenter.samples.changes', { list: words.join(', ') }).replace(/^./, (c) => c.toUpperCase())
|
|
342
|
+
: t('helpcenter.samples.held');
|
|
343
|
+
if (this.app.library && typeof this.app.library.refresh === 'function' && result.changes.some((change) => change.id === 'starter-team')) {
|
|
344
|
+
this.app.library.refresh();
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
this.sampleNote.textContent = err.message;
|
|
348
|
+
}
|
|
349
|
+
this.renderSamples();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
openSample(entry) {
|
|
353
|
+
if (entry.kind === 'recording') {
|
|
354
|
+
this.app.openMatchInWatch(entry.target);
|
|
355
|
+
} else {
|
|
356
|
+
this.app.showPage('teams');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// A button of the first-run tip starts the tour.
|
|
361
|
+
bindTip() {
|
|
362
|
+
const actions = this.app.firstRun && this.app.firstRun.tip && this.app.firstRun.tip.querySelector('.tip-actions');
|
|
363
|
+
if (!actions) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const button = h('button', { type: 'button', class: 'btn small', onclick: () => this.tour.start() }, t('helpcenter.tip.tour'));
|
|
367
|
+
// Got it stays the first button of the tip.
|
|
368
|
+
if (actions.firstElementChild) {
|
|
369
|
+
actions.firstElementChild.after(button);
|
|
370
|
+
} else {
|
|
371
|
+
actions.append(button);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The command palette lists the tour, the lessons, the samples and the report.
|
|
376
|
+
bindPalette() {
|
|
377
|
+
const palette = this.app.palette;
|
|
378
|
+
if (!palette || typeof palette.actions !== 'function') {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const own = palette.actions.bind(palette);
|
|
382
|
+
palette.actions = () => [...own(), ...this.actions()];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
actions() {
|
|
386
|
+
const list = [];
|
|
387
|
+
// The words of each action are its English words and the message helpcenter.words.<key>.
|
|
388
|
+
const add = (title, run, key, keywords) => list.push(paletteAction('Help', title, run, searchWords(`helpcenter.words.${key}`, keywords)));
|
|
389
|
+
const at = this.tour.resumeAt;
|
|
390
|
+
add(at > 0 ? t('helpcenter.action.resume', { index: at + 1 }) : t('helpcenter.tour.take'), () => this.tour.start(), 'tour', 'tour walkthrough learn start help');
|
|
391
|
+
if (this.lessons) {
|
|
392
|
+
const next = this.lessons.state().next;
|
|
393
|
+
add(next ? t('helpcenter.lessons.continue') : t('helpcenter.action.practice'), () => this.lessons.start(), 'lessons', 'tutorial lesson learn practice hands on');
|
|
394
|
+
for (const lesson of this.lessons.lessons) {
|
|
395
|
+
add(t('helpcenter.action.lesson', { title: lessonTitle(lesson) }), () => this.lessons.start(lesson.id), 'lesson', 'tutorial lesson learn');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
add(t('helpcenter.action.samples'), () => this.addSamples(null), 'samples', 'sample recording starter team restore trash');
|
|
399
|
+
add(t('helpcenter.action.doctor'), () => this.openPart('doctor'), 'doctor', 'doctor report diagnostics problem bug webgl');
|
|
400
|
+
add(t('helpcenter.action.install'), () => this.openPart('install'), 'install', 'install app pwa offline');
|
|
401
|
+
return list;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Shows a part of the Help page, and makes the report when asked for it.
|
|
405
|
+
openPart(name) {
|
|
406
|
+
const id = name === 'doctor' ? 'help-doctor-title' : 'help-install-title';
|
|
407
|
+
if (this.app.palette) {
|
|
408
|
+
this.app.palette.helpTopic(id);
|
|
409
|
+
} else {
|
|
410
|
+
this.app.showPage('help');
|
|
411
|
+
}
|
|
412
|
+
if (name === 'doctor' && this.doctor && typeof this.doctor.gather === 'function') {
|
|
413
|
+
this.doctor.gather().catch(() => {});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The diagnostic report and the installable page, each its own module.
|
|
418
|
+
async loadParts() {
|
|
419
|
+
const parts = [
|
|
420
|
+
['./doctor.js', 'DoctorSection', this.doctorHost, 'doctor'],
|
|
421
|
+
['./pwa.js', 'AppInstall', this.installHost, 'install'],
|
|
422
|
+
];
|
|
423
|
+
await Promise.allSettled(parts.map(async ([path, name, host, key]) => {
|
|
424
|
+
if (!host) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const module = await import(path);
|
|
429
|
+
const part = new module[name](this.app, host);
|
|
430
|
+
this[key] = part.script();
|
|
431
|
+
this.app[key] = this[key];
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.warn(`${key} not loaded`, err);
|
|
434
|
+
host.hidden = true;
|
|
435
|
+
}
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The offer to install the page as an app, a small card on Home. It shows once the browser holds
|
|
5
|
+
// its own offer, from the second visit on, while the page is no installed app and nothing opens
|
|
6
|
+
// over Home, such as the tour, a lesson, a dialog or broadcast mode. Install shows the browser's
|
|
7
|
+
// offer, and Not now, or a No in the browser's offer, keeps the card away in this browser for good.
|
|
8
|
+
// The Install the app part of Help keeps the install within reach afterwards. Scripts reach the
|
|
9
|
+
// card as window.cuju.installOffer.
|
|
10
|
+
|
|
11
|
+
import { t } from '../core/i18n.js';
|
|
12
|
+
import { icon, symbolOnly } from '../core/icons.js';
|
|
13
|
+
import {
|
|
14
|
+
OFFER_KEY, countVisit, dismissOffer, heldPrompt, installedNow, offerShown, offerState,
|
|
15
|
+
onInstallChange, promptInstall,
|
|
16
|
+
} from './install-state.js';
|
|
17
|
+
|
|
18
|
+
function h(tag, attributes = {}, ...children) {
|
|
19
|
+
const node = document.createElement(tag);
|
|
20
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
21
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
22
|
+
node.addEventListener(key.slice(2), value);
|
|
23
|
+
} else if (value !== null && value !== undefined && value !== false) {
|
|
24
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
node.append(...children.flat().filter((child) => child !== null && child !== undefined));
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadState() {
|
|
32
|
+
try {
|
|
33
|
+
return offerState(JSON.parse(localStorage.getItem(OFFER_KEY)));
|
|
34
|
+
} catch {
|
|
35
|
+
return offerState(null);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function injectStyle() {
|
|
40
|
+
if (document.getElementById('install-offer-style')) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const style = document.createElement('style');
|
|
44
|
+
style.id = 'install-offer-style';
|
|
45
|
+
style.textContent = `
|
|
46
|
+
.install-offer { margin: 16px auto 0; padding: 0 16px; max-width: 1120px; box-sizing: border-box; }
|
|
47
|
+
.install-offer[hidden] { display: none; }
|
|
48
|
+
.install-offer-card { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; justify-content: space-between;
|
|
49
|
+
padding: 12px 14px; border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 10px;
|
|
50
|
+
background: var(--surface); color: var(--text); }
|
|
51
|
+
.install-offer-text { flex: 1 1 280px; min-width: 0; }
|
|
52
|
+
.install-offer-symbol { flex: none; width: 28px; height: 28px; color: var(--accent); }
|
|
53
|
+
.install-offer h2 { margin: 0 0 2px; font-size: 16px; }
|
|
54
|
+
.install-offer p { margin: 0; }
|
|
55
|
+
.install-offer-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
56
|
+
#app.tv .install-offer { display: none; }
|
|
57
|
+
`;
|
|
58
|
+
document.head.appendChild(style);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class InstallOffer {
|
|
62
|
+
constructor(app) {
|
|
63
|
+
this.app = app;
|
|
64
|
+
this.state = countVisit(loadState());
|
|
65
|
+
this.save();
|
|
66
|
+
injectStyle();
|
|
67
|
+
// Install keeps its words as the action of the card, and Not now shows its symbol alone.
|
|
68
|
+
this.installButton = h('button', { type: 'button', class: 'btn primary small', onclick: () => this.install() }, icon('download'), t('installOffer.install'));
|
|
69
|
+
this.laterButton = h('button', { type: 'button', class: 'btn small', ...symbolOnly(t('installOffer.later')), onclick: () => this.dismiss() }, icon('close'));
|
|
70
|
+
this.card = h('aside', { id: 'install-offer', class: 'install-offer', 'aria-labelledby': 'install-offer-title', hidden: true },
|
|
71
|
+
h('div', { class: 'install-offer-card' },
|
|
72
|
+
icon('install', 'icon install-offer-symbol'),
|
|
73
|
+
h('div', { class: 'install-offer-text' },
|
|
74
|
+
h('h2', { id: 'install-offer-title' }, t('installOffer.title')),
|
|
75
|
+
h('p', {}, t('installOffer.text')),
|
|
76
|
+
h('p', { class: 'muted small' }, t('installOffer.later.note'))),
|
|
77
|
+
h('div', { class: 'install-offer-actions' }, this.installButton, this.laterButton)));
|
|
78
|
+
this.attach();
|
|
79
|
+
onInstallChange(() => this.update());
|
|
80
|
+
const root = document.getElementById('app');
|
|
81
|
+
if (root) {
|
|
82
|
+
new MutationObserver(() => this.update()).observe(root, { attributes: true, attributeFilter: ['data-page', 'class'] });
|
|
83
|
+
}
|
|
84
|
+
if (window.matchMedia) {
|
|
85
|
+
window.matchMedia('(display-mode: standalone)').addEventListener?.('change', () => this.update());
|
|
86
|
+
}
|
|
87
|
+
// The tour, a lesson and the dialogs open without a change of page, so a slow look follows them.
|
|
88
|
+
this.timer = setInterval(() => this.update(), 1000);
|
|
89
|
+
this.update();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The card sits on Home below its opening band, or at the top of Home before that band exists.
|
|
93
|
+
attach() {
|
|
94
|
+
const home = document.getElementById('page-home');
|
|
95
|
+
if (!home || home.contains(this.card)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const hero = home.querySelector('.home-hero');
|
|
99
|
+
if (hero) {
|
|
100
|
+
hero.after(this.card);
|
|
101
|
+
} else {
|
|
102
|
+
home.prepend(this.card);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
save() {
|
|
107
|
+
try {
|
|
108
|
+
localStorage.setItem(OFFER_KEY, JSON.stringify(this.state));
|
|
109
|
+
} catch {
|
|
110
|
+
// Storage can be unavailable, a dismissal then lasts for this visit only.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Whether something opens over Home, which the card then leaves alone.
|
|
115
|
+
busy() {
|
|
116
|
+
const shown = (node) => node.getClientRects().length > 0;
|
|
117
|
+
return !!this.app.tvMode || !!(this.app.photo && this.app.photo.isOpen)
|
|
118
|
+
|| [...document.querySelectorAll('dialog[open], [role="dialog"], .dd-panel.open, .lesson-panel')].some(shown);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
shown() {
|
|
122
|
+
return offerShown({
|
|
123
|
+
state: this.state, prompt: heldPrompt(), installed: installedNow(), page: this.app.page, busy: this.busy(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
update() {
|
|
128
|
+
this.attach();
|
|
129
|
+
const show = this.shown();
|
|
130
|
+
if (this.card.hidden === show) {
|
|
131
|
+
// Focus inside a card that goes moves to the page, so the keyboard keeps its place.
|
|
132
|
+
if (!show && this.card.contains(document.activeElement)) {
|
|
133
|
+
document.getElementById('pages')?.focus({ preventScroll: true });
|
|
134
|
+
}
|
|
135
|
+
this.card.hidden = !show;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async install() {
|
|
140
|
+
const { outcome } = await promptInstall();
|
|
141
|
+
// A No in the browser's own offer counts as Not now.
|
|
142
|
+
if (outcome === 'dismissed') {
|
|
143
|
+
this.state = dismissOffer(this.state);
|
|
144
|
+
this.save();
|
|
145
|
+
}
|
|
146
|
+
this.update();
|
|
147
|
+
return { outcome };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dismiss() {
|
|
151
|
+
this.state = dismissOffer(this.state);
|
|
152
|
+
this.save();
|
|
153
|
+
this.update();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The functions of window.cuju.installOffer.
|
|
157
|
+
script() {
|
|
158
|
+
return {
|
|
159
|
+
state: () => ({ ...offerState(this.state), shown: !this.card.hidden, prompt: !!heldPrompt(), installed: installedNow() }),
|
|
160
|
+
install: () => this.install(),
|
|
161
|
+
dismiss: () => this.dismiss(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|