├── src ├── preload.js ├── icons │ ├── fia.png │ ├── edit.png │ ├── save.png │ ├── compass.png │ ├── denied.png │ ├── layout.png │ ├── noicon.png │ ├── peanut.png │ ├── restore.png │ ├── settings.png │ ├── arrowleft.png │ ├── arrowright.png │ ├── checkmark.png │ ├── teams │ │ ├── haas.png │ │ ├── alpine.png │ │ ├── ferrari.png │ │ ├── mclaren.png │ │ ├── renault.png │ │ ├── haas-red.png │ │ ├── mercedes.png │ │ ├── red-bull.png │ │ ├── williams.png │ │ ├── alfa-romeo.png │ │ ├── alpha-tauri.png │ │ ├── aston-martin.png │ │ ├── force-india.png │ │ ├── racing-point.png │ │ ├── toro-rosso.png │ │ ├── mclaren-white.png │ │ └── williams-white.png │ ├── tires │ │ ├── hard.png │ │ ├── soft.png │ │ ├── test.png │ │ ├── wet.png │ │ ├── medium.png │ │ ├── unknown.png │ │ ├── hard_real.png │ │ ├── soft_real.png │ │ ├── test_real.png │ │ ├── wet_real.png │ │ ├── intermediate.png │ │ ├── medium_real.png │ │ ├── test_unknown.png │ │ └── intermediate_real.png │ ├── green-heart.png │ ├── open-external.png │ ├── unknowndriver.png │ ├── windows │ │ ├── color.png │ │ ├── govee.png │ │ ├── logo.png │ │ ├── compass.png │ │ ├── weather.png │ │ ├── battlemode.png │ │ ├── sessionlog.png │ │ ├── singlercm.png │ │ ├── statuses.png │ │ ├── tirestats.png │ │ ├── trackinfo.png │ │ ├── tracktime.png │ │ ├── currentlaps.png │ │ ├── flagdisplay.png │ │ └── crashdetection.png │ ├── messages │ │ ├── blank.png │ │ ├── car_spun.png │ │ ├── grip_low.png │ │ ├── weather.png │ │ ├── car_stopped.png │ │ ├── correction.png │ │ ├── drs_enabled.png │ │ ├── grip_normal.png │ │ ├── car_missedapex.png │ │ ├── car_offtrack.png │ │ ├── drs_disabled.png │ │ ├── grip_slippery.png │ │ ├── incident_noted.png │ │ ├── penalty_time.png │ │ ├── pit_entry_open.png │ │ ├── pit_exit_open.png │ │ ├── session_resume.png │ │ ├── car_tracklimits.png │ │ ├── pit_entry_closed.png │ │ ├── pit_exit_closed.png │ │ ├── penalty_stopandgo.png │ │ ├── safetycar_pitlane.png │ │ ├── safetycar_startfinish.png │ │ ├── session_startdelayed.png │ │ ├── session_durationchanged.png │ │ ├── incident_nofurther_action.png │ │ ├── incident_underinvestigation.png │ │ ├── incident_nofurther_investigation.png │ │ └── incident_investigationafterthesession.png │ └── flags │ │ ├── flag_chequered.png │ │ ├── flag_blackandorange.png │ │ └── flag_blackandwhite.png ├── fonts │ ├── Inter-Bold.ttf │ ├── Formula1-Wide.ttf │ ├── Inter-Medium.ttf │ ├── Inter-Regular.ttf │ ├── TitilliumWeb-Bold.ttf │ ├── Formula1-Bold_web_0.ttf │ ├── TitilliumWeb-Italic.ttf │ ├── TitilliumWeb-Regular.ttf │ ├── Exo2-VariableFont_wght.ttf │ ├── Formula1-Regular_web_0.ttf │ ├── TitilliumWeb-BoldItalic.ttf │ ├── TitilliumWeb-SemiBold.ttf │ ├── Formula1-Bold_web_0_modified.ttf │ ├── TitilliumWeb-SemiBoldItalic.ttf │ └── fonts.css ├── statuses │ ├── slippery.png │ ├── index.html │ ├── style.css │ └── index.js ├── flagdisplay │ ├── Chequered.png │ ├── index.html │ ├── govee │ │ ├── index.html │ │ └── style.css │ ├── style.css │ └── index.js ├── weather │ ├── build │ │ └── js │ │ │ ├── 20b52d0c6ea89997befb.ttf │ │ │ ├── 425b19b61e022e504f51.ttf │ │ │ ├── 50c688fdc8d382c66303.ttf │ │ │ ├── 6dcbc9bed1ec438907ee.ttf │ │ │ ├── 718ec91358aa7ee8083b.ttf │ │ │ ├── 823d9b7475c12cab2754.ttf │ │ │ ├── 88fa7ae373b07b41ecce.ttf │ │ │ ├── 88fc88a1d67e79aaacde.ttf │ │ │ ├── 8e2a3df21501da2e09fa.ttf │ │ │ ├── a10812e5dfa71f8496d3.ttf │ │ │ ├── b1f4d2c69fa808ebca12.otf │ │ │ ├── b85d841dacb040b84951.otf │ │ │ ├── cac06450c6935ef24aa9.ttf │ │ │ ├── ce61e3623e6de15381db.ttf │ │ │ ├── d1f5e6e16dd4f75c3950.otf │ │ │ └── e89cb19905e7db5591b0.ttf │ ├── src │ │ ├── index.js │ │ ├── trackRotation.js │ │ ├── index.css │ │ └── modifyData.js │ ├── index.html │ └── webpack.common.js ├── compass │ ├── style.css │ ├── index.html │ └── index.js ├── main │ ├── tooltip.css │ ├── tempstream │ │ ├── style.css │ │ └── index.html │ ├── RPC.js │ ├── style.css │ ├── tool_buttons.css │ ├── popup.css │ ├── windows_section.css │ ├── layout_section.css │ ├── connection_section.css │ └── settings_section.css ├── tracktime │ ├── style.css │ ├── index.html │ └── index.js ├── scripts │ └── movemode.js ├── crashdetection │ ├── index.html │ ├── style.css │ └── index.js ├── styles │ └── window_info.css ├── battlemode │ ├── index.html │ └── style.css ├── currentlaps │ ├── index.html │ └── style.css ├── sessionlog │ ├── index.html │ └── style.css ├── functions │ ├── times.js │ ├── car.js │ ├── colors.js │ └── driver.js ├── singlercm │ ├── index.html │ ├── style.css │ └── index.js ├── autoswitch │ ├── style.css │ └── index.html ├── trackinfo │ ├── index.html │ ├── style.css │ └── index.js └── tirestats │ ├── style.css │ ├── index.js │ └── index.html ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug_report.md └── workflows │ └── main.yml ├── store_manifest.json ├── electron-builder.yml ├── .gitignore ├── package.json └── readme.md /src/preload.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/fia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/fia.png -------------------------------------------------------------------------------- /src/icons/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/edit.png -------------------------------------------------------------------------------- /src/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/save.png -------------------------------------------------------------------------------- /src/icons/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/compass.png -------------------------------------------------------------------------------- /src/icons/denied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/denied.png -------------------------------------------------------------------------------- /src/icons/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/layout.png -------------------------------------------------------------------------------- /src/icons/noicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/noicon.png -------------------------------------------------------------------------------- /src/icons/peanut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/peanut.png -------------------------------------------------------------------------------- /src/icons/restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/restore.png -------------------------------------------------------------------------------- /src/icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/settings.png -------------------------------------------------------------------------------- /src/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /src/icons/arrowleft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/arrowleft.png -------------------------------------------------------------------------------- /src/icons/arrowright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/arrowright.png -------------------------------------------------------------------------------- /src/icons/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/checkmark.png -------------------------------------------------------------------------------- /src/icons/teams/haas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/haas.png -------------------------------------------------------------------------------- /src/icons/tires/hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/hard.png -------------------------------------------------------------------------------- /src/icons/tires/soft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/soft.png -------------------------------------------------------------------------------- /src/icons/tires/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/test.png -------------------------------------------------------------------------------- /src/icons/tires/wet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/wet.png -------------------------------------------------------------------------------- /src/fonts/Formula1-Wide.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Formula1-Wide.ttf -------------------------------------------------------------------------------- /src/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /src/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /src/icons/green-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/green-heart.png -------------------------------------------------------------------------------- /src/icons/open-external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/open-external.png -------------------------------------------------------------------------------- /src/icons/teams/alpine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/alpine.png -------------------------------------------------------------------------------- /src/icons/teams/ferrari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/ferrari.png -------------------------------------------------------------------------------- /src/icons/teams/mclaren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/mclaren.png -------------------------------------------------------------------------------- /src/icons/teams/renault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/renault.png -------------------------------------------------------------------------------- /src/icons/tires/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/medium.png -------------------------------------------------------------------------------- /src/icons/tires/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/unknown.png -------------------------------------------------------------------------------- /src/icons/unknowndriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/unknowndriver.png -------------------------------------------------------------------------------- /src/icons/windows/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/color.png -------------------------------------------------------------------------------- /src/icons/windows/govee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/govee.png -------------------------------------------------------------------------------- /src/icons/windows/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/logo.png -------------------------------------------------------------------------------- /src/statuses/slippery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/statuses/slippery.png -------------------------------------------------------------------------------- /src/flagdisplay/Chequered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/flagdisplay/Chequered.png -------------------------------------------------------------------------------- /src/icons/messages/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/blank.png -------------------------------------------------------------------------------- /src/icons/teams/haas-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/haas-red.png -------------------------------------------------------------------------------- /src/icons/teams/mercedes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/mercedes.png -------------------------------------------------------------------------------- /src/icons/teams/red-bull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/red-bull.png -------------------------------------------------------------------------------- /src/icons/teams/williams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/williams.png -------------------------------------------------------------------------------- /src/icons/tires/hard_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/hard_real.png -------------------------------------------------------------------------------- /src/icons/tires/soft_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/soft_real.png -------------------------------------------------------------------------------- /src/icons/tires/test_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/test_real.png -------------------------------------------------------------------------------- /src/icons/tires/wet_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/wet_real.png -------------------------------------------------------------------------------- /src/icons/windows/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/compass.png -------------------------------------------------------------------------------- /src/icons/windows/weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/weather.png -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-Bold.ttf -------------------------------------------------------------------------------- /src/icons/messages/car_spun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/car_spun.png -------------------------------------------------------------------------------- /src/icons/messages/grip_low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/grip_low.png -------------------------------------------------------------------------------- /src/icons/messages/weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/weather.png -------------------------------------------------------------------------------- /src/icons/teams/alfa-romeo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/alfa-romeo.png -------------------------------------------------------------------------------- /src/icons/teams/alpha-tauri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/alpha-tauri.png -------------------------------------------------------------------------------- /src/icons/teams/aston-martin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/aston-martin.png -------------------------------------------------------------------------------- /src/icons/teams/force-india.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/force-india.png -------------------------------------------------------------------------------- /src/icons/teams/racing-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/racing-point.png -------------------------------------------------------------------------------- /src/icons/teams/toro-rosso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/toro-rosso.png -------------------------------------------------------------------------------- /src/icons/tires/intermediate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/intermediate.png -------------------------------------------------------------------------------- /src/icons/tires/medium_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/medium_real.png -------------------------------------------------------------------------------- /src/icons/tires/test_unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/test_unknown.png -------------------------------------------------------------------------------- /src/icons/windows/battlemode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/battlemode.png -------------------------------------------------------------------------------- /src/icons/windows/sessionlog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/sessionlog.png -------------------------------------------------------------------------------- /src/icons/windows/singlercm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/singlercm.png -------------------------------------------------------------------------------- /src/icons/windows/statuses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/statuses.png -------------------------------------------------------------------------------- /src/icons/windows/tirestats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/tirestats.png -------------------------------------------------------------------------------- /src/icons/windows/trackinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/trackinfo.png -------------------------------------------------------------------------------- /src/icons/windows/tracktime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/tracktime.png -------------------------------------------------------------------------------- /src/fonts/Formula1-Bold_web_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Formula1-Bold_web_0.ttf -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-Italic.ttf -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-Regular.ttf -------------------------------------------------------------------------------- /src/icons/flags/flag_chequered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/flags/flag_chequered.png -------------------------------------------------------------------------------- /src/icons/messages/car_stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/car_stopped.png -------------------------------------------------------------------------------- /src/icons/messages/correction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/correction.png -------------------------------------------------------------------------------- /src/icons/messages/drs_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/drs_enabled.png -------------------------------------------------------------------------------- /src/icons/messages/grip_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/grip_normal.png -------------------------------------------------------------------------------- /src/icons/teams/mclaren-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/mclaren-white.png -------------------------------------------------------------------------------- /src/icons/teams/williams-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/teams/williams-white.png -------------------------------------------------------------------------------- /src/icons/windows/currentlaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/currentlaps.png -------------------------------------------------------------------------------- /src/icons/windows/flagdisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/flagdisplay.png -------------------------------------------------------------------------------- /src/fonts/Exo2-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Exo2-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/fonts/Formula1-Regular_web_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Formula1-Regular_web_0.ttf -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-BoldItalic.ttf -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-SemiBold.ttf -------------------------------------------------------------------------------- /src/icons/messages/car_missedapex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/car_missedapex.png -------------------------------------------------------------------------------- /src/icons/messages/car_offtrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/car_offtrack.png -------------------------------------------------------------------------------- /src/icons/messages/drs_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/drs_disabled.png -------------------------------------------------------------------------------- /src/icons/messages/grip_slippery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/grip_slippery.png -------------------------------------------------------------------------------- /src/icons/messages/incident_noted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/incident_noted.png -------------------------------------------------------------------------------- /src/icons/messages/penalty_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/penalty_time.png -------------------------------------------------------------------------------- /src/icons/messages/pit_entry_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/pit_entry_open.png -------------------------------------------------------------------------------- /src/icons/messages/pit_exit_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/pit_exit_open.png -------------------------------------------------------------------------------- /src/icons/messages/session_resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/session_resume.png -------------------------------------------------------------------------------- /src/icons/tires/intermediate_real.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/tires/intermediate_real.png -------------------------------------------------------------------------------- /src/icons/windows/crashdetection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/windows/crashdetection.png -------------------------------------------------------------------------------- /src/icons/flags/flag_blackandorange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/flags/flag_blackandorange.png -------------------------------------------------------------------------------- /src/icons/flags/flag_blackandwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/flags/flag_blackandwhite.png -------------------------------------------------------------------------------- /src/icons/messages/car_tracklimits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/car_tracklimits.png -------------------------------------------------------------------------------- /src/icons/messages/pit_entry_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/pit_entry_closed.png -------------------------------------------------------------------------------- /src/icons/messages/pit_exit_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/pit_exit_closed.png -------------------------------------------------------------------------------- /src/fonts/Formula1-Bold_web_0_modified.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/Formula1-Bold_web_0_modified.ttf -------------------------------------------------------------------------------- /src/fonts/TitilliumWeb-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/fonts/TitilliumWeb-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/icons/messages/penalty_stopandgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/penalty_stopandgo.png -------------------------------------------------------------------------------- /src/icons/messages/safetycar_pitlane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/safetycar_pitlane.png -------------------------------------------------------------------------------- /src/icons/messages/safetycar_startfinish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/safetycar_startfinish.png -------------------------------------------------------------------------------- /src/icons/messages/session_startdelayed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/session_startdelayed.png -------------------------------------------------------------------------------- /src/icons/messages/session_durationchanged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/session_durationchanged.png -------------------------------------------------------------------------------- /src/weather/build/js/20b52d0c6ea89997befb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/20b52d0c6ea89997befb.ttf -------------------------------------------------------------------------------- /src/weather/build/js/425b19b61e022e504f51.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/425b19b61e022e504f51.ttf -------------------------------------------------------------------------------- /src/weather/build/js/50c688fdc8d382c66303.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/50c688fdc8d382c66303.ttf -------------------------------------------------------------------------------- /src/weather/build/js/6dcbc9bed1ec438907ee.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/6dcbc9bed1ec438907ee.ttf -------------------------------------------------------------------------------- /src/weather/build/js/718ec91358aa7ee8083b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/718ec91358aa7ee8083b.ttf -------------------------------------------------------------------------------- /src/weather/build/js/823d9b7475c12cab2754.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/823d9b7475c12cab2754.ttf -------------------------------------------------------------------------------- /src/weather/build/js/88fa7ae373b07b41ecce.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/88fa7ae373b07b41ecce.ttf -------------------------------------------------------------------------------- /src/weather/build/js/88fc88a1d67e79aaacde.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/88fc88a1d67e79aaacde.ttf -------------------------------------------------------------------------------- /src/weather/build/js/8e2a3df21501da2e09fa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/8e2a3df21501da2e09fa.ttf -------------------------------------------------------------------------------- /src/weather/build/js/a10812e5dfa71f8496d3.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/a10812e5dfa71f8496d3.ttf -------------------------------------------------------------------------------- /src/weather/build/js/b1f4d2c69fa808ebca12.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/b1f4d2c69fa808ebca12.otf -------------------------------------------------------------------------------- /src/weather/build/js/b85d841dacb040b84951.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/b85d841dacb040b84951.otf -------------------------------------------------------------------------------- /src/weather/build/js/cac06450c6935ef24aa9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/cac06450c6935ef24aa9.ttf -------------------------------------------------------------------------------- /src/weather/build/js/ce61e3623e6de15381db.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/ce61e3623e6de15381db.ttf -------------------------------------------------------------------------------- /src/weather/build/js/d1f5e6e16dd4f75c3950.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/d1f5e6e16dd4f75c3950.otf -------------------------------------------------------------------------------- /src/weather/build/js/e89cb19905e7db5591b0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/weather/build/js/e89cb19905e7db5591b0.ttf -------------------------------------------------------------------------------- /src/icons/messages/incident_nofurther_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/incident_nofurther_action.png -------------------------------------------------------------------------------- /src/icons/messages/incident_underinvestigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/incident_underinvestigation.png -------------------------------------------------------------------------------- /src/icons/messages/incident_nofurther_investigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/incident_nofurther_investigation.png -------------------------------------------------------------------------------- /src/icons/messages/incident_investigationafterthesession.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MRAJEKO/UF1-Viewer-with-F1MV/HEAD/src/icons/messages/incident_investigationafterthesession.png -------------------------------------------------------------------------------- /src/compass/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | #compass { 8 | width: calc(100vw); 9 | } 10 | 11 | .drag { 12 | -webkit-app-region: drag; 13 | } 14 | -------------------------------------------------------------------------------- /src/weather/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | import Graph from "./graph"; 5 | 6 | import "../../fonts/fonts.css"; 7 | 8 | import "../../styles/window_info.css"; 9 | 10 | import "./index.css"; 11 | 12 | const rootElement = document.getElementById("root"); 13 | const root = createRoot(rootElement); 14 | root.render( 15 |
16 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A feature you would like to see integrated 4 | title: "" 5 | labels: Feature Request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Where does this feature apply to** 11 | The overall app, layouts, a specific window, ect. 12 | 13 | **The feature** 14 | The feature you would like to see integrated. 15 | 16 | **Additional Content** 17 | Any extra info, video's or screenshots. 18 | -------------------------------------------------------------------------------- /src/main/tooltip.css: -------------------------------------------------------------------------------- 1 | #tooltip { 2 | width: 60%; 3 | position: fixed; 4 | bottom: -100px; 5 | left: 50%; 6 | transform: translateX(-50%); 7 | padding: 10px 20px; 8 | border-radius: 30px; 9 | border: 2px solid black; 10 | transition: all 0.5s ease; 11 | z-index: 4; 12 | display: grid; 13 | place-items: center; 14 | color: rgb(41, 41, 41); 15 | } 16 | 17 | #tooltip.show { 18 | bottom: 20px !important; 19 | } 20 | -------------------------------------------------------------------------------- /src/tracktime/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | -webkit-app-region: drag; 9 | } 10 | 11 | .container { 12 | background-color: #0a0a0faf; 13 | height: 100vh; 14 | border-radius: 7vw; 15 | display: grid; 16 | place-items: center; 17 | } 18 | 19 | p { 20 | text-align: center; 21 | font-size: 18vw; 22 | padding: 5vw; 23 | color: white; 24 | font-family: "SFBold"; 25 | } 26 | -------------------------------------------------------------------------------- /src/weather/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Weather Information 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/weather/src/trackRotation.js: -------------------------------------------------------------------------------- 1 | const f1mvApi = require("npm_f1mv_api"); 2 | 3 | export const trackRotation = async (config) => { 4 | console.log(config); 5 | if (config.port !== undefined) { 6 | const sessionInfo = (await f1mvApi.LiveTimingAPIGraphQL(config, "SessionInfo")).SessionInfo; 7 | console.log(sessionInfo); 8 | const circuitId = sessionInfo.Meeting.Circuit.Key; 9 | const year = new Date(sessionInfo.StartDate).getFullYear(); 10 | const circuitRotation = (await f1mvApi.getCircuitInfo(circuitId, year)).rotation; 11 | return circuitRotation; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/tracktime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Delayed Track Time 8 | 9 | 10 | 11 | 12 |
13 |

00:00:00

14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /store_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ultimate F1 Viewer", 3 | "description": "Ultimate Formula 1 Viewer is a MultiViewer for F1 integration to make the experience even better. By opening different windows you can create your custom 'pit wall' to get all the latest updates live on track.", 4 | "author": "MR.AJEKO", 5 | "image": "https://cdn.lapstime.fr/lADO1/qoyOwENA89.png/raw", 6 | "endWith": { 7 | "windows": { 8 | "x64": ".exe" 9 | }, 10 | "linux": { 11 | "x64": ".AppImage" 12 | }, 13 | "darwin": { 14 | "x64": ".dmg" 15 | } 16 | }, 17 | "installer": true 18 | } 19 | -------------------------------------------------------------------------------- /src/flagdisplay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flag Display 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/scripts/movemode.js: -------------------------------------------------------------------------------- 1 | function toggleMoveMode() { 2 | document.getElementById("background").classList.toggle("drag"); 3 | 4 | const infoElement = document.getElementById("move_mode_info"); 5 | 6 | infoElement.style.display = infoElement.style.display === "none" ? "flex" : "none"; 7 | } 8 | 9 | document.addEventListener("keydown", (event) => { 10 | if (event.key === "Escape") { 11 | toggleMoveMode(); 12 | } 13 | }); 14 | 15 | document.getElementById( 16 | "background" 17 | ).innerHTML += `

Move Mode

This window is currently in move mode. You can press 'Escape' to toggle it and switch between moving and functionality

`; 18 | -------------------------------------------------------------------------------- /src/main/tempstream/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #container { 7 | background-color: #0a0a0faf; 8 | height: calc(100vh - 2px); 9 | width: calc(100vw - 2px); 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | border-radius: 10px; 15 | border: 1px solid #fff; 16 | } 17 | 18 | #title { 19 | font-family: "SFBold"; 20 | font-size: 10vw; 21 | color: #fff; 22 | } 23 | 24 | .info { 25 | position: absolute; 26 | bottom: 20px; 27 | font-family: "SFRegular"; 28 | transform: skew(-10deg); 29 | opacity: 0.2; 30 | color: white; 31 | font-size: 2.3vw; 32 | text-align: center; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/tempstream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Waiting for live session to start 8 | 9 | 10 | 11 | 12 |
13 |

TITLE

14 |

When the live session starts, your MultiViewer window will automatically open.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/flagdisplay/govee/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Govee Lights Integration 8 | 9 | 10 | 11 | 12 |
13 |

Govee

14 |
15 |

Connected:

16 |

0

17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/compass/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Track Rotation Compass 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Compass image not loading 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/crashdetection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Crash Detection 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Crash Detection

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: "" 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Where does this bug apply to** 11 | The overall app, layouts, a specific window, ect. 12 | 13 | **Describe the bug** 14 | Give a good and detailed explanation of the bug 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear description of what you expected to happen. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS and version: [e.g. MacOS, Windows, Linux] 28 | - UF1 version: [e.g. 1.7.1] 29 | 30 | **Additional Content** 31 | If applicable, add screenshots to help explain your problem. 32 | -------------------------------------------------------------------------------- /src/statuses/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sector Statuses 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |

ALL CLEAR

16 |
17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/styles/window_info.css: -------------------------------------------------------------------------------- 1 | #move_mode_info { 2 | border: 4px solid white; 3 | border-radius: 20px; 4 | height: calc(100% - 8px); 5 | width: calc(100% - 8px); 6 | position: fixed; 7 | top: 0; 8 | background-color: rgba(0, 0, 0, 0.74); 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | color: white; 14 | } 15 | 16 | #move_mode_info h1 { 17 | font-size: 40px !important; 18 | margin: 2vh 0; 19 | font-family: "SFBold"; 20 | width: 90% !important; 21 | text-align: center; 22 | } 23 | 24 | #move_mode_info p { 25 | opacity: 0.8; 26 | font-size: 15px !important; 27 | margin: 2vh 0; 28 | font-family: "SFRegular"; 29 | width: 90% !important; 30 | text-align: center; 31 | transform: skew(-10deg); 32 | } 33 | -------------------------------------------------------------------------------- /src/battlemode/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Battle mode 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/currentlaps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Current Push Laps 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/flagdisplay/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | background-color: #1a1a1a; 9 | } 10 | 11 | .move { 12 | -webkit-app-region: drag; 13 | width: 100vw; 14 | height: 100vh; 15 | } 16 | 17 | #extra, 18 | #flag, 19 | #chequered { 20 | position: absolute; 21 | width: 100vw; 22 | height: 100vh; 23 | transition: all 0.5s ease; 24 | opacity: 1; 25 | } 26 | 27 | #chequered { 28 | background-color: transparent; 29 | } 30 | 31 | .white { 32 | background-color: rgb(255, 255, 255) !important; 33 | } 34 | 35 | .green { 36 | background-color: rgb(0, 175, 0) !important; 37 | } 38 | 39 | .black { 40 | background-color: rgb(0, 0, 0); 41 | } 42 | 43 | .yellow { 44 | background-color: rgb(255, 230, 0) !important; 45 | } 46 | 47 | .red { 48 | background-color: rgb(209, 0, 0) !important; 49 | } 50 | 51 | .fastest-lap { 52 | background-color: rgb(185, 0, 185); 53 | } 54 | -------------------------------------------------------------------------------- /src/sessionlog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Session Log 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |

Session Log

20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/RPC.js: -------------------------------------------------------------------------------- 1 | const clientId = "1027664070993772594"; 2 | const DiscordRPC = require("discord-rpc"); 3 | const RPC = new DiscordRPC.Client({ transport: "ipc" }); 4 | 5 | DiscordRPC.register(clientId); 6 | 7 | let nowDate = Date.now(); 8 | 9 | async function setActivity() { 10 | const { SessionInfo } = await (await fetch("http://127.0.0.1:10101/api/v2/live-timing/state")).json(); 11 | 12 | if (!RPC) return; 13 | RPC.setActivity({ 14 | details: `Watching ${SessionInfo.Name} with MultiViewer for F1`, 15 | startTimestamp: nowDate, 16 | largeImageKey: "f1mv_logo", 17 | largeImageText: "Logo of F1MV", 18 | instance: false, 19 | buttons: [ 20 | { 21 | label: `Download MultiViewer for F1!`, 22 | url: "https://multiviewer.app/download", 23 | }, 24 | ], 25 | }); 26 | } 27 | 28 | RPC.on("ready", async () => { 29 | setActivity(); 30 | 31 | setInterval(setActivity, 15000); 32 | }); 33 | 34 | RPC.login({ clientId }); 35 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | productName: "Ultimate-F1Viewer-With-F1MV" 2 | appId: "com.mrajeko.uf1" 3 | copyright: "Copyright © 2023 MRAJEKO" 4 | 5 | generateUpdatesFilesForAllChannels: true 6 | asar: true 7 | 8 | npmRebuild: false 9 | nodeGypRebuild: false 10 | buildDependenciesFromSource: false 11 | 12 | directories: 13 | output: "out" 14 | 15 | win: 16 | icon: "src/icons/windows/logo.png" 17 | target: 18 | - target: "nsis" 19 | arch: 20 | - x64 21 | - ia32 22 | 23 | mac: 24 | category: "public.app-category.utilities" 25 | minimumSystemVersion: "10.12.0" 26 | icon: "src/icons/windows/logo.png" 27 | target: 28 | - target: "dmg" 29 | arch: 30 | - x64 31 | - arm64 32 | 33 | linux: 34 | icon: "src/icons/windows/logo.png" 35 | target: 36 | - target: "AppImage" 37 | arch: 38 | - x64 39 | 40 | nsis: 41 | oneClick: true 42 | perMachine: true 43 | deleteAppDataOnUninstall: true 44 | 45 | appImage: 46 | category: "Utility" 47 | 48 | publish: 49 | provider: "github" 50 | -------------------------------------------------------------------------------- /src/functions/times.js: -------------------------------------------------------------------------------- 1 | // A lap or sector time can be send through and will return as a number in seconds 2 | function parseLapOrSectorTime(time) { 3 | // Split the input into 3 variables by checking if there is a : or a . in the time. Then replace any starting 0's by nothing and convert them to numbers using parseInt. 4 | const [minutes, seconds, milliseconds] = time 5 | .split(/[:.]/) 6 | .map((number) => parseInt(number.replace(/^0+/, "") || "0", 10)); 7 | 8 | if (milliseconds === undefined) return minutes + seconds / 1000; 9 | 10 | return minutes * 60 + seconds + milliseconds / 1000; 11 | } 12 | 13 | function formatMsToF1(ms, fixedAmount) { 14 | const minutes = Math.floor(ms / 60000); 15 | 16 | let milliseconds = ms % 60000; 17 | 18 | const seconds = 19 | milliseconds / 1000 < 10 && minutes > 0 20 | ? "0" + (milliseconds / 1000).toFixed(fixedAmount) 21 | : (milliseconds / 1000).toFixed(fixedAmount); 22 | 23 | return minutes > 0 ? minutes + ":" + seconds : seconds; 24 | } 25 | 26 | module.exports = { 27 | parseLapOrSectorTime, 28 | formatMsToF1, 29 | }; 30 | -------------------------------------------------------------------------------- /src/flagdisplay/govee/style.css: -------------------------------------------------------------------------------- 1 | /* Base styling */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | color: white; 6 | } 7 | 8 | body { 9 | height: 100vh; 10 | } 11 | 12 | .container { 13 | background-color: #0a0a0faf; 14 | border-radius: 20px; 15 | height: 100vh; 16 | overflow: hidden; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: space-evenly; 21 | opacity: 1; 22 | transition: all 0.5s ease; 23 | } 24 | 25 | .transparent { 26 | background-color: transparent; 27 | } 28 | 29 | .wrapper { 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: space-between; 34 | height: 55vh; 35 | } 36 | 37 | h1 { 38 | font-family: "SFBold", Arial, Helvetica, sans-serif; 39 | font-size: 25px; 40 | } 41 | 42 | h2 { 43 | font-family: "SFBold", Arial, Helvetica, sans-serif; 44 | font-size: 20px; 45 | } 46 | 47 | p { 48 | color: green; 49 | border: 2px solid green; 50 | border-radius: 10px; 51 | font-family: "SFRegular", Arial, Helvetica, sans-serif; 52 | font-size: 30px; 53 | padding: 4px 15px; 54 | } 55 | 56 | .hide { 57 | opacity: 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/singlercm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Single Race Control Message 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/crashdetection/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | transition: all 0.5s ease; 5 | overflow: hidden; 6 | } 7 | 8 | h1 { 9 | font-family: "SFBold", Arial, Helvetica, sans-serif; 10 | padding: 5vw; 11 | background-color: #0a0a0faf; 12 | border-radius: 15px; 13 | font-size: 8vw; 14 | text-align: center; 15 | color: white; 16 | } 17 | 18 | #list { 19 | display: flex; 20 | flex-direction: column-reverse; 21 | width: 100vw; 22 | list-style: none; 23 | } 24 | 25 | .line { 26 | margin: 3vw 0; 27 | width: 100vw; 28 | height: 2px; 29 | background-color: #0a0a0faf; 30 | } 31 | 32 | #list li { 33 | font-family: "SFRegular", Arial, Helvetica, sans-serif; 34 | font-size: 5vw; 35 | padding: 15px; 36 | overflow: hidden; 37 | max-height: 0; 38 | background-color: #0a0a0faf; 39 | border-radius: 15px; 40 | } 41 | 42 | li.show { 43 | margin-bottom: 3vw; 44 | max-height: 3rem !important; 45 | } 46 | 47 | /* ANIMATIONS */ 48 | 49 | #list li { 50 | transition: all 0.4s ease-out; 51 | opacity: 0; 52 | } 53 | #list li.show { 54 | opacity: 1; 55 | } 56 | 57 | body { 58 | height: 100vh; 59 | } 60 | 61 | .drag { 62 | -webkit-app-region: drag; 63 | } 64 | 65 | #container { 66 | display: flex; 67 | flex-direction: column-reverse; 68 | } 69 | 70 | .hidden { 71 | display: none; 72 | opacity: 0; 73 | } 74 | 75 | .warning { 76 | background-color: red !important; 77 | } 78 | -------------------------------------------------------------------------------- /src/singlercm/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | transition: 1s all ease; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | 13 | .drag { 14 | -webkit-app-region: drag; 15 | } 16 | 17 | h1 { 18 | text-align: center; 19 | font-size: 21px; 20 | color: white; 21 | width: 750px; 22 | font-family: "SFBold", Arial, Helvetica, sans-serif; 23 | } 24 | 25 | .icons-container { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | width: 100px !important; 30 | height: 80px; 31 | position: absolute; 32 | top: 50%; 33 | transform: translateY(-50%); 34 | } 35 | 36 | .logo-container { 37 | left: 0; 38 | border-right: 1px solid rgb(90, 90, 90); 39 | } 40 | 41 | .icon-container { 42 | right: 0; 43 | border-left: 1px solid rgb(90, 90, 90); 44 | } 45 | 46 | #RCM { 47 | width: 100vw; 48 | height: 100px; 49 | background-color: #0a0a0faf; 50 | border-radius: 0 0 40px 40px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | 56 | .red { 57 | color: rgb(207, 0, 0); 58 | } 59 | 60 | .orange { 61 | color: rgb(255, 165, 0); 62 | } 63 | 64 | .green { 65 | color: rgb(0, 207, 0); 66 | } 67 | 68 | .logo { 69 | width: 60px; 70 | } 71 | 72 | #icon { 73 | width: 50px; 74 | } 75 | 76 | .shown { 77 | transform: translateY(0px); 78 | } 79 | 80 | .hidden { 81 | transform: translateY(-100px); 82 | } 83 | -------------------------------------------------------------------------------- /src/main/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "SFBold", Arial, Helvetica, sans-serif; 5 | color-scheme: dark; 6 | } 7 | 8 | body { 9 | background-color: rgb(15, 15, 15); 10 | text-align: center; 11 | height: 100vh; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | display: none; 16 | } 17 | 18 | #sections { 19 | height: calc(100vh - 80px); 20 | overflow: auto; 21 | transition: all 0.5s ease; 22 | } 23 | 24 | .disabled { 25 | background-color: transparent; 26 | border: 2px solid #9f313162 !important; 27 | color: #9f313162 !important; 28 | transform: translate(0, 0) !important; 29 | } 30 | 31 | .disabled:hover { 32 | transform: translate(0, 0); 33 | box-shadow: 0 0 #000000; 34 | } 35 | 36 | h1 { 37 | font-size: 30px; 38 | padding: 30px; 39 | padding-bottom: 22.5px; 40 | color: white; 41 | } 42 | 43 | #connection { 44 | left: 0; 45 | right: auto; 46 | } 47 | 48 | .mode { 49 | background-color: #0290d0; 50 | } 51 | 52 | .animation { 53 | transition: all 0.7s ease-in-out; 54 | } 55 | 56 | /* Settings typer */ 57 | .typ { 58 | width: 100%; 59 | } 60 | 61 | 62 | 63 | .shown { 64 | height: calc(100vh - 80px) !important; 65 | max-height: calc(100vh - 80px) !important; 66 | } 67 | 68 | #connect.shown { 69 | height: 100vh !important; 70 | max-height: 100vh !important; 71 | } 72 | 73 | .overflow { 74 | overflow: auto !important; 75 | } 76 | 77 | h2 { 78 | font-size: 20px; 79 | padding-bottom: 10px; 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build/release 2 | 3 | on: 4 | push: 5 | branches: 6 | - Working 7 | 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-12, ubuntu-latest, windows-latest] 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v1 19 | 20 | - name: Install Node.js, NPM and Yarn 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 18 24 | 25 | - name: Build/release Electron app 26 | uses: Yan-Jobs/action-electron-builder@v1.7.0 27 | with: 28 | # GitHub token, automatically provided to the action 29 | # (No need to define this secret in the repo settings) 30 | github_token: ${{ secrets.github_token }} 31 | 32 | # If the commit is tagged with a version (e.g. "v1.0.0"), 33 | # release the app after building 34 | release: ${{ startsWith(github.ref, 'refs/tags/v') }} 35 | 36 | - name: Upload a Build Artifact 37 | uses: actions/upload-artifact@v3.1.1 38 | with: 39 | name: build 40 | path: | 41 | out/win-unpacked 42 | out/linux-unpacked 43 | out/*.dmg 44 | out/*.AppImage 45 | out/*.exe 46 | -------------------------------------------------------------------------------- /src/weather/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: "./src/weather/src/index.js", 6 | devtool: "inline-source-map", 7 | target: "electron-renderer", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js$/, 12 | exclude: /node_modules/, 13 | use: { 14 | loader: "babel-loader", 15 | options: { 16 | presets: [ 17 | [ 18 | "@babel/preset-env", 19 | { 20 | targets: { 21 | esmodules: true, 22 | }, 23 | }, 24 | ], 25 | "@babel/preset-react", 26 | ], 27 | }, 28 | }, 29 | }, 30 | { 31 | test: [/\.s[ac]ss$/i, /\.css$/i], 32 | use: [ 33 | // Creates `style` nodes from JS strings 34 | "style-loader", 35 | // Translates CSS into CommonJS 36 | "css-loader", 37 | ], 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | extensions: [".js"], 43 | }, 44 | output: { 45 | filename: "app.js", 46 | path: path.resolve(__dirname, "build", "js"), 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Electron-Forge 89 | out/ -------------------------------------------------------------------------------- /src/main/tool_buttons.css: -------------------------------------------------------------------------------- 1 | .tools-wrapper { 2 | display: flex; 3 | width: 90%; 4 | justify-content: space-between; 5 | margin: 0 auto; 6 | align-items: center; 7 | } 8 | 9 | .tools { 10 | height: 79px; 11 | display: grid; 12 | place-items: center; 13 | background-color: #ffffff05; 14 | /* background: linear-gradient( 15 | 180deg, 16 | rgba(26, 26, 26, 1) 0%, 17 | rgba(26, 26, 26, 0.6) 20%, 18 | rgba(26, 26, 26, 0.3) 40%, 19 | rgba(26, 26, 26, 0) 60%, 20 | rgba(26, 26, 26, 0) 80%, 21 | rgba(26, 26, 26, 0) 100% 22 | ); */ 23 | border-top: 1px solid #9f9f9f; 24 | z-index: 5; 25 | } 26 | 27 | .connection-status p { 28 | color: #9f9f9f; 29 | } 30 | 31 | .dynamic-button { 32 | position: relative; 33 | width: fit-content; 34 | height: fit-content; 35 | aspect-ratio: 1/1; 36 | } 37 | 38 | #layout-button { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | } 43 | 44 | #reset-defaults { 45 | position: absolute; 46 | border-radius: 30px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | transition: all 0.4s ease-in-out; 51 | } 52 | 53 | #reset-icon, 54 | #settings-icon, 55 | #layout-icon, 56 | .dynamic-button { 57 | transition: all 0.5s ease; 58 | width: 30px; 59 | } 60 | 61 | .buttons { 62 | display: grid; 63 | place-items: center; 64 | background: transparent; 65 | border: none; 66 | z-index: 4; 67 | opacity: 1; 68 | } 69 | 70 | .hidden { 71 | z-index: 1; 72 | opacity: 0; 73 | } 74 | -------------------------------------------------------------------------------- /src/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "SFRegular"; 3 | src: url("Inter-Regular.ttf") format("truetype"); 4 | } 5 | @font-face { 6 | font-family: "SFMedium"; 7 | src: url("Inter-Medium.ttf") format("truetype"); 8 | } 9 | @font-face { 10 | font-family: "SFBold"; 11 | src: url("Inter-Bold.ttf") format("truetype"); 12 | } 13 | 14 | @font-face { 15 | font-family: "F1Bold"; 16 | src: url("TitilliumWeb-Bold.ttf") format("truetype"); 17 | } 18 | 19 | @font-face { 20 | font-family: "F1Italic-Bold"; 21 | src: url("TitilliumWeb-BoldItalic.ttf") format("truetype"); 22 | } 23 | 24 | @font-face { 25 | font-family: "F1Italic"; 26 | src: url("TitilliumWeb-Italic.ttf") format("truetype"); 27 | } 28 | 29 | @font-face { 30 | font-family: "F1Regular"; 31 | src: url("TitilliumWeb-Regular.ttf") format("truetype"); 32 | } 33 | 34 | @font-face { 35 | font-family: "F1SemiBold"; 36 | src: url("TitilliumWeb-SemiBold.ttf") format("truetype"); 37 | } 38 | 39 | @font-face { 40 | font-family: "F1Italic-SemiBold"; 41 | src: url("TitilliumWeb-SemiBoldItalic.ttf") format("truetype"); 42 | } 43 | 44 | @font-face { 45 | font-family: "Exo2"; 46 | src: url("Exo2-VariableFont_wght.ttf") format("truetype"); 47 | } 48 | 49 | /* Official FORMULA 1 fonts */ 50 | 51 | @font-face { 52 | font-family: "OF1Regular"; 53 | src: url("Formula1-Regular_web_0.ttf"); 54 | } 55 | 56 | @font-face { 57 | font-family: "OF1Bold"; 58 | src: url("Formula1-Bold_web_0_modified.ttf"); 59 | } 60 | 61 | @font-face { 62 | font-family: "F1Wide"; 63 | src: url("Formula1-Wide.ttf") format("truetype"); 64 | } 65 | -------------------------------------------------------------------------------- /src/functions/car.js: -------------------------------------------------------------------------------- 1 | function getCarData(driverNumber, carData) { 2 | try { 3 | carData[0].Cars[driverNumber].Channels; 4 | } catch (error) { 5 | return "error"; 6 | } 7 | return carData[0].Cars[driverNumber].Channels; 8 | } 9 | 10 | function getSpeedThreshold(sessionType, sessionStatus, trackStatus) { 11 | if ( 12 | sessionType === "Qualifying" || 13 | sessionType === "Practice" || 14 | trackStatus.Status === "4" || 15 | trackStatus.Status === "6" || 16 | trackStatus.Status === "7" 17 | ) 18 | return 10; 19 | if (sessionStatus === "Inactive" || sessionStatus === "Aborted") return 0; 20 | return 30; 21 | } 22 | 23 | function weirdCarBehaviour(racingNumber, timingData, carData, sessionType, sessionStatus, trackStatus) { 24 | const driverCarData = getCarData(racingNumber, carData); 25 | 26 | if (driverCarData === "error") return false; 27 | 28 | const driverTimingData = timingData[racingNumber]; 29 | 30 | const rpm = driverCarData[0]; 31 | 32 | const speed = driverCarData[2]; 33 | 34 | const gear = driverCarData[3]; 35 | 36 | const speedLimit = getSpeedThreshold(sessionType, sessionStatus, trackStatus); 37 | 38 | return ( 39 | rpm === 0 || 40 | speed <= speedLimit || 41 | gear > 8 || 42 | gear === 43 | (sessionStatus === "Inactive" || 44 | sessionStatus === "Aborted" || 45 | (sessionType !== "Race" && driverTimingData.PitOut) 46 | ? "" 47 | : 0) 48 | ); 49 | } 50 | 51 | module.exports = { 52 | getCarData, 53 | getSpeedThreshold, 54 | weirdCarBehaviour, 55 | }; 56 | -------------------------------------------------------------------------------- /src/main/popup.css: -------------------------------------------------------------------------------- 1 | #popup { 2 | height: 150px; 3 | width: 70%; 4 | background-color: rgba(0, 0, 0, 0.457); 5 | backdrop-filter: blur(10px); 6 | border-radius: 30px; 7 | position: fixed; 8 | z-index: 5; 9 | top: 100%; 10 | left: 50%; 11 | transform: translate(-50%); 12 | display: flex; 13 | flex-direction: column; 14 | transition: all 0.7s ease; 15 | border: 2px solid #9f9f9f; 16 | justify-content: space-evenly; 17 | align-items: center; 18 | } 19 | 20 | #popup form { 21 | height: 35%; 22 | width: 100%; 23 | } 24 | 25 | #popup input { 26 | border: 2px solid #9f9f9f; 27 | background-color: transparent; 28 | padding: 0 20px; 29 | border-radius: 20px; 30 | width: 77%; 31 | height: 100%; 32 | font-size: 17px; 33 | } 34 | 35 | #popup .buttons { 36 | width: 85%; 37 | display: flex; 38 | justify-content: space-evenly; 39 | gap: 7%; 40 | bottom: 20px; 41 | } 42 | 43 | #popup .buttons button { 44 | height: 30px; 45 | border: none; 46 | border-radius: 20px; 47 | background-color: transparent; 48 | color: white; 49 | font-size: 15px; 50 | width: 30%; 51 | transition: all 0.5s ease; 52 | } 53 | 54 | #popup .buttons button:hover { 55 | transform: translate(2px, 2px); 56 | } 57 | 58 | #popup .buttons button:nth-child(1) { 59 | border: 2px solid rgb(90, 90, 90); 60 | color: rgb(90, 90, 90); 61 | } 62 | 63 | #popup .buttons button:nth-child(2) { 64 | border: 2px solid rgb(255, 111, 111); 65 | color: rgb(255, 111, 111); 66 | } 67 | 68 | #popup .buttons button:nth-child(3) { 69 | border: 2px solid rgb(111, 255, 118); 70 | color: rgb(111, 255, 118); 71 | } 72 | 73 | #popup.show { 74 | transform: translate(-50%, -50%) !important; 75 | top: 50%; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/windows_section.css: -------------------------------------------------------------------------------- 1 | .new-window-section { 2 | display: flex; 3 | width: 60%; 4 | justify-content: space-evenly; 5 | color: gray !important; 6 | font-size: 12px; 7 | align-items: center; 8 | white-space: nowrap; 9 | } 10 | 11 | .line { 12 | background-color: gray; 13 | height: 2px; 14 | width: 100%; 15 | margin: 10px; 16 | } 17 | 18 | .windows { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | /* max-height: 100vh; */ 23 | margin-bottom: 7.5px; 24 | } 25 | 26 | .window { 27 | background-color: transparent; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | border-radius: 10px; 32 | transition: all 0.5s ease; 33 | font-size: 17px; 34 | width: 60%; 35 | min-height: 40px; 36 | height: 40px; 37 | margin: 7.5px; 38 | border: 2px solid transparent; 39 | } 40 | 41 | .link.gray, 42 | .window.gray { 43 | border-color: #9f9f9f; 44 | color: white; 45 | } 46 | 47 | .link.default, 48 | .window.default { 49 | border-color: rgb(255, 50, 50); 50 | color: rgb(255, 50, 50); 51 | } 52 | 53 | .link.beta, 54 | .window.beta { 55 | border-color: #c76cff; 56 | color: #c76cff; 57 | } 58 | 59 | .window:hover { 60 | transform: translate(3px, 3px); 61 | } 62 | 63 | #solid-color-windows { 64 | display: flex; 65 | justify-content: space-evenly; 66 | } 67 | 68 | #solid-color-windows:hover { 69 | transform: none; 70 | } 71 | 72 | .solid-color-window { 73 | width: 100%; 74 | height: 100%; 75 | border-radius: 10px; 76 | box-shadow: 3px 3px #000000; 77 | transition: all 0.5s ease; 78 | border: 2px solid #9f9f9f; 79 | margin: 7.5px; 80 | } 81 | 82 | .solid-color-window:hover { 83 | transform: translate(3px, 3px); 84 | box-shadow: 0 0 #000000; 85 | } 86 | -------------------------------------------------------------------------------- /src/autoswitch/style.css: -------------------------------------------------------------------------------- 1 | /* Base styling */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | color: white; 6 | } 7 | 8 | body { 9 | height: 100vh; 10 | } 11 | 12 | .drag { 13 | -webkit-app-region: drag; 14 | } 15 | 16 | #container { 17 | background-color: #0a0a0faf; 18 | border-radius: 20px; 19 | height: 100vh; 20 | overflow: hidden; 21 | } 22 | 23 | .wrapper { 24 | padding: 10px; 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | justify-content: space-evenly; 29 | height: calc(90vh); 30 | overflow: hidden; 31 | } 32 | 33 | .transparent { 34 | background-color: transparent; 35 | } 36 | 37 | button { 38 | -webkit-app-region: no-drag; 39 | } 40 | 41 | h1 { 42 | text-align: center; 43 | font-family: "SFBold", Arial, Helvetica, sans-serif; 44 | } 45 | 46 | h2 { 47 | font-family: "SFRegular", Arial, Helvetica, sans-serif; 48 | } 49 | 50 | .status { 51 | border: 2px solid rgb(0, 175, 0); 52 | color: rgb(0, 175, 0); 53 | background-color: transparent; 54 | width: fit-content; 55 | padding: 5px; 56 | border-radius: 10px; 57 | } 58 | 59 | p { 60 | font-family: "SFRegular", Arial, Helvetica, sans-serif; 61 | width: 50px !important; 62 | font-size: 30px; 63 | text-align: center; 64 | } 65 | 66 | .disclamer { 67 | font-size: 12px; 68 | width: 90% !important; 69 | position: absolute; 70 | bottom: 10px; 71 | color: gray; 72 | } 73 | 74 | button { 75 | background-color: transparent; 76 | padding: 10px 20px; 77 | font-family: "SFRegular", Arial, Helvetica, sans-serif; 78 | border-radius: 10px; 79 | outline: none; 80 | width: 130px; 81 | margin: 10px; 82 | } 83 | 84 | .hide { 85 | border: 2px solid red; 86 | color: red; 87 | } 88 | 89 | .small { 90 | border: 2px solid rgb(0, 102, 255); 91 | color: rgb(0, 102, 255); 92 | } 93 | 94 | .hidden { 95 | display: none; 96 | } 97 | -------------------------------------------------------------------------------- /src/functions/colors.js: -------------------------------------------------------------------------------- 1 | // Set all statusses or names to the corect hex code 2 | function getColorFromStatusCodeOrName(codeOrName) { 3 | switch (codeOrName) { 4 | case 2048: 5 | return "#fdd835"; 6 | case 2049: 7 | return "#4caf50"; 8 | case 2051: 9 | return "#9c27b0"; 10 | case 2052: 11 | return "#f44336"; 12 | case 2064: 13 | return "#2196f3"; 14 | case 2068: 15 | return "#f44336"; 16 | 17 | case "red": 18 | return "#f44336"; 19 | case "yellow": 20 | return "#fdd835"; 21 | case "green": 22 | return "#4caf50"; 23 | case "purple": 24 | return "#9c27b0"; 25 | case "white": 26 | return "#ffffff"; 27 | 28 | case "S": 29 | return "#ff0000"; 30 | case "M": 31 | return "#ffde00"; 32 | case "H": 33 | return "#dbdada"; 34 | case "I": 35 | return "#2c7515"; 36 | case "W": 37 | return "#3d7ba3"; 38 | 39 | case "ob": 40 | return "#9c27b0"; 41 | case "pb": 42 | return "#4caf50"; 43 | case "ni": 44 | return "#fdd835"; 45 | 46 | default: 47 | return "#5b5b5d"; 48 | } 49 | } 50 | 51 | // Convert the RBG values to hex 52 | function rgbToHex(rgb) { 53 | // Extract the red, green, and blue components from the RGB value 54 | const [r, g, b] = rgb.match(/\d+/g).map((x) => parseInt(x, 10)); 55 | 56 | // Convert the red, green, and blue values to hexadecimal 57 | const hexR = r.toString(16).padStart(2, "0"); 58 | const hexG = g.toString(16).padStart(2, "0"); 59 | const hexB = b.toString(16).padStart(2, "0"); 60 | 61 | // Return the hexadecimal color code 62 | return `#${hexR}${hexG}${hexB}`; 63 | } 64 | 65 | module.exports = { 66 | getColorFromStatusCodeOrName, 67 | rgbToHex, 68 | }; 69 | -------------------------------------------------------------------------------- /src/autoswitch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Auto Onboard Camera Switching 8 | 9 | 10 | 12 | 13 | 14 |
15 |
16 |

Auto Onboard Switcher

17 |

Enabled

18 |

Currently connected onboards:

19 |

20 |
21 | 22 | 23 |
24 |

25 | Important: This window may never be minimized and should always be on top in order for it to 26 | function properly hiding it will not disable the script. You can press 'Hide' to make the window 27 | fully transparent. Press 'Escape' to exit the 'hidden' state. 28 |

29 |
30 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/compass/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | async function getConfigurations() { 8 | const configFile = (await ipcRenderer.invoke("get_store")).config; 9 | host = configFile.network.host; 10 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 11 | if (debug) { 12 | console.log(host); 13 | console.log(port); 14 | } 15 | } 16 | 17 | // Request the session info 18 | async function apiRequests() { 19 | const config = { 20 | host: host, 21 | port: port, 22 | }; 23 | const liveTimingState = await f1mvApi.LiveTimingAPIGraphQL(config, "SessionInfo"); 24 | sessionInfo = liveTimingState.SessionInfo; 25 | } 26 | 27 | async function rotate() { 28 | // Get the circuit key 29 | let circuitKey = sessionInfo.Meeting.Circuit.Key; 30 | 31 | // Get the current season 32 | let season = sessionInfo.StartDate.slice(0, 4); 33 | 34 | // Get the rotation of the track map according to the circuit key and current season 35 | let trackRotation = (await f1mvApi.getCircuitInfo(circuitKey, season)).rotation; 36 | 37 | // Set the track rotation to the compass image 38 | document.getElementById("compass").style.rotate = trackRotation + "deg"; 39 | } 40 | 41 | // Check if 'escape' is being pressed to trigger 'toggleBackground' 42 | document.addEventListener("keydown", (event) => { 43 | if (event.key == "Escape") { 44 | toggleBackground(); 45 | } 46 | }); 47 | 48 | // Toggle the background from gray to transparent by giving or removing the class transparent and checking if transparent is set to 'true' 49 | let transparent = false; 50 | function toggleBackground() { 51 | if (transparent) { 52 | document.getElementById("background").className = ""; 53 | transparent = false; 54 | } else { 55 | document.getElementById("background").className = "transparent"; 56 | transparent = true; 57 | } 58 | } 59 | 60 | async function run() { 61 | await getConfigurations(); 62 | await apiRequests(); 63 | await rotate(); 64 | } 65 | 66 | run(); 67 | -------------------------------------------------------------------------------- /src/statuses/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | transition: 0.5s all ease; 5 | font-family: "SFBold"; 6 | color: white; 7 | } 8 | 9 | body { 10 | -webkit-app-region: drag; 11 | height: 100vh; 12 | width: 100vw; 13 | } 14 | 15 | .container { 16 | background-color: #0a0a0faf; 17 | padding: 3vw; 18 | height: calc(100vh - 6vw); 19 | border-radius: 10vw; 20 | } 21 | 22 | .wrapper { 23 | width: 100%; 24 | height: 100%; 25 | display: flex; 26 | align-items: center; 27 | justify-content: space-evenly; 28 | flex-direction: column; 29 | } 30 | 31 | .status { 32 | display: grid; 33 | grid-template-columns: 4fr 5fr; 34 | place-items: start; 35 | width: 90%; 36 | } 37 | 38 | h1 { 39 | padding: 5vw; 40 | color: gray; 41 | font-size: 10vw; 42 | text-align: center; 43 | border-radius: 7vw; 44 | margin-bottom: 4vw; 45 | } 46 | 47 | .header { 48 | width: 100%; 49 | } 50 | 51 | h2 { 52 | text-align: center; 53 | font-size: 6vw; 54 | align-self: center; 55 | } 56 | 57 | p { 58 | white-space: nowrap; 59 | width: fit-content; 60 | font-size: 5vw; 61 | padding: 2vw; 62 | border-radius: 3vw; 63 | width: 90%; 64 | text-align: center; 65 | } 66 | 67 | .hidden { 68 | display: none !important; 69 | } 70 | 71 | /* Colors and backgrounds */ 72 | .green { 73 | background-color: rgb(0, 175, 0) !important; 74 | color: white; 75 | } 76 | 77 | .yellow { 78 | background-color: rgb(255, 230, 0) !important; 79 | color: black; 80 | } 81 | 82 | .orange { 83 | background-color: rgb(255, 123, 0) !important; 84 | color: white; 85 | } 86 | 87 | .red { 88 | background-color: rgb(209, 0, 0) !important; 89 | color: white; 90 | } 91 | 92 | .white { 93 | background-color: white; 94 | color: black; 95 | } 96 | 97 | .gray { 98 | background-color: gray; 99 | color: black; 100 | } 101 | 102 | .black-background { 103 | background-color: black !important; 104 | } 105 | 106 | .black-text { 107 | color: black !important; 108 | } 109 | 110 | .slippery { 111 | background-image: url(./slippery.png); 112 | background-size: cover; 113 | color: black; 114 | } 115 | -------------------------------------------------------------------------------- /src/tracktime/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | // Set sleep 8 | const sleep = (milliseconds) => { 9 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 10 | }; 11 | 12 | function parseTime(time) { 13 | console.log(time); 14 | const [seconds, minutes, hours] = time 15 | .split(":") 16 | .reverse() 17 | .map((number) => parseInt(number)); 18 | 19 | if (hours === undefined) return (minutes * 60 + seconds) * 1000; 20 | 21 | return (hours * 3600 + minutes * 60 + seconds) * 1000; 22 | } 23 | 24 | function getTime(ms) { 25 | const date = new Date(ms); 26 | 27 | console.log(date); 28 | 29 | const hours = date.getUTCHours().toString().padStart(2, "0"); 30 | const minutes = date.getUTCMinutes().toString().padStart(2, "0"); 31 | const seconds = date.getUTCSeconds().toString().padStart(2, "0"); 32 | 33 | if (parseInt(hours) === 0) { 34 | return `${minutes}:${seconds}`; 35 | } 36 | 37 | return `${hours}:${minutes}:${seconds}`; 38 | } 39 | 40 | async function getConfigurations() { 41 | const config = (await ipcRenderer.invoke("get_store")).config.network; 42 | host = config.host; 43 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 44 | if (debug) { 45 | console.log(host); 46 | console.log(port); 47 | } 48 | } 49 | 50 | async function setTime() { 51 | const config = { 52 | host: host, 53 | port: port, 54 | }; 55 | 56 | const api = await f1mvApi.LiveTimingAPIGraphQL(config, "SessionInfo"); 57 | 58 | const utcOffset = parseTime(api.SessionInfo.GmtOffset); 59 | 60 | const clockData = await f1mvApi.LiveTimingClockAPIGraphQL(config, ["paused", "systemTime", "trackTime"]); 61 | 62 | const now = new Date(); 63 | const systemTime = clockData.systemTime; 64 | const trackTime = clockData.trackTime; 65 | const paused = clockData.paused; 66 | 67 | const localTime = parseInt(paused ? trackTime : now - (systemTime - trackTime)) + utcOffset; 68 | 69 | const displayTime = getTime(localTime); 70 | 71 | document.getElementById("time").textContent = displayTime; 72 | } 73 | 74 | async function run() { 75 | await getConfigurations(); 76 | 77 | await setTime(); 78 | setInterval(async () => { 79 | await setTime(); 80 | }, 500); 81 | } 82 | 83 | run(); 84 | -------------------------------------------------------------------------------- /src/main/layout_section.css: -------------------------------------------------------------------------------- 1 | #layouts-container { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .layout { 9 | background-color: transparent; 10 | display: grid; 11 | grid-template-columns: 20fr 6fr 7fr; 12 | justify-content: center; 13 | align-items: center; 14 | transition: all 0.5s ease; 15 | font-size: 17px; 16 | width: 90%; 17 | min-height: 40px; 18 | /* height: 40px; */ 19 | margin: 10px; 20 | gap: 10px; 21 | border: 2px solid transparent; 22 | border-radius: 10px; 23 | } 24 | 25 | .layout.gray { 26 | color: black; 27 | width: 30%; 28 | height: 40px; 29 | display: inherit; 30 | background-color: #9f9f9f; 31 | font-size: 14px; 32 | } 33 | 34 | .layout .part { 35 | border: 2px solid #9f9f9f; 36 | height: 100%; 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | padding: 2px 15px; 41 | border-radius: 10px; 42 | text-align: left; 43 | transition: all 0.5s ease; 44 | user-select: none; 45 | } 46 | 47 | .layout .part:hover { 48 | transform: translate(3px, 3px); 49 | } 50 | 51 | .layout .part.name p { 52 | gap: 10px; 53 | } 54 | 55 | .layout .part.save p, 56 | .layout .part.load p { 57 | color: #9f9f9f; 58 | } 59 | 60 | .layout .part.name p, 61 | .layout .part.save p { 62 | display: flex; 63 | align-items: center; 64 | justify-content: space-between; 65 | width: 100%; 66 | } 67 | 68 | .layout .icon-container { 69 | display: grid; 70 | place-items: center; 71 | } 72 | 73 | .layout .icon-container img { 74 | height: 20px; 75 | } 76 | 77 | .layouts { 78 | position: fixed; 79 | height: 0vh; 80 | width: 100vw; 81 | background-color: rgb(15, 15, 15); 82 | overflow: scroll; 83 | transition: all 0.7s ease; 84 | display: flex; 85 | align-items: center; 86 | flex-direction: column; 87 | z-index: 1; 88 | } 89 | 90 | #content-id-field { 91 | padding: 10px; 92 | border-radius: 10px; 93 | border: none; 94 | position: fixed; 95 | bottom: 100px; 96 | text-align: center; 97 | display: none; 98 | } 99 | 100 | .new { 101 | margin-bottom: 80px; 102 | } 103 | 104 | .new:hover { 105 | transform: translate(3px, 3px); 106 | } 107 | 108 | #layout.shown #content-id-field { 109 | display: block; 110 | } 111 | -------------------------------------------------------------------------------- /src/trackinfo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Track Information 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |

QUALIFYING

16 |
17 | 18 |

Session

19 |

ONSCHEDULE

20 |

12:34:56

21 |
22 | 23 |

Timers

24 |

12:34:56

25 |

12:34:56

26 |
27 | 28 |

Progress

29 |

43% - 56%

30 |

56/93

31 |

Q1/Q3

32 |
33 | 34 |

Grip

35 |

NORMAL

36 |
37 | 38 |

Padding

39 |

PINK

40 |
41 | 42 |

DRS

43 |

ENABLED

44 |
45 | 46 |

Man. Tires

47 |

NONE

48 |
49 | 50 |

Pitlane

51 |

ENTRY

52 |

EXIT

53 |
54 |
55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/connection_section.css: -------------------------------------------------------------------------------- 1 | /* Connect menu */ 2 | 3 | .connections { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | #connect { 11 | height: 0vh; 12 | width: 100vw; 13 | backdrop-filter: blur(1.5rem); 14 | background-color: #000000b4; 15 | position: fixed; 16 | display: flex; 17 | align-items: center; 18 | flex-direction: column; 19 | overflow: hidden; 20 | z-index: 5; 21 | justify-content: center; 22 | } 23 | 24 | #connect h1 { 25 | position: absolute; 26 | top: 0; 27 | } 28 | 29 | #connect .wrapper { 30 | height: 70%; 31 | width: 100%; 32 | display: flex; 33 | flex-direction: column; 34 | align-items: center; 35 | } 36 | 37 | #connect .container { 38 | width: 100%; 39 | display: flex; 40 | flex-direction: column; 41 | gap: 20px; 42 | } 43 | 44 | #connect .text { 45 | margin-top: 30px; 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | border: 2px solid black; 50 | background-color: #3030306b; 51 | padding: 10px !important; 52 | width: 400px; 53 | border-radius: 40px; 54 | } 55 | 56 | .connected { 57 | color: rgb(94, 255, 94); 58 | } 59 | 60 | .disconnected { 61 | color: rgb(255, 62, 62); 62 | } 63 | 64 | #connect .link { 65 | background-color: transparent; 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | border-radius: 10px; 70 | transition: all 0.5s ease; 71 | font-size: 17px; 72 | width: 60%; 73 | min-height: 40px; 74 | height: 50px; 75 | margin: 7.5px; 76 | color: white; 77 | text-align: center; 78 | gap: 10px; 79 | border: 2px solid; 80 | } 81 | 82 | #connect .link:hover { 83 | transform: translate(3px, 3px); 84 | } 85 | 86 | .link.disabled { 87 | color: rgba(255, 255, 255, 0.26) !important; 88 | border-color: #9f9f9f38 !important; 89 | } 90 | 91 | #connect .link.connected { 92 | border-color: rgb(94, 255, 94); 93 | } 94 | 95 | #connect .link.connected span { 96 | color: rgb(94, 255, 94); 97 | } 98 | 99 | #connect .link.disconnected { 100 | border-color: rgb(255, 62, 62); 101 | } 102 | 103 | #connect .link.disconnected span { 104 | color: rgb(255, 62, 62); 105 | } 106 | 107 | .continue { 108 | all: unset; 109 | color: #9f9f9f; 110 | background-color: none; 111 | margin: 15px; 112 | font-size: 15px; 113 | position: absolute; 114 | bottom: 30px; 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uf1", 3 | "productName": "Ultimate-F1Viewer-With-F1MV", 4 | "version": "1.4.8", 5 | "description": "A integration with multiple windows to enchance your F1TV experience using the MultiViewer for F1 app.", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "start": "electron .", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "lint": "echo \"No linting configured\"", 13 | "watch_weather": "webpack --config src/weather/webpack.common.js --watch", 14 | "pack": "electron-builder --dir", 15 | "dist": "electron-builder", 16 | "postinstall": "electron-builder install-app-deps" 17 | }, 18 | "keywords": [], 19 | "author": "MR. AJEKO", 20 | "license": "MIT", 21 | "config": { 22 | "forge": { 23 | "packagerConfig": {}, 24 | "makers": [ 25 | { 26 | "name": "@electron-forge/maker-squirrel", 27 | "config": { 28 | "name": "UltimateF1Viewer" 29 | } 30 | }, 31 | { 32 | "name": "@electron-forge/maker-zip", 33 | "platforms": [ 34 | "darwin" 35 | ] 36 | }, 37 | { 38 | "name": "@electron-forge/maker-deb", 39 | "config": {} 40 | }, 41 | { 42 | "name": "@electron-forge/maker-rpm", 43 | "config": {} 44 | } 45 | ] 46 | } 47 | }, 48 | "dependencies": { 49 | "@nivo/bullet": "^0.80.0", 50 | "@nivo/line": "^0.80.0", 51 | "adm-zip": "^0.5.9", 52 | "discord-rpc": "^4.0.1", 53 | "electron-fetch": "^1.9.1", 54 | "electron-reload": "^2.0.0-alpha.1", 55 | "electron-squirrel-startup": "^1.0.0", 56 | "electron-store": "^8.1.0", 57 | "fs-extra": "^8.1.0", 58 | "govee-lan-control": "^2.1.0", 59 | "npm_f1mv_api": "^1.4.5", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0", 62 | "request": "^2.88.0", 63 | "requirejs": "^2.3.6" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.20.12", 67 | "@babel/preset-env": "^7.20.2", 68 | "@babel/preset-react": "^7.18.6", 69 | "@electron-forge/cli": "^6.0.0-beta.66", 70 | "@electron-forge/maker-deb": "^6.0.0-beta.66", 71 | "@electron-forge/maker-rpm": "^6.0.0-beta.66", 72 | "@electron-forge/maker-squirrel": "^6.0.0-beta.66", 73 | "@electron-forge/maker-zip": "^6.0.0-beta.66", 74 | "babel-loader": "^9.1.2", 75 | "css-loader": "^6.7.3", 76 | "electron": "^22.1.0", 77 | "electron-builder": "^24.0.0-alpha.9", 78 | "electron-packager": "^16.0.0", 79 | "electron-prebuilt-compile": "^1.3.2", 80 | "style-loader": "^3.3.1", 81 | "webpack": "^5.75.0", 82 | "webpack-cli": "^5.0.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/trackinfo/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | transition: 0.5s all ease; 5 | font-family: "SFBold"; 6 | color: white; 7 | } 8 | 9 | body { 10 | -webkit-app-region: drag; 11 | height: 100vh; 12 | width: 100vw; 13 | } 14 | 15 | .container { 16 | background-color: #0a0a0faf; 17 | } 18 | 19 | .containerw { 20 | padding: 3vw; 21 | height: calc(100vh - 6vw); 22 | border-radius: 10vw; 23 | } 24 | 25 | .containerh { 26 | padding: 3vh; 27 | height: calc(100vh - 6vh); 28 | width: calc(100vw - 6vh); 29 | border-radius: 20vh; 30 | } 31 | 32 | .wrapper { 33 | width: 100%; 34 | height: 100%; 35 | display: flex; 36 | /* align-items: center; */ 37 | justify-content: space-evenly; 38 | } 39 | 40 | .wrapperh { 41 | flex-direction: row; 42 | } 43 | 44 | .wrapperw { 45 | flex-direction: column; 46 | } 47 | 48 | span { 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | } 53 | 54 | h1 { 55 | color: gray; 56 | font-size: 15vw; 57 | } 58 | 59 | h2 { 60 | text-align: center; 61 | } 62 | 63 | .h2w { 64 | font-size: 8vw; 65 | } 66 | 67 | .h2h { 68 | font-size: 20vh; 69 | } 70 | 71 | p { 72 | display: grid; 73 | place-items: center; 74 | } 75 | 76 | .pw { 77 | font-size: 6vw; 78 | padding: 2vw; 79 | border-radius: 5vw; 80 | margin-top: 2vw; 81 | width: 50%; 82 | min-width: fit-content; 83 | } 84 | 85 | .ph { 86 | width: 100%; 87 | font-size: 15vh; 88 | padding: 5vh; 89 | border-radius: 15vh; 90 | margin-top: 5vh; 91 | } 92 | 93 | .hidden { 94 | display: none !important; 95 | } 96 | 97 | /* Colors and backgrounds */ 98 | .green { 99 | background-color: rgb(0, 175, 0) !important; 100 | color: white; 101 | } 102 | 103 | .yellow { 104 | background-color: rgb(255, 230, 0) !important; 105 | color: black; 106 | } 107 | 108 | .orange { 109 | background-color: rgb(255, 123, 0) !important; 110 | color: white; 111 | } 112 | 113 | .red { 114 | background-color: rgb(209, 0, 0) !important; 115 | } 116 | 117 | .pink { 118 | background-color: pink !important; 119 | color: black; 120 | } 121 | 122 | .blue { 123 | background-color: rgb(0, 0, 199) !important; 124 | color: white; 125 | } 126 | 127 | .light-blue { 128 | background-color: rgb(53, 201, 251); 129 | color: black; 130 | } 131 | 132 | .white { 133 | background-color: white; 134 | color: black; 135 | } 136 | 137 | .gray { 138 | background-color: gray; 139 | color: black; 140 | } 141 | 142 | .black-background { 143 | background-color: black !important; 144 | } 145 | 146 | .black-text { 147 | color: black !important; 148 | } 149 | -------------------------------------------------------------------------------- /src/tirestats/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "SFBold"; 5 | color: white; 6 | overflow: hidden; 7 | } 8 | 9 | body { 10 | height: 100vh; 11 | width: 100vw; 12 | } 13 | 14 | .drag { 15 | -webkit-app-region: drag; 16 | } 17 | 18 | .container { 19 | background-color: #0a0a0faf; 20 | height: 100vh; 21 | width: 100vw; 22 | display: flex; 23 | border-radius: 6vw; 24 | } 25 | 26 | .wrapper { 27 | padding: 4vw; 28 | text-align: center; 29 | width: 100%; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .header h1 { 35 | font-size: 4vw; 36 | } 37 | 38 | .info { 39 | display: grid; 40 | grid-template-columns: 1fr 1fr 1fr; 41 | column-gap: 4vw; 42 | height: 100%; 43 | margin-top: 3vw; 44 | } 45 | 46 | .tire { 47 | height: 100%; 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: space-between; 51 | } 52 | 53 | .tire-image img { 54 | width: 70%; 55 | } 56 | 57 | .tire-image p { 58 | font-size: 3.5vw; 59 | font-family: "OF1Bold"; 60 | transform: skew(-10deg); 61 | text-shadow: 0.25vw 0.25vw 0.25vw black; 62 | margin-top: 1.5vw; 63 | } 64 | 65 | .title { 66 | font-size: 3vw; 67 | font-family: "OF1Bold"; 68 | color: red; 69 | transform: skew(-10deg); 70 | text-shadow: 0.25vw 0.25vw 0.25vw black; 71 | } 72 | 73 | .row { 74 | display: flex; 75 | justify-content: space-between; 76 | align-items: center; 77 | margin: 0.8vw 0; 78 | background-color: rgba(255, 255, 255, 0.048); 79 | border-radius: 1vw; 80 | padding: 0.5vw 1vw; 81 | } 82 | 83 | .row p { 84 | opacity: 0.8; 85 | } 86 | 87 | .split { 88 | width: 0.2vw; 89 | margin: auto; 90 | background-color: white; 91 | height: 80%; 92 | align-items: center; 93 | } 94 | 95 | .driver { 96 | display: flex; 97 | align-items: center; 98 | gap: 1vw; 99 | } 100 | 101 | .driver p { 102 | font-size: 2.2vw; 103 | font-family: "OF1Bold"; 104 | } 105 | 106 | .time p { 107 | font-size: 2vw; 108 | } 109 | 110 | .delta-time { 111 | padding: 1vw; 112 | } 113 | 114 | .delta-time p { 115 | font-size: 3vw; 116 | padding: 0.5vw 2vw; 117 | border-radius: 3vw; 118 | display: inline; 119 | } 120 | 121 | .stats .row p { 122 | font-size: 2.5vw; 123 | } 124 | 125 | .unknown { 126 | background-color: rgba(0, 0, 0, 0.3); 127 | } 128 | 129 | .soft { 130 | color: red; 131 | } 132 | 133 | .medium { 134 | color: yellow; 135 | } 136 | 137 | .hard { 138 | color: white; 139 | } 140 | 141 | .intermediate { 142 | color: green; 143 | } 144 | 145 | .wet { 146 | color: blue; 147 | } 148 | 149 | .test { 150 | color: rgb(192, 192, 192); 151 | } 152 | 153 | .fastest { 154 | background-color: rgba(128, 0, 128, 0.3); 155 | } 156 | 157 | .good { 158 | background-color: rgba(0, 128, 0, 0.3); 159 | } 160 | 161 | .bad { 162 | background-color: rgba(255, 102, 0, 0.3); 163 | } 164 | -------------------------------------------------------------------------------- /src/main/settings_section.css: -------------------------------------------------------------------------------- 1 | /* Settings menu */ 2 | #menu { 3 | overflow-y: scroll; 4 | padding: 0 8.5px; 5 | position: fixed; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | height: 0; 10 | transition: height 0.7s ease; 11 | width: 100vw; 12 | backdrop-filter: blur(1rem); 13 | background-color: #0f0f0f50; 14 | z-index: 2; 15 | } 16 | 17 | #menu * { 18 | color: white; 19 | } 20 | 21 | #menu p { 22 | font-family: "SFMedium", Arial, Helvetica, sans-serif; 23 | } 24 | 25 | .setting { 26 | display: flex; 27 | justify-content: space-between; 28 | /* height: 30px; */ 29 | padding: 15px; 30 | } 31 | 32 | .setting p { 33 | text-align: left; 34 | } 35 | 36 | .setting_info .info { 37 | font-size: 12px; 38 | color: gray !important; 39 | } 40 | 41 | .settings-section { 42 | padding: 20px 0; 43 | margin-top: 30px; 44 | background-color: #1f1f1fb9; 45 | border: 2px solid #9f9f9f; 46 | border-radius: 20px; 47 | max-width: 1000px; 48 | width: 90%; 49 | } 50 | 51 | .description { 52 | font-size: 20px; 53 | } 54 | 55 | #thanks { 56 | margin-bottom: 50px; 57 | } 58 | 59 | #credits * { 60 | text-decoration: none; 61 | } 62 | 63 | #credits img { 64 | padding-left: 5px; 65 | } 66 | 67 | .option { 68 | position: relative; 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | width: 100px; 73 | } 74 | 75 | .option option { 76 | background-color: black; 77 | border: 1px solid gray; 78 | } 79 | 80 | .selector { 81 | outline: none; 82 | border: 1px solid gray; 83 | background: transparent; 84 | padding: 5px; 85 | } 86 | 87 | /* Setting toggle switch */ 88 | .switch { 89 | position: relative; 90 | left: 0; 91 | display: inline-block; 92 | width: 40px; 93 | height: 24px; 94 | } 95 | 96 | .switch input { 97 | opacity: 0; 98 | width: 0; 99 | height: 0; 100 | } 101 | 102 | .slider { 103 | position: absolute; 104 | cursor: pointer; 105 | top: 0; 106 | left: 0; 107 | right: 0; 108 | bottom: 0; 109 | background-color: #ccc; 110 | -webkit-transition: 0.4s; 111 | transition: 0.4s; 112 | } 113 | 114 | .slider:before { 115 | position: absolute; 116 | content: ""; 117 | height: 16px; 118 | width: 16px; 119 | left: 4px; 120 | bottom: 4px; 121 | background-color: white; 122 | -webkit-transition: 0.4s; 123 | transition: 0.4s; 124 | } 125 | 126 | input:checked + .slider { 127 | background-color: #2196f3; 128 | } 129 | 130 | input:focus + .slider { 131 | box-shadow: 0 0 1px #2196f3; 132 | } 133 | 134 | input:checked + .slider:before { 135 | -webkit-transform: translateX(16px); 136 | -ms-transform: translateX(16px); 137 | transform: translateX(16px); 138 | } 139 | 140 | /* Rounded sliders */ 141 | .slider.round { 142 | border-radius: 34px; 143 | } 144 | 145 | .slider.round:before { 146 | border-radius: 50%; 147 | } 148 | -------------------------------------------------------------------------------- /src/weather/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "SFBold", Arial, Helvetica, sans-serif; 5 | } 6 | 7 | body { 8 | overflow: hidden; 9 | } 10 | 11 | .drag { 12 | -webkit-app-region: drag; 13 | } 14 | 15 | .container { 16 | display: grid; 17 | grid-template-columns: 1fr 10fr 5fr; 18 | grid-template-rows: 100vh; 19 | background-color: #0a0a0faf; 20 | padding: 0 10px; 21 | border-radius: 40px; 22 | } 23 | 24 | .graphs { 25 | display: grid; 26 | grid-template-rows: calc(45vh) calc(45vh) 10vh; 27 | margin-top: 2.5vw; 28 | margin-right: 5vw; 29 | } 30 | 31 | .graph { 32 | height: 100% !important; 33 | } 34 | 35 | .buttons { 36 | display: flex; 37 | align-items: center; 38 | flex-direction: column; 39 | justify-content: space-evenly; 40 | } 41 | 42 | .buttons button { 43 | width: 50px; 44 | padding: 8px 8px; 45 | border-radius: 10px; 46 | background-color: transparent; 47 | transition: all 0.3s ease; 48 | outline: none; 49 | } 50 | 51 | .big-decrease { 52 | border: 2px solid rgba(196, 0, 0, 0.774); 53 | color: rgba(196, 0, 0, 0.774); 54 | } 55 | 56 | .big-decrease:hover { 57 | background-color: rgba(196, 0, 0, 0.774); 58 | color: black; 59 | } 60 | 61 | .small-decrease { 62 | border: 2px solid rgba(255, 0, 0, 0.774); 63 | color: rgba(255, 0, 0, 0.774); 64 | } 65 | 66 | .small-decrease:hover { 67 | background-color: rgba(255, 0, 0, 0.774); 68 | color: black; 69 | } 70 | 71 | .reset { 72 | width: 60px !important; 73 | border: 2px solid #0069d9ab; 74 | color: #0069d9ab; 75 | } 76 | 77 | .reset:hover { 78 | background-color: #0069d9ab; 79 | color: black; 80 | } 81 | 82 | .small-increase { 83 | border: 2px solid #00ff3cab; 84 | color: #00ff3cab; 85 | } 86 | 87 | .small-increase:hover { 88 | background-color: #00ff3cab; 89 | color: black; 90 | } 91 | 92 | .big-increase { 93 | border: 2px solid #00b82bab; 94 | color: #00b82bab; 95 | } 96 | 97 | .big-increase:hover { 98 | background-color: #00b82bab; 99 | color: black; 100 | } 101 | 102 | .info { 103 | margin-top: 2.5vh; 104 | height: 95vh; 105 | display: grid; 106 | grid-template-rows: 5fr 1fr 5fr; 107 | place-items: center; 108 | } 109 | 110 | .bullet { 111 | height: 40vh; 112 | display: flex; 113 | width: 100%; 114 | } 115 | 116 | .rain { 117 | padding: 2vh; 118 | border-radius: 2vh; 119 | place-self: start; 120 | } 121 | 122 | .blue { 123 | background-color: #1e90ff; 124 | } 125 | 126 | .red { 127 | background-color: #ff4500; 128 | } 129 | 130 | .wind-direction { 131 | height: 40vh; 132 | width: 100%; 133 | display: flex; 134 | flex-direction: column; 135 | justify-content: space-evenly; 136 | align-items: center; 137 | } 138 | 139 | #compass-container { 140 | height: 65%; 141 | display: flex; 142 | justify-content: center; 143 | align-items: center; 144 | } 145 | 146 | #compass { 147 | height: 90%; 148 | } 149 | 150 | .wind-direction p { 151 | color: white; 152 | } 153 | -------------------------------------------------------------------------------- /src/weather/src/modifyData.js: -------------------------------------------------------------------------------- 1 | import { setLimit } from "./graph"; 2 | 3 | function parseTime(time) { 4 | const [seconds, minutes, hours] = time 5 | .split(":") 6 | .reverse() 7 | .map((number) => parseInt(number)); 8 | 9 | if (hours === undefined) return (minutes * 60 + seconds) * 1000; 10 | 11 | return (hours * 3600 + minutes * 60 + seconds) * 1000; 12 | } 13 | 14 | export const modifyData = (data) => { 15 | const timeOffset = parseTime(data.SessionInfo.GmtOffset); 16 | const weatherDataSeries = data.WeatherDataSeries.Series.slice(-setLimit); 17 | const airTempCoords = []; 18 | const trackTempCoords = []; 19 | const windSpeedCoords = []; 20 | for (let serieIndex = 0; serieIndex < weatherDataSeries.length; serieIndex++) { 21 | const serie = weatherDataSeries[serieIndex]; 22 | 23 | const date = new Date(new Date(serie.Timestamp).getTime() + timeOffset); 24 | const hours = date.getHours(); 25 | const minutes = date.getMinutes().toString().padStart(2, "0"); 26 | 27 | const airTemp = serie.Weather.AirTemp; 28 | const airTempCoord = { x: `${hours}:${minutes}`, y: airTemp }; 29 | airTempCoords.push(airTempCoord); 30 | 31 | const trackTemp = serie.Weather.TrackTemp; 32 | const trackTempCoord = { x: `${hours}:${minutes}`, y: trackTemp }; 33 | trackTempCoords.push(trackTempCoord); 34 | 35 | const windSpeed = serie.Weather.WindSpeed; 36 | const windSpeedCoord = { x: `${hours}:${minutes}`, y: parseFloat(windSpeed) * 3.6 }; 37 | windSpeedCoords.push(windSpeedCoord); 38 | } 39 | 40 | let maxHumidity = parseFloat(data.WeatherData.Humidity); 41 | let maxPressure = parseFloat(data.WeatherData.Pressure); 42 | for (const serie of weatherDataSeries) { 43 | console.log(serie); 44 | const humidity = parseFloat(serie.Weather.Humidity); 45 | const pressure = parseFloat(serie.Weather.Pressure); 46 | if (humidity > maxHumidity) maxHumidity = humidity; 47 | 48 | if (pressure > maxPressure) maxPressure = pressure; 49 | } 50 | 51 | const humidity = parseFloat(data.WeatherData.Humidity); 52 | const pressure = parseFloat(data.WeatherData.Pressure); 53 | 54 | const modifiedData = [ 55 | [ 56 | { 57 | id: "Air Temp", 58 | color: "hsl(0, 100%, 36%)", 59 | data: airTempCoords, 60 | }, 61 | { id: "Track Temp", color: "hsl(131, 100%, 36%)", data: trackTempCoords }, 62 | ], 63 | [ 64 | { 65 | id: "Wind Speed", 66 | color: "hsl(0, 100%, 36%)", 67 | data: windSpeedCoords, 68 | }, 69 | ], 70 | [ 71 | { 72 | id: "Humidity", 73 | ranges: [0, 40, 100], 74 | measures: [humidity], 75 | markers: [maxHumidity], 76 | }, 77 | ], 78 | [ 79 | { 80 | id: "Pressure", 81 | ranges: [0, 950, 1050, 1075], 82 | measures: [pressure], 83 | markers: [maxPressure], 84 | }, 85 | ], 86 | ]; 87 | return modifiedData; 88 | }; 89 | -------------------------------------------------------------------------------- /src/sessionlog/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "SFBold"; 5 | color: white; 6 | overflow: hidden; 7 | } 8 | 9 | body { 10 | height: 100vh; 11 | width: 100vw; 12 | } 13 | 14 | .drag { 15 | -webkit-app-region: drag; 16 | } 17 | 18 | .header { 19 | padding: 5vw; 20 | } 21 | 22 | .header h1 { 23 | font-size: 8vw; 24 | } 25 | 26 | .container { 27 | height: 100vh; 28 | width: 100vw; 29 | } 30 | 31 | .line { 32 | height: 2px; 33 | width: 100%; 34 | border-radius: 5vw; 35 | background-color: #0a0a0faf; 36 | margin: 3vw 0; 37 | } 38 | 39 | .wrapper { 40 | background-color: #0a0a0faf; 41 | border-radius: 6vw; 42 | text-align: center; 43 | margin-bottom: 3vw; 44 | } 45 | 46 | #logs .wrapper { 47 | height: 0; 48 | transition: all 0.5s ease; 49 | } 50 | 51 | #logs .wrapper.shown { 52 | height: 41.5vw; 53 | } 54 | 55 | #logs { 56 | display: flex; 57 | flex-direction: column-reverse; 58 | } 59 | 60 | .type { 61 | background-color: rgb(242, 254, 255); 62 | } 63 | 64 | .type p { 65 | color: black; 66 | padding: 3vw; 67 | font-size: 4.5vw; 68 | } 69 | 70 | .status { 71 | height: 20vw; 72 | place-items: center; 73 | width: 100vw; 74 | border-bottom: 1px solid white; 75 | display: grid; 76 | } 77 | 78 | .status p { 79 | font-size: 6vw; 80 | } 81 | 82 | .double { 83 | grid-template-columns: 1fr 1fr; 84 | place-items: initial !important; 85 | } 86 | 87 | .penalty { 88 | display: flex; 89 | flex-direction: column; 90 | width: 100%; 91 | height: 80%; 92 | justify-content: space-evenly; 93 | } 94 | 95 | .driver-first-name { 96 | font-size: 5vw !important; 97 | font-family: "SFRegular", Arial, Helvetica, sans-serif !important; 98 | } 99 | 100 | .driver-last-name { 101 | font-size: 6vw !important; 102 | transform: skew(-5deg); 103 | } 104 | 105 | .tires { 106 | display: flex; 107 | width: 100%; 108 | justify-content: space-evenly; 109 | align-items: center; 110 | } 111 | 112 | .tires div { 113 | display: flex; 114 | align-items: center; 115 | } 116 | 117 | .tires img { 118 | height: 7vw !important; 119 | } 120 | 121 | .arrow img { 122 | height: 4vw !important; 123 | width: 7vw !important; 124 | } 125 | 126 | .stoptime { 127 | color: aqua; 128 | transform: skew(-5deg); 129 | text-shadow: 0.5vw 0.5vw 0.5vw black; 130 | } 131 | 132 | .left { 133 | height: 100%; 134 | display: flex; 135 | flex-direction: column; 136 | justify-content: center; 137 | align-items: center; 138 | border-right: 1px solid white; 139 | } 140 | 141 | .highlight .type { 142 | background-color: rgb(255, 255, 37); 143 | } 144 | 145 | .highlight * { 146 | border-color: rgb(255, 255, 37) !important; 147 | } 148 | 149 | .right { 150 | height: 100%; 151 | display: flex; 152 | flex-direction: column; 153 | justify-content: center; 154 | align-items: center; 155 | border-left: 0.5vw solid white; 156 | } 157 | 158 | .right img { 159 | height: 90%; 160 | } 161 | 162 | .time { 163 | height: 10vw; 164 | width: 100%; 165 | background-color: transparent; 166 | display: grid; 167 | grid-template-columns: 1fr 1fr; 168 | place-items: center; 169 | overflow: visible; 170 | border-top: 0.5vw solid white; 171 | } 172 | 173 | .time p { 174 | font-size: 4vw; 175 | } 176 | 177 | .small-text { 178 | font-size: 4vw !important; 179 | color: rgba(255, 255, 255, 0.664); 180 | } 181 | 182 | .green { 183 | background: linear-gradient(to bottom, transparent 0%, rgba(0, 86, 0, 0.438) 100%); 184 | } 185 | 186 | .red { 187 | background: linear-gradient(to bottom, transparent 0%, rgba(197, 0, 0, 0.438) 100%); 188 | } 189 | 190 | .blue { 191 | background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 172, 0.438) 100%); 192 | } 193 | 194 | .yellow { 195 | background: linear-gradient(to bottom, transparent 0%, rgba(255, 255, 0, 0.438) 100%); 196 | } 197 | 198 | .orange { 199 | background: linear-gradient(to bottom, transparent 0%, rgba(255, 165, 0, 0.438) 100%); 200 | } 201 | 202 | .purple { 203 | background: linear-gradient(to bottom, transparent 0%, rgba(255, 0, 255, 0.438) 100%); 204 | } 205 | 206 | .white { 207 | background: linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.438) 100%); 208 | } 209 | -------------------------------------------------------------------------------- /src/statuses/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | async function getConfigurations() { 8 | const networkConfig = (await ipcRenderer.invoke("get_store")).config.network; 9 | host = networkConfig.host; 10 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 11 | } 12 | 13 | let sessionInfo; 14 | 15 | // Requesting the information needed from the api 16 | async function apiRequests() { 17 | const config = { 18 | host: host, 19 | port: port, 20 | }; 21 | const data = await f1mvApi.LiveTimingAPIGraphQL(config, ["TrackStatus", "RaceControlMessages", "SessionInfo"]); 22 | trackStatus = data.TrackStatus; 23 | raceControlMessages = data.RaceControlMessages.Messages; 24 | sessionInfo = data.SessionInfo; 25 | } 26 | 27 | async function addTrackSectors() { 28 | const circuitId = sessionInfo.Meeting.Circuit.Key; 29 | 30 | const year = new Date(sessionInfo.StartDate).getFullYear(); 31 | 32 | const sectorCount = (await f1mvApi.getCircuitInfo(circuitId, year)).marshalSectors.length; 33 | 34 | const container = document.getElementById("wrapper"); 35 | 36 | for (let count = 1; count <= sectorCount; count++) { 37 | const newStatus = `

SECTOR ${count}

CLEAR

`; 38 | container.innerHTML += newStatus; 39 | } 40 | } 41 | 42 | function setFullStatus() { 43 | const status = parseInt(trackStatus.Status); 44 | 45 | let message = "TRACK CLEAR"; 46 | let color = "green"; 47 | switch (status) { 48 | case 2: 49 | message = "YELLOW FLAG"; 50 | color = "yellow"; 51 | break; 52 | case 4: 53 | message = "SC DEPLOYED"; 54 | color = "yellow"; 55 | break; 56 | case 5: 57 | message = "RED FLAG"; 58 | color = "red"; 59 | break; 60 | case 6: 61 | message = "VSC DEPLOYED"; 62 | color = "yellow"; 63 | break; 64 | case 7: 65 | message = "VSC ENDING"; 66 | color = "yellow"; 67 | break; 68 | } 69 | 70 | const fullStatusElement = document.getElementById("track-status"); 71 | 72 | fullStatusElement.textContent = message; 73 | fullStatusElement.className = color; 74 | } 75 | 76 | let pastRaceControlMessages = []; 77 | function setTrackSector() { 78 | for (const message of raceControlMessages) { 79 | if (pastRaceControlMessages.includes(JSON.stringify(message))) continue; 80 | 81 | pastRaceControlMessages.push(JSON.stringify(message)); 82 | 83 | if (message.Category === "Flag" && message.Scope === "Track" && message.Flag === "CLEAR") { 84 | const allSectorElements = document.getElementsByClassName("status"); 85 | 86 | for (const sectorElement of allSectorElements) { 87 | sectorElement.children[1].textContent = "CLEAR"; 88 | sectorElement.children[1].className = "green"; 89 | } 90 | } 91 | 92 | if ( 93 | (message.Category !== "Flag" || message.Scope !== "Sector") && 94 | message.SubCategory !== "TrackSurfaceSlippery" 95 | ) 96 | continue; 97 | 98 | let sector = message.Sector; 99 | 100 | let flag = message.Flag; 101 | 102 | if (sector === undefined) { 103 | sector = message.Message.match(/(\d+)/)[0]; 104 | flag = "SLIPPERY"; 105 | } 106 | 107 | let color = "green"; 108 | switch (flag) { 109 | case "YELLOW": 110 | color = "yellow"; 111 | break; 112 | case "DOUBLE YELLOW": 113 | color = "orange"; 114 | break; 115 | case "SLIPPERY": 116 | color = "slippery"; 117 | } 118 | 119 | const sectorElement = document.getElementById(`sector${sector}`); 120 | sectorElement.textContent = flag; 121 | sectorElement.className = color; 122 | } 123 | } 124 | 125 | async function run() { 126 | await getConfigurations(); 127 | await apiRequests(); 128 | await addTrackSectors(); 129 | setInterval(async () => { 130 | await apiRequests(); 131 | setFullStatus(); 132 | setTrackSector(); 133 | }, 500); 134 | } 135 | 136 | run(); 137 | 138 | // {"Status":"1","Message":"AllClear"} 139 | // {"Status":"2","Message":"Yellow"} 140 | // event 3 has never been seen 141 | // {"Status":"4","Message":"SCDeployed"} 142 | // {"Status":"5","Message":"Red"} 143 | // {"Status":"6","Message":"VSCDeployed"} 144 | // {"Status":"7","Message":"VSCEnding"} 145 | -------------------------------------------------------------------------------- /src/currentlaps/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | height: 100vh; 8 | width: 100vw; 9 | overflow: hidden; 10 | } 11 | 12 | .drag { 13 | -webkit-app-region: drag; 14 | } 15 | 16 | /* Container and animations */ 17 | 18 | .window_header { 19 | padding: 5vw; 20 | } 21 | 22 | .window_header h1 { 23 | font-size: 8vw; 24 | color: white; 25 | font-family: "SFBold"; 26 | } 27 | 28 | .line { 29 | height: 2px; 30 | width: 100%; 31 | border-radius: 5vw; 32 | background-color: #0a0a0faf; 33 | margin: 3vw 0; 34 | } 35 | 36 | .wrapper { 37 | background-color: #0a0a0faf; 38 | border-radius: 6vw; 39 | text-align: center; 40 | margin-bottom: 3vw; 41 | } 42 | 43 | #container li { 44 | height: 0; 45 | margin-bottom: 0; 46 | overflow: hidden; 47 | transition: all 0.5s ease-in-out; 48 | border: none; 49 | } 50 | 51 | #container li.show { 52 | height: 37vw !important; 53 | margin-bottom: 5px; 54 | } 55 | 56 | #container li.show.highlight { 57 | border-left: 1.5vw solid rgb(255, 251, 0); 58 | } 59 | 60 | #container li.show.highlight .header { 61 | border-left: 0.5vw solid black; 62 | } 63 | 64 | /* General time styling */ 65 | .improved-time, 66 | .pushlap { 67 | background-color: #0a0a0f; 68 | margin-bottom: 2vw; 69 | } 70 | 71 | /* Header styling */ 72 | .header { 73 | display: flex; 74 | background-color: white; 75 | color: black; 76 | height: 10vw; 77 | width: 100%; 78 | } 79 | 80 | .danger { 81 | background-color: #b60000 !important; 82 | color: white !important; 83 | } 84 | 85 | .icon { 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | height: 10vw; 90 | width: 10vw; 91 | } 92 | 93 | .icon img { 94 | height: 9vw; 95 | } 96 | 97 | .position { 98 | width: 10vw; 99 | height: 10vw; 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | border-right: 1px solid #2d2d33; 104 | } 105 | 106 | .position p { 107 | font-family: "OF1Regular", Arial, Helvetica, sans-serif; 108 | transform: skew(-10deg); 109 | font-size: 5vw; 110 | width: 5vw; 111 | display: flex; 112 | align-items: center; 113 | justify-content: center; 114 | } 115 | 116 | .name { 117 | width: 70vw; 118 | height: 10vw; 119 | display: flex; 120 | align-items: center; 121 | } 122 | 123 | .name p { 124 | font-size: 6vw; 125 | padding-left: 2vw; 126 | } 127 | 128 | .tire { 129 | display: flex; 130 | justify-content: center; 131 | align-items: center; 132 | width: 10vw; 133 | height: 10vw; 134 | float: right; 135 | background-color: #00000a; 136 | } 137 | 138 | .tire p { 139 | font-family: "OF1Regular", Arial, Helvetica, sans-serif; 140 | transform: skew(0deg); 141 | font-size: 4.5vw; 142 | } 143 | 144 | /* Time styling */ 145 | .times { 146 | height: 16vw; 147 | display: flex; 148 | } 149 | 150 | .personal, 151 | .target { 152 | width: 50vw; 153 | } 154 | 155 | .personal { 156 | border-right: 0.1vw solid #2d2d33; 157 | display: flex; 158 | justify-content: center; 159 | align-items: center; 160 | } 161 | 162 | .personal p { 163 | font-size: 8.5vw; 164 | } 165 | 166 | .target { 167 | border-left: 0.1vw solid #2d2d33; 168 | gap: 3vw; 169 | display: flex; 170 | justify-content: center; 171 | align-items: center; 172 | color: white; 173 | } 174 | 175 | .target p { 176 | font-size: 4vw; 177 | } 178 | 179 | .target-name p { 180 | font-size: 3vw; 181 | color: #5b5b5d; 182 | } 183 | 184 | .top, 185 | .bottom { 186 | height: 100%; 187 | display: flex; 188 | flex-direction: column; 189 | justify-content: space-evenly; 190 | align-items: center; 191 | } 192 | 193 | /* Sector styling */ 194 | .sectors { 195 | border-top: 0.2vw solid #2d2d33; 196 | display: flex; 197 | justify-content: space-between; 198 | align-items: flex-end; 199 | } 200 | 201 | .sector { 202 | width: 100%; 203 | padding: 0 1vw 1vw; 204 | } 205 | 206 | #sector1 { 207 | border-left: 0.1vw solid #2d2d33; 208 | border-right: 0.1vw solid #2d2d33; 209 | } 210 | 211 | .sector-times { 212 | display: flex; 213 | justify-content: space-evenly; 214 | } 215 | 216 | .sector-time { 217 | display: flex; 218 | align-items: center; 219 | justify-content: center; 220 | text-align: center; 221 | color: white; 222 | font-size: 4.5vw; 223 | margin: 1.5vw 0; 224 | } 225 | 226 | .sector-delta { 227 | display: flex; 228 | align-items: center; 229 | justify-content: center; 230 | text-align: center; 231 | color: white; 232 | font-size: 3vw; 233 | } 234 | 235 | .sector-color { 236 | height: 1vw; 237 | } 238 | 239 | .sectors p { 240 | font-family: "OF1Regular"; 241 | } 242 | 243 | p { 244 | font-family: "OF1Bold"; 245 | transform: skew(-10deg); 246 | } 247 | 248 | /* New styling */ 249 | .segments { 250 | display: flex; 251 | width: 100%; 252 | height: 1vw; 253 | gap: 0.4vw; 254 | } 255 | 256 | .segment { 257 | background-color: #5b5b5d; 258 | width: 100%; 259 | } 260 | -------------------------------------------------------------------------------- /src/functions/driver.js: -------------------------------------------------------------------------------- 1 | const { parseLapOrSectorTime } = require("./times"); 2 | 3 | // Check if the driver is on a push lap or not 4 | function isDriverOnPushLap(sessionStatus, trackStatus, timingData, bestTimes, sessionType, driverNumber) { 5 | if (sessionStatus === "Aborted" || sessionStatus === "Inactive" || [4, 5, 6, 7].includes(trackStatus)) return false; 6 | 7 | const driverTimingData = timingData[driverNumber]; 8 | const driverBestTimes = bestTimes[driverNumber]; 9 | 10 | if (sessionType === "Race" && (driverTimingData.NumberOfLaps === undefined || driverTimingData.NumberOfLaps <= 1)) 11 | return false; 12 | 13 | if (driverTimingData.InPit) return false; 14 | 15 | // If the first mini sector time is status 2064, meaning he is on a out lap, return false 16 | if (driverTimingData.Sectors[0].Segments?.[0].Status === 2064) return false; 17 | 18 | // Get the threshold to which the sector time should be compared to the best personal sector time. 19 | const pushDeltaThreshold = sessionType === "Race" ? 0.2 : sessionType === "Qualifying" ? 1 : 3; 20 | 21 | const sectors = driverTimingData.Sectors; 22 | 23 | const lastSector = sectors.slice(-1)[0]; 24 | 25 | if (sectors.slice(-1)[0].Value !== "" && (sectors.slice(-1)[0].Segments?.slice(-1)[0].Status !== 0 ?? true)) 26 | return false; 27 | 28 | const completedFirstSector = sectors[0].Segments 29 | ? (sectors[0].Segments.slice(-1)[0].Status !== 0 && lastSector.Value === "") || 30 | (lastSector.Segments.slice(-1)[0].Status === 0 && 31 | lastSector.Value !== "" && 32 | sectors[1].Segments[0].Status !== 0 && 33 | sectors[0].Segments.slice(-1)[0].Status !== 0) 34 | : sectors[0].Value !== 0 && lastSector.Value === ""; 35 | 36 | let isPushing = false; 37 | for (let sectorIndex = 0; sectorIndex < driverTimingData.Sectors.length; sectorIndex++) { 38 | const sector = sectors[sectorIndex]; 39 | const bestSector = driverBestTimes.BestSectors[sectorIndex]; 40 | 41 | const sectorTime = parseLapOrSectorTime(sector.Value); 42 | const bestSectorTime = parseLapOrSectorTime(bestSector.Value); 43 | 44 | // Check if the first sector is completed by checking if the last segment of the first sector has a value meaning he has crossed the last point of that sector and the final sector time does not have a value. The last check is done because sometimes the segment already has a status but the times are not updated yet. 45 | 46 | // If the first sector time is above the threshold it should imidiately break because it will not be a push lap 47 | if (sectorTime - bestSectorTime > pushDeltaThreshold && completedFirstSector) { 48 | isPushing = false; 49 | break; 50 | } 51 | 52 | // If the first sector time is lower then the threshold it should temporarily set pushing to true because the driver could have still backed out in a later stage 53 | if (sectorTime - bestSectorTime <= pushDeltaThreshold && completedFirstSector) { 54 | isPushing = true; 55 | continue; 56 | } 57 | 58 | // If the driver has a fastest segment overall it would temporarily set pushing to true because the driver could have still backed out in a later stage 59 | if (sector.Segments?.some((segment) => segment.Status === 2051) && sessionType !== "Race") { 60 | isPushing = true; 61 | continue; 62 | } 63 | } 64 | 65 | // Return the final pushing state 66 | return isPushing; 67 | } 68 | 69 | // Get the position of the driver based on their segments 70 | function getDriverPosition(driverNumber, timingData) { 71 | const driverTimingData = timingData[driverNumber]; 72 | const sectors = driverTimingData.Sectors; 73 | 74 | // The starting segment will always be 0 because if there is no 0 state anywhere all segments will be completed and the current segment will be the first one. 75 | let currentSegment = -1; 76 | 77 | const driverCount = Object.keys(timingData).length; 78 | 79 | if (sectors[0].Segments) { 80 | for (const sectorIndex in sectors) { 81 | const segments = sectors[sectorIndex].Segments; 82 | for (const segmentIndex in segments) { 83 | const segment = segments[segmentIndex]; 84 | if (segment.Status === 0) return parseInt(currentSegment) + parseInt(segmentIndex); 85 | } 86 | currentSegment += segments.length; 87 | } 88 | } else { 89 | currentSegment = driverCount - parseInt(driverTimingData.Position); 90 | } 91 | 92 | const lastSectorValue = sectors.slice(-1)[0].Value; 93 | 94 | if (lastSectorValue === "") return currentSegment; 95 | 96 | return 0; 97 | } 98 | 99 | function getDriversTrackOrder(timingData) { 100 | const driverOrder = Object.keys(timingData).sort((a, b) => { 101 | const positionDriverA = getDriverPosition(a, timingData); 102 | const positionDriverB = getDriverPosition(b, timingData); 103 | 104 | if (positionDriverA === positionDriverB) return timingData[a].Position - timingData[b].Position; 105 | 106 | return positionDriverB - positionDriverA; 107 | }); 108 | 109 | return driverOrder; 110 | } 111 | 112 | module.exports = { 113 | isDriverOnPushLap, 114 | getDriverPosition, 115 | getDriversTrackOrder, 116 | }; 117 | -------------------------------------------------------------------------------- /src/battlemode/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | color: white; 5 | } 6 | 7 | body { 8 | height: 100vh; 9 | width: 100vw; 10 | overflow: hidden; 11 | } 12 | 13 | .drag { 14 | -webkit-app-region: drag; 15 | } 16 | 17 | .transparent { 18 | background-color: transparent !important; 19 | } 20 | 21 | #container { 22 | height: 100vh; 23 | width: 100vw; 24 | } 25 | 26 | #wrapper { 27 | background-color: #0a0a0faf; 28 | border-radius: 25px; 29 | padding: 4vh 8vh; 30 | height: calc(55% - 8vh); 31 | width: calc(100% - 16vh); 32 | display: flex; 33 | justify-content: space-between; 34 | position: absolute; 35 | bottom: 20vh; 36 | } 37 | 38 | .driver { 39 | height: 100%; 40 | display: flex; 41 | gap: 7vh; 42 | } 43 | 44 | .driver-info { 45 | position: relative; 46 | top: calc(-40% - 3.5vh); 47 | height: calc(140% + 3.5vh); 48 | width: 65vh; 49 | } 50 | 51 | .driver-headshot { 52 | height: 70%; 53 | position: relative; 54 | border-bottom: 2px solid white; 55 | display: flex; 56 | justify-content: center; 57 | } 58 | 59 | .driver-headshot img { 60 | position: absolute; 61 | bottom: 0; 62 | height: 110%; 63 | } 64 | 65 | .driver-name { 66 | padding: 2vh 5vh; 67 | } 68 | 69 | .driver-name .first-name { 70 | padding-left: 1vh; 71 | font-family: "OF1Regular"; 72 | font-size: 5vh; 73 | transform: skew(-10deg); 74 | text-shadow: 0.5vh 0.5vh 0.5vh black; 75 | } 76 | 77 | .driver-name .last-name { 78 | font-family: "OF1Bold"; 79 | font-size: 7vh; 80 | transform: skew(-10deg); 81 | text-shadow: 0.5vh 0.5vh 0.5vh black; 82 | } 83 | 84 | .pitlane { 85 | padding: 1vh 0; 86 | display: flex; 87 | flex-direction: column; 88 | align-items: center; 89 | justify-content: space-between; 90 | } 91 | 92 | .position p { 93 | font-family: "OF1Bold"; 94 | font-size: 6vh; 95 | transform: skew(-10deg); 96 | } 97 | 98 | .tires { 99 | width: 10vh; 100 | display: flex; 101 | flex-direction: column; 102 | align-items: center; 103 | justify-content: center; 104 | } 105 | 106 | .tires .current-tire img { 107 | width: 100%; 108 | } 109 | 110 | .tire-age { 111 | font-family: "SFBold"; 112 | font-size: 7vh; 113 | } 114 | 115 | .pit { 116 | width: 100%; 117 | display: grid; 118 | place-items: center; 119 | } 120 | 121 | .pit p { 122 | font-family: "OF1Bold"; 123 | border: 2px solid red; 124 | color: red; 125 | width: 10vh; 126 | padding: 1vh; 127 | border-radius: 3vh; 128 | display: grid; 129 | place-items: center; 130 | font-size: 6vh; 131 | } 132 | 133 | .times { 134 | display: flex; 135 | flex-direction: column; 136 | justify-content: space-around; 137 | width: 40vh; 138 | } 139 | 140 | .time-name { 141 | font-family: "SFRegular"; 142 | font-size: 7vh; 143 | } 144 | 145 | .time-time { 146 | font-family: "SFBold"; 147 | font-size: 9vh; 148 | } 149 | 150 | .telemetry { 151 | display: flex; 152 | flex-direction: column; 153 | align-items: center; 154 | justify-content: space-around; 155 | width: 50vh; 156 | } 157 | 158 | .drs p { 159 | padding: 1vh 5vh; 160 | width: fit-content; 161 | font-size: 8vh; 162 | border-radius: 2vh; 163 | font-family: "OF1Bold"; 164 | text-align: center; 165 | border: 2px solid green; 166 | color: green; 167 | } 168 | 169 | .off { 170 | opacity: 0.3; 171 | } 172 | 173 | .speed { 174 | display: flex; 175 | gap: 2vh; 176 | } 177 | 178 | .speed-number { 179 | font-family: "OF1Bold"; 180 | font-size: 8vh; 181 | } 182 | 183 | .metric { 184 | font-family: "OF1Regular"; 185 | font-size: 5vh; 186 | top: 3vh; 187 | position: relative; 188 | } 189 | 190 | .pedals { 191 | height: 9vh; 192 | display: flex; 193 | flex-direction: column; 194 | justify-content: space-between; 195 | width: 80%; 196 | } 197 | 198 | .pedal { 199 | border: 0.5vh solid black; 200 | height: 2vh; 201 | border-radius: 4vh; 202 | overflow: hidden; 203 | } 204 | 205 | .pedal div { 206 | width: 0; 207 | height: 100%; 208 | transition: all 0.2s ease; 209 | } 210 | 211 | .gap { 212 | width: 100%; 213 | display: flex; 214 | align-items: center; 215 | gap: 10vh; 216 | padding: 0 10vh; 217 | } 218 | 219 | .gap .line { 220 | width: 100%; 221 | border-bottom: 1vh dashed white; 222 | } 223 | 224 | .gap .gap-time { 225 | display: flex; 226 | flex-direction: column; 227 | align-items: center; 228 | justify-content: center; 229 | } 230 | 231 | .gap .gap-time-time { 232 | font-family: "OF1Bold"; 233 | white-space: nowrap; 234 | font-size: 10vh; 235 | transform: skew(-10deg); 236 | } 237 | 238 | .gap .gap-time-format { 239 | font-family: "OF1Regular"; 240 | font-size: 6vh; 241 | } 242 | 243 | .shown { 244 | opacity: 1 !important; 245 | } 246 | 247 | #buttons { 248 | position: absolute; 249 | bottom: 0; 250 | left: 0; 251 | width: 100vw; 252 | background-color: #0a0a0faf; 253 | height: 18vh; 254 | border-radius: 5vh; 255 | opacity: 0; 256 | transition: all 0.5s ease; 257 | display: flex; 258 | justify-content: space-evenly; 259 | gap: 2vh; 260 | align-items: center; 261 | } 262 | 263 | .button { 264 | width: 24vh; 265 | height: 14vh; 266 | background-color: transparent; 267 | border-radius: 5vh; 268 | display: grid; 269 | place-items: center; 270 | border: 0.5vh solid; 271 | opacity: 0.6; 272 | outline: none; 273 | transition: all 0.5s ease; 274 | font-family: "SFBold"; 275 | font-size: 7vh; 276 | } 277 | 278 | .button:hover { 279 | opacity: 1; 280 | } 281 | 282 | .green { 283 | background-color: green; 284 | } 285 | 286 | .fastest-time { 287 | color: #9c27b0; 288 | } 289 | 290 | .personal-best { 291 | color: #4caf50; 292 | } 293 | 294 | .red-text { 295 | color: red; 296 | } 297 | 298 | .slow { 299 | color: #fdd835; 300 | } 301 | 302 | .red { 303 | background-color: red; 304 | } 305 | 306 | .gray { 307 | color: rgba(255, 255, 255, 0.233); 308 | } 309 | -------------------------------------------------------------------------------- /src/flagdisplay/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const f1mvApi = require("npm_f1mv_api"); 4 | 5 | const { ipcRenderer } = require("electron"); 6 | 7 | let goveeConnected = false; 8 | let goveeDevices = []; 9 | 10 | async function goveeHandler() { 11 | const goveeEnabled = await (async () => (await ipcRenderer.invoke("get_store")).config.flag_display.govee)(); 12 | if (goveeEnabled) { 13 | if (await ipcRenderer.invoke("checkGoveeWindowExistence")) return; 14 | 15 | const Govee = require("govee-lan-control"); 16 | const govee = new Govee.default(); 17 | 18 | async function showGoveeWindow() { 19 | const goveePanel = window.open( 20 | "govee/index.html", 21 | "_blank", 22 | `width=150,height=150,frame=false,transparent=true,hideMenuBar=true,hasShadow=false,alwaysOnTop=true,movable=false,resizable=false` 23 | ); 24 | 25 | await sleep(100); 26 | 27 | goveePanel.document.getElementById("connected").textContent = goveeDevices.length; 28 | 29 | await sleep(5000); 30 | 31 | goveePanel.document.getElementById("container").classList.add("hide"); 32 | 33 | await sleep(500); 34 | 35 | goveePanel.close(); 36 | } 37 | 38 | govee.on("deviceAdded", async (device) => { 39 | console.log("Connected to Govee device: " + device.model); 40 | 41 | goveeDevices.push(device); 42 | 43 | setGoveeLight("green"); 44 | 45 | await sleep(1000); 46 | 47 | setGoveeLight("default"); 48 | 49 | goveeConnected = true; 50 | 51 | await showGoveeWindow(); 52 | }); 53 | 54 | govee.on("deviceRemoved", async (device) => { 55 | console.log("Govee device disconnected: " + device.model); 56 | const deviceIndex = goveeDevices.indexOf(device); 57 | 58 | goveeDevices.splice(deviceIndex, 1); 59 | 60 | if (goveeDevices.length === 0) goveeConnected = false; 61 | 62 | await showGoveeWindow(); 63 | }); 64 | } 65 | } 66 | 67 | // Set sleep 68 | const sleep = (milliseconds) => { 69 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 70 | }; 71 | 72 | // Set basic info 73 | const flag = document.getElementById("flag"); 74 | const extra = document.getElementById("extra"); 75 | const chequered = document.getElementById("chequered"); 76 | 77 | async function getConfigurations() { 78 | const configfile = (await ipcRenderer.invoke("get_store")).config; 79 | host = configfile.network.host; 80 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 81 | config = { 82 | host: host, 83 | port: port, 84 | }; 85 | if (debug) { 86 | console.log(host); 87 | console.log(port); 88 | } 89 | } 90 | 91 | async function setGoveeLight(color) { 92 | const ledColors = (await ipcRenderer.invoke("get_store")).led_colors; 93 | const rgbColor = ledColors[color]; 94 | 95 | console.log("Set govee light to: " + color); 96 | 97 | console.log(goveeDevices); 98 | for (const device of goveeDevices) { 99 | await device.actions.fadeColor({ 100 | time: 500, 101 | color: { 102 | rgb: rgbColor, 103 | }, 104 | brightness: 100, 105 | }); 106 | } 107 | } 108 | 109 | let prevTrackStatus; 110 | let prevSessionStatus; 111 | let prevFastestLap; 112 | async function getCurrentStatus() { 113 | const data = await f1mvApi.LiveTimingAPIGraphQL(config, ["TrackStatus", "SessionStatus", "TimingData"]); 114 | 115 | const trackStatus = parseInt(data.TrackStatus.Status); 116 | if (trackStatus !== prevTrackStatus) { 117 | prevTrackStatus = trackStatus; 118 | switch (trackStatus) { 119 | case 1: 120 | if (goveeConnected) setGoveeLight("green"); 121 | flag.classList.remove("red"); 122 | flag.classList.remove("yellow"); 123 | flag.classList.add("green"); 124 | await sleep(5000); 125 | if (goveeConnected) setGoveeLight("default"); 126 | flag.classList.remove("green"); 127 | break; 128 | case 2: 129 | if (goveeConnected) setGoveeLight("yellow"); 130 | flag.classList.add("yellow"); 131 | break; 132 | case 4: 133 | await blink("yellow", 3, 500); 134 | if (goveeConnected) setGoveeLight("yellow"); 135 | flag.classList.add("yellow"); 136 | break; 137 | case 5: 138 | if (goveeConnected) setGoveeLight("red"); 139 | flag.classList.remove("yellow"); 140 | flag.classList.add("red"); 141 | break; 142 | case 6: 143 | case 7: 144 | await blink("yellow", 3, 750); 145 | if (goveeConnected) setGoveeLight("yellow"); 146 | flag.classList.add("yellow"); 147 | break; 148 | } 149 | } 150 | 151 | const sessionStatus = data.SessionStatus.Status; 152 | if ((sessionStatus === "Finished" || sessionStatus === "Finalised") && prevSessionStatus !== sessionStatus) { 153 | prevSessionStatus = sessionStatus; 154 | await finishBlink("white", 5, 1000); 155 | if (goveeConnected) setGoveeLight("default"); 156 | } 157 | 158 | console.log(trackStatus); 159 | 160 | const timingData = data.TimingData.Lines; 161 | for (const driverNumber in timingData) { 162 | const driverTimingData = timingData[driverNumber]; 163 | 164 | const driverFastestLap = driverTimingData.LastLapTime.OverallFastest; 165 | const driverFastestLapTime = driverTimingData.LastLapTime.Value; 166 | 167 | if (driverFastestLap) { 168 | console.log(driverFastestLap && driverFastestLapTime !== prevFastestLap); 169 | } 170 | 171 | if (driverFastestLap && driverFastestLapTime !== prevFastestLap) { 172 | prevFastestLap = driverFastestLapTime; 173 | if (goveeConnected) setGoveeLight("purple"); 174 | extra.classList.add("fastest-lap"); 175 | await sleep(3000); 176 | if (goveeConnected) setGoveeLight("default"); 177 | extra.classList.remove("fastest-lap"); 178 | } 179 | } 180 | } 181 | 182 | async function finishBlink(color, amount, interval) { 183 | for (let count = 0; count < amount; count++) { 184 | if (goveeConnected) setGoveeLight(color); 185 | chequered.classList.add(color); 186 | await sleep(interval); 187 | if (goveeConnected) setGoveeLight("black"); 188 | chequered.classList.remove(color); 189 | await sleep(interval); 190 | } 191 | } 192 | 193 | async function blink(color, amount, interval) { 194 | for (let count = 0; count < amount; count++) { 195 | if (goveeConnected) setGoveeLight(color); 196 | flag.classList.add(color); 197 | await sleep(interval); 198 | if (goveeConnected) setGoveeLight("black"); 199 | flag.classList.remove(color); 200 | await sleep(interval); 201 | } 202 | } 203 | 204 | async function run() { 205 | await goveeHandler(); 206 | await getConfigurations(); 207 | 208 | while (true) { 209 | await getCurrentStatus(); 210 | await sleep(250); 211 | } 212 | } 213 | 214 | run(); 215 | 216 | // {"Status":"1","Message":"AllClear"} 217 | // {"Status":"2","Message":"Yellow"} 218 | // {Status: "3", Message: ""} 219 | // {Status: "4", Message: "SCDeployed"} 220 | // {"Status":"5","Message":"Red"} 221 | // {"Status":"6","Message":"VSCDeployed"} 222 | // {"Status":"7","Message":"VSCEnding"} 223 | -------------------------------------------------------------------------------- /src/crashdetection/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | // Set sleep 8 | const sleep = (milliseconds) => { 9 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 10 | }; 11 | 12 | let crashCount = 0; 13 | async function getConfigurations() { 14 | const configFile = (await ipcRenderer.invoke("get_store")).config; 15 | host = configFile.network.host; 16 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 17 | if (debug) { 18 | console.log(host); 19 | console.log(port); 20 | } 21 | } 22 | 23 | async function apiRequests() { 24 | const config = { 25 | host: host, 26 | port: port, 27 | }; 28 | 29 | const liveTimingState = await f1mvApi.LiveTimingAPIGraphQL(config, [ 30 | "DriverList", 31 | "CarData", 32 | "TimingData", 33 | "SessionStatus", 34 | "SessionInfo", 35 | "TrackStatus", 36 | "LapCount", 37 | ]); 38 | driverList = liveTimingState.DriverList; 39 | carData = liveTimingState.CarData.Entries; 40 | timingData = liveTimingState.TimingData.Lines; 41 | sessionStatus = liveTimingState.SessionStatus.Status; 42 | sessionInfo = liveTimingState.SessionInfo; 43 | sessionType = sessionInfo.Type; 44 | trackStatus = liveTimingState.TrackStatus; 45 | if (sessionType === "Race") { 46 | lapCount = liveTimingState.LapCount; 47 | } 48 | } 49 | 50 | function getCarData(driverNumber) { 51 | try { 52 | carData[0].Cars[driverNumber].Channels; 53 | } catch (error) { 54 | return "error"; 55 | } 56 | return carData[0].Cars[driverNumber].Channels; 57 | } 58 | 59 | function getSpeedThreshold() { 60 | if ( 61 | sessionType === "Qualifying" || 62 | sessionType === "Practice" || 63 | trackStatus.Status === "4" || 64 | trackStatus.Status === "6" || 65 | trackStatus.Status === "7" 66 | ) 67 | return 10; 68 | if (sessionStatus === "Inactive" || sessionStatus === "Aborted") return 0; 69 | return 30; 70 | } 71 | 72 | function weirdCarBehaviour(driverCarData, racingNumber) { 73 | const driverTimingData = timingData[racingNumber]; 74 | 75 | const rpm = driverCarData[0]; 76 | 77 | const speed = driverCarData[2]; 78 | 79 | const gear = driverCarData[3]; 80 | 81 | const speedLimit = getSpeedThreshold(); 82 | 83 | return ( 84 | rpm === 0 || 85 | speed <= speedLimit || 86 | gear > 8 || 87 | gear === 88 | (sessionStatus === "Inactive" || 89 | sessionStatus === "Aborted" || 90 | (sessionType !== "Race" && driverTimingData.PitOut) 91 | ? "" 92 | : 0) 93 | ); 94 | } 95 | 96 | function overwriteCrashedStatus(racingNumber) { 97 | const driverTimingData = timingData[racingNumber]; 98 | 99 | if (driverTimingData.InPit === true) return true; 100 | if (driverTimingData.Retired === true) return true; 101 | if (driverTimingData.Stopped === true) return true; 102 | 103 | const lastSectorSegments = driverTimingData.Sectors.slice(-1)[0].Segments; 104 | 105 | const sessionInactive = 106 | sessionStatus === "Inactive" || sessionStatus === "Finished" || sessionStatus === "Finalised"; 107 | 108 | if (!lastSectorSegments && sessionInactive) return true; 109 | 110 | if (!lastSectorSegments) return false; 111 | 112 | // Detect if grid start during inactive (formation lap) during a 'Race' session 113 | // If the final to last mini sector has a value (is not 0). Check if the session is 'Inactive' and if the session type is 'Race' 114 | if (lastSectorSegments.slice(-2, -1)[0].Status !== 0 && sessionInactive && !driverTimingData.PitOut) { 115 | console.log(racingNumber + " is lining up for a race start"); 116 | return true; 117 | } 118 | 119 | // If the race is started and the last mini sector has a different value then 0 (has a value) 120 | if ( 121 | sessionType === "Race" && 122 | sessionStatus === "Started" && 123 | (lastSectorSegments.slice(-3)[0].Status !== 0 || driverTimingData.Sectors[0].Segments[1].Status === 0) && 124 | lapCount.CurrentLap === 1 125 | ) { 126 | console.log(racingNumber + " is doing a race start"); 127 | return true; 128 | } 129 | 130 | // Detect if practice pitstop 131 | // If the session is 'practice' and the second mini sector does have a value. 132 | if (sessionType === "Practice" && driverTimingData.PitOut) { 133 | console.log(racingNumber + " is doing a practice start"); 134 | return true; 135 | } 136 | 137 | // Detect if car is in parc ferme 138 | // If the car has stopped anywhere in the final sector and the 'race' has 'finished' 139 | if ( 140 | sessionType === "Race" && 141 | (sessionStatus === "Finished" || sessionStatus === "Finalised") && 142 | lastSectorSegments.some((segment) => segment.Status !== 0) 143 | ) { 144 | console.log(racingNumber + " is in parc ferme"); 145 | return true; 146 | } 147 | 148 | return false; 149 | } 150 | 151 | function driverHasCrashed(driverNumber) { 152 | const driverCarData = getCarData(driverNumber); 153 | 154 | if (!weirdCarBehaviour(driverCarData, driverNumber)) return false; 155 | 156 | if (overwriteCrashedStatus(driverNumber)) return false; 157 | 158 | return true; 159 | } 160 | 161 | async function run() { 162 | await getConfigurations(); 163 | while (true) { 164 | await apiRequests(); 165 | 166 | for (const i in driverList) { 167 | const driverInfo = driverList[i]; 168 | 169 | const driverNumber = driverInfo.RacingNumber; 170 | 171 | const name = driverInfo.FirstName 172 | ? `${driverInfo.FirstName} ${driverInfo.LastName.toUpperCase()}` 173 | : driverInfo.Tla; 174 | 175 | const color = driverInfo.TeamColour || "808080"; 176 | 177 | const driverCarData = getCarData(driverNumber); 178 | 179 | if (driverCarData !== "error") { 180 | const HTMLDisplayList = document.getElementById("list"); 181 | 182 | const driverElement = document.getElementById(driverNumber); 183 | 184 | const crashed = driverHasCrashed(driverNumber); 185 | 186 | if (crashed) { 187 | if (driverElement === null) { 188 | const newDriverElement = document.createElement("li"); 189 | newDriverElement.id = driverNumber; 190 | newDriverElement.style.color = "#" + color; 191 | newDriverElement.innerHTML = name; 192 | HTMLDisplayList.appendChild(newDriverElement); 193 | await sleep(10); 194 | newDriverElement.className = "show"; 195 | } 196 | 197 | console.log(name + " has crashed"); 198 | } else { 199 | if (driverElement !== null) { 200 | document.getElementById(driverNumber).className = ""; 201 | 202 | await sleep(400); 203 | 204 | driverElement.remove(); 205 | } 206 | } 207 | } 208 | } 209 | 210 | await sleep(250); 211 | 212 | const HTMLDisplayListLength = document.getElementById("list").childNodes.length; 213 | 214 | if (HTMLDisplayListLength > crashCount) triggerWarning(); 215 | crashCount = HTMLDisplayListLength; 216 | } 217 | } 218 | run(); 219 | 220 | async function triggerWarning() { 221 | console.log("trigger warning"); 222 | const title = document.querySelector("h1"); 223 | let loop = 0; 224 | while (loop <= 10) { 225 | await sleep(200); 226 | 227 | title.className = "warning"; 228 | 229 | await sleep(200); 230 | 231 | title.className = ""; 232 | 233 | loop++; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/tirestats/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | const tireStats = { 8 | SOFT: { 9 | laps: 0, 10 | sets: 0, 11 | times: [], 12 | toptimes: [], 13 | }, 14 | MEDIUM: { 15 | laps: 0, 16 | sets: 0, 17 | times: [], 18 | toptimes: [], 19 | }, 20 | HARD: { 21 | laps: 0, 22 | sets: 0, 23 | times: [], 24 | toptimes: [], 25 | }, 26 | INTERMEDIATE: { 27 | laps: 0, 28 | sets: 0, 29 | times: [], 30 | toptimes: [], 31 | }, 32 | WET: { 33 | laps: 0, 34 | sets: 0, 35 | times: [], 36 | toptimes: [], 37 | }, 38 | TEST: { 39 | laps: 0, 40 | sets: 0, 41 | times: [], 42 | toptimes: [], 43 | }, 44 | }; 45 | 46 | const tireOrder = ["SOFT", "MEDIUM", "HARD", "INTERMEDIATE", "WET", "TEST"]; 47 | 48 | const topLimit = 3; 49 | 50 | const tireLimit = 3; 51 | 52 | const sleep = (milliseconds) => { 53 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 54 | }; 55 | 56 | document.addEventListener("keydown", (event) => { 57 | if (event.key == "Escape") document.getElementById("background").classList.toggle("transparent"); 58 | }); 59 | 60 | async function getConfigurations() { 61 | const configFile = (await ipcRenderer.invoke("get_store")).config; 62 | const networkConfig = configFile.network; 63 | const host = networkConfig.host; 64 | const port = (await f1mvApi.discoverF1MVInstances(host)).port; 65 | config = { 66 | host: host, 67 | port: port, 68 | }; 69 | } 70 | 71 | function parseLapTime(time) { 72 | const [minutes, seconds, milliseconds] = time 73 | .split(/[:.]/) 74 | .map((number) => parseInt(number.replace(/^0+/, "") || "0", 10)); 75 | 76 | if (milliseconds === undefined) return minutes + seconds / 1000; 77 | 78 | return minutes * 60 + seconds + milliseconds / 1000; 79 | } 80 | 81 | function getLapTime(time) { 82 | const minutes = Math.floor(time / 60); 83 | const seconds = time - minutes * 60; 84 | // const milliseconds = Math.floor((time - minutes * 60 - seconds) * 1000); 85 | 86 | const display0 = seconds < 10 ? "0" : ""; 87 | 88 | return `${minutes}:${display0}${seconds.toFixed(3)}`; 89 | } 90 | 91 | async function apiRequests() { 92 | const api = await f1mvApi.LiveTimingAPIGraphQL(config, ["DriverList", "TimingAppData"]); 93 | tireData = api.TimingAppData?.Lines; 94 | driverList = api.DriverList; 95 | } 96 | 97 | let pastTireData = {}; 98 | function getTireStats() { 99 | for (const compound in tireStats) { 100 | tireStats[compound].laps = 0; 101 | tireStats[compound].sets = 0; 102 | tireStats[compound].times = []; 103 | tireStats[compound].toptimes = []; 104 | } 105 | 106 | for (const driver in tireData) { 107 | const driverTireData = tireData[driver].Stints; 108 | 109 | console.log(driver); 110 | 111 | for (const compound of tireOrder) { 112 | let compoundTime = null; 113 | for (const stint of driverTireData) { 114 | const driverCompound = stint.Compound === "TEST_UNKNOWN" ? "TEST" : stint.Compound; 115 | 116 | if (driverCompound !== compound) continue; 117 | 118 | // Set laps and sets 119 | tireStats[compound].laps += stint.TotalLaps - stint.StartLaps; 120 | if (stint.New === "true") tireStats[compound].sets++; 121 | 122 | const lapTime = stint.LapTime ? parseLapTime(stint.LapTime) : null; 123 | 124 | if (lapTime && (lapTime < compoundTime || compoundTime === null)) compoundTime = lapTime; 125 | } 126 | 127 | if (!compoundTime) continue; 128 | 129 | tireStats[compound].times.push(compoundTime); 130 | 131 | const compountStats = tireStats[compound]; 132 | 133 | if (compountStats.toptimes.length < topLimit) { 134 | compountStats.toptimes.push({ 135 | driver: driver, 136 | time: compoundTime, 137 | }); 138 | } else { 139 | for (let count = 0; count < topLimit; count++) { 140 | if (compountStats.toptimes[count].time > compoundTime) { 141 | compountStats.toptimes[count] = { 142 | driver: driver, 143 | time: compoundTime, 144 | }; 145 | break; 146 | } 147 | } 148 | } 149 | 150 | tireStats[compound].toptimes.sort((a, b) => a.time - b.time); 151 | } 152 | } 153 | 154 | tireOrder.sort((a, b) => { 155 | const aTimeAverage = 156 | tireStats[a].times.reduce(function (aa, ab) { 157 | return aa + ab; 158 | }, 0) / tireStats[a].times.length; 159 | const bTimeAverage = 160 | tireStats[b].times.reduce(function (ba, bb) { 161 | return ba + bb; 162 | }, 0) / tireStats[b].times.length; 163 | if (aTimeAverage === bTimeAverage) return 0; 164 | if (aTimeAverage < bTimeAverage) return -1; 165 | if (isNaN(aTimeAverage)) return 1; 166 | if (isNaN(bTimeAverage)) return -1; 167 | return 1; 168 | }); 169 | 170 | console.log(tireStats); 171 | } 172 | 173 | function setTireStats() { 174 | const compoundElements = document.getElementsByClassName("tire"); 175 | 176 | for (let compoundIndex = 0; compoundIndex < tireLimit && compoundIndex < tireOrder.length; compoundIndex++) { 177 | const compound = tireOrder[compoundIndex]; 178 | 179 | const compoundStats = tireStats[compound]; 180 | 181 | const compoundElement = compoundElements[compoundIndex]; 182 | 183 | // Set compount name and image 184 | const tireImageElement = compoundElement.querySelector(".tire-image"); 185 | tireImageElement.children[0].src = `../icons/tires/${compound.toLowerCase()}_real.png`; 186 | tireImageElement.children[1].textContent = compound === "INTERMEDIATE" ? "INTERS" : compound; 187 | tireImageElement.children[1].className = compound.toLowerCase(); 188 | 189 | // Set top times 190 | const topTimesElements = compoundElement.querySelectorAll(".top-times .row"); 191 | for (const topTimeIndex in compoundStats.toptimes) { 192 | const topTime = compoundStats.toptimes[topTimeIndex]; 193 | const topTimeElement = topTimesElements[topTimeIndex]; 194 | 195 | // Set driver info 196 | const topDriverElement = topTimeElement.querySelector(".driver p"); 197 | 198 | topDriverElement.textContent = driverList[topTime.driver].LastName; 199 | topDriverElement.style.color = "#" + driverList[topTime.driver].TeamColour; 200 | 201 | // Set time info 202 | const topTimeElementTime = topTimeElement.querySelector(".time p"); 203 | topTimeElementTime.textContent = getLapTime(topTime.time); 204 | } 205 | 206 | // Set stats 207 | const statElements = compoundElement.querySelectorAll(".stats .row"); 208 | 209 | statElements[0].children[1].textContent = compoundStats.laps; 210 | statElements[1].children[1].textContent = compoundStats.sets; 211 | 212 | // Set delta 213 | 214 | if (compoundStats.times.length === 0 || tireStats[tireOrder[0]].times.length === 0) continue; 215 | 216 | const fastestAverage = 217 | tireStats[tireOrder[0]].times.reduce((a, b) => a + b) / tireStats[tireOrder[0]].times.length; 218 | 219 | if (compoundIndex === 0) continue; 220 | 221 | const deltaTime = compoundElement.querySelector(".delta .delta-time p"); 222 | 223 | const compoundTimeAverage = compoundStats.times.reduce((a, b) => a + b) / compoundStats.times.length; 224 | 225 | const delta = compoundTimeAverage - fastestAverage; 226 | 227 | deltaTime.textContent = `+${delta.toFixed(3)}`; 228 | } 229 | } 230 | 231 | async function run() { 232 | await getConfigurations(); 233 | await apiRequests(); 234 | getTireStats(); 235 | setTireStats(); 236 | setInterval(async () => { 237 | await apiRequests(); 238 | getTireStats(); 239 | setTireStats(); 240 | }, 5000); 241 | } 242 | 243 | run(); 244 | -------------------------------------------------------------------------------- /src/tirestats/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tire Stats 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |

Tire Statistics

17 |
18 |
19 |
20 |
21 | 22 |

UNKNOWN

23 |
24 |
25 |
26 |

Top Times

27 |
28 |
29 |
30 |

No Driver

31 |
32 |
33 |

No Time

34 |
35 |
36 |
37 |
38 |

No Driver

39 |
40 |
41 |

No Time

42 |
43 |
44 |
45 |
46 |

No Driver

47 |
48 |
49 |

No Time

50 |
51 |
52 |
53 |
54 |
55 |

Stats

56 |
57 |
58 |

Laps Driven

59 |

0

60 |
61 |
62 |

Sets Used

63 |

0

64 |
65 |
66 |
67 |
68 |

Delta

69 |
70 |
71 |

FASTEST

72 |
73 |
74 |
75 |
76 |
77 | 78 |

UNKNOWN

79 |
80 |
81 |
82 |

Top Times

83 |
84 |
85 |
86 |

No Driver

87 |
88 |
89 |

No Time

90 |
91 |
92 |
93 |
94 |

No Driver

95 |
96 |
97 |

No Time

98 |
99 |
100 |
101 |
102 |

No Driver

103 |
104 |
105 |

No Time

106 |
107 |
108 |
109 |
110 |
111 |

Stats

112 |
113 |
114 |

Laps Driven

115 |

0

116 |
117 |
118 |

Sets Used

119 |

0

120 |
121 |
122 |
123 |
124 |

Delta

125 |
126 |
127 |

+0.000

128 |
129 |
130 |
131 |
132 |
133 | 134 |

UNKNOWN

135 |
136 |
137 |
138 |

Top Times

139 |
140 |
141 |
142 |

No Driver

143 |
144 |
145 |

No Time

146 |
147 |
148 |
149 |
150 |

No Driver

151 |
152 |
153 |

No Time

154 |
155 |
156 |
157 |
158 |

No Driver

159 |
160 |
161 |

No Time

162 |
163 |
164 |
165 |
166 |
167 |

Stats

168 |
169 |
170 |

Laps Driven

171 |

0

172 |
173 |
174 |

Sets Used

175 |

0

176 |
177 |
178 |
179 |
180 |

Delta

181 |
182 |
183 |

+0.000

184 |
185 |
186 |
187 |
188 |
189 |
190 | 191 | 192 | -------------------------------------------------------------------------------- /src/singlercm/index.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require("electron"); 2 | 3 | const debug = false; 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | // Set sleep 8 | const sleep = (milliseconds) => { 9 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 10 | }; 11 | 12 | // URL output function 13 | function httpGet(theUrl) { 14 | let xmlHttpReq = new XMLHttpRequest(); 15 | xmlHttpReq.open("GET", theUrl, false); 16 | xmlHttpReq.send(null); 17 | return xmlHttpReq.responseText; 18 | } 19 | 20 | WANTED_CATEGORIES = [ 21 | "Flag", 22 | "IncidentNoted", 23 | "IncidentUnderInvestigation", 24 | "IncidentNoFurtherInvestigation", 25 | "IncidentNoFurtherAction", 26 | "LapTimeDeleted", 27 | "OffTrackAndContinued", 28 | "SpunAndContinued", 29 | "MissedApex", 30 | "CarStopped", 31 | "LowGripConditions", 32 | "TimePenalty", 33 | "StopGoPenalty", 34 | "TrackTestCompleted", 35 | "TrackSurfaceSlippery", 36 | "SessionResume", 37 | "SessionStartDelayed", 38 | "SessionDurationChanged", 39 | "Correction", 40 | "Weather", 41 | "PitEntry", 42 | "PitExit", 43 | "NormalGripConditions", 44 | "SafetyCar", 45 | "Drs", 46 | "LappedCarsMayOvertake", 47 | "LappedCarsMayNotOvertake", 48 | "IncidentInvestigationAfterSession", 49 | "Other", 50 | ]; 51 | 52 | WANTED_FLAGS = ["BLACK AND ORANGE", "BLACK AND WHITE", "CHEQUERED"]; 53 | 54 | WANTED_SAFETYCAR = ["SAFETY CAR WILL USE START/FINISH STRAIGHT", "SAFETY CAR THROUGH THE PIT LANE"]; 55 | 56 | async function getConfigurations() { 57 | const configFile = (await ipcRenderer.invoke("get_store")).config; 58 | const host = configFile.network.host; 59 | const port = (await f1mvApi.discoverF1MVInstances(host)).port; 60 | 61 | displayLength = parseInt(configFile.singlercm?.display_duration) ?? 10000; 62 | 63 | config = { 64 | host: host, 65 | port: port, 66 | }; 67 | } 68 | 69 | let queue = []; 70 | let oldRaceControlMessages = []; 71 | async function getRaceControlMessages() { 72 | const raceControlMessages = (await f1mvApi.LiveTimingAPIGraphQL(config, "RaceControlMessages")).RaceControlMessages 73 | .Messages; 74 | for (const message of raceControlMessages) { 75 | const stringMessage = JSON.stringify(message); 76 | if (!oldRaceControlMessages.includes(stringMessage)) { 77 | oldRaceControlMessages.push(stringMessage); 78 | console.log(message); 79 | queue.push(stringMessage); 80 | } 81 | } 82 | } 83 | 84 | async function runQueue(count) { 85 | if (count === 0) queue = []; 86 | for (const message of queue) { 87 | const jsonMessage = JSON.parse(message); 88 | if (isMessageWanted(jsonMessage)) await showMessage(jsonMessage); 89 | } 90 | queue = []; 91 | } 92 | 93 | async function run() { 94 | await getConfigurations(); 95 | let count = 0; 96 | while (true) { 97 | await getRaceControlMessages(); 98 | await runQueue(count); 99 | await sleep(1000); 100 | count++; 101 | } 102 | } 103 | run(); 104 | 105 | async function showMessage(raceControlMessage) { 106 | console.log("Showing message"); 107 | const category = raceControlMessage.SubCategory ? raceControlMessage.SubCategory : raceControlMessage.Category; 108 | const message = await (async () => { 109 | let formattedMessage = raceControlMessage.Message; 110 | 111 | const messageArray = formattedMessage.split(" "); 112 | 113 | console.log(formattedMessage); 114 | 115 | formattedMessage = formattedMessage.replace("DELETED", 'DELETED'); 116 | formattedMessage = formattedMessage.replace("PENALTY", 'PENALTY'); 117 | formattedMessage = formattedMessage.replace("WARNING", 'WARNING'); 118 | formattedMessage = formattedMessage.replace("INCIDENT", 'INCIDENT'); 119 | 120 | formattedMessage = formattedMessage.replace("OPEN", 'OPEN'); 121 | formattedMessage = formattedMessage.replace("CLOSED", 'CLOSED'); 122 | 123 | formattedMessage = formattedMessage.replace("ENABLED", 'ENABLED'); 124 | formattedMessage = formattedMessage.replace("DISABLED", 'DISABLED'); 125 | 126 | const driverList = (await f1mvApi.LiveTimingAPIGraphQL(config, "DriverList")).DriverList; 127 | for (const driver in driverList) { 128 | const driverTla = driverList[driver].Tla; 129 | if (formattedMessage.includes(`(${driverTla})`)) { 130 | const messageDriverIndex = messageArray.indexOf(`(${driverTla})`); 131 | const driverNumber = messageArray[messageDriverIndex - 1]; 132 | 133 | formattedMessage = formattedMessage.replace( 134 | `${driverNumber} (${driverTla})`, 135 | `${driverNumber} (${driverTla})` 136 | ); 137 | 138 | console.log(formattedMessage); 139 | } 140 | } 141 | 142 | return formattedMessage; 143 | })(); 144 | 145 | const bar = document.getElementById("RCM"); 146 | const text = document.getElementById("message"); 147 | const icon = document.getElementById("icon"); 148 | 149 | const image = getImage(raceControlMessage, category); 150 | const path = "../icons/" + image; 151 | 152 | icon.src = path; 153 | text.innerHTML = message; 154 | 155 | bar.className = "shown"; 156 | 157 | await sleep(displayLength); 158 | 159 | bar.className = "hidden"; 160 | 161 | await sleep(2000); 162 | } 163 | 164 | function isMessageWanted(message) { 165 | const category = message.SubCategory ? message.SubCategory : message.Category; 166 | 167 | console.log(category); 168 | 169 | if (WANTED_CATEGORIES.includes(category)) { 170 | switch (category) { 171 | case "Flag": 172 | return WANTED_FLAGS.includes(message.Flag); 173 | case "SafetyCar": 174 | return WANTED_SAFETYCAR.includes(message.Message); 175 | default: 176 | return true; 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | function getImage(message, category) { 183 | switch (WANTED_CATEGORIES.indexOf(category)) { 184 | case 0: 185 | if (message.Flag === "BLACK AND ORANGE") { 186 | return "flags/flag_blackandorange.png"; 187 | } 188 | if (message.Flag === "BLACK AND WHITE") { 189 | return "flags/flag_blackandwhite.png"; 190 | } 191 | if (message.Flag === "CHEQUERED") { 192 | return "flags/flag_chequered.png"; 193 | } 194 | case 1: 195 | return "messages/incident_noted.png"; 196 | case 2: 197 | return "messages/incident_underinvestigation.png"; 198 | case 3: 199 | return "messages/incident_nofurther_investigation.png"; 200 | case 4: 201 | return "messages/Incident_nofurther_action.png"; 202 | case 5: 203 | return "messages/car_tracklimits.png"; 204 | case 6: 205 | return "messages/car_offtrack.png"; 206 | case 7: 207 | return "messages/car_spun.png"; 208 | case 8: 209 | return "messages/car_missedapex.png"; 210 | case 9: 211 | return "messages/car_stopped.png"; 212 | case 10: 213 | return "messages/grip_low.png"; 214 | case 11: 215 | return "messages/penalty_time.png"; 216 | case 12: 217 | return "messages/penalty_stopandgo.png"; 218 | case 14: 219 | return "messages/grip_slippery.png"; 220 | case 15: 221 | return "messages/session_resume.png"; 222 | case 16: 223 | return "messages/session_startdelayed.png"; 224 | case 17: 225 | return "messages/session_durationchanged.png"; 226 | case 18: 227 | return "messages/correction.png"; 228 | case 19: 229 | return "messages/weather.png"; 230 | case 20: 231 | if (message.Flag === "OPEN") return "messages/pit_entry_open.png"; 232 | if (message.Flag === "CLOSED") return "messages/pit_entry_closed.png"; 233 | case 21: 234 | if (message.Flag === "OPEN") return "messages/pit_exit_open.png"; 235 | if (message.Flag === "CLOSED") return "messages/pit_exit_closed.png"; 236 | case 22: 237 | return "messages/grip_normal.png"; 238 | case 23: 239 | if (message.Message === "SAFETY CAR THROUGH THE PIT LANE") return "messages/safetycar_pitlane.png"; 240 | if (message.Message === "SAFETY CAR WILL USE START/FINISH STRAIGHT") 241 | return "messages/safetycar_startfinish.png"; 242 | case 24: 243 | if (message.Flag == "ENABLED") return "messages/drs_enabled.png"; 244 | if (message.Flag == "DISABLED") return "messages/drs_disabled.png"; 245 | case 25: 246 | return "messages/lappedcars_overtake.png"; 247 | case 26: 248 | return "messages/lappedcars_notovertake.png"; 249 | case 27: 250 | return "messages/incident_investigationafterthesession.png"; 251 | default: 252 | return "blank.png"; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Logo 5 | 6 | 7 |

Ultimate Formula 1 Viewer

8 |

Using MultiViewer for F1

9 | 10 |
11 |
12 |
13 |
14 | 15 | # Information 16 | 17 | ## What is Ultimate Formula 1 Viewer (UF1)? 18 | 19 | This app is a integration on MultiViewer for F1. It adds a lot of visuals and information windows using the MultiViewer API. It is mainly build for personal use but is now open source for everyone to use. The goal of is to make watching Formula 1 more enjoyable and easier to follow. The app is focused on showing information that really helps you understand what is going on the session. A lot of status information, changes in the session and automatic switching between the most important stuff happening on track. 20 | 21 | > **Note:** This app requires MultiViewer for F1 to be installed. 22 | 23 |
24 | 25 | # Installation 26 | 27 | ## Download 28 | 29 | Open the latest release page and download the, for your OS compatible, version of UF1 30 | 31 | > https://github.com/MRAJEKO/UF1-Viewer-with-F1MV/releases/latest 32 | 33 | ## Installation 34 | 35 | Open the file you just downloaded and follow all the steps in the prompt. These windows will guide you through the installation process. 36 | 37 | ## Running 38 | 39 | After the installation is complete you can run the application by opening the start menu and searching for '**Ultimate-F1Viewer-With-F1MV**' or by opening the folder where you installed it and opening the executable file. 40 | 41 |
42 |
43 | 44 | # Usage 45 | 46 | ## Startup Window 47 | 48 | When you first start the application you will be promted to open MultiViewer for F1 and start a (replay) live timing session. You can open MultiViewer by pressing the 'Open MultiViewer' button. To start a (replay) live timing you will need to select a session and start the (replay) live timing at the top. The status pills will change color to show you the status of the connection. If you open your MultiViewer the 'Not connected to MultiViewer' will turn green and show 'Connected to Multiviewer'. If you also launch a (replay) live timing the bottom one will turn green showing a live timing has been found and the window will automatically disappear. 49 | 50 | > **Note:** You are also able to 'continue either way' but this can cause issues and bugs when opening windows. Use at your own risk. 51 | 52 | 53 | 54 |
55 | 56 | ## Main Window 57 | 58 | The main window will be used as the hub for everything else. You are able to launch windows, change settings and choose layouts all from here. You are also able to launch certain windows from MultiViewer for ease of access. If the main window gets closed, all other windows will also close automatically making it very easy to quit. 59 | 60 | ### Launcher 61 | 62 | > The main window is used as a launcher for all other windows. You can launch MultiViewer for F1 from here as well. 63 | 64 | 65 | 66 |
67 |
68 | 69 | ### Settings 70 | 71 | > There is also a settings window that can be opened by clicking the settings icon in the bottom right corner. It will show a section where you can change the settings of different windows. you can also change settings like network settings. 72 | 73 | 74 | 75 |
76 |
77 | 78 | --- 79 | 80 |
81 | 82 | ## Layouts (main window) 83 | 84 | You also have the ability to save layouts and load layouts. 85 | 86 | ### Adding a new layout 87 | 88 | > To create a layout you must press the 'Add new layout' button. A empty layout will then be added. 89 | 90 | ### Saving a layout 91 | 92 | > If you want to save the currently opened windows you can press the save icon to right of you newly added layout. If you have any MultiViewer streams open, these will also be saved. 93 | 94 | ### Editing the layout 95 | 96 | > Pressing the pencil icon to the left will give you the ability to edit the layout. You can change the name of the layout by overwriting the text in the center. When you are done, you are able to press 'confirm' to save the changes or 'cancel' to discard the changes. Deleting the layout will also be possible by pressing the 'delete' button when editing. 97 | 98 | ### Loading a layout 99 | 100 | > To load a layout you can press the name of the layout. This will open all the UF1 windows that are saved in the layout. If there currently is a live session it will also automatically load those correct stream and start the live timing. If this is not the case it will not open any MultiViewer Streams. You are also able to provide a 'contentId' to open a corresponding session which will also launch the correct MultiViewer streams **but not the correct live timing**. 101 | 102 | 103 | 104 |
105 | 106 | ## **Important note** 107 | 108 | For all windows with a gray background, when you press 'Escape' the window will become transparent making it look a lot better. For some windows such as 'Weather Information' or 'Battle mode' you must have it as a transparent window to have all its functionality. For some windows resizing and moving will only be possible when the are **not** transparent 109 | 110 |
111 | 112 | ## Multiviewer Windows 113 | 114 | ### **'MultiViewer'** 115 | 116 | The MultiViewer button will open MultiViewer in the main tab. From there you will be able to select your requested live session or replay and launch it. You can also launch MultiViewer from the begin page. 117 | 118 | > **Note:** Launching MultiViewer this way will use deeplinking through UF1. This means that MultiViewer is connected to UF1 and will automatically close when the UF1 main window is closed. 119 | 120 | ### **'Live Timing'** 121 | 122 | Pressing this button will open the live timing page if there is a live session. This button will be grayed out if there is o live session. 123 | 124 | > **Note:** Launching the live timing this way will use deeplinking through UF1. This means that the live timing page is connected to UF1 and will automatically close when the UF1 main window is closed. 125 | 126 |
127 | 128 | ## Ultimate Formula 1 Viewer (UF1) Windows 129 | 130 | ### **'Flag Display'** 131 | 132 | Will show the current track status. It will blink yellow when a SC or a VSC is triggered and it will turn purple when a new fastest lap has been set. If the track is clear it will show a green flag for a few seconds and then return to a darkgray/black so you can use it as neutral background. 133 | 134 | 135 | 136 | 137 | 138 |
139 | 140 | ### **'Govee Lights'** 141 | 142 | Shows how many govee lights are connected. The Govee integration is disabled by default and you will need to enable LAN Control for you lights in order for it to connect. (the lights are connected to the flag display window) 143 | 144 | 145 | 146 |
147 | 148 | ### **'Delayed Track Time'** 149 | 150 | Shows the current time on track in their timezone. 151 | 152 | 153 | 154 |
155 | 156 | ### **'Session Log'** 157 | 158 | Shows a list of the past change of events in the session. Driver pit entry or exit, pit stop times and tire changes (race only), new fastest laps, new lap starts (race only), DRS changes, new team radio's (disabled by default), ect. A new bar will be generated if a event occurs. 159 | 160 | 161 | 162 |
163 | 164 | ### **'Track Information'** 165 | 166 | Shows information about the track such as DRS being enabled, pit exit or entry being open, session timer, status of the session (whether the session is started), ect. 167 | 168 | 169 | 170 |
171 | 172 | ### **'Sector Statuses'** 173 | 174 | Shows the statuses of all mini sectors (segments) on track. It also shows whether the segment has a slippery surface or not. 175 | 176 | 177 | 178 |
179 | 180 | ### **'Single Race Control Message'** 181 | 182 | Show new race control messages that are coming through. It will also show icons based on their type so you can quickly see the type and importance of the message. It will be shown for a few seconds and then disapear. 183 | 184 | 185 | 186 |
187 | 188 | ### **'Crash Detection'** 189 | 190 | Will show cars that have crashed or need to retire based on the car driving slow or stopping. 191 | 192 | 193 | 194 |
195 | 196 | ### **'Track Rotation Compass'** 197 | 198 | Will point to the north of the track. This is the rotation relative to the MultiViewer track map than might be rotated. North might not be the top of the track but the direction where the compass points to. 199 | 200 | 201 | 202 |
203 | 204 | ### **'Tire Stats'** 205 | 206 | Will show information about the used tires. It will show the top 3 times set using that tire, the total laps driven and sets used of that tire and it will also show the delta to the other tire compounds so you can see which on is the quickest. 207 | 208 | 209 | 210 |
211 | 212 | ### **'Current Laps'** 213 | 214 | Shows all the drivers that are on a push lap. It shows the mini sectors and the sector times. It also shows information to a 'target' driver which is picked based on the session and position. This is most useful during a practice or qualifying session. It also sorts on order of the driver on track meaning the top one will finish their lap first and so forth. 215 | 216 | 217 | 218 |
219 | 220 | ### **'Battle Mode'** 221 | 222 | Select drivers you want to see the gap between. The amount of driver you can select depents on the width of the window. It also shows telemetry and lap times per driver but that gets removed if it wouldn't fit all the selected driver. 223 | 224 | 225 | 226 |
227 | 228 | ### **'Weather Information'** 229 | 230 | It shows the track and air temperature over time and shows the current humidity and pressure. It also has the wind direction and wind speed plus if it is raining or not. 231 | 232 | 233 | 234 |
235 | 236 | ### **'Auto Onboard Camera switcher'** 237 | 238 | Will switch the onboards based on priority. This would be very useful if you can't open all the onboards but don't want to miss anything that is happening on track. It will take in account, pit stops, crashes, within DRS, catching and retirements to set the priority of the drivers. Then it will check how many onboards are show and it will make sure to put the (amount of opened OBC's) most important drivers on display. It will automatically be synced to the main feed meaning you don't need to do anything. You can also reduce the window for it to be less intrusive. 239 | 240 | > **Important:** This will only work if you have the 'Auto Onboard Camera switcher' window on top. Minimizing or putting a different window on top of it will break the functionality. 241 | 242 | > **Note:** You must select your main feed in the settings for it to sync without buffering. Default is the 'international' feed. 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /src/trackinfo/index.js: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | const f1mvApi = require("npm_f1mv_api"); 6 | 7 | // Set global variables 8 | const qualiPartLengths = ["00:18:00", "00:15:00", "00:12:00"]; 9 | const extraTime = "01:00:00"; 10 | 11 | async function getConfigurations() { 12 | const configFile = (await ipcRenderer.invoke("get_store")).config.trackinfo; 13 | const networkConfig = (await ipcRenderer.invoke("get_store")).config.network; 14 | const orientation = configFile.orientation; 15 | const styleType = orientation === "vertical" ? "w" : "h"; 16 | const h2Elements = document.querySelectorAll("h2"); 17 | for (const element of h2Elements) { 18 | element.classList.add(`h2${styleType}`); 19 | } 20 | const pElements = document.querySelectorAll("p"); 21 | for (const element of pElements) { 22 | element.classList.add(`p${styleType}`); 23 | } 24 | pClass = `p${styleType}`; 25 | document.getElementById("wrapper").classList.add(`wrapper${styleType}`); 26 | document.getElementById("container").classList.add(`container${styleType}`); 27 | 28 | if (orientation === "horizontal") { 29 | document.getElementById("session-name-container").style.display = "none"; 30 | } 31 | 32 | host = networkConfig.host; 33 | port = (await f1mvApi.discoverF1MVInstances(host)).port; 34 | } 35 | 36 | let sessionInfo; 37 | 38 | // Requesting the information needed from the api 39 | async function apiRequests() { 40 | const config = { 41 | host: host, 42 | port: port, 43 | }; 44 | const data = await f1mvApi.LiveTimingAPIGraphQL(config, [ 45 | "LapCount", 46 | "TrackStatus", 47 | "SessionStatus", 48 | "TimingData", 49 | "TimingStats", 50 | "ExtrapolatedClock", 51 | "SessionData", 52 | "RaceControlMessages", 53 | "SessionInfo", 54 | ]); 55 | lapCount = data.LapCount; 56 | trackStatus = data.TrackStatus; 57 | sessionStatus = data.SessionStatus.Status; 58 | timingData = data.TimingData; 59 | timingStats = data.TimingStats.Lines; 60 | extrapolatedClock = data.ExtrapolatedClock; 61 | sessionData = data.SessionData; 62 | raceControlMessages = data.RaceControlMessages.Messages; 63 | sessionInfo = data.SessionInfo; 64 | console.log(data); 65 | clockData = await f1mvApi.LiveTimingClockAPIGraphQL(config, [ 66 | "paused", 67 | "systemTime", 68 | "trackTime", 69 | "liveTimingStartTime", 70 | ]); 71 | } 72 | 73 | function parseTime(time) { 74 | console.log(time); 75 | const [seconds, minutes, hours] = time 76 | .split(":") 77 | .reverse() 78 | .map((number) => parseInt(number)); 79 | 80 | if (hours === undefined) return (minutes * 60 + seconds) * 1000; 81 | 82 | return (hours * 3600 + minutes * 60 + seconds) * 1000; 83 | } 84 | 85 | function parseLapTime(lapTime) { 86 | const [minutes, seconds, milliseconds] = lapTime 87 | .split(/[:.]/) 88 | .map((number) => parseInt(number.replace(/^0+/, "") || "0", 10)); 89 | 90 | if (milliseconds === undefined) { 91 | return minutes + seconds / 1000; 92 | } 93 | 94 | return minutes * 60 + seconds + milliseconds / 1000; 95 | } 96 | 97 | function getTime(ms) { 98 | const date = new Date(ms); 99 | 100 | console.log(date); 101 | 102 | const hours = date.getUTCHours().toString().padStart(2, "0"); 103 | const minutes = date.getUTCMinutes().toString().padStart(2, "0"); 104 | const seconds = date.getUTCSeconds().toString().padStart(2, "0"); 105 | 106 | if (parseInt(hours) === 0) { 107 | return `${minutes}:${seconds}`; 108 | } 109 | 110 | return `${hours}:${minutes}:${seconds}`; 111 | } 112 | 113 | // Set the status from the session 114 | function setSession() { 115 | let status = "ONSCHEDULE"; 116 | let color = "green"; 117 | switch (sessionStatus) { 118 | case "Started": 119 | status = "ONGOING"; 120 | break; 121 | case "Aborted": 122 | status = "SUSPENDED"; 123 | color = "red"; 124 | break; 125 | case "Finished": 126 | case "Finalised": 127 | case "Ends": 128 | status = "FINISHED"; 129 | color = "gray"; 130 | break; 131 | case "Inactive": 132 | for (const message of raceControlMessages) { 133 | if (!message.Message.includes("DELAYED")) break; 134 | status = "DELAYED"; 135 | color = "orange"; 136 | } 137 | break; 138 | } 139 | 140 | const statusElement = document.getElementById("session"); 141 | statusElement.textContent = status; 142 | statusElement.className = `${pClass} ${color}`; 143 | 144 | const sessionNameElement = document.getElementById("session-name"); 145 | sessionNameElement.textContent = sessionInfo.Name.toUpperCase(); 146 | } 147 | 148 | // Setting the timers for the current session 149 | function setTrackTime() { 150 | const now = new Date(); 151 | const systemTime = clockData.systemTime; 152 | const trackTime = clockData.trackTime; 153 | const paused = clockData.paused; 154 | 155 | const offset = parseTime(sessionInfo.GmtOffset); 156 | 157 | console.log(offset); 158 | 159 | console.log(now.getTime()); 160 | console.log(systemTime); 161 | console.log(trackTime); 162 | 163 | const localTime = parseInt(paused ? trackTime : now - (systemTime - trackTime)) + offset; 164 | 165 | const displayTime = getTime(localTime); 166 | 167 | const color = paused ? "gray" : "green"; 168 | 169 | const trackTimeElement = document.getElementById("time"); 170 | trackTimeElement.textContent = displayTime; 171 | trackTimeElement.className = `${pClass} ${color}`; 172 | } 173 | 174 | function setSessionTimer() { 175 | const now = new Date(); 176 | const paused = clockData.paused; 177 | const extrapolatedClockStart = new Date(extrapolatedClock.Utc); 178 | const extrapolatedTime = extrapolatedClock.Remaining; 179 | const systemTime = clockData.systemTime; 180 | const trackTime = clockData.trackTime; 181 | const extrapolating = extrapolatedClock.Extrapolating; 182 | 183 | const sessionDuration = parseTime(extrapolatedTime) + 1000; 184 | 185 | console.log(sessionDuration); 186 | 187 | const timer = extrapolating 188 | ? paused 189 | ? getTime(sessionDuration - (trackTime - extrapolatedClockStart)) 190 | : getTime(sessionDuration - (now - (systemTime - trackTime) - extrapolatedClockStart)) 191 | : extrapolatedTime; 192 | 193 | const color = paused ? "gray" : extrapolating ? "green" : "gray"; 194 | 195 | const sessionTimerElement = document.getElementById("session-timer"); 196 | sessionTimerElement.textContent = timer; 197 | sessionTimerElement.className = `${pClass} ${color}`; 198 | 199 | return parseTime(timer); 200 | } 201 | 202 | function setExtraTimer() { 203 | const extraTimerElement = document.getElementById("extra-timer"); 204 | 205 | if (sessionInfo.Type !== "Race" || !sessionData) { 206 | extraTimerElement.className = "hidden"; 207 | return; 208 | } 209 | 210 | let color = "gray"; 211 | let displayTime; 212 | 213 | const now = new Date(); 214 | const paused = clockData.paused; 215 | const systemTime = clockData.systemTime; 216 | const trackTime = clockData.trackTime; 217 | const extrapolatedClockStart = new Date(extrapolatedClock.Utc); 218 | 219 | let extraTimeUsed = 0; 220 | let aborted = false; 221 | let startTime = 0; 222 | for (const status of sessionData.StatusSeries) { 223 | if (aborted && status.SessionStatus === "Started") { 224 | extraTimeUsed = new Date(status.Utc) - startTime; 225 | aborted = false; 226 | continue; 227 | } 228 | 229 | if (status.SessionStatus === "Aborted") { 230 | startTime = new Date(status.Utc); 231 | aborted = true; 232 | } 233 | } 234 | 235 | const extraTimeMs = parseTime(extraTime); 236 | 237 | const remaining = extraTimeMs - extraTimeUsed; 238 | 239 | if (sessionStatus === "Aborted" && !extrapolatedClock.Extrapolating) { 240 | displayTime = paused 241 | ? getTime(remaining - (trackTime - extrapolatedClockStart)) 242 | : getTime(remaining - (now - (systemTime - trackTime) - extrapolatedClockStart)); 243 | color = "green"; 244 | } else { 245 | displayTime = remaining > 0 ? getTime(remaining) : getTime(0); 246 | } 247 | 248 | extraTimerElement.textContent = displayTime; 249 | extraTimerElement.className = `${pClass} ${color}`; 250 | } 251 | 252 | function setProgress(timer) { 253 | const sessionLength = new Date(sessionInfo.EndDate) - new Date(sessionInfo.StartDate); 254 | 255 | const progressElement = document.getElementById("progress"); 256 | 257 | let totalTimers = sessionLength; 258 | 259 | if (sessionInfo.Type === "Race") { 260 | document.getElementById("quali-part").className = "hidden"; 261 | 262 | let fastestLap = 0; 263 | for (const driverNumber in timingStats) { 264 | const driverTimingStats = timingStats[driverNumber]; 265 | 266 | if (driverTimingStats.PersonalBestLapTime.Position === 1) { 267 | fastestLap = parseLapTime(driverTimingStats.PersonalBestLapTime.Value); 268 | break; 269 | } 270 | } 271 | 272 | console.log(timer); 273 | 274 | const finished = sessionStatus === "Finished" || sessionStatus === "Finalised" || sessionStatus === "Ends"; 275 | 276 | const maxLaps = 277 | fastestLap === 0 278 | ? lapCount.TotalLaps 279 | : finished 280 | ? lapCount.CurrentLap 281 | : Math.ceil(timer / (fastestLap * 1000) + lapCount.CurrentLap + 1); 282 | 283 | const maxLapPercentage = Math.floor((maxLaps / lapCount.TotalLaps) * 100); 284 | const color = finished 285 | ? "gray" 286 | : maxLaps < lapCount.TotalLaps 287 | ? maxLapPercentage <= 75 288 | ? "red" 289 | : "orange" 290 | : "green"; 291 | 292 | console.log(lapCount.CurrentLap / lapCount.TotalLaps); 293 | 294 | const lapPercentage = Math.floor((lapCount.CurrentLap / lapCount.TotalLaps) * 100); 295 | 296 | progressElement.textContent = 297 | maxLapPercentage === 100 && finished 298 | ? "CONCLUDED" 299 | : `${lapPercentage === 100 ? "99" : lapPercentage}% - ${ 300 | maxLapPercentage > 100 ? "100" : maxLapPercentage 301 | }%`; 302 | progressElement.className = `${pClass} ${color}`; 303 | 304 | const displayLapCount = `Lap: ${lapCount.CurrentLap}/${ 305 | maxLaps > lapCount.TotalLaps ? lapCount.TotalLaps : maxLaps 306 | }`; 307 | 308 | const lapCountElement = document.getElementById("lapcount"); 309 | 310 | lapCountElement.textContent = displayLapCount; 311 | lapCountElement.className = `${pClass} ${color}`; 312 | } else { 313 | document.getElementById("lapcount").className = "hidden"; 314 | 315 | if (sessionInfo.Type === "Qualifying") { 316 | const qualiPartElement = document.getElementById("quali-part"); 317 | 318 | const qualiPart = timingData.SessionPart; 319 | totalTimers = parseTime(qualiPartLengths[qualiPart - 1]); 320 | 321 | qualiPartElement.textContent = `Q${qualiPart} - Q${qualiPartLengths.length}`; 322 | qualiPartElement.classList = `${pClass} ${timer === 0 && qualiPart === 3 ? "gray" : "green"}`; 323 | } else { 324 | document.getElementById("quali-part").className = "hidden"; 325 | } 326 | 327 | const timerProgress = totalTimers - timer; 328 | 329 | const progress = Math.floor((timerProgress / totalTimers) * 100); 330 | 331 | const max = 100; 332 | 333 | const sessionEnded = timer === 0; 334 | 335 | const displayProgress = sessionEnded ? "CONCLUDED" : `${progress}% - ${max}%`; 336 | 337 | progressElement.textContent = displayProgress; 338 | progressElement.className = `${pClass} ${sessionEnded ? "gray" : "green"}`; 339 | 340 | console.log(progress); 341 | 342 | console.log(timer); 343 | console.log(totalTimers); 344 | } 345 | } 346 | 347 | function setPitlane(message) { 348 | if ( 349 | !( 350 | message.SubCategory === "PitEntry" || 351 | message.SubCategory === "PitExit" || 352 | message.Flag === "RED" || 353 | sessionStatus === "Aborted" 354 | ) 355 | ) 356 | return; 357 | 358 | const pitExitElement = document.getElementById(`pit-exit`); 359 | 360 | if (message.Flag === "RED") { 361 | pitExitElement.className = `${pClass} red`; 362 | return; 363 | } 364 | 365 | const type = message.SubCategory === "PitEntry" ? "entry" : "exit"; 366 | 367 | let color = "red"; 368 | switch (message.Flag) { 369 | case "OPEN": 370 | color = "green"; 371 | break; 372 | case "CLOSED": 373 | color = "red"; 374 | break; 375 | } 376 | 377 | const pitLaneElement = document.getElementById(`pit-${type}`); 378 | pitLaneElement.textContent = type.toUpperCase(); 379 | pitLaneElement.className = `${pClass} ${color}`; 380 | } 381 | 382 | // Setting the grip status to the information screen 383 | function setGrip(message) { 384 | switch (message.SubCategory) { 385 | case "LowGripConditions": 386 | grip = "LOW"; 387 | color = "orange"; 388 | break; 389 | case "NormalGripConditions": 390 | grip = "NORMAL"; 391 | color = "green"; 392 | break; 393 | default: 394 | return; 395 | } 396 | 397 | const gripElement = document.getElementById("grip"); 398 | gripElement.textContent = grip; 399 | gripElement.className = `${pClass} ${color}`; 400 | } 401 | 402 | function setHeadPadding(message) { 403 | if (!(message.Category === "Other" && message.Message.includes("HEAD PADDING MATERIAL"))) return; 404 | 405 | let padding = "UNKNOWN"; 406 | let color = "white"; 407 | if (message.Message.includes("BLUE")) { 408 | padding = "BLUE"; 409 | color = "blue"; 410 | } else if (message.Message.includes("LIGHT BLUE")) { 411 | padding = "LIGHT BLUE"; 412 | color = "light-blue"; 413 | } else if (message.Message.includes("PINK")) { 414 | padding = "PINK"; 415 | color = "pink"; 416 | } 417 | const headPaddingElement = document.getElementById("head-padding"); 418 | 419 | headPaddingElement.textContent = padding; 420 | headPaddingElement.className = `${pClass} ${color}`; 421 | } 422 | 423 | function setDrs(message) { 424 | const category = message.Category; 425 | 426 | if (category !== "Drs") return; 427 | 428 | let status = "DISABLED"; 429 | let color = "red"; 430 | switch (message.Status) { 431 | case "DISABLED": 432 | status = message.Status; 433 | color = "red"; 434 | break; 435 | case "ENABLED": 436 | status = message.Status; 437 | color = "green"; 438 | break; 439 | } 440 | 441 | const drsElement = document.getElementById("drs"); 442 | drsElement.textContent = status; 443 | drsElement.className = `${pClass} ${color}`; 444 | } 445 | 446 | function setManTires(message) { 447 | if (!message.Message.includes("TYRES" || "TIRES")) return; 448 | 449 | let tires = "NONE"; 450 | let color = "white"; 451 | 452 | if (message.Message.includes("USE OF WET WEATHER")) { 453 | tires = "INTERMEDIATES"; 454 | backgroundColor = "green"; 455 | } else if (message.Message.includes("EXTREME TIRES" || "EXTREME TYRES")) { 456 | tires = "FULL WETS"; 457 | backgroundColor = "blue"; 458 | } 459 | 460 | const manTiresElement = document.getElementById("tires"); 461 | manTiresElement.textContent = tires; 462 | manTiresElement.className = `${pClass} ${color}`; 463 | } 464 | 465 | const pastMessages = []; 466 | function forRaceControlMessages() { 467 | for (const message of raceControlMessages) { 468 | if (pastMessages.includes(JSON.stringify(message))) continue; 469 | pastMessages.push(JSON.stringify(message)); 470 | console.log(message); 471 | setGrip(message); 472 | setHeadPadding(message); 473 | setDrs(message); 474 | setPitlane(message); 475 | } 476 | } 477 | 478 | // Running all the functions 479 | async function run() { 480 | await getConfigurations(); 481 | await apiRequests(); 482 | setSession(); 483 | setTrackTime(); 484 | const timer = setSessionTimer(); 485 | setExtraTimer(); 486 | setProgress(timer); 487 | forRaceControlMessages(); 488 | } 489 | 490 | // Running the whole screen 491 | run(); 492 | setInterval(async () => { 493 | await run(); 494 | }, 500); 495 | --------------------------------------------------------------------------------