embed frontend static files

This commit is contained in:
Arkadiy Kukarkin
2025-12-23 12:23:39 +01:00
parent 5661cc3f04
commit 331218b7f7
38 changed files with 2783 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
hauk

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View 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ó dubicació 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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": "위치 정보가 만료됨"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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": "Защищено паролем"
}

View 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"
}

View 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": "Термін дії закінчився"
}

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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>

View 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;
}

File diff suppressed because one or more lines are too long

946
frontend/main.js Normal file
View 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
View 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;
}