embed frontend static files
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hauk
|
||||||
7
frontend/assets/controls/locate-active.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(0 -270.54)">
|
||||||
|
<path d="m15.53 280.84a2.3011 2.3011 0 0 1-2.3011 2.3011 2.3011 2.3011 0 0 1-2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011 2.3011zm-2.2892 10.177c-0.33195-1.6295-0.91726-2.9856-1.6261-4.2424-0.52582-0.93224-1.1349-1.7927-1.6985-2.6968-0.18814-0.30178-0.35051-0.6206-0.5313-0.93378-0.36149-0.62626-0.65458-1.3524-0.63596-2.2942 0.018196-0.92025 0.28436-1.6584 0.66816-2.2621 0.63125-0.99278 1.6886-1.8067 3.1073-2.0206 1.16-0.17489 2.2476 0.12058 3.0188 0.57155 0.63019 0.36853 1.1183 0.86082 1.4893 1.441 0.38721 0.60553 0.65386 1.3209 0.67621 2.254 0.01147 0.47806-0.06679 0.92076-0.17708 1.288-0.11158 0.37171-0.29105 0.68242-0.45075 1.0143-0.31178 0.64786-0.70258 1.2414-1.0948 1.8354-1.1683 1.7692-2.2649 3.5734-2.7451 6.0456z" fill="#1e90ff" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
7
frontend/assets/controls/locate-inactive.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(0 -270.54)">
|
||||||
|
<path d="m15.53 280.84a2.3011 2.3011 0 0 1-2.3011 2.3011 2.3011 2.3011 0 0 1-2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011 2.3011zm-2.2892 10.177c-0.33195-1.6295-0.91726-2.9856-1.6261-4.2424-0.52582-0.93224-1.1349-1.7927-1.6985-2.6968-0.18814-0.30178-0.35051-0.6206-0.5313-0.93378-0.36149-0.62626-0.65458-1.3524-0.63596-2.2942 0.018196-0.92025 0.28436-1.6584 0.66816-2.2621 0.63125-0.99278 1.6886-1.8067 3.1073-2.0206 1.16-0.17489 2.2476 0.12058 3.0188 0.57155 0.63019 0.36853 1.1183 0.86082 1.4893 1.441 0.38721 0.60553 0.65386 1.3209 0.67621 2.254 0.01147 0.47806-0.06679 0.92076-0.17708 1.288-0.11158 0.37171-0.29105 0.68242-0.45075 1.0143-0.31178 0.64786-0.70258 1.2414-1.0948 1.8354-1.1683 1.7692-2.2649 3.5734-2.7451 6.0456z" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
9
frontend/assets/controls/locate-pending.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(0 -270.54)">
|
||||||
|
<path d="m15.53 280.84a2.3011 2.3011 0 0 1-2.3011 2.3011 2.3011 2.3011 0 0 1-2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011-2.3011 2.3011 2.3011 0 0 1 2.3011 2.3011zm-2.2892 10.177c-0.33195-1.6295-0.91726-2.9856-1.6261-4.2424-0.52582-0.93224-1.1349-1.7927-1.6985-2.6968-0.18814-0.30178-0.35051-0.6206-0.5313-0.93378-0.36149-0.62626-0.65458-1.3524-0.63596-2.2942 0.018196-0.92025 0.28436-1.6584 0.66816-2.2621 0.63125-0.99278 1.6886-1.8067 3.1073-2.0206 1.16-0.17489 2.2476 0.12058 3.0188 0.57155 0.63019 0.36853 1.1183 0.86082 1.4893 1.441 0.38721 0.60553 0.65386 1.3209 0.67621 2.254 0.01147 0.47806-0.06679 0.92076-0.17708 1.288-0.11158 0.37171-0.29105 0.68242-0.45075 1.0143-0.31178 0.64786-0.70258 1.2414-1.0948 1.8354-1.1683 1.7692-2.2649 3.5734-2.7451 6.0456z" fill-rule="evenodd">
|
||||||
|
<animate attributeName="fill" values="#000;#1e90ff;#000" dur="1s" repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
9
frontend/assets/controls/radar.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="100px" height="100px" enable-background="new 0 0 348.6 349" fill="#000000" version="1.1" viewBox="0 0 348.6 349" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(.51243 0 0 .51243 84.626 84.826)">
|
||||||
|
<path d="m175 171.17c38.914 0 70.463-38.318 70.463-85.586 0-47.269-10.358-85.587-70.463-85.587s-70.465 38.318-70.465 85.587c0 47.268 31.549 85.586 70.465 85.586z"/>
|
||||||
|
<path d="m41.909 301.85c-0.012-2.882-0.024-0.812 0 0z"/>
|
||||||
|
<path d="m308.08 304.1c0.038-0.789 0.013-5.474 0 0z"/>
|
||||||
|
<path d="m307.94 298.4c-1.305-82.342-12.059-105.8-94.352-120.66 0 0-11.584 14.761-38.584 14.761s-38.586-14.761-38.586-14.761c-81.395 14.69-92.803 37.805-94.303 117.98-0.123 6.547-0.18 6.891-0.202 6.131 5e-3 1.424 0.011 4.058 0.011 8.651 0 0 19.592 39.496 133.08 39.496 113.49 0 133.08-39.496 133.08-39.496 0-2.951 2e-3 -5.003 5e-3 -6.399-0.022 0.47-0.066-0.441-0.149-5.708z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 962 B |
BIN
frontend/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
8
frontend/assets/favicon.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
28
frontend/assets/lang/ca.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/ca_ES/badges/static/images/badges/ca_badge_web_generic.png",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ca.png",
|
||||||
|
"btn_decrypt": "Desxifrar",
|
||||||
|
"btn_cancel": "Cancel·la",
|
||||||
|
"btn_dismiss": "Rebutjar",
|
||||||
|
"last_update_days": "Fa {{time}} dies",
|
||||||
|
"last_update_hours": "Fa {{time}} hores",
|
||||||
|
"last_update_minutes": "Fa {{time}} minuts",
|
||||||
|
"last_update_seconds": "Fa {{time}} segons",
|
||||||
|
"status_offline": "Fora de línia",
|
||||||
|
"status_expired": "Caducat",
|
||||||
|
"dialog_connection_body": "S'ha perdut la connexió amb el servidor Hauk. Hauk intentarà tornar a connectar en segon pla. Comproveu que teniu connexió a la xarxa.",
|
||||||
|
"dialog_connection_head": "Error de connexió",
|
||||||
|
"dialog_expired_body": "Aquesta compartició d’ubicació ha caducat.",
|
||||||
|
"dialog_expired_head": "La compartició ha caducat",
|
||||||
|
"point_app_to": "Apunteu l'aplicació Hauk a aquest servidor per compartir la vostra ubicació:",
|
||||||
|
"gnss_signal_body": "El remitent està esperant el senyal GPS",
|
||||||
|
"gnss_signal_head": "Si us plau, esperi",
|
||||||
|
"e2e_unsupported": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Sembla que el vostre navegador no admet les funcions criptogràfiques necessàries per desxifrar aquestes accions. Torneu-ho a provar amb un altre navegador web.",
|
||||||
|
"e2e_unavailable_secure": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Actualment, el desxiframent no està disponible perquè no utilitzeu HTTPS. Assegureu-vos que utilitzeu HTTPS i, després, torneu-ho a provar.",
|
||||||
|
"e2e_incorrect": "La contrasenya de xifrat que heu introduït no és correcta. Si us plau torna-ho a provar.",
|
||||||
|
"e2e_password_prompt": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Introduïu la contrasenya de xifratge per accedir al recurs compartit.",
|
||||||
|
"e2e_placeholder": "Contrasenya de xifrat",
|
||||||
|
"e2e_title": "Xifrat d'extrem a extrem",
|
||||||
|
"expired_body": "La ubicació compartida a la qual heu intentat accedir no s'ha trobat al servidor. Si aquest enllaç funcionava abans, la participació podria haver caducat.",
|
||||||
|
"expired_head": "L'ubicació ha expirat"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/de.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/de_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Verwerfen",
|
||||||
|
"status_offline": "Offline",
|
||||||
|
"status_expired": "Abgelaufen",
|
||||||
|
"dialog_connection_body": "Die Verbindung zum Hauk-Server wurde unterbrochen. Hauk wird versuchen, sich im Hintergrund wieder zu verbinden. Bitte prüfen Sie, ob Sie über eine Netzwerkverbindung verfügen.",
|
||||||
|
"dialog_connection_head": "Verbindungsfehler",
|
||||||
|
"dialog_expired_body": "Diese Standortfreigabe ist abgelaufen.",
|
||||||
|
"dialog_expired_head": "Freigabe abgelaufen",
|
||||||
|
"point_app_to": "In der Hauk App diese URL eingeben, um Ihren Standort zu teilen:",
|
||||||
|
"gnss_signal_body": "Der Sender wartet auf ein GPS-Signal",
|
||||||
|
"gnss_signal_head": "Bitte warten",
|
||||||
|
"expired_body": "Die Standortfreigabe, auf die Sie zugreifen wollen, wurde nicht auf dem Server gefunden. Wenn dieser Link zuvor funktioniert hat, ist die Freigabe möglicherweise abgelaufen.",
|
||||||
|
"expired_head": "Standortfreigabe abgelaufen",
|
||||||
|
"btn_decrypt": "Entsperren",
|
||||||
|
"btn_cancel": "Abbrechen",
|
||||||
|
"last_update_days": "{{time}}T her",
|
||||||
|
"last_update_hours": "{{time}}S her",
|
||||||
|
"last_update_minutes": "{{time}}M her",
|
||||||
|
"last_update_seconds": "{{time}}S her",
|
||||||
|
"e2e_unsupported": "Diese Freigabe ist passwortgeschützt. Dem Browser fehlt die Funktionalität solche Freigaben zu öffnen. Bitte mit einem anderen Browser noch einmal probieren.",
|
||||||
|
"e2e_unavailable_secure": "Diese Freigabe ist passwortgeschützt. Die Freigabe kann nicht entsperrt werden, da kein HTTPS benutzt wird. Bitte sicherstellen, dass HTTPS benutzt wird, dann noch einmal probieren.",
|
||||||
|
"e2e_incorrect": "Das eingegebene Passwort ist falsch. Bitte noch einmal probieren.",
|
||||||
|
"e2e_password_prompt": "Diese Freigabe ist passwortgeschützt. Bitte das Passwort eingeben, um der Freigabe beizutreten.",
|
||||||
|
"e2e_placeholder": "Passwort eingeben",
|
||||||
|
"e2e_title": "Passwortgeschützt",
|
||||||
|
"google_play_badge_text": "Hole es von Google Play",
|
||||||
|
"f_droid_badge_text": "Hole es von F-Droid",
|
||||||
|
"btn_show_all": "Zeige alle",
|
||||||
|
"btn_close": "Schließen",
|
||||||
|
"control_show_self": "Zeige meine Position",
|
||||||
|
"dialog_user_navigate": "Navigiere zu",
|
||||||
|
"dialog_user_follow": "Auf der Karte zeigen",
|
||||||
|
"dialog_active_head": "Aktive Personen"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/en.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"expired_head": "Location expired",
|
||||||
|
"expired_body": "The shared location you tried to access was not found on the server. If this link worked before, the share might have expired.",
|
||||||
|
"e2e_title": "Password protected",
|
||||||
|
"e2e_placeholder": "Enter password",
|
||||||
|
"e2e_password_prompt": "This share is password protected. Please enter the password to access the share.",
|
||||||
|
"e2e_incorrect": "The password you entered was wrong. Please try again.",
|
||||||
|
"e2e_unavailable_secure": "This share is password protected. You cannot unlock this share because you are not using HTTPS. Please ensure you are using HTTPS, then try again.",
|
||||||
|
"e2e_unsupported": "This share is password protected. Your browser lacks the functionality required to open such shares. Please try again with another web browser.",
|
||||||
|
"gnss_signal_head": "Please wait",
|
||||||
|
"gnss_signal_body": "Sender is waiting for GPS signal",
|
||||||
|
"point_app_to": "Point the Hauk app to this server to share your location:",
|
||||||
|
"dialog_expired_head": "Share expired",
|
||||||
|
"dialog_expired_body": "This location share has expired.",
|
||||||
|
"dialog_connection_head": "Connection error",
|
||||||
|
"dialog_connection_body": "Connection to the Hauk server was lost. Hauk will try to reconnect in the background. Please check that you have network connectivity.",
|
||||||
|
"dialog_active_head": "Active users",
|
||||||
|
"dialog_user_follow": "Show on map",
|
||||||
|
"dialog_user_navigate": "Navigate to",
|
||||||
|
"status_expired": "Expired",
|
||||||
|
"status_offline": "Offline",
|
||||||
|
"control_show_self": "Show my location",
|
||||||
|
"last_update_seconds": "{{time}}s ago",
|
||||||
|
"last_update_minutes": "{{time}}m ago",
|
||||||
|
"last_update_hours": "{{time}}h ago",
|
||||||
|
"last_update_days": "{{time}}d ago",
|
||||||
|
"btn_dismiss": "Dismiss",
|
||||||
|
"btn_cancel": "Cancel",
|
||||||
|
"btn_decrypt": "Unlock",
|
||||||
|
"btn_close": "Close",
|
||||||
|
"btn_show_all": "Show all",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on.png",
|
||||||
|
"f_droid_badge_text": "Get it on F-Droid",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png",
|
||||||
|
"google_play_badge_text": "Get it on Google Play"
|
||||||
|
}
|
||||||
16
frontend/assets/lang/eu.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-eu.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/eu_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Baztertu",
|
||||||
|
"status_offline": "Deskonektatuta",
|
||||||
|
"status_expired": "Iraungita",
|
||||||
|
"dialog_connection_body": "Hauk zerbitzariarekin konexioa galdu da. Hauk bigarren planoan konektatzen saiatuko da. Egiaztatu baduzula sarera konexioa.",
|
||||||
|
"dialog_connection_head": "Konexio errorea",
|
||||||
|
"dialog_expired_body": "Kokaleku partekatze honek iraungi du.",
|
||||||
|
"dialog_expired_head": "Partekatzea iraungita",
|
||||||
|
"point_app_to": "Zuzendu Hauk aplikazioa zerbitzari honetara kokalekua partekatzeko:",
|
||||||
|
"gnss_signal_body": "Igorlea GPS seinalearen zain dago",
|
||||||
|
"gnss_signal_head": "Itxaron mesedez",
|
||||||
|
"expired_body": "Atzitzen saiatu zaren partekatutako kokalekua ez da zerbitzarian aurkitu. Esteka honek lehen funtzionatzen bazuen, agian partekatzeak iraungi du.",
|
||||||
|
"expired_head": "Kokalekua iraungita"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/fr.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"btn_dismiss": "Annuler",
|
||||||
|
"status_offline": "Hors ligne",
|
||||||
|
"status_expired": "Expiré",
|
||||||
|
"dialog_connection_body": "La connection avec le serveur Hauk a été perdue. Hauk essayera de se reconnecter en tâche de fond. Veuillez vérifier votre connexion réseau.",
|
||||||
|
"dialog_connection_head": "Erreur de connection",
|
||||||
|
"dialog_expired_body": "Ce partage de localisation a expiré.",
|
||||||
|
"dialog_expired_head": "Partage expiré",
|
||||||
|
"point_app_to": "Renseignez ce serveur dans votre application Hauk pour partager votre localisation:",
|
||||||
|
"gnss_signal_body": "La source manque de signal GPS",
|
||||||
|
"gnss_signal_head": "Veuillez patienter",
|
||||||
|
"expired_body": "La localisation que vous recherchez n'a pas été trouvée. Si ce lien fonctionnait, le partage de localisation a peut-être expiré.",
|
||||||
|
"expired_head": "Localisation expirée",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-fr.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/fr_badge_web_generic.png",
|
||||||
|
"e2e_password_prompt": "Ce partage est protégé par un mot de passe. Entrez le mot de passe pour y accéder.",
|
||||||
|
"google_play_badge_text": "Installer par Google Play",
|
||||||
|
"f_droid_badge_text": "Installer par F-Droid",
|
||||||
|
"btn_show_all": "Monter tout",
|
||||||
|
"btn_close": "Fermer",
|
||||||
|
"btn_decrypt": "Déverrouiller",
|
||||||
|
"btn_cancel": "Annuler",
|
||||||
|
"last_update_days": "Il y a {{time}}j",
|
||||||
|
"last_update_hours": "Il y a {{time}}h",
|
||||||
|
"last_update_minutes": "Il y a {{time}}m",
|
||||||
|
"last_update_seconds": "Il y a {{time}}s",
|
||||||
|
"control_show_self": "Montrer ma position",
|
||||||
|
"dialog_user_navigate": "Aller à",
|
||||||
|
"dialog_user_follow": "Monter sur la carte",
|
||||||
|
"dialog_active_head": "Utilisateurs actifs",
|
||||||
|
"e2e_unsupported": "Ce partage est protégé par un mot de passe. Votre navigateur ne dispose pas de la fonctionnalité permettant d'ouvrir ce type de partage. Merci de réessayer avec un autre navigateur.",
|
||||||
|
"e2e_unavailable_secure": "Ce partage est protégé par un mot de passe. Vous ne pouvez pas le débloquer car vous n'utilisez pas HTTPS. Assurez-vous d'utiliser HTTPS, puis réessayez.",
|
||||||
|
"e2e_incorrect": "Le mot de passe que vous avez saisi est incorrect. Veuillez réessayer.",
|
||||||
|
"e2e_placeholder": "Entrez votre mot de passe",
|
||||||
|
"e2e_title": "Protégé par mot de passe"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/it.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"google_play_badge_text": "Disponibile su Google Play",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/it_it/badges/static/images/badges/it_badge_web_generic.png",
|
||||||
|
"f_droid_badge_text": "Disponibile su F-Droid",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-it.png",
|
||||||
|
"btn_show_all": "Mostra tutto",
|
||||||
|
"btn_close": "Chiudi",
|
||||||
|
"btn_decrypt": "Sblocca",
|
||||||
|
"btn_cancel": "Annulla",
|
||||||
|
"btn_dismiss": "Ignora",
|
||||||
|
"last_update_days": "{{time}}g fa",
|
||||||
|
"last_update_hours": "{{time}} ore fa",
|
||||||
|
"last_update_minutes": "{{time}}min fa",
|
||||||
|
"last_update_seconds": "{{time}}sec fa",
|
||||||
|
"control_show_self": "Mostra la mia posizione",
|
||||||
|
"status_offline": "Non in linea",
|
||||||
|
"status_expired": "Scaduto",
|
||||||
|
"dialog_user_navigate": "Naviga verso",
|
||||||
|
"dialog_user_follow": "Mostra sulla mappa",
|
||||||
|
"dialog_active_head": "Utenti attivi",
|
||||||
|
"dialog_connection_body": "La connessione al server Hauk è stata perduta. Hauk tenterà di riconnersi. Controlla di avere una connessione di rete attiva.",
|
||||||
|
"dialog_connection_head": "Errore di connessione",
|
||||||
|
"dialog_expired_body": "Questa condivisione della posizione è scaduta.",
|
||||||
|
"dialog_expired_head": "Condivisione scaduta",
|
||||||
|
"point_app_to": "Inserisci il seguente link nell'app Hauk per condividere la tua posizione:",
|
||||||
|
"gnss_signal_body": "Il mittente è in attesa del segnale GPS",
|
||||||
|
"gnss_signal_head": "Attendere prego",
|
||||||
|
"e2e_unsupported": "Questa condivisione è protetta da password. Il tuo browser non ha le funzionalità necessarie per accedervi. Riprova con un altro browser.",
|
||||||
|
"e2e_unavailable_secure": "La condivisione è protetta da password. Non puoi accedervi perché non stai usando HTTPS. Assicurati di star usando HTTPS e riprova.",
|
||||||
|
"e2e_incorrect": "La password inserita non è corretta. Riprova.",
|
||||||
|
"e2e_password_prompt": "Questa condivisione è protetta da password. Inserire la password per accedervi.",
|
||||||
|
"e2e_placeholder": "Inserire password",
|
||||||
|
"e2e_title": "Protetto da password",
|
||||||
|
"expired_body": "La posizione condivisa a cui hai tentato di accedere non è stata trovata sul server. Se questo indirizzo funzionava in passato, la condivisione potrebbe essere scaduta.",
|
||||||
|
"expired_head": "Posizione scaduta"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/ko.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"btn_dismiss": "닫기",
|
||||||
|
"google_play_badge_text": "Google Play 에서 다운로드",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ko_badge_web_generic.png",
|
||||||
|
"f_droid_badge_text": "F-Droid 에서 다운로드",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ko.png",
|
||||||
|
"btn_show_all": "모두 보기",
|
||||||
|
"btn_close": "닫기",
|
||||||
|
"btn_decrypt": "잠금 해제",
|
||||||
|
"btn_cancel": "취소",
|
||||||
|
"last_update_days": "{{time}}일 전",
|
||||||
|
"last_update_hours": "{{time}}시간 전",
|
||||||
|
"last_update_minutes": "{{time}}분 전",
|
||||||
|
"last_update_seconds": "{{time}}초 전",
|
||||||
|
"control_show_self": "내 위치 표시하기",
|
||||||
|
"status_offline": "오프라인",
|
||||||
|
"status_expired": "만료됨",
|
||||||
|
"dialog_user_navigate": "이 위치로 안내",
|
||||||
|
"dialog_user_follow": "지도에서 보기",
|
||||||
|
"dialog_active_head": "활성 사용자들",
|
||||||
|
"dialog_connection_body": "Hauk 서버로의 접속이 끊겼습니다. Hauk은 백그라운드에서 재접속을 시도하겠습니다. 네트워크 접속 여부를 확인해주세요.",
|
||||||
|
"dialog_connection_head": "접속 오류",
|
||||||
|
"dialog_expired_body": "이 위치 공유가 만료되었습니다.",
|
||||||
|
"dialog_expired_head": "링크가 만료됨",
|
||||||
|
"point_app_to": "위치를 공유하기 위해 Hauk 앱을 이 서버로 설정하여 주십시오:",
|
||||||
|
"gnss_signal_body": "발신자가 GPS 신호를 기다리고 있습니다",
|
||||||
|
"gnss_signal_head": "잠시만 기다려주세요",
|
||||||
|
"e2e_unsupported": "이 링크는 비밀번호로 보호되어 있습니다. 사용하고 계시는 브라우저는 잠금을 해제하기 위한 기능을 지원하지 않습니다. 다른 브라우저로 다시 시도해주세요.",
|
||||||
|
"e2e_unavailable_secure": "이 링크는 비밀번호로 보호되어 있습니다. HTTPS를 사용하고 있지 않기 때문에 보호를 해제할 수 없습니다. HTTPS를 사용중인지 확인하시고 다시 시도해주세요.",
|
||||||
|
"e2e_incorrect": "입력한 비밀번호가 올바르지 않습니다. 다시 시도해주세요.",
|
||||||
|
"e2e_password_prompt": "이 링크는 비밀번호로 보호되어 있습니다. 링크에 접근하기 위해 비밀번호를 입력해주세요.",
|
||||||
|
"e2e_placeholder": "비밀번호를 입력하세요",
|
||||||
|
"e2e_title": "비밀번호로 보호됨",
|
||||||
|
"expired_body": "요청하신 위치정보를 서버에서 찾을 수 없습니다. 기존에 작동하던 링크라면, 위치 공유가 만료되었을 수 있습니다.",
|
||||||
|
"expired_head": "위치 정보가 만료됨"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/nb_NO.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-no.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/no_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Lukk",
|
||||||
|
"status_offline": "Frakoblet",
|
||||||
|
"status_expired": "Utløpt",
|
||||||
|
"dialog_connection_body": "Tilkoblingen til Hauk-serveren forsvant. Hauk vil prøve å koble til igjen i bakgrunnen. Vennligst sjekk at du er tilkoblet nettet.",
|
||||||
|
"dialog_connection_head": "Tilkoblingsfeil",
|
||||||
|
"dialog_expired_body": "Den delte posisjonen du fulgte har utløpt.",
|
||||||
|
"dialog_expired_head": "Deling utløpt",
|
||||||
|
"point_app_to": "Skriv inn følgende link i Hauk-appen for å dele posisjonen din:",
|
||||||
|
"gnss_signal_body": "Sender venter på GPS-signal",
|
||||||
|
"gnss_signal_head": "Vennligst vent",
|
||||||
|
"expired_body": "Den delte posisjonen du prøvde å åpne ble ikke funnet på serveren. Hvis denne linken virket tidligere, kan det hende at delingen har utløpt.",
|
||||||
|
"expired_head": "Posisjon utløpt",
|
||||||
|
"btn_decrypt": "Åpne",
|
||||||
|
"btn_cancel": "Avbryt",
|
||||||
|
"e2e_unsupported": "Denne delte posisjonen er passordbeskyttet. Nettleseren din mangler funksjonaliteten som kreves for å åpne slike delinger. Vennligst prøv igjen med en annen nettleser.",
|
||||||
|
"e2e_unavailable_secure": "Denne delte posisjonen er passordbeskyttet. Du kan ikke åpne denne delingen fordi du ikke bruker HTTPS for øyeblikket. Vennligst last inn siden med HTTPS og prøv igjen.",
|
||||||
|
"e2e_incorrect": "Passordet du skrev inn er feil. Vennligst prøv igjen.",
|
||||||
|
"e2e_password_prompt": "Denne delte posisjonen er passordbeskyttet Vennligst skriv inn passordet for å få tilgang til delingen.",
|
||||||
|
"e2e_placeholder": "Skriv inn passord",
|
||||||
|
"e2e_title": "Passordbeskyttet",
|
||||||
|
"last_update_days": "{{time}}d siden",
|
||||||
|
"last_update_hours": "{{time}}t siden",
|
||||||
|
"last_update_minutes": "{{time}}m siden",
|
||||||
|
"last_update_seconds": "{{time}}s siden",
|
||||||
|
"btn_show_all": "Vis alle",
|
||||||
|
"btn_close": "Lukk",
|
||||||
|
"control_show_self": "Vis posisjonen min",
|
||||||
|
"dialog_user_navigate": "Naviger til",
|
||||||
|
"dialog_user_follow": "Vis på kartet",
|
||||||
|
"dialog_active_head": "Aktive brukere",
|
||||||
|
"google_play_badge_text": "Tilgjengelig på Google Play",
|
||||||
|
"f_droid_badge_text": "Tilgjengelig på F-Droid"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/nl.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"dialog_expired_body": "De locatiedeling is verlopen.",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-nl.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/nl_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Sluiten",
|
||||||
|
"status_offline": "Offline",
|
||||||
|
"status_expired": "Verlopen",
|
||||||
|
"dialog_connection_body": "De verbinding met de Hauk server is verbroken. Hauk zal proberen om opnieuw te verbinden in de achtergrond. Controleer alstublieft dat u een werkende internetverbinding heeft.",
|
||||||
|
"dialog_connection_head": "Verbindingsfout",
|
||||||
|
"dialog_expired_head": "Locatiedeling verlopen",
|
||||||
|
"point_app_to": "Stuur de Hauk app naar deze server om uw locatie te delen:",
|
||||||
|
"gnss_signal_body": "Verzender is aan het wachten op GPS signaal",
|
||||||
|
"gnss_signal_head": "Wacht alstublieft",
|
||||||
|
"expired_body": "The locatiedeling die u probeerde te bekijken kon niet gevonden worden op de server. Als deze link eerder wel werkte, dan zou de sessie verlopen kunnen zijn.",
|
||||||
|
"expired_head": "Locatie verlopen",
|
||||||
|
"btn_decrypt": "Ontgrendelen",
|
||||||
|
"btn_cancel": "Annuleren",
|
||||||
|
"last_update_days": "{{time}}d geleden",
|
||||||
|
"last_update_hours": "{{time}}u geleden",
|
||||||
|
"last_update_minutes": "{{time}}m geleden",
|
||||||
|
"last_update_seconds": "{{time}}s geleden",
|
||||||
|
"e2e_unsupported": "Deze locatiedeling is met wachtwoord beveiligd. Uw browser mist functionaliteit vereist om deze locatiedelingen te openen. Probeer het alstublieft met een andere browser.",
|
||||||
|
"e2e_unavailable_secure": "Deze locatiedeling is met wachtwoord beveiligd. U kunt de locatiedeling niet ontgrendelen omdat u geen HTTPS gebruikt. Zorg ervoor dat u met HTTPS verbindt, en probeer het dan opnieuw.",
|
||||||
|
"e2e_incorrect": "Het ingevoerde wachtwoord is incorrect. Probeer het alstublieft nog eens.",
|
||||||
|
"e2e_password_prompt": "Deze deling is met wachtwoord beveiligd. Vul het wachtwoord in om toegang te krijgen tot de deling.",
|
||||||
|
"e2e_placeholder": "Voer wachtwoord in",
|
||||||
|
"e2e_title": "Beveiligd met wachtwoord",
|
||||||
|
"google_play_badge_text": "Download op Google Play",
|
||||||
|
"f_droid_badge_text": "Download op F-Droid",
|
||||||
|
"btn_show_all": "Toon alle",
|
||||||
|
"btn_close": "Sluiten",
|
||||||
|
"control_show_self": "Toon mijn locatie",
|
||||||
|
"dialog_user_navigate": "Navigeer naar",
|
||||||
|
"dialog_user_follow": "Toon op kaart",
|
||||||
|
"dialog_active_head": "Actieve gebruikers"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/nn.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-no.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/no_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Lukk",
|
||||||
|
"status_offline": "Fråkopla",
|
||||||
|
"status_expired": "Gått ut",
|
||||||
|
"dialog_connection_body": "Tilkoplinga til Hauk-serveren forsvann. Hauk vil prøva å kopla til igjen i bakgrunnen. Vennlegast sjekk at du er tilkopla nett.",
|
||||||
|
"dialog_connection_head": "Tilkoplingsfeil",
|
||||||
|
"dialog_expired_body": "Den delte posisjonen du følgde har gått ut.",
|
||||||
|
"dialog_expired_head": "Deling gått ut",
|
||||||
|
"point_app_to": "Skriv inn følgjande link i Hauk-appen for å dela posisjonen din:",
|
||||||
|
"gnss_signal_body": "Sendar ventar på GPS-signal",
|
||||||
|
"gnss_signal_head": "Ver vennleg og vent",
|
||||||
|
"expired_body": "Den delte posisjonen du prøvde å opna vart ikkje funne på serveren. Viss denne linken verkte tidlegare, kan det hendet at delingen har gått ut.",
|
||||||
|
"expired_head": "Posisjon gått ut",
|
||||||
|
"btn_decrypt": "Opne",
|
||||||
|
"btn_cancel": "Avbryt",
|
||||||
|
"e2e_unsupported": "Denne delte posisjonen krev passord for å opnast. Nettlesaren din mangler dei funksjonane som krevjast for å opna slike delingar. Ver vennleg og prøv igjen med ein annan nettlesar.",
|
||||||
|
"e2e_unavailable_secure": "Denne delte posisjonen krev passord for å opnast. Du kan ikkje opna denne delingen fordi du ikkje bruker HTTPS. Ver venleg og last inn sidan med HTTPS og prøv igjen.",
|
||||||
|
"e2e_incorrect": "Passordet du skreiv inn er feil. Ver vennleg og prøv igjen.",
|
||||||
|
"e2e_password_prompt": "Denne delte posisjonen krev passord for å opnast. Ver vennleg og skriv inn passordet for å få tilgang til delinga.",
|
||||||
|
"e2e_placeholder": "Skriv inn passord",
|
||||||
|
"e2e_title": "Passord krevjast",
|
||||||
|
"last_update_days": "{{time}}d sidan",
|
||||||
|
"last_update_hours": "{{time}}t sidan",
|
||||||
|
"last_update_minutes": "{{time}}m sidan",
|
||||||
|
"last_update_seconds": "{{time}}s sidan",
|
||||||
|
"btn_show_all": "Vis alle",
|
||||||
|
"btn_close": "Lukk",
|
||||||
|
"control_show_self": "Vis posisjonen min",
|
||||||
|
"dialog_user_navigate": "Naviger til",
|
||||||
|
"dialog_user_follow": "Vis på kartet",
|
||||||
|
"dialog_active_head": "Aktive brukarar",
|
||||||
|
"google_play_badge_text": "Tilgjengeleg på Google Play",
|
||||||
|
"f_droid_badge_text": "Tilgjengeleg på F-Droid"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/pl.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"google_play_badge_text": "Pobierz z Google Play",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/pl_badge_web_generic.png",
|
||||||
|
"f_droid_badge_text": "Pobierz z F-Droid",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-pl.png",
|
||||||
|
"btn_show_all": "Pokaż wszystko",
|
||||||
|
"btn_close": "Zamknij",
|
||||||
|
"btn_decrypt": "Odblokuj",
|
||||||
|
"btn_cancel": "Anuluj",
|
||||||
|
"btn_dismiss": "Odrzuć",
|
||||||
|
"last_update_days": "{{time}} dni temu",
|
||||||
|
"last_update_hours": "{{time}}h temu",
|
||||||
|
"last_update_minutes": "{{time}}m temu",
|
||||||
|
"last_update_seconds": "{{time}s temu",
|
||||||
|
"control_show_self": "Pokaż moją lokalizację",
|
||||||
|
"status_offline": "Offline",
|
||||||
|
"status_expired": "Wygasło",
|
||||||
|
"dialog_user_navigate": "Nawiguj do",
|
||||||
|
"dialog_user_follow": "Pokaż na mapie",
|
||||||
|
"dialog_active_head": "Aktywni użytkownicy",
|
||||||
|
"dialog_connection_body": "Połączenie z serwerem Hauk zostało utracone. Hauk spróbuje odzyskać połączenie. Proszę sprawdzić swoje połączenie sieciowe.",
|
||||||
|
"dialog_connection_head": "Błąd połączenia",
|
||||||
|
"dialog_expired_body": "Ta sesja udostępniania wygasła.",
|
||||||
|
"dialog_expired_head": "Sesja wygasła",
|
||||||
|
"point_app_to": "Wprowadź w aplikacji Hauk ten adres serwera:",
|
||||||
|
"gnss_signal_body": "Nadawca oczekuje na sygnał GPS",
|
||||||
|
"gnss_signal_head": "Proszę czekać",
|
||||||
|
"e2e_unsupported": "Ta sesja jest chroniona hasłem. Twoja przeglądarka nie obsługuje funkcji potrzebnych do otwierania takich sesji. Spróbuj użyć innej przeglądarki.",
|
||||||
|
"e2e_unavailable_secure": "Ta sesja jest chroniona hasłem. Nie można jej odblokować, gdyż nie używasz protokołu HTTPS. Upewnij się, że używasz protokołu HTTPS i spróbuj ponownie.",
|
||||||
|
"e2e_incorrect": "Niepoprawne hasło. Spróbuj ponownie.",
|
||||||
|
"e2e_password_prompt": "Ta sesja jest chroniona hasłem. Wprowadź hasło.",
|
||||||
|
"e2e_placeholder": "Wprowadź hasło",
|
||||||
|
"e2e_title": "Chronione hasłem",
|
||||||
|
"expired_body": "Lokalizacja którą próbujesz zobaczyć nie została odnaleziona na serwerze. Jeśli ten link wcześniej działał, oznacza to, że sesja udostępniania mogła wygasnąć.",
|
||||||
|
"expired_head": "Lokalizacja wygasła"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/pt_BR.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"expired_head": "Localização Expirada",
|
||||||
|
"expired_body": "O local compartilhado que você tentou acessar não foi encontrado no servidor. Se esse link funcionou antes, o compartilhamento pode ter expirado.",
|
||||||
|
"e2e_title": "Senha protegia",
|
||||||
|
"e2e_placeholder": "Digite a senha",
|
||||||
|
"e2e_password_prompt": "Esse compartilhamento é protegido por senha. Digite a senha para acessar o compartilhamento.",
|
||||||
|
"e2e_incorrect": "A senha que você digitou estava errada. Por favor, tente novamente.",
|
||||||
|
"e2e_unavailable_secure": "Esse compartilhamento é protegido por senha. Você não pode desbloquear esse compartilhamento porque não está usando HTTPS. Verifique se você está usando HTTPS e tente novamente.",
|
||||||
|
"e2e_unsupported": "Esse compartilhamento é protegido por senha. Seu navegador não possui a funcionalidade necessária para abrir esses compartilhamentos. Tente novamente com outro navegador da web.",
|
||||||
|
"gnss_signal_head": "Por favor, aguarde",
|
||||||
|
"gnss_signal_body": "Remetente está aguardando sinal de GPS",
|
||||||
|
"point_app_to": "Aponte o aplicativo Hauk para este servidor para compartilhar sua localização:",
|
||||||
|
"dialog_expired_head": "Compartilhamento expirado",
|
||||||
|
"dialog_expired_body": "Este compartilhamento de local expirou.",
|
||||||
|
"dialog_connection_head": "Erro na conecção",
|
||||||
|
"dialog_connection_body": "A conexão com o servidor Hauk foi perdida. Hauk tentará se reconectar em segundo plano. Verifique se você possui conectividade de rede.",
|
||||||
|
"dialog_active_head": "Usuarios Ativos",
|
||||||
|
"dialog_user_follow": "Mostrar no mapa",
|
||||||
|
"dialog_user_navigate": "Navegar para",
|
||||||
|
"status_expired": "Expirado",
|
||||||
|
"status_offline": "Offline",
|
||||||
|
"control_show_self": "Mostrar minha localização",
|
||||||
|
"last_update_seconds": "{{time}} segundos atras",
|
||||||
|
"last_update_minutes": "{{time}} minutos atras",
|
||||||
|
"last_update_hours": "{{time}} horas atras",
|
||||||
|
"last_update_days": "{{time}} dias atras",
|
||||||
|
"btn_dismiss": "Dispensar",
|
||||||
|
"btn_cancel": "Cancelar",
|
||||||
|
"btn_decrypt": "Desbloquear",
|
||||||
|
"btn_close": "Fechar",
|
||||||
|
"btn_show_all": "Mostrar Todos",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-pt-br.png",
|
||||||
|
"f_droid_badge_text": "Obtê-lo no F-Droid",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/pt_br/badges/static/images/badges/pt_badge_web_generic.png",
|
||||||
|
"google_play_badge_text": "Obtê-lo no Google Play"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/ro.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"dialog_connection_body": "S-a pierdut conexiunea la serverul. Hauk va încerca să se reconecteze în fundal. Vă rugăm să verificați dacă aveți conectivitate la rețea.",
|
||||||
|
"e2e_unsupported": "Această partajare este protejată cu parolă. Aplicația dumneavoastră de navigare pe internet nu are funcționalitatea necesară pentru a deschide o asemenea partajare. Vă rugăm să reîncercați cu alta.",
|
||||||
|
"e2e_unavailable_secure": "Această partajare este protejată cu parolă. Nu puteți debloca această partajare pentru ca nu utilizați HTTPS. Vă rugăm să vă asigurați că folosiți HTTPS, apoi reîncercați.",
|
||||||
|
"e2e_incorrect": "Parola introdusă este eronată. Vă rugăm să reîncercați.",
|
||||||
|
"e2e_password_prompt": "Această partajare este protejată cu parolă. Vă rugăm să introduceți parola pentru a o accesa.",
|
||||||
|
"expired_body": "Locația partajată pe care ați încercat să o accesați nu a fost găsită pe server. Dacă această adresă a funcționat înainte, se poate să fi expirat deja.",
|
||||||
|
"btn_cancel": "Anulare",
|
||||||
|
"btn_dismiss": "Revocare",
|
||||||
|
"dialog_connection_head": "Eroare de conexiune",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/ro_ro/badges/static/images/badges/ro_badge_web_generic.png",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ro.png",
|
||||||
|
"btn_decrypt": "Deblocare",
|
||||||
|
"last_update_days": "Acum {{time}}z",
|
||||||
|
"last_update_hours": "Acum {{time}}o",
|
||||||
|
"last_update_minutes": "Acum {{time}}m",
|
||||||
|
"last_update_seconds": "Acum {{time}}s",
|
||||||
|
"status_offline": "Deconectat",
|
||||||
|
"status_expired": "Expirată",
|
||||||
|
"dialog_expired_body": "Această partajare de locației a expirat.",
|
||||||
|
"dialog_expired_head": "Partajare expirată",
|
||||||
|
"point_app_to": "Folosiți aplicația Hauk cu acest server pentru a vă partaja locația:",
|
||||||
|
"gnss_signal_body": "Expeditorul așteaptă semnalul GPS",
|
||||||
|
"gnss_signal_head": "Vă rugăm să așteptați",
|
||||||
|
"e2e_placeholder": "Introduceți parola",
|
||||||
|
"e2e_title": "Protejată cu parolă",
|
||||||
|
"expired_head": "Sesiune expirată",
|
||||||
|
"btn_show_all": "Arată toate",
|
||||||
|
"btn_close": "Închide",
|
||||||
|
"control_show_self": "Arată locația mea",
|
||||||
|
"dialog_user_navigate": "Navighează",
|
||||||
|
"dialog_user_follow": "Arată pe hartă",
|
||||||
|
"dialog_active_head": "Persoane active",
|
||||||
|
"google_play_badge_text": "Acum pe Google Play",
|
||||||
|
"f_droid_badge_text": "Acum pe F-Droid"
|
||||||
|
}
|
||||||
32
frontend/assets/lang/ru.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ru.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ru_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Отменить",
|
||||||
|
"status_offline": "Не подключено",
|
||||||
|
"status_expired": "Истекло",
|
||||||
|
"dialog_connection_body": "Соединение с сервером Hauk потеряно. Hauk будет пытаться переподключится в фоне. Пожалуйста проверьте соединение с сетью.",
|
||||||
|
"dialog_connection_head": "Ошибка подлючения",
|
||||||
|
"dialog_expired_body": "Эта шара устарела.",
|
||||||
|
"dialog_expired_head": "Шара устарела",
|
||||||
|
"point_app_to": "Укажите этот сервер в приложении Hauk что бы делится своим местоположением:",
|
||||||
|
"gnss_signal_body": "Ждем сигнала GPS",
|
||||||
|
"gnss_signal_head": "Пожалуйста подождите",
|
||||||
|
"expired_body": "Шара к которой вы пытаетесь получить доступ не найдена на сервере. Если эта ссылка работала в прошлом, возможно истекло время жизни шары.",
|
||||||
|
"expired_head": "Местоположение устарело",
|
||||||
|
"btn_decrypt": "Разблокировать",
|
||||||
|
"btn_cancel": "Отмена",
|
||||||
|
"e2e_placeholder": "Введите пароль",
|
||||||
|
"google_play_badge_text": "Установить из Google Play",
|
||||||
|
"btn_show_all": "Показать все",
|
||||||
|
"btn_close": "Закрыть",
|
||||||
|
"last_update_days": "был {{time}} дней назад",
|
||||||
|
"last_update_hours": "был {{time}} час(ов) назад",
|
||||||
|
"last_update_minutes": "был {{time}} минут назад",
|
||||||
|
"last_update_seconds": "был {{time}} назад",
|
||||||
|
"control_show_self": "Показать мое местоположение",
|
||||||
|
"dialog_user_follow": "Показать на карте",
|
||||||
|
"dialog_active_head": "Активные пользователи",
|
||||||
|
"e2e_incorrect": "Неверный пароль. Попробуйте еще раз.",
|
||||||
|
"e2e_password_prompt": "Это местоположение защищено паролем. Пожалуйста, введите пароль.",
|
||||||
|
"e2e_title": "Защищено паролем"
|
||||||
|
}
|
||||||
36
frontend/assets/lang/tr.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"google_play_badge_text": "Google Play'den alın",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/tr_badge_web_generic.png",
|
||||||
|
"f_droid_badge_text": "F-Droid'den alın",
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-tr.png",
|
||||||
|
"btn_show_all": "Hepsini göster",
|
||||||
|
"btn_close": "Kapat",
|
||||||
|
"btn_decrypt": "Kilidi aç",
|
||||||
|
"btn_cancel": "Vazgeç",
|
||||||
|
"btn_dismiss": "Yoksay",
|
||||||
|
"last_update_days": "{{time}} gün önce",
|
||||||
|
"last_update_hours": "{{time}} saat önce",
|
||||||
|
"last_update_minutes": "{{time}} dakika önce",
|
||||||
|
"last_update_seconds": "{{time}} saniye önce",
|
||||||
|
"control_show_self": "Konumumu göster",
|
||||||
|
"status_offline": "Çevrimdışı",
|
||||||
|
"status_expired": "Süresi doldu",
|
||||||
|
"dialog_user_navigate": "Şu dizine git",
|
||||||
|
"dialog_user_follow": "Haritada göster",
|
||||||
|
"dialog_active_head": "Aktif kullanıcılar",
|
||||||
|
"dialog_connection_body": "Hauk sunucusuna bağlantı koptu. Hauk arkaplanda bağlanmayı tekrar deneyecek. İnternet bağlantınızın çalışır olduğundan emin olunuz.",
|
||||||
|
"dialog_connection_head": "Bağlantı hatası",
|
||||||
|
"dialog_expired_body": "Bu konumun paylaşım süresi doldu.",
|
||||||
|
"dialog_expired_head": "Paylaşım süresi doldu",
|
||||||
|
"point_app_to": "Konumunuzu paylaşmak için Hauk uygulamasını şu sunucuya yönlendirin:",
|
||||||
|
"gnss_signal_body": "GPS sinyali bekleniyor",
|
||||||
|
"gnss_signal_head": "Lütfen bekleyin",
|
||||||
|
"e2e_unsupported": "Bu paylaşım parola korumalı. Tarayıcınız paylaşımın açılabilmesi için gerekli özellikleri karşılamıyor. Lütfen başka bir tarayıcıyla tekrar deneyiniz.",
|
||||||
|
"e2e_unavailable_secure": "Bu paylaşım parola korumalı. Bu paylaşımın kilidini açamazsınız çünkü HTTPS kullanmıyorsunuz. Lütfen HTTPS kullandığınıza emin olun ve tekrar deneyiniz.",
|
||||||
|
"e2e_incorrect": "Girdiğiniz parola yanlış. Lütfen tekrar deneyiniz.",
|
||||||
|
"e2e_password_prompt": "Bu paylaşım parola korumalı. Lütfen erişebilmek için parolayı giriniz.",
|
||||||
|
"e2e_placeholder": "Parolayı giriniz",
|
||||||
|
"e2e_title": "Parola korumalı",
|
||||||
|
"expired_body": "Ulaşmaya çalıştığınız paylaşılmış lokasyon sunucuda bulunamadı. Eğer bu link daha önce çalıştıysa, paylaşımın süresi dolmuş olabilir.",
|
||||||
|
"expired_head": "Konum süresi doldu"
|
||||||
|
}
|
||||||
16
frontend/assets/lang/uk.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ua.png",
|
||||||
|
"google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ua_badge_web_generic.png",
|
||||||
|
"btn_dismiss": "Відмінити",
|
||||||
|
"status_offline": "Офлайн",
|
||||||
|
"status_expired": "Термін дії закінчився",
|
||||||
|
"dialog_connection_body": "З’єднання з сервером Hauk було втрачено. Hauk спробує знову підключитися на задньому плані. Перевірте наявність у вас підключення до мережі.",
|
||||||
|
"dialog_connection_head": "Помилка з'єднання",
|
||||||
|
"dialog_expired_body": "Термін дії цієї шари закінчився.",
|
||||||
|
"dialog_expired_head": "Шара недійсна",
|
||||||
|
"point_app_to": "Налаштуйте Hauk на цей сервер щоб поділитися своїм місцеположенням:",
|
||||||
|
"gnss_signal_body": "Чекаємо на сигнал GPS",
|
||||||
|
"gnss_signal_head": "Будь ласка зачекайте",
|
||||||
|
"expired_body": "Розшарене місцезнаходження, до якого ви намагалися отримати доступ, не знайдено на сервері. Якщо це посилання працювало раніше, шара, можливо, померла.",
|
||||||
|
"expired_head": "Термін дії закінчився"
|
||||||
|
}
|
||||||
13
frontend/assets/location-pending.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(0 -270.54)">
|
||||||
|
<circle transform="scale(1,-1)" cx="13.229" cy="-283.77" r="12.127" fill="#000">
|
||||||
|
<animate attributeName="opacity" values="0.7;0" dur="2.5s" repeatCount="indefinite"/>
|
||||||
|
<animate attributeName="r" values="0;12" dur="2.5s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<path d="m18.477 291.85-5.248-3.8398-5.248 3.8398 5.248-16.154 2.624 8.0768z" fill="#d80037">
|
||||||
|
<animate attributeName="fill" values="#555;#d80037;#555" dur="1s" repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 781 B |
30
frontend/assets/logo.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
26
frontend/assets/markers/moving-dead.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="26.350771mm"
|
||||||
|
height="26.350771mm"
|
||||||
|
viewBox="0 0 26.350771 26.350772"
|
||||||
|
version="1.1">
|
||||||
|
<g
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1"
|
||||||
|
transform="matrix(0.98406556,0,0,0.98406556,-95.58598,-133.28638)">
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1-1"
|
||||||
|
transform="matrix(0.53514791,0,0,0.53514791,-45.970488,-65.761033)">
|
||||||
|
<path
|
||||||
|
style="fill:#555555;fill-opacity:1;stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387-2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
26
frontend/assets/markers/moving-live.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="26.350771mm"
|
||||||
|
height="26.350771mm"
|
||||||
|
viewBox="0 0 26.350771 26.350772"
|
||||||
|
version="1.1">
|
||||||
|
<g
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1"
|
||||||
|
transform="matrix(0.98406556,0,0,0.98406556,-95.58598,-133.28638)">
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1-1"
|
||||||
|
transform="matrix(0.53514791,0,0,0.53514791,-45.970488,-65.761033)">
|
||||||
|
<path
|
||||||
|
style="fill:#d80037;fill-opacity:1;stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387-2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
26
frontend/assets/markers/moving-rough.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="26.350771mm"
|
||||||
|
height="26.350771mm"
|
||||||
|
viewBox="0 0 26.350771 26.350772"
|
||||||
|
version="1.1">
|
||||||
|
<g
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1"
|
||||||
|
transform="matrix(0.98406556,0,0,0.98406556,-95.58598,-133.28638)">
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0.49803922;stroke:none;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="layer1-1"
|
||||||
|
transform="matrix(0.53514791,0,0,0.53514791,-45.970488,-65.761033)">
|
||||||
|
<path
|
||||||
|
style="fill:#ff9c00;fill-opacity:1;stroke:#ffffff;stroke-width:1.68011534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 119.22206,162.22206 -8.69958,-6.36525 -8.69958,6.36525 4.34979,-13.38873 4.34979,-13.38872 4.34979,13.38872 z"
|
||||||
|
id="path1387-2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
5
frontend/assets/markers/still-dead.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="26.351mm" height="26.351mm" version="1.1" viewBox="0 0 26.351 26.351" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="13.175" cy="13.175" r="7.2422" fill-opacity=".49804"/>
|
||||||
|
<circle cx="13.175" cy="13.175" r="4.5895" fill="#555555" stroke="#fff" stroke-width="1.6437"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
5
frontend/assets/markers/still-live.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="26.351mm" height="26.351mm" version="1.1" viewBox="0 0 26.351 26.351" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="13.175" cy="13.175" r="7.2422" fill-opacity=".49804"/>
|
||||||
|
<circle cx="13.175" cy="13.175" r="4.5895" fill="#d80037" stroke="#fff" stroke-width="1.6437"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
5
frontend/assets/markers/still-rough.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="26.351mm" height="26.351mm" version="1.1" viewBox="0 0 26.351 26.351" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="13.175" cy="13.175" r="7.2422" fill-opacity=".49804"/>
|
||||||
|
<circle cx="13.175" cy="13.175" r="4.5895" fill="#ff9c00" stroke="#fff" stroke-width="1.6437"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
5
frontend/assets/markers/still-self.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="26.351mm" height="26.351mm" version="1.1" viewBox="0 0 26.351 26.351" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="13.175" cy="13.175" r="7.2422" fill-opacity=".49804"/>
|
||||||
|
<circle cx="13.175" cy="13.175" r="4.5895" fill="#1e90ff" stroke="#fff" stroke-width="1.6437"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
6
frontend/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package frontend
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed index.html main.js style.css lib assets
|
||||||
|
var Files embed.FS
|
||||||
135
frontend/index.html
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src * data:; connect-src 'self'; font-src 'none'; object-src 'none'; media-src 'none'; frame-src 'none'; form-action 'none'; worker-src 'none'; manifest-src 'none';">
|
||||||
|
|
||||||
|
<!-- Load Leaflet for the map. -->
|
||||||
|
<link rel="stylesheet" href="./lib/leaflet/1.6.0/leaflet.css" />
|
||||||
|
<script src="./lib/leaflet/1.6.0/leaflet.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<link rel="icon" type="image/png" href="./assets/favicon.png">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg">
|
||||||
|
<script src="./dynamic.js.php"></script>
|
||||||
|
<title>Hauk</title>
|
||||||
|
|
||||||
|
<!-- The page should not be scalable, since users can just zoom in on
|
||||||
|
the map itself directly. -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mapouter">
|
||||||
|
<!-- The container for the Leaflet map. -->
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<!-- A countdown timer in a notch at the top of the screen. -->
|
||||||
|
<div id="notch">
|
||||||
|
<div class="tri t-left"></div>
|
||||||
|
<div class="inner" id="countdown"></div>
|
||||||
|
<div class="inner hidden offline" data-i18n="status_offline"></div>
|
||||||
|
<div class="tri t-right"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- The Hauk logo is displayed in the bottom left or right corner,
|
||||||
|
depending on screen resolution. -->
|
||||||
|
<a href="https://github.com/bilde2910/Hauk" target="_blank">
|
||||||
|
<div id="logo">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript is required for Leaflet and AJAX requests to the local
|
||||||
|
Hauk API. Inform the user if JavaScript is disabled. -->
|
||||||
|
<noscript>
|
||||||
|
<div class="cover">
|
||||||
|
<img src="./assets/logo.svg">
|
||||||
|
<p class="header">JavaScript disabled</p>
|
||||||
|
<p class="body">Hauk fundamentally requires JavaScript to function. Please enable JavaScript and reload the page to view the Hauk map.</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div class="cover hidden" id="notfound">
|
||||||
|
<img src="./assets/logo.svg" class="logo">
|
||||||
|
<p class="header" data-i18n="expired_head"></p>
|
||||||
|
<p class="body" data-i18n="expired_body"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover hidden" id="searching">
|
||||||
|
<img src="./assets/location-pending.svg" class="pending">
|
||||||
|
<p class="header" data-i18n="gnss_signal_head"></p>
|
||||||
|
<p class="body" data-i18n="gnss_signal_body"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover hidden" id="index">
|
||||||
|
<img src="./assets/logo.svg" class="logo">
|
||||||
|
<p class="body point-app-to" data-i18n="point_app_to"></p>
|
||||||
|
<p class="body" id="url">https://localhost/</p>
|
||||||
|
<p class="body"><a href="https://f-droid.org/packages/info.varden.hauk">
|
||||||
|
<img data-i18n="f_droid_badge_text"
|
||||||
|
data-i18n-attr="alt"
|
||||||
|
class="store-icon"
|
||||||
|
id="store-icon-fdroid"></a>
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=info.varden.hauk">
|
||||||
|
<img data-i18n="google_play_badge_text"
|
||||||
|
data-i18n-attr="alt"
|
||||||
|
class="store-icon"
|
||||||
|
id="store-icon-gplay">
|
||||||
|
</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-popup" class="dialog hidden">
|
||||||
|
<div>
|
||||||
|
<p class="header" id="message-title"></p>
|
||||||
|
<p class="body" id="message-body"></p>
|
||||||
|
<p class="button"><input type="button" id="dismiss-message" data-i18n-attr="value" data-i18n="btn_dismiss"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="user-list-popup" class="dialog hidden">
|
||||||
|
<div>
|
||||||
|
<p class="header" data-i18n="dialog_active_head"></p>
|
||||||
|
<div id="user-list" class="popup-list">
|
||||||
|
</div>
|
||||||
|
<p class="button">
|
||||||
|
<input type="button" id="close-user-list" data-i18n-attr="value" data-i18n="btn_close">
|
||||||
|
<input type="button" id="btn-show-all" data-i18n-attr="value" data-i18n="btn_show_all">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="user-details-popup" class="dialog hidden">
|
||||||
|
<div>
|
||||||
|
<p id="user-details-header" class="header"></p>
|
||||||
|
<div class="popup-list">
|
||||||
|
<p id="user-details-follow" data-i18n="dialog_user_follow"></p>
|
||||||
|
<p id="user-details-navigate" data-i18n="dialog_user_navigate"></p>
|
||||||
|
</div>
|
||||||
|
<p class="button"><input type="button" id="close-user-details" data-i18n-attr="value" data-i18n="btn_close"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="offline" class="dialog hidden">
|
||||||
|
<div>
|
||||||
|
<p class="header" data-i18n="dialog_connection_head"></p>
|
||||||
|
<p class="body" data-i18n="dialog_connection_body"></p>
|
||||||
|
<p class="button"><input type="button" id="dismiss-offline" data-i18n-attr="value" data-i18n="btn_dismiss"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="e2e-prompt" class="dialog hidden">
|
||||||
|
<div>
|
||||||
|
<p class="header" data-i18n="e2e_title"></p>
|
||||||
|
<p class="body" id="e2e-password-label" data-i18n="e2e_password_prompt"></p>
|
||||||
|
<p class="body"><input type="password" id="e2e-password" data-i18n-attr="placeholder" data-i18n="e2e_placeholder"></p>
|
||||||
|
<p class="button">
|
||||||
|
<input type="button" id="cancel-e2e-password" data-i18n-attr="value" data-i18n="btn_cancel">
|
||||||
|
<input type="button" id="decrypt-e2e-password" data-i18n-attr="value" data-i18n="btn_decrypt">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./main.js" charset="UTF-8"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
640
frontend/lib/leaflet/1.6.0/leaflet.css
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg,
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-tile {
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-active {
|
||||||
|
outline: 2px solid orange;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path {
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-control-attribution,
|
||||||
|
.leaflet-container .leaflet-control-scale {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 19px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 4px 4px 0 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 14px;
|
||||||
|
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #c3c3c3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip-container {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
5
frontend/lib/leaflet/1.6.0/leaflet.js
Normal file
946
frontend/main.js
Normal file
@@ -0,0 +1,946 @@
|
|||||||
|
// This is the main script file for Hauk's web view client.
|
||||||
|
|
||||||
|
const SHARE_TYPE_ALONE = 0;
|
||||||
|
const SHARE_TYPE_GROUP = 1;
|
||||||
|
|
||||||
|
const LOC_PROVIDER_FINE = 0;
|
||||||
|
const LOC_PROVIDER_COARSE = 1;
|
||||||
|
|
||||||
|
const STATE_LIVE_COLOR = '#d80037';
|
||||||
|
const STATE_ROUGH_COLOR = '#ff9c00';
|
||||||
|
const STATE_DEAD_COLOR = '#555555';
|
||||||
|
|
||||||
|
const EARTH_DIAMETER_KM = 6371 * 2;
|
||||||
|
const HAV_MOD = EARTH_DIAMETER_KM * 1000;
|
||||||
|
|
||||||
|
// Find preferred language.
|
||||||
|
var locales = ['ca', 'de', 'en', 'eu', 'fr', 'it', 'nb_NO', 'nl', 'nn', 'pt_BR', 'ro', 'ru', 'tr', 'uk'];
|
||||||
|
var prefLang = 'en';
|
||||||
|
if (navigator.languages) {
|
||||||
|
for (var i = navigator.languages.length - 1; i >= 0; i--) {
|
||||||
|
for (var j = 0; j < locales.length; j++) {
|
||||||
|
if (navigator.languages[i] == locales[j]) {
|
||||||
|
prefLang = locales[j].split('-').join('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load localization English as fallback.
|
||||||
|
var LANG = {};
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4 && this.status == 200) {
|
||||||
|
LANG = JSON.parse(this.responseText);
|
||||||
|
// Overwrite the L10N data with localizations for the preferred language.
|
||||||
|
if (prefLang != 'en') {
|
||||||
|
var xhr2 = new XMLHttpRequest();
|
||||||
|
xhr2.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4 && this.status == 200) {
|
||||||
|
var data = JSON.parse(this.responseText);
|
||||||
|
for (var key in data) {
|
||||||
|
if (!data.hasOwnProperty(key)) continue;
|
||||||
|
LANG[key] = data[key];
|
||||||
|
}
|
||||||
|
localizeHTML();
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr2.open('GET', './assets/lang/' + prefLang + '.json', true);
|
||||||
|
xhr2.send();
|
||||||
|
} else {
|
||||||
|
localizeHTML();
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open('GET', './assets/lang/en.json', true);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
// Put localized strings in HTML.
|
||||||
|
function localizeHTML() {
|
||||||
|
var tags = document.querySelectorAll('[data-i18n]');
|
||||||
|
for (var key in tags) {
|
||||||
|
if (!tags.hasOwnProperty(key)) continue;
|
||||||
|
var i18nKey = tags[key].getAttribute('data-i18n');
|
||||||
|
if (tags[key].hasAttribute('data-i18n-attr')) {
|
||||||
|
tags[key].setAttribute(tags[key].getAttribute('data-i18n-attr'), LANG[i18nKey]);
|
||||||
|
} else {
|
||||||
|
tags[key].textContent = LANG[i18nKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts fetching location data from the server. This is called after I18N has
|
||||||
|
// loaded to ensure strings are present for prompts.
|
||||||
|
function init() {
|
||||||
|
if (location.href.indexOf("?") === -1 || id == "") {
|
||||||
|
// If there is no share ID, show the root page.
|
||||||
|
var url = location.href.indexOf("?") === -1 ? location.href : location.href.substring(0, location.href.indexOf("?"));
|
||||||
|
|
||||||
|
var urlE = document.getElementById("url");
|
||||||
|
var indexE = document.getElementById("index");
|
||||||
|
var storeIconFdroidE = document.getElementById("store-icon-fdroid");
|
||||||
|
var storeIconGplayE = document.getElementById("store-icon-gplay");
|
||||||
|
if (urlE !== null) urlE.textContent = url;
|
||||||
|
if (indexE !== null) indexE.style.display = "block";
|
||||||
|
if (storeIconFdroidE !== null) storeIconFdroidE.src = LANG["f_droid_badge_url"];
|
||||||
|
if (storeIconGplayE !== null) storeIconGplayE.src = LANG["google_play_badge_url"];
|
||||||
|
} else {
|
||||||
|
// Attempt to fetch location data from the server once.
|
||||||
|
getJSON("./api/fetch.php?id=" + id, function(data) {
|
||||||
|
// Initialize the Leaflet map.
|
||||||
|
initMap();
|
||||||
|
noGPS.style.display = "block";
|
||||||
|
processUpdate(data, true);
|
||||||
|
}, function() {
|
||||||
|
var notFoundE = document.getElementById("notfound");
|
||||||
|
if (notFoundE !== null) notFoundE.style.display = "block";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For locating the viewer of the map. Watcher is a watchPosition() reference.
|
||||||
|
var watcher = null;
|
||||||
|
|
||||||
|
// Marker and accuracy circle of the person viewing the map.
|
||||||
|
var selfCircle = null;
|
||||||
|
var selfMarker = null;
|
||||||
|
|
||||||
|
// Create a geolocation control.
|
||||||
|
L.control.Locate = L.Control.extend({
|
||||||
|
onAdd: function(map) {
|
||||||
|
var btn = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
|
||||||
|
var anchor = L.DomUtil.create('a', 'leaflet-control-locate-inactive');
|
||||||
|
|
||||||
|
btn.style.backgroundColor = '#fff';
|
||||||
|
btn.appendChild(anchor);
|
||||||
|
|
||||||
|
anchor.href = '#';
|
||||||
|
anchor.title = LANG["control_show_self"];
|
||||||
|
anchor.onclick = function(e) {
|
||||||
|
// Prevent # from appearing in URL.
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (watcher === null) {
|
||||||
|
// If there is no location watcher active, create one.
|
||||||
|
// Update the icon to indicate that the location is pending.
|
||||||
|
anchor.className = 'leaflet-control-locate-pending';
|
||||||
|
watcher = navigator.geolocation.watchPosition(function(pos) {
|
||||||
|
// If the circle and marker is missing, add it and set the
|
||||||
|
// location status to active.
|
||||||
|
if (selfCircle == null && selfMarker == null) {
|
||||||
|
anchor.className = 'leaflet-control-locate-active';
|
||||||
|
selfCircle = L.circle([pos.coords.latitude, pos.coords.longitude], {
|
||||||
|
radius: pos.coords.accuracy,
|
||||||
|
fillColor: '#1e90ff',
|
||||||
|
fillOpacity: 0.25,
|
||||||
|
color: '#1e90ff',
|
||||||
|
opacity: 0.5,
|
||||||
|
interactive: false
|
||||||
|
}).addTo(circleLayer);
|
||||||
|
var selfIcon = L.divIcon({
|
||||||
|
html: '<div class="marker"><div class="arrow still-self"></div></div>',
|
||||||
|
iconAnchor: [33, 18]
|
||||||
|
});
|
||||||
|
selfMarker = L.marker([pos.coords.latitude, pos.coords.longitude], {
|
||||||
|
icon: selfIcon,
|
||||||
|
interactive: false
|
||||||
|
}).addTo(markerLayer);
|
||||||
|
|
||||||
|
// Unfollow any curretly followed user, then pan to the
|
||||||
|
// device location.
|
||||||
|
following = null;
|
||||||
|
map.panTo([pos.coords.latitude, pos.coords.longitude]);
|
||||||
|
} else {
|
||||||
|
selfCircle.setLatLng([pos.coords.latitude, pos.coords.longitude]);
|
||||||
|
selfMarker.setLatLng([pos.coords.latitude, pos.coords.longitude]);
|
||||||
|
selfCircle.setRadius(pos.coords.accuracy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there is already a watcher, clicking the control should
|
||||||
|
// unregister it and hide the user's location.
|
||||||
|
anchor.className = 'leaflet-control-locate-inactive';
|
||||||
|
navigator.geolocation.clearWatch(watcher);
|
||||||
|
watcher = null;
|
||||||
|
circleLayer.removeLayer(selfCircle);
|
||||||
|
markerLayer.removeLayer(selfMarker);
|
||||||
|
selfCircle = null;
|
||||||
|
selfMarker = null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a "show list of users" control.
|
||||||
|
L.control.Radar = L.Control.extend({
|
||||||
|
onAdd: function(map) {
|
||||||
|
var btn = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
|
||||||
|
var anchor = L.DomUtil.create('a', 'leaflet-control-radar');
|
||||||
|
|
||||||
|
btn.style.backgroundColor = '#fff';
|
||||||
|
btn.appendChild(anchor);
|
||||||
|
|
||||||
|
anchor.href = '#';
|
||||||
|
anchor.title = LANG["control_show_self"];
|
||||||
|
anchor.onclick = function(e) {
|
||||||
|
// Prevent # from appearing in URL.
|
||||||
|
e.preventDefault();
|
||||||
|
var userListPopupE = document.getElementById("user-list-popup");
|
||||||
|
if (userListPopupE !== null) {
|
||||||
|
userListPopupE.style.display = 'block';
|
||||||
|
}
|
||||||
|
var userListE = document.getElementById("user-list");
|
||||||
|
if (userListE !== null) {
|
||||||
|
var users = userListE.getElementsByTagName("p");
|
||||||
|
if (users.length == 1) users[0].click();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function for spawning the controls.
|
||||||
|
L.control.locate = function(opts) {
|
||||||
|
return new L.control.Locate(opts);
|
||||||
|
}
|
||||||
|
L.control.radar = function(opts) {
|
||||||
|
return new L.control.Radar(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
var map, circleLayer, markerLayer;
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
// Create a Leaflet map.
|
||||||
|
map = L.map('map').setView([0, 0], DEFAULT_ZOOM);
|
||||||
|
L.tileLayer(TILE_URI, {
|
||||||
|
attribution: ATTRIBUTION,
|
||||||
|
maxZoom: MAX_ZOOM
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add the geolocation control to the map if supported by the browser.
|
||||||
|
if ("geolocation" in navigator && window.isSecureContext) {
|
||||||
|
L.control.locate({ position: 'topleft' }).addTo(map);
|
||||||
|
}
|
||||||
|
L.control.radar({ position: 'topleft' }).addTo(map);
|
||||||
|
|
||||||
|
circleLayer = L.layerGroup().addTo(map);
|
||||||
|
markerLayer = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
|
// Unfollow the user when the map is panned.
|
||||||
|
map.on('mousedown', function() {
|
||||||
|
following = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The leaflet markers and associated data.
|
||||||
|
var shares = {};
|
||||||
|
|
||||||
|
// Retrieve the sharing link ID from the URL. E.g.
|
||||||
|
// https://example.com/?ABCD-1234 --> "ABCD-1234"
|
||||||
|
var id = location.href.substr(location.href.indexOf("?") + 1);
|
||||||
|
if (id.indexOf("&") !== -1) id = id.substr(0, id.indexOf("&"));
|
||||||
|
|
||||||
|
// Whether the "offline" popup has appeared when the browser is offline.
|
||||||
|
var knownOffline = false;
|
||||||
|
|
||||||
|
function getJSON(url, callback, invalid) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.timeout = REQUEST_TIMEOUT * 1000;
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
var offlineE = document.getElementById("offline");
|
||||||
|
var notchE = document.getElementById("notch");
|
||||||
|
if (this.status === 200) {
|
||||||
|
// Request successful. Reset offline state and parse the JSON.
|
||||||
|
knownOffline = false;
|
||||||
|
if (offlineE !== null) {
|
||||||
|
document.getElementById("offline").style.display = "none";
|
||||||
|
}
|
||||||
|
if (notchE !== null) {
|
||||||
|
document.getElementById("notch").className = "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var json = JSON.parse(this.responseText);
|
||||||
|
callback(json);
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex);
|
||||||
|
invalid();
|
||||||
|
}
|
||||||
|
} else if (this.status === 404) {
|
||||||
|
invalid();
|
||||||
|
} else {
|
||||||
|
// Requested failed; offline.
|
||||||
|
if (!knownOffline) {
|
||||||
|
knownOffline = true;
|
||||||
|
if (offlineE !== null) {
|
||||||
|
document.getElementById("offline").style.display = "block";
|
||||||
|
}
|
||||||
|
if (notchE !== null) {
|
||||||
|
document.getElementById("notch").className = "offline";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// General message popup box. Reused for several popups.
|
||||||
|
var dismissMessageE = document.getElementById("dismiss-message");
|
||||||
|
if (dismissMessageE !== null) {
|
||||||
|
dismissMessageE.addEventListener("click", function() {
|
||||||
|
var messageE = document.getElementById("message-popup");
|
||||||
|
if (messageE !== null) messageE.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows a dialog box with a title and message.
|
||||||
|
function showMessage(title, message) {
|
||||||
|
var messageE = document.getElementById("message-popup");
|
||||||
|
var titleE = document.getElementById("message-title");
|
||||||
|
var bodyE = document.getElementById("message-body");
|
||||||
|
if (titleE !== null) titleE.textContent = title;
|
||||||
|
if (bodyE !== null) bodyE.textContent = message;
|
||||||
|
if (messageE !== null) messageE.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
var dismissOfflineE = document.getElementById("dismiss-offline");
|
||||||
|
if (dismissOfflineE !== null) {
|
||||||
|
dismissOfflineE.addEventListener("click", function() {
|
||||||
|
var offlineE = document.getElementById("offline");
|
||||||
|
if (offlineE !== null) offlineE.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-to-end encryption password prompt handlers.
|
||||||
|
var passwordInputE = document.getElementById("e2e-password");
|
||||||
|
var passwordDecryptE = document.getElementById("decrypt-e2e-password");
|
||||||
|
if (passwordInputE !== null) {
|
||||||
|
passwordInputE.addEventListener("keyup", function(e) {
|
||||||
|
if (e.keyCode == 13) {
|
||||||
|
if (passwordDecryptE !== null) {
|
||||||
|
passwordDecryptE.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordCancelE = document.getElementById("cancel-e2e-password");
|
||||||
|
if (passwordCancelE !== null) {
|
||||||
|
passwordCancelE.addEventListener("click", function() {
|
||||||
|
var promptE = document.getElementById("e2e-prompt");
|
||||||
|
if (promptE !== null) promptE.style.display = "none";
|
||||||
|
if (passwordDecryptE !== null && acceptKeyFunc !== null) passwordDecryptE.removeEventListener("click", acceptKeyFunc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var userDetailsE = document.getElementById("user-details-popup");
|
||||||
|
|
||||||
|
var userDetailsFollowE = document.getElementById("user-details-follow");
|
||||||
|
if (userDetailsFollowE !== null) {
|
||||||
|
userDetailsFollowE.addEventListener("click", function() {
|
||||||
|
var user = userDetailsFollowE.dataset.user;
|
||||||
|
follow(user);
|
||||||
|
if (userDetailsE !== null) userDetailsE.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var userDetailsNavigateE = document.getElementById("user-details-navigate");
|
||||||
|
if (userDetailsNavigateE !== null) {
|
||||||
|
userDetailsNavigateE.addEventListener("click", function() {
|
||||||
|
var user = userDetailsNavigateE.dataset.user;
|
||||||
|
var points = shares[user].points;
|
||||||
|
if (points.length > 0) {
|
||||||
|
var last = points[points.length - 1];
|
||||||
|
window.open("geo:" + last.lat + "," + last.lon);
|
||||||
|
}
|
||||||
|
if (userDetailsE !== null) userDetailsE.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeUserListE = document.getElementById("close-user-list");
|
||||||
|
if (closeUserListE !== null) {
|
||||||
|
closeUserListE.addEventListener("click", function() {
|
||||||
|
var userListPopupE = document.getElementById("user-list-popup");
|
||||||
|
if (userListPopupE !== null) {
|
||||||
|
userListPopupE.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var showAllUsersE = document.getElementById("btn-show-all");
|
||||||
|
if (showAllUsersE !== null) {
|
||||||
|
showAllUsersE.addEventListener("click", function() {
|
||||||
|
autoCenter();
|
||||||
|
var userListPopupE = document.getElementById("user-list-popup");
|
||||||
|
if (userListPopupE !== null) {
|
||||||
|
userListPopupE.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeUserDetailsE = document.getElementById("close-user-details");
|
||||||
|
if (closeUserDetailsE !== null) {
|
||||||
|
closeUserDetailsE.addEventListener("click", function() {
|
||||||
|
if (userDetailsE !== null) {
|
||||||
|
userDetailsE.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchIntv;
|
||||||
|
var countIntv;
|
||||||
|
|
||||||
|
function setNewInterval(expire, interval, serverTime) {
|
||||||
|
var countdownE = document.getElementById("countdown");
|
||||||
|
var timeOffset = Date.now() / 1000 - serverTime;
|
||||||
|
|
||||||
|
// The data contains an expiration time. Create a countdown at the top of
|
||||||
|
// the map screen that ends when the share is over.
|
||||||
|
countIntv = setInterval(function() {
|
||||||
|
var seconds = expire - Math.round((Date.now() - timeOffset) / 1000);
|
||||||
|
if (seconds < 0) {
|
||||||
|
clearInterval(countIntv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var h = Math.floor(seconds / 3600);
|
||||||
|
var m = Math.floor((seconds % 3600) / 60);
|
||||||
|
var s = seconds % 60;
|
||||||
|
|
||||||
|
var time = "";
|
||||||
|
if (h > 0) time += h + ":";
|
||||||
|
if (h > 0 && m < 10) time += "0";
|
||||||
|
time += m + ":";
|
||||||
|
if (s < 10) time += "0";
|
||||||
|
time += s;
|
||||||
|
|
||||||
|
if (countdownE !== null) countdownE.textContent = time;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// The location data contains an interval. Schedule a task that fetches data
|
||||||
|
// once per interval time.
|
||||||
|
fetchIntv = setInterval(function() {
|
||||||
|
// Stop the task if the share has expired.
|
||||||
|
if ((Date.now() - timeOffset) / 1000 >= expire) {
|
||||||
|
clearInterval(fetchIntv);
|
||||||
|
clearInterval(countIntv);
|
||||||
|
if (countdownE !== null) countdownE.textContent = LANG["status_expired"];
|
||||||
|
showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start incremental fetch
|
||||||
|
getJSON("./api/fetch.php?id=" + id + "&since=" + getOldestPointTime(), function(data) {
|
||||||
|
// Recreate the interval timers if the interval or expiration
|
||||||
|
// change.
|
||||||
|
if (data.expire != expire || data.interval != interval) {
|
||||||
|
clearInterval(fetchIntv);
|
||||||
|
clearInterval(countIntv);
|
||||||
|
setNewInterval(data.expire, data.interval, data.serverTime);
|
||||||
|
}
|
||||||
|
processUpdate(data, false);
|
||||||
|
}, function() {
|
||||||
|
// On failure to get new location data:
|
||||||
|
clearInterval(fetchIntv);
|
||||||
|
clearInterval(countIntv);
|
||||||
|
if (countdownE !== null) countdownE.textContent = LANG["status_expired"];
|
||||||
|
showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]);
|
||||||
|
});
|
||||||
|
}, interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scans across all most recent points and returns the time of the oldest one
|
||||||
|
function getOldestPointTime() {
|
||||||
|
var oldestTime = Number.MAX_VALUE;
|
||||||
|
var foundTime = false;
|
||||||
|
for (var share in shares) {
|
||||||
|
var points = shares[share].points
|
||||||
|
if (points && points.length > 0 ) {
|
||||||
|
var mostRecentTime = points[ points.length-1 ].time
|
||||||
|
oldestTime = mostRecentTime < oldestTime ? mostRecentTime : oldestTime;
|
||||||
|
foundTime = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundTime ? oldestTime : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var noGPS = document.getElementById("searching");
|
||||||
|
|
||||||
|
// Whether or not an initial location has been received.
|
||||||
|
var hasReceivedFirst = false;
|
||||||
|
|
||||||
|
// Whether the map has been initially centered.
|
||||||
|
var hasInitiated = false;
|
||||||
|
|
||||||
|
// The user being followed on the map.
|
||||||
|
var following = null;
|
||||||
|
|
||||||
|
// The decryption key for end-to-end encrypted shares.
|
||||||
|
var aesKey = null;
|
||||||
|
|
||||||
|
// Button handler for the "Decrypt" button on the E2E password prompt.
|
||||||
|
var acceptKeyFunc = null;
|
||||||
|
|
||||||
|
// Converts a base64-encoded string to a Uint8Array ArrayBuffer for use with
|
||||||
|
// WebCrypto.
|
||||||
|
function byteArray(base64) {
|
||||||
|
var raw = atob(base64);
|
||||||
|
var len = raw.length;
|
||||||
|
var arr = new Uint8Array(new ArrayBuffer(len));
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
arr[i] = raw.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow a user on the map.
|
||||||
|
function follow(user) {
|
||||||
|
following = shares[user].id;
|
||||||
|
var last = shares[user].points[shares[user].points.length - 1];
|
||||||
|
map.panTo([last.lat, last.lon]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom and pan map to show all users.
|
||||||
|
function autoCenter() {
|
||||||
|
var markers = [];
|
||||||
|
for (var share in shares) {
|
||||||
|
if (!shares.hasOwnProperty(share)) continue;
|
||||||
|
if (shares[share].marker === null) continue;
|
||||||
|
markers.push(shares[share].marker);
|
||||||
|
}
|
||||||
|
var fg = new L.featureGroup(markers);
|
||||||
|
map.fitBounds(fg.getBounds().pad(0.5));
|
||||||
|
|
||||||
|
// Do not exceed the default zoom level.
|
||||||
|
if (map.getZoom() > DEFAULT_ZOOM) map.setZoom(DEFAULT_ZOOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the data returned from ./api/fetch.php and updates the map marker.
|
||||||
|
function processUpdate(data, init) {
|
||||||
|
var users = {};
|
||||||
|
var multiUser = false;
|
||||||
|
if (data.type == SHARE_TYPE_ALONE) {
|
||||||
|
users[""] = data.points;
|
||||||
|
multiUser = false;
|
||||||
|
} else if (data.type == SHARE_TYPE_GROUP) {
|
||||||
|
users = data.points;
|
||||||
|
multiUser = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for crypto support if necessary.
|
||||||
|
if (data.encrypted && !("crypto" in window)) {
|
||||||
|
showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]);
|
||||||
|
return;
|
||||||
|
} else if (data.encrypted && !("subtle" in window.crypto)) {
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
showMessage(LANG["e2e_title"], LANG["e2e_unavailable_secure"]);
|
||||||
|
} else {
|
||||||
|
showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.encrypted && aesKey == null) {
|
||||||
|
// If using end-to-end encryption, we need to decrypt the data. We have
|
||||||
|
// not obtained an AES key yet, so prompt the user for it.
|
||||||
|
var promptE = document.getElementById("e2e-prompt");
|
||||||
|
var labelE = document.getElementById("e2e-password-label");
|
||||||
|
if (promptE !== null && passwordInputE !== null && passwordDecryptE !== null && labelE !== null) {
|
||||||
|
acceptKeyFunc = function() {
|
||||||
|
// Remove the event listener, hide the dialog and fetch the
|
||||||
|
// password.
|
||||||
|
passwordDecryptE.removeEventListener("click", acceptKeyFunc);
|
||||||
|
promptE.style.display = "none";
|
||||||
|
labelE.textContent = LANG["e2e_incorrect"];
|
||||||
|
var password = passwordInputE.value;
|
||||||
|
|
||||||
|
// Get the salt in binary format.
|
||||||
|
var salt = byteArray(data.salt);
|
||||||
|
|
||||||
|
// Derive the encryption key using PBKDF2 with SHA-1. SHA-1 was chosen
|
||||||
|
// because of availability in Android.
|
||||||
|
crypto.subtle
|
||||||
|
.importKey("raw", new TextEncoder("utf-8").encode(password), "PBKDF2", false, ["deriveKey"])
|
||||||
|
.then(key => crypto.subtle.deriveKey(
|
||||||
|
{name: "PBKDF2", salt: salt, iterations: 65536, hash: "SHA-1"},
|
||||||
|
key,
|
||||||
|
{name: "AES-CBC", length: 256},
|
||||||
|
false,
|
||||||
|
["decrypt"]
|
||||||
|
))
|
||||||
|
.then(key => {
|
||||||
|
// Store the crypto key and re-process the update.
|
||||||
|
aesKey = key;
|
||||||
|
processUpdate(data, init);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach the listener to the dialog box and show it.
|
||||||
|
passwordDecryptE.addEventListener("click", acceptKeyFunc);
|
||||||
|
passwordInputE.value = "";
|
||||||
|
promptE.style.display = "block";
|
||||||
|
passwordInputE.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if (data.encrypted) {
|
||||||
|
// The data is encrypted, but now we have a key we can use to decrypt
|
||||||
|
// it. Decrypt each point using the key.
|
||||||
|
var algo = {name: "AES-CBC"};
|
||||||
|
|
||||||
|
var pointPromises = [];
|
||||||
|
for (var i = 0; i < data.points.length; i++) {
|
||||||
|
algo.iv = byteArray(data.points[i][0]);
|
||||||
|
var promises = [];
|
||||||
|
for (var j = 1; j < data.points[i].length; j++) {
|
||||||
|
// Check that the array entry is not null to prevent an
|
||||||
|
// exception. If the entry is null, push a promise that returns
|
||||||
|
// null to the array to maintain indexing in the decrypted
|
||||||
|
// result.
|
||||||
|
if (data.points[i][j] !== null) {
|
||||||
|
promises.push(crypto.subtle.decrypt(algo, aesKey, byteArray(data.points[i][j])));
|
||||||
|
} else {
|
||||||
|
promises.push(new Promise(function(resolve, reject) {
|
||||||
|
resolve(null);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pointPromises.push(Promise.all(promises));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all points to be decrypted.
|
||||||
|
Promise
|
||||||
|
.all(pointPromises)
|
||||||
|
.then(function(values) {
|
||||||
|
// Parse all points and conver them to floating point values
|
||||||
|
// (all values in the array are currently numbers).
|
||||||
|
var decoder = new TextDecoder("utf-8");
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
for (var j = 0; j < values[i].length; j++) {
|
||||||
|
// Check that the value isn't null to avoid exceptions.
|
||||||
|
if (values[i][j] !== null) {
|
||||||
|
data.points[i][j] = parseFloat(decoder.decode(values[i][j]));
|
||||||
|
} else {
|
||||||
|
data.points[i][j] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The IV was the first item in the array, so all items have
|
||||||
|
// been shifted up once. Pop the last array element off.
|
||||||
|
data.points[i].pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag the data as unencrypted and re-process the update.
|
||||||
|
data.encrypted = false;
|
||||||
|
processUpdate(data, init);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
// Decryption error. Most likely incorrect password. Reset the
|
||||||
|
// key and prompt the user for the password again.
|
||||||
|
console.log(error);
|
||||||
|
aesKey = null;
|
||||||
|
if (!init) {
|
||||||
|
clearInterval(fetchIntv);
|
||||||
|
clearInterval(countIntv);
|
||||||
|
}
|
||||||
|
processUpdate(data, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If flagged to initialize, set up polling.
|
||||||
|
if (init) setNewInterval(data.expire, data.interval, data.serverTime);
|
||||||
|
var userListE = document.getElementById("user-list");
|
||||||
|
|
||||||
|
for (var user in users) {
|
||||||
|
if (!users.hasOwnProperty(user)) continue;
|
||||||
|
var locData = users[user];
|
||||||
|
|
||||||
|
if (!shares.hasOwnProperty(user)) {
|
||||||
|
// Add an entry to the user list.
|
||||||
|
var listE = document.createElement("p");
|
||||||
|
listE.textContent = user;
|
||||||
|
listE.dataset.user = user;
|
||||||
|
listE.style.display = "none";
|
||||||
|
listE.addEventListener("click", function() {
|
||||||
|
var userListPopupE = document.getElementById("user-list-popup");
|
||||||
|
if (userListPopupE !== null && userDetailsE !== null) {
|
||||||
|
userListPopupE.style.display = "none";
|
||||||
|
userDetailsE.style.display = "block";
|
||||||
|
var user = this.dataset.user;
|
||||||
|
var userDetailsHeaderE = document.getElementById("user-details-header");
|
||||||
|
if (userDetailsHeaderE !== null) userDetailsHeaderE.textContent = user;
|
||||||
|
if (userDetailsFollowE !== null) userDetailsFollowE.dataset.user = user;
|
||||||
|
if (userDetailsNavigateE !== null) userDetailsNavigateE.dataset.user = user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (userListE !== null) userListE.appendChild(listE);
|
||||||
|
|
||||||
|
shares[user] = {
|
||||||
|
"marker": null,
|
||||||
|
"circle": null,
|
||||||
|
"icon": null,
|
||||||
|
"points": [],
|
||||||
|
"id": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5),
|
||||||
|
"listEntry": listE,
|
||||||
|
"state": "live"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last location received.
|
||||||
|
var lastPoint = shares[user].points.length > 0 ? shares[user].points[shares[user].points.length - 1] : null;
|
||||||
|
|
||||||
|
for (var i = 0; i < users[user].length; i++) {
|
||||||
|
var lat = users[user][i][0];
|
||||||
|
var lon = users[user][i][1];
|
||||||
|
var time = users[user][i][2];
|
||||||
|
var prov = users[user][i][3];
|
||||||
|
var acc = users[user][i][4];
|
||||||
|
var spd = users[user][i][5];
|
||||||
|
|
||||||
|
// Default to "Fine" provider for older clients.
|
||||||
|
if (prov === null) prov = LOC_PROVIDER_FINE;
|
||||||
|
// Determine the icon color to use depending on provider.
|
||||||
|
shares[user].state = prov == LOC_PROVIDER_FINE ? "live" : "rough";
|
||||||
|
var iconColor = STATE_LIVE_COLOR;
|
||||||
|
if (shares[user].state == "rough") iconColor = STATE_ROUGH_COLOR;
|
||||||
|
|
||||||
|
// Check if the location should be added. Only add new location points
|
||||||
|
// if the point was not recorded before the last recorded point.
|
||||||
|
if (lastPoint === null || time > lastPoint.time) {
|
||||||
|
var line = null;
|
||||||
|
if (shares[user].marker == null) {
|
||||||
|
// Add a marker to the map if it's not already there.
|
||||||
|
shares[user].icon = L.divIcon({
|
||||||
|
html:
|
||||||
|
'<div class="marker">' +
|
||||||
|
'<div class="arrow still-' + shares[user].state + '" id="arrow-' + shares[user].id + '"></div>' +
|
||||||
|
'<p class="' + shares[user].state + '" id="label-' + shares[user].id + '">' +
|
||||||
|
'<span id="nickname-' + shares[user].id + '"></span>' +
|
||||||
|
'<span class="velocity">' +
|
||||||
|
'<span id="velocity-' + shares[user].id + '">0.0</span> ' +
|
||||||
|
VELOCITY_UNIT.unit +
|
||||||
|
'</span><span class="offline" id="last-seen-' + shares[user].id + '">' +
|
||||||
|
'</span>' +
|
||||||
|
'</p>' +
|
||||||
|
'</div>',
|
||||||
|
iconAnchor: [33, 18]
|
||||||
|
});
|
||||||
|
shares[user].marker = L.marker([lat, lon], {icon: shares[user].icon}).on("click", function() {
|
||||||
|
follow(this.haukUser);
|
||||||
|
});
|
||||||
|
shares[user].marker.haukUser = user;
|
||||||
|
shares[user].marker.addTo(markerLayer);
|
||||||
|
} else {
|
||||||
|
// If there is a marker, draw a line from its last location
|
||||||
|
// instead and move the marker.
|
||||||
|
line = L.polyline([shares[user].marker.getLatLng(), [lat, lon]], {color: TRAIL_COLOR}).addTo(markerLayer);
|
||||||
|
shares[user].marker.setLatLng([lat, lon]);
|
||||||
|
}
|
||||||
|
// Draw an accuracy circle if GPS accuracy was provided by the
|
||||||
|
// client.
|
||||||
|
if (acc !== null && shares[user].circle == null) {
|
||||||
|
shares[user].circle = L.circle([lat, lon], {radius: acc, fillColor: iconColor, fillOpacity: 0.25, color: iconColor, opacity: 0.5, interactive: false}).addTo(circleLayer);
|
||||||
|
} else if (shares[user].circle !== null) {
|
||||||
|
shares[user].circle.setLatLng([lat, lon]);
|
||||||
|
if (acc !== null) shares[user].circle.setRadius(acc);
|
||||||
|
}
|
||||||
|
shares[user].points.push({lat: lat, lon: lon, line: line, time: time, spd: spd, acc: acc});
|
||||||
|
lastPoint = shares[user].points[shares[user].points.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPoint !== null) shares[user].listEntry.style.display = "block";
|
||||||
|
|
||||||
|
var eVelocity = document.getElementById("velocity-" + shares[user].id)
|
||||||
|
var vel = 0;
|
||||||
|
if (lastPoint !== null && lastPoint.spd !== null && eVelocity !== null) {
|
||||||
|
// Prefer client-provided speed if possible.
|
||||||
|
vel = lastPoint.spd * VELOCITY_UNIT.mpsMultiplier;
|
||||||
|
eVelocity.textContent = vel.toFixed(1);
|
||||||
|
} else if (eVelocity !== null) {
|
||||||
|
// If the client didn't provide its speed, calculate it locally from its
|
||||||
|
// list of locations.
|
||||||
|
var dist = 0;
|
||||||
|
var time = 0;
|
||||||
|
var idx = shares[user].points.length;
|
||||||
|
|
||||||
|
// Iterate over all locations backwards until we either reach our
|
||||||
|
// required VELOCITY_DELTA_TIME, or we run out of points.
|
||||||
|
while (idx > 2) {
|
||||||
|
idx--;
|
||||||
|
var pt1 = shares[user].points[idx - 1];
|
||||||
|
var pt2 = shares[user].points[idx];
|
||||||
|
var dTime = pt2.time - pt1.time;
|
||||||
|
|
||||||
|
// If the new time does not exceed the VELOCITY_DELTA_TIME, add the
|
||||||
|
// time and distance deltas to the appropriate sum for averaging;
|
||||||
|
// otherwise, break the loop and proceed to calculate.
|
||||||
|
if (time + dTime <= VELOCITY_DELTA_TIME) {
|
||||||
|
time += dTime;
|
||||||
|
dist += distance(pt1, pt2);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the UI with the velocity.
|
||||||
|
vel = velocity(dist, time);
|
||||||
|
eVelocity.textContent = vel.toFixed(1);;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag that the first location has been received, for map centering.
|
||||||
|
if (lastPoint !== null && !hasReceivedFirst) {
|
||||||
|
hasReceivedFirst = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the marker if needed.
|
||||||
|
if (lastPoint !== null && !multiUser && following !== null) {
|
||||||
|
map.panTo([lastPoint.lat, lastPoint.lon]);
|
||||||
|
} else if (lastPoint !== null && multiUser && following == shares[user].id) {
|
||||||
|
map.panTo([lastPoint.lat, lastPoint.lon]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate the marker to the direction of movement.
|
||||||
|
var eArrow = document.getElementById("arrow-" + shares[user].id);
|
||||||
|
if (eArrow !== null && shares[user].points.length >= 2) {
|
||||||
|
var last = shares[user].points.length - 1;
|
||||||
|
eArrow.style.transform = "rotate(" + angle(shares[user].points[last - 1], shares[user].points[last]) + "deg)";
|
||||||
|
if (vel.toFixed(1) == "0.0") {
|
||||||
|
eArrow.className = "arrow still-" + shares[user].state;
|
||||||
|
} else {
|
||||||
|
eArrow.className = "arrow moving-" + shares[user].state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune the array of locations so it does not exceed our MAX_POINTS defined
|
||||||
|
// in the config.
|
||||||
|
if (shares[user].points.length > MAX_POINTS) {
|
||||||
|
var remove = shares[user].points.splice(0, shares[user].points.length - MAX_POINTS);
|
||||||
|
for (var j = 0; j < remove.length; j++) if (remove[j].line !== null) map.removeLayer(remove[j].line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the user's nickname if this is a group share.
|
||||||
|
var nameE = document.getElementById("nickname-" + shares[user].id);
|
||||||
|
if (nameE !== null && multiUser) {
|
||||||
|
nameE.textContent = user;
|
||||||
|
nameE.innerHTML += "<br />";
|
||||||
|
nameE.style.fontWeight = "bold";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var user in shares) {
|
||||||
|
if (!shares.hasOwnProperty(user)) continue;
|
||||||
|
|
||||||
|
// Gray out the user's location if no data has been received for the
|
||||||
|
// OFFLINE_TIMEOUT.
|
||||||
|
var eArrow = document.getElementById("arrow-" + shares[user].id);
|
||||||
|
if (eArrow !== null) {
|
||||||
|
var last = shares[user].points.length - 1;
|
||||||
|
var point = shares[user].points[last];
|
||||||
|
var eLabel = document.getElementById("label-" + shares[user].id);
|
||||||
|
var eLastSeen = document.getElementById("last-seen-" + shares[user].id);
|
||||||
|
|
||||||
|
if (point.time < data.serverTime - OFFLINE_TIMEOUT) {
|
||||||
|
eArrow.className = eArrow.className.split("live").join("dead").split("rough").join("dead");
|
||||||
|
if (eLabel !== null) eLabel.className = 'dead';
|
||||||
|
if (eLastSeen !== null) {
|
||||||
|
// Calculate time since last update and choose an
|
||||||
|
// appropriate unit.
|
||||||
|
var time = Math.round(data.serverTime - point.time);
|
||||||
|
var unit = LANG["last_update_seconds"];
|
||||||
|
if (time >= 60) {
|
||||||
|
time = Math.floor(time / 60);
|
||||||
|
unit = LANG["last_update_minutes"];
|
||||||
|
if (time >= 60) {
|
||||||
|
time = Math.floor(time / 60);
|
||||||
|
unit = LANG["last_update_hours"];
|
||||||
|
if (time >= 24) {
|
||||||
|
time = Math.floor(time / 24);
|
||||||
|
unit = LANG["last_update_days"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eLastSeen.textContent = unit.split("{{time}}").join(time);
|
||||||
|
}
|
||||||
|
setAccuracyCircleColor(shares[user].circle, STATE_DEAD_COLOR);
|
||||||
|
} else {
|
||||||
|
eArrow.className = eArrow.className.split("dead").join(shares[user].state);
|
||||||
|
if (eLabel !== null) eLabel.className = shares[user].state;
|
||||||
|
var iconColor = STATE_LIVE_COLOR;
|
||||||
|
if (shares[user].state == "rough") iconColor = STATE_ROUGH_COLOR;
|
||||||
|
setAccuracyCircleColor(shares[user].circle, iconColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On first location update, center the map so it shows all participants.
|
||||||
|
if (hasReceivedFirst && !hasInitiated) {
|
||||||
|
hasInitiated = true;
|
||||||
|
noGPS.style.display = "none";
|
||||||
|
var mapOuterE = document.getElementById("mapouter");
|
||||||
|
if (mapOuterE !== null) {
|
||||||
|
mapOuterE.style.visibility = "visible";
|
||||||
|
}
|
||||||
|
autoCenter();
|
||||||
|
|
||||||
|
// Auto-follow single-user shares.
|
||||||
|
if (!multiUser) {
|
||||||
|
following = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccuracyCircleColor(circle, color) {
|
||||||
|
if (circle) {
|
||||||
|
circle.setStyle({
|
||||||
|
fillColor: color,
|
||||||
|
color: color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the distance between two points on a sphere using the Haversine
|
||||||
|
// algorithm.
|
||||||
|
function distance(from, to) {
|
||||||
|
var d2r = Math.PI / 180;
|
||||||
|
var fla = from.lat * d2r, tla = to.lat * d2r;
|
||||||
|
var flo = from.lon * d2r, tlo = to.lon * d2r;
|
||||||
|
var havLat = Math.sin((tla - fla) / 2); havLat *= havLat;
|
||||||
|
var havLon = Math.sin((tlo - flo) / 2); havLon *= havLon;
|
||||||
|
var hav = havLat + Math.cos(fla) * Math.cos(tla) * havLon;
|
||||||
|
var d = Math.asin(Math.sqrt(hav));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates a velocity using the velocity unit from the config.
|
||||||
|
function velocity(distance, intv) {
|
||||||
|
if (intv == 0) return 0.0;
|
||||||
|
return distance * VELOCITY_UNIT.mpsMultiplier * HAV_MOD / intv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the bearing between two points on a sphere in degrees.
|
||||||
|
function angle(from, to) {
|
||||||
|
var d2r = Math.PI / 180;
|
||||||
|
var fromLat = from.lat * d2r, toLat = to.lat * d2r;
|
||||||
|
var fromLon = from.lon * d2r, toLon = to.lon * d2r;
|
||||||
|
/*
|
||||||
|
Calculation code by krishnar from
|
||||||
|
https://stackoverflow.com/a/52079217
|
||||||
|
*/
|
||||||
|
var x = Math.cos(fromLat) * Math.sin(toLat)
|
||||||
|
- Math.sin(fromLat) * Math.cos(toLat) * Math.cos(toLon - fromLon);
|
||||||
|
var y = Math.sin(toLon - fromLon) * Math.cos(toLat);
|
||||||
|
var heading = Math.atan2(y, x) / d2r;
|
||||||
|
|
||||||
|
return (heading + 360) % 360;
|
||||||
|
}
|
||||||
345
frontend/style.css
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
@charset "UTF-8";
|
||||||
|
/* The main stylesheet for the Hauk web view interface. */
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override browser-specific defaults. */
|
||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The map should cover the entire viewport. */
|
||||||
|
#mapouter {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
/* Hide by default while we check whether or not the share exists. */
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup covers that should display on top of the map. */
|
||||||
|
.cover {
|
||||||
|
position: absolute;
|
||||||
|
width: 80vmin;
|
||||||
|
height: 80vmin;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2000;
|
||||||
|
background-color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10vmin;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hauk logo. */
|
||||||
|
.cover > img.logo {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hauk logo. */
|
||||||
|
.cover > img.pending {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup header. */
|
||||||
|
.cover > p.header {
|
||||||
|
font-size: 8vmin;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Searching for GNSS signal header (SVG image above, no need for margin). */
|
||||||
|
#searching > p.header {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default page header. (Logo above, need custom margin.) */
|
||||||
|
.point-app-to {
|
||||||
|
margin-top: 7vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup information. */
|
||||||
|
.cover > p.body {
|
||||||
|
font-size: 4vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog window that contains a title, message and button. This is the outer
|
||||||
|
box, with a semitransparent black background to provide shading against the
|
||||||
|
map, which it renders on top of. */
|
||||||
|
.dialog {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The actual message dialog itself. */
|
||||||
|
.dialog > div {
|
||||||
|
width: 300px;
|
||||||
|
max-width: 80vw;
|
||||||
|
background-color: white;
|
||||||
|
padding: 5px 20px;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog p.header {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the button is big enough to be clicked on all devices. */
|
||||||
|
.dialog input[type=button] {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the password prompt fills the dialog box. */
|
||||||
|
.dialog input[type=password] {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visible on the root page of Hauk. */
|
||||||
|
#url {
|
||||||
|
color: #d80037;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 5vmin;
|
||||||
|
}
|
||||||
|
.store-icon {
|
||||||
|
margin-top: 1vmin;
|
||||||
|
width: 40vmin;
|
||||||
|
}
|
||||||
|
a:first-child > .store-icon {
|
||||||
|
margin-left: -5vmin;
|
||||||
|
}
|
||||||
|
a:last-child > .store-icon {
|
||||||
|
margin-right: -5vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display the Hauk logo in the bottom left corner of the map if the viewport is
|
||||||
|
wide enough to accomodate it. If it's so narrow that the Leaflet attribution
|
||||||
|
could be covered by it, display it in the bottom left corner instead, above
|
||||||
|
the attribution. */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
#logo {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 700.001px) {
|
||||||
|
#logo {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo div {
|
||||||
|
width: 73.33479px;
|
||||||
|
height: 28.853556px;
|
||||||
|
background: url(./assets/logo.svg) no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control-locate-inactive {
|
||||||
|
background: url(./assets/controls/locate-inactive.svg) no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control-locate-pending {
|
||||||
|
background: url(./assets/controls/locate-pending.svg) no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control-locate-active {
|
||||||
|
background: url(./assets/controls/locate-active.svg) no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control-radar {
|
||||||
|
background: url(./assets/controls/radar.svg) no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User list. **/
|
||||||
|
.popup-list {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.popup-list > a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.popup-list p {
|
||||||
|
background-color: #eee;
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.popup-list p:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The notch at the top of the screen that shows the time remaining of the
|
||||||
|
share. Outer container. */
|
||||||
|
#notch {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The left and right triangles. */
|
||||||
|
#notch div.tri {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The left triangle. */
|
||||||
|
#notch div.t-left {
|
||||||
|
left: 0.15px;
|
||||||
|
top: 0;
|
||||||
|
border-width: 0 23px 23px 0;
|
||||||
|
border-color: transparent rgba(0,0,0,0.5) transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The right triangle. */
|
||||||
|
#notch div.t-right {
|
||||||
|
right: 0.15px;
|
||||||
|
top: 0;
|
||||||
|
border-width: 23px 23px 0 0;
|
||||||
|
border-color: rgba(0,0,0,0.5) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The middle part of the notch, with the countdown itself. */
|
||||||
|
#notch div.inner {
|
||||||
|
width: 80px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5px;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change the color if client is offline. */
|
||||||
|
#notch.offline div.t-left {
|
||||||
|
border-color: transparent rgba(165,0,42,0.5) transparent transparent;
|
||||||
|
}
|
||||||
|
#notch.offline div.t-right {
|
||||||
|
border-color: rgba(165,0,42,0.5) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
#notch.offline div.inner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#notch.offline div.inner.offline {
|
||||||
|
background-color: rgba(165,0,42,0.5);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The outer marker div. */
|
||||||
|
.marker {
|
||||||
|
width: 66px;
|
||||||
|
height: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The arrow within the marker div. */
|
||||||
|
.arrow.moving-live {
|
||||||
|
background-image: url(./assets/markers/moving-live.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.moving-rough {
|
||||||
|
background-image: url(./assets/markers/moving-rough.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.moving-dead {
|
||||||
|
background-image: url(./assets/markers/moving-dead.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.still-live {
|
||||||
|
background-image: url(./assets/markers/still-live.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.still-rough {
|
||||||
|
background-image: url(./assets/markers/still-rough.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.still-dead {
|
||||||
|
background-image: url(./assets/markers/still-dead.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.still-self {
|
||||||
|
background-image: url(./assets/markers/still-self.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The velocity indicator on the marker div. */
|
||||||
|
.marker p {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: white;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 0;
|
||||||
|
line-height: 100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Location data has been received recently. */
|
||||||
|
.marker p.live, .marker p.rough {
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker p.live > span.offline, .marker p.rough > span.offline {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No location data has been received for a while. */
|
||||||
|
.marker p.dead {
|
||||||
|
background-color: rgba(165,0,42,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker p.dead > span.velocity {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the default white box in the top left corner of the marker div. */
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||