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,359 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The search of moments across the library, a panel of the Recordings page and entries of the
|
|
5
|
+
// command palette. A query goes to the server's index, `GET /api/search?q=`, in the small query
|
|
6
|
+
// language of the guide, such as `goal team:Greens after:60`, and the filters of the panel add
|
|
7
|
+
// their terms to it. A result opens Watch a few seconds before its moment.
|
|
8
|
+
|
|
9
|
+
import { formatDate } from '../core/api.js';
|
|
10
|
+
import { hiddenRecording } from '../core/spoilers.js';
|
|
11
|
+
import { serverNodes, serverText, t, tn } from '../core/i18n.js';
|
|
12
|
+
import { enhanceSelect } from '../core/menu.js';
|
|
13
|
+
import { icon } from '../core/icons.js';
|
|
14
|
+
import { composeQuery, looksLikeMoments as looksMoments } from '../recording/moments-query.js';
|
|
15
|
+
|
|
16
|
+
export { KIND_WORDS, composeQuery, looksLikeMoments } from '../recording/moments-query.js';
|
|
17
|
+
|
|
18
|
+
// The day a number of days before now, as the query language takes dates.
|
|
19
|
+
function daysAgo(days) {
|
|
20
|
+
const d = new Date(Date.now() - days * 864e5);
|
|
21
|
+
const pad = (x) => String(x).padStart(2, '0');
|
|
22
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The choices of each filter, each a value of the query language with the message of its label,
|
|
26
|
+
// whose id ends in the value with its colons as dots, or in any for the empty value.
|
|
27
|
+
const choices = (group, values) => values.map((v) => [v, t(`moments.${group}.${v.replace(/:/g, '.') || 'any'}`)]);
|
|
28
|
+
const KIND_CHOICES = choices('kind', ['', 'goal', 'shot', 'save', 'block', 'pass', 'tackle', 'card', 'foul', 'offside',
|
|
29
|
+
'set-piece', 'corner', 'free-kick', 'penalty', 'throw-in', 'possession', 'woodwork']);
|
|
30
|
+
const TIME_CHOICES = choices('time', ['', 'before:45', 'after:45', 'after:75', 'before:15']);
|
|
31
|
+
const SCORE_CHOICES = choices('score', ['', 'leading', 'level', 'trailing']);
|
|
32
|
+
const SOURCE_CHOICES = choices('source', ['', 'requested', 'game', 'tournament', 'challenge', 'recorder', 'live']);
|
|
33
|
+
const DATE_CHOICES = choices('date', ['', '0', '7', '30']);
|
|
34
|
+
|
|
35
|
+
function el(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((c) => c !== null && c !== undefined));
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchSearch(query, limit, signal = undefined) {
|
|
49
|
+
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=${limit}`, { cache: 'no-store', signal });
|
|
50
|
+
const body = await res.json().catch(() => ({}));
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const error = new Error(body.error ? serverText(body) : t('moments.serverStatus', { status: res.status }));
|
|
53
|
+
// The answer itself, whose message shows with its marks, such as a filter as code.
|
|
54
|
+
error.answer = body.error ? body : null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
// A moment of an unwatched match of a tournament without spoilers stays out of the hits.
|
|
58
|
+
if (body && Array.isArray(body.results)) {
|
|
59
|
+
body.results = body.results.filter((hit) => !hiddenRecording(hit.match));
|
|
60
|
+
}
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Opens Watch a few seconds before a moment, seeking in place when its recording is open.
|
|
65
|
+
export function openMoment(app, result) {
|
|
66
|
+
const tl = app.timeline;
|
|
67
|
+
if (app.source && app.source.kind === 'match' && app.source.id === result.match && tl) {
|
|
68
|
+
app.showPage('watch');
|
|
69
|
+
if (app.director) {
|
|
70
|
+
app.director.stop(false);
|
|
71
|
+
}
|
|
72
|
+
tl.seek(tl.first + result.seek * tl.sps);
|
|
73
|
+
app.afterSeek();
|
|
74
|
+
if (app.updatePlayUi) {
|
|
75
|
+
app.updatePlayUi();
|
|
76
|
+
}
|
|
77
|
+
app.updateHash(true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
app.openMatch(result.match, result.seek);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class MomentSearch {
|
|
84
|
+
constructor(app) {
|
|
85
|
+
this.app = app;
|
|
86
|
+
this.text = '';
|
|
87
|
+
this.filters = { kind: '', team: '', player: '', scenario: '', source: '', since: '', score: '', time: '' };
|
|
88
|
+
this.date = '';
|
|
89
|
+
this.open = false;
|
|
90
|
+
this.results = null;
|
|
91
|
+
this.error = null;
|
|
92
|
+
this.token = 0;
|
|
93
|
+
this.timer = null;
|
|
94
|
+
this.palette = { query: null, results: [], busy: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The query of the panel as it stands.
|
|
98
|
+
query() {
|
|
99
|
+
return composeQuery(this.text, { ...this.filters, since: this.date === '' ? '' : daysAgo(Number(this.date)) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A select of the filters, which shows a symbol where one reads clearly and its label otherwise.
|
|
103
|
+
select(label, choices, value, pick, symbol = null) {
|
|
104
|
+
const node = el('select', { 'aria-label': label }, choices.map(([v, t]) => el('option', { value: v }, t)));
|
|
105
|
+
node.value = value;
|
|
106
|
+
node.addEventListener('change', () => pick(node.value));
|
|
107
|
+
queueMicrotask(() => enhanceSelect(node, symbol ? { label, icon: symbol } : { label, prefix: label }));
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Builds the panel into a host of the Recordings page.
|
|
112
|
+
mount(host, scenarios = [], teams = []) {
|
|
113
|
+
this.host = host;
|
|
114
|
+
this.toggle = el('button', {
|
|
115
|
+
type: 'button', class: 'btn', id: 'moments-toggle', 'aria-expanded': this.open ? 'true' : 'false',
|
|
116
|
+
'aria-controls': 'moments-panel', onclick: () => this.setOpen(!this.open),
|
|
117
|
+
}, icon('search'), t('moments.toggle'));
|
|
118
|
+
this.input = el('input', {
|
|
119
|
+
type: 'search', id: 'moments-query', class: 'search', value: this.text, spellcheck: 'false',
|
|
120
|
+
'aria-label': t('moments.query.label'), placeholder: t('moments.query.placeholder'),
|
|
121
|
+
'aria-describedby': 'moments-help',
|
|
122
|
+
});
|
|
123
|
+
this.input.addEventListener('input', () => {
|
|
124
|
+
this.text = this.input.value;
|
|
125
|
+
this.schedule();
|
|
126
|
+
});
|
|
127
|
+
this.input.addEventListener('keydown', (e) => {
|
|
128
|
+
if (e.key === 'Enter') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
this.run();
|
|
131
|
+
} else if (e.key === 'ArrowDown' && this.list && this.list.firstElementChild) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
this.list.firstElementChild.focus();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const pick = (key) => (value) => {
|
|
137
|
+
this.filters[key] = value;
|
|
138
|
+
this.run();
|
|
139
|
+
};
|
|
140
|
+
const player = el('input', {
|
|
141
|
+
type: 'text', id: 'moments-player', class: 'small-input', value: this.filters.player,
|
|
142
|
+
'aria-label': t('moments.player'), placeholder: t('moments.player'), spellcheck: 'false',
|
|
143
|
+
});
|
|
144
|
+
player.addEventListener('input', () => {
|
|
145
|
+
this.filters.player = player.value;
|
|
146
|
+
this.schedule();
|
|
147
|
+
});
|
|
148
|
+
const filters = el('div', { class: 'filter-bar moments-filters' },
|
|
149
|
+
this.select(t('moments.filter.kind'), KIND_CHOICES, this.filters.kind, pick('kind')),
|
|
150
|
+
this.select(t('moments.filter.team'), [['', t('moments.team.any')], ['left', t('moments.team.left')],
|
|
151
|
+
['right', t('moments.team.right')], ...teams.map((name) => [name, name])], this.filters.team, pick('team'), 'shirt'),
|
|
152
|
+
player,
|
|
153
|
+
this.select(t('moments.filter.time'), TIME_CHOICES, this.filters.time, pick('time'), 'clock'),
|
|
154
|
+
this.select(t('moments.filter.score'), SCORE_CHOICES, this.filters.score, pick('score')),
|
|
155
|
+
this.select(t('moments.filter.scenario'), [['', t('moments.scenario.any')], ...scenarios.map(([v, name]) => [v, name])],
|
|
156
|
+
this.filters.scenario, pick('scenario'), 'field'),
|
|
157
|
+
this.select(t('moments.filter.source'), SOURCE_CHOICES, this.filters.source, pick('source')),
|
|
158
|
+
this.select(t('moments.filter.date'), DATE_CHOICES, this.date, (v) => {
|
|
159
|
+
this.date = v;
|
|
160
|
+
this.run();
|
|
161
|
+
}, 'calendar'));
|
|
162
|
+
this.status = el('p', { class: 'muted small moments-status', role: 'status', 'aria-live': 'polite' });
|
|
163
|
+
this.list = el('ul', { class: 'moments-list', id: 'moments-results', 'aria-label': t('moments.results') });
|
|
164
|
+
this.list.addEventListener('keydown', (e) => this.onListKey(e));
|
|
165
|
+
this.panel = el('section', { id: 'moments-panel', class: 'moments-panel', 'aria-label': t('moments.panel'), hidden: !this.open },
|
|
166
|
+
el('div', { class: 'toolbar' }, this.input, filters),
|
|
167
|
+
el('p', { id: 'moments-help', class: 'muted small' },
|
|
168
|
+
tn('moments.help')),
|
|
169
|
+
this.status, this.list);
|
|
170
|
+
host.replaceChildren(this.panel);
|
|
171
|
+
this.renderResults();
|
|
172
|
+
return this.toggle;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setOpen(open, focus = true) {
|
|
176
|
+
this.open = !!open;
|
|
177
|
+
if (this.panel) {
|
|
178
|
+
this.panel.hidden = !this.open;
|
|
179
|
+
}
|
|
180
|
+
if (this.toggle) {
|
|
181
|
+
this.toggle.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
|
182
|
+
}
|
|
183
|
+
if (this.open) {
|
|
184
|
+
this.run();
|
|
185
|
+
if (focus && this.input) {
|
|
186
|
+
this.input.focus();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Shows the panel with a query, from the palette or the Match menu.
|
|
192
|
+
show(text) {
|
|
193
|
+
this.text = text;
|
|
194
|
+
this.app.showPage('recordings');
|
|
195
|
+
const apply = () => {
|
|
196
|
+
if (this.input) {
|
|
197
|
+
this.input.value = text;
|
|
198
|
+
}
|
|
199
|
+
this.setOpen(true);
|
|
200
|
+
};
|
|
201
|
+
if (this.panel && this.panel.isConnected) {
|
|
202
|
+
apply();
|
|
203
|
+
} else {
|
|
204
|
+
setTimeout(apply, 0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
schedule() {
|
|
209
|
+
clearTimeout(this.timer);
|
|
210
|
+
this.timer = setTimeout(() => this.run(), 250);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async run() {
|
|
214
|
+
clearTimeout(this.timer);
|
|
215
|
+
if (!this.open) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const token = ++this.token;
|
|
219
|
+
const query = this.query();
|
|
220
|
+
// An empty query names every moment of the library, so the panel waits for a word or a
|
|
221
|
+
// filter and lists nothing until then.
|
|
222
|
+
if (!query.trim()) {
|
|
223
|
+
this.results = null;
|
|
224
|
+
this.error = null;
|
|
225
|
+
this.renderResults();
|
|
226
|
+
if (this.status) {
|
|
227
|
+
this.status.textContent = t('moments.status.empty');
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (this.status) {
|
|
232
|
+
this.status.textContent = t('moments.status.searching');
|
|
233
|
+
}
|
|
234
|
+
// A newer query cancels the request of an older one, which frees its connection.
|
|
235
|
+
if (this.request) {
|
|
236
|
+
this.request.abort();
|
|
237
|
+
}
|
|
238
|
+
const request = (this.request = new AbortController());
|
|
239
|
+
try {
|
|
240
|
+
const found = await fetchSearch(query, 200, request.signal);
|
|
241
|
+
if (token !== this.token) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.results = found;
|
|
245
|
+
this.error = null;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (token !== this.token) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.results = null;
|
|
251
|
+
this.error = err.message;
|
|
252
|
+
this.errorAnswer = err.answer || null;
|
|
253
|
+
}
|
|
254
|
+
this.renderResults();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
renderResults() {
|
|
258
|
+
if (!this.list) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.error) {
|
|
262
|
+
this.status.replaceChildren(...(this.errorAnswer ? serverNodes(this.errorAnswer) : [this.error]));
|
|
263
|
+
this.list.replaceChildren();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const found = this.results;
|
|
267
|
+
if (!found) {
|
|
268
|
+
this.status.textContent = '';
|
|
269
|
+
this.list.replaceChildren();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const shown = found.results.length;
|
|
273
|
+
const recordings = t('moments.status.recordings', { count: found.recordings });
|
|
274
|
+
this.status.textContent = found.total === 0 ? t('moments.status.none', { recordings })
|
|
275
|
+
: t(shown < found.total ? 'moments.status.foundFirst' : 'moments.status.found', { count: found.total, recordings, shown });
|
|
276
|
+
this.list.replaceChildren(...found.results.map((r) => el('li', {
|
|
277
|
+
class: 'moment-row', tabindex: '-1', role: 'button', 'data-match': r.match, 'data-kind': r.kind,
|
|
278
|
+
'aria-label': t('moments.result', { text: serverText(r, 'text'), minute: r.minute, title: serverText(r, 'title') }),
|
|
279
|
+
onclick: () => openMoment(this.app, r),
|
|
280
|
+
}, el('span', { class: 'moment-minute' }, r.minute),
|
|
281
|
+
el('span', { class: 'moment-text' }, serverText(r, 'text')),
|
|
282
|
+
el('span', { class: 'moment-match muted small' }, [serverText(r, 'title'), formatDate(r.created)].filter(Boolean).join(', ')))));
|
|
283
|
+
const first = this.list.firstElementChild;
|
|
284
|
+
if (first) {
|
|
285
|
+
first.tabIndex = 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
onListKey(e) {
|
|
290
|
+
const row = e.target.closest('.moment-row');
|
|
291
|
+
if (!row) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const rows = [...this.list.children];
|
|
295
|
+
const at = rows.indexOf(row);
|
|
296
|
+
const go = (i) => {
|
|
297
|
+
const next = rows[Math.max(0, Math.min(rows.length - 1, i))];
|
|
298
|
+
for (const r of rows) {
|
|
299
|
+
r.tabIndex = -1;
|
|
300
|
+
}
|
|
301
|
+
next.tabIndex = 0;
|
|
302
|
+
next.focus();
|
|
303
|
+
};
|
|
304
|
+
if (e.key === 'ArrowDown') {
|
|
305
|
+
go(at + 1);
|
|
306
|
+
} else if (e.key === 'ArrowUp') {
|
|
307
|
+
if (at === 0) {
|
|
308
|
+
this.input.focus();
|
|
309
|
+
} else {
|
|
310
|
+
go(at - 1);
|
|
311
|
+
}
|
|
312
|
+
} else if (e.key === 'Home') {
|
|
313
|
+
go(0);
|
|
314
|
+
} else if (e.key === 'End') {
|
|
315
|
+
go(rows.length - 1);
|
|
316
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
317
|
+
row.click();
|
|
318
|
+
} else {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// The entries of the palette for a typed line, the moments found for it, best first, and one
|
|
325
|
+
// that opens the panel with it. The search runs in the background, and refresh draws the
|
|
326
|
+
// palette again when it answers.
|
|
327
|
+
paletteActions(query, refresh) {
|
|
328
|
+
if (!looksMoments(query)) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const text = query.trim();
|
|
332
|
+
const keywords = `${text} ${t('moments.palette.keywords')}`;
|
|
333
|
+
const list = [{
|
|
334
|
+
group: t('moments.palette.group'), title: t('moments.palette.search', { query: text }), keywords, hint: t('moments.palette.hint'),
|
|
335
|
+
run: () => this.show(text),
|
|
336
|
+
}];
|
|
337
|
+
const state = this.palette;
|
|
338
|
+
if (state.query !== text) {
|
|
339
|
+
state.query = text;
|
|
340
|
+
state.results = [];
|
|
341
|
+
clearTimeout(state.timer);
|
|
342
|
+
state.timer = setTimeout(() => {
|
|
343
|
+
fetchSearch(text, 8).then((found) => {
|
|
344
|
+
if (state.query === text) {
|
|
345
|
+
state.results = found.results;
|
|
346
|
+
refresh();
|
|
347
|
+
}
|
|
348
|
+
}).catch(() => {});
|
|
349
|
+
}, 200);
|
|
350
|
+
}
|
|
351
|
+
for (const r of state.results) {
|
|
352
|
+
list.push({
|
|
353
|
+
group: t('moments.palette.group'), title: `${r.minute} ${serverText(r, 'text')}`, keywords, hint: serverText(r, 'title'),
|
|
354
|
+
run: () => openMoment(this.app, r),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return list;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The Team and Scenario filters of the list of requested matches on the Matches page, what they
|
|
5
|
+
// offer and which requests they keep. A part drawn at random stays out of them while its match
|
|
6
|
+
// waits, as the list shows it as Random, so no filter tells what the draw picked before the match
|
|
7
|
+
// starts. The module touches no page, so the Node tests run it alone.
|
|
8
|
+
|
|
9
|
+
import { hiddenPart } from './draw.js';
|
|
10
|
+
|
|
11
|
+
// The teams of a request that the list shows by name, null for a side that shows as Random.
|
|
12
|
+
export function shownTeams(r) {
|
|
13
|
+
return [hiddenPart(r, 'left') ? null : r.left, hiddenPart(r, 'right') ? null : r.right];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The scenario of a request that the list shows by name, null while it shows as Random.
|
|
17
|
+
export function shownScenario(r) {
|
|
18
|
+
return hiddenPart(r, 'scenario') ? null : r.scenario;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The teams that the Team filter offers, those the list shows by name. They come in the order of
|
|
22
|
+
// the registry, the order of the team lists of a new match, so they keep their places as requests
|
|
23
|
+
// come and go, and a team that the registry no longer holds comes after them.
|
|
24
|
+
export function filterTeams(requests, teams) {
|
|
25
|
+
const named = new Set(requests.flatMap(shownTeams).filter((id) => typeof id === 'string'));
|
|
26
|
+
const known = new Set(teams.map((x) => x.id));
|
|
27
|
+
return [...teams.map((x) => x.id).filter((id) => named.has(id)), ...[...named].filter((id) => !known.has(id))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The scenarios that the Scenario filter offers, those the list shows by name, in order.
|
|
31
|
+
export function filterScenarios(requests) {
|
|
32
|
+
return [...new Set(requests.map(shownScenario).filter((sc) => typeof sc === 'string'))].sort();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The values of the filters, Every team and Every scenario as ALL, and each team or scenario
|
|
36
|
+
// after a prefix, so a team or a scenario whose name is the word all never reads as every one.
|
|
37
|
+
export const ALL = 'all';
|
|
38
|
+
|
|
39
|
+
export function teamValue(id) {
|
|
40
|
+
return `team:${id}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function scenarioValue(name) {
|
|
44
|
+
return `scenario:${name}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Whether a request passes the Team filter and the Scenario filter by their values, ALL passing
|
|
48
|
+
// every request.
|
|
49
|
+
export function passesTeam(r, value) {
|
|
50
|
+
return value === ALL || shownTeams(r).some((id) => id !== null && teamValue(id) === value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function passesScenario(r, value) {
|
|
54
|
+
const scenario = shownScenario(r);
|
|
55
|
+
return value === ALL || (scenario !== null && scenarioValue(scenario) === value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The teams that the requests name and the registry does not hold, a team added since the page
|
|
59
|
+
// loaded the registry or one removed since.
|
|
60
|
+
export function unknownTeams(requests, teams) {
|
|
61
|
+
const known = new Set(teams.map((x) => x.id));
|
|
62
|
+
return [...new Set(requests.flatMap((r) => [r.left, r.right]))].filter((id) => typeof id === 'string' && !known.has(id));
|
|
63
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The groups of the Recordings page. The recordings of one run of a challenge share a group, which
|
|
5
|
+
// the table shows as one row that names the challenge, the competitor and the run's result, and
|
|
6
|
+
// which opens to the rows of its attempts. This module reads summaries alone, so Node tests it.
|
|
7
|
+
|
|
8
|
+
// The group of a summary, or null for a recording outside any group.
|
|
9
|
+
export function groupOf(summary) {
|
|
10
|
+
const group = summary && summary.group;
|
|
11
|
+
return group && typeof group === 'object' && typeof group.id === 'string' && group.id ? group : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function attemptOf(summary) {
|
|
15
|
+
const attempt = groupOf(summary).attempt;
|
|
16
|
+
return Number.isInteger(attempt) ? attempt : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The items of the table for rows already filtered and sorted. A recording outside a group is
|
|
20
|
+
// { kind: 'recording', recording }, and a group is { kind: 'group', id, group, members } at the
|
|
21
|
+
// place of its first row, with its members in the order of their attempts.
|
|
22
|
+
export function groupRows(rows) {
|
|
23
|
+
const items = [];
|
|
24
|
+
const groups = new Map();
|
|
25
|
+
for (const summary of rows) {
|
|
26
|
+
const group = groupOf(summary);
|
|
27
|
+
if (!group) {
|
|
28
|
+
items.push({ kind: 'recording', recording: summary });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let item = groups.get(group.id);
|
|
32
|
+
if (!item) {
|
|
33
|
+
item = { kind: 'group', id: group.id, group, members: [] };
|
|
34
|
+
groups.set(group.id, item);
|
|
35
|
+
items.push(item);
|
|
36
|
+
}
|
|
37
|
+
item.members.push(summary);
|
|
38
|
+
}
|
|
39
|
+
for (const item of groups.values()) {
|
|
40
|
+
item.members.sort((a, b) => attemptOf(a) - attemptOf(b));
|
|
41
|
+
}
|
|
42
|
+
return items;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The totals of a group's members, their steps and bytes added up and the newest date.
|
|
46
|
+
export function groupTotals(members) {
|
|
47
|
+
let steps = 0;
|
|
48
|
+
let bytes = 0;
|
|
49
|
+
let created = null;
|
|
50
|
+
let newest = -Infinity;
|
|
51
|
+
for (const summary of members) {
|
|
52
|
+
steps += Number(summary.steps) || 0;
|
|
53
|
+
bytes += Number(summary.bytes) || 0;
|
|
54
|
+
const time = Date.parse(summary.created);
|
|
55
|
+
if (!Number.isNaN(time) && time > newest) {
|
|
56
|
+
newest = time;
|
|
57
|
+
created = summary.created;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { steps, bytes, created };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The rows that the keyboard and the selection walk, in the order of the table. A group is one row
|
|
64
|
+
// that selects all its members, and the rows of its members follow while it is open.
|
|
65
|
+
export function navigationRows(items, open) {
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (item.kind === 'recording') {
|
|
69
|
+
rows.push({ key: item.recording.id, ids: [item.recording.id], recording: item.recording, group: null });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
rows.push({ key: `group:${item.id}`, ids: item.members.map((m) => m.id), recording: null, group: item });
|
|
73
|
+
if (open.has(item.id)) {
|
|
74
|
+
for (const member of item.members) {
|
|
75
|
+
rows.push({ key: member.id, ids: [member.id], recording: member, group: null, parent: item.id });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return rows;
|
|
80
|
+
}
|