2212 Commits

Author SHA1 Message Date
vcoppe cfb629dcfb New translations elevation.mdx (Serbian (Latin)) 2025-11-12 18:00:29 +01:00
vcoppe 268afe5f84 New translations elevation.mdx (Chinese Traditional, Hong Kong) 2025-11-12 18:00:27 +01:00
vcoppe 6832b7b1ca New translations elevation.mdx (Latvian) 2025-11-12 18:00:26 +01:00
vcoppe 042c900f80 New translations elevation.mdx (Thai) 2025-11-12 18:00:24 +01:00
vcoppe 6524864dea New translations elevation.mdx (Indonesian) 2025-11-12 18:00:23 +01:00
vcoppe 3ad29a2f8b New translations elevation.mdx (Portuguese, Brazilian) 2025-11-12 18:00:22 +01:00
vcoppe f40c01109a New translations elevation.mdx (Vietnamese) 2025-11-12 18:00:20 +01:00
vcoppe 2c875bd96b New translations elevation.mdx (Chinese Simplified) 2025-11-12 18:00:18 +01:00
vcoppe ace184c5f4 New translations elevation.mdx (Ukrainian) 2025-11-12 18:00:17 +01:00
vcoppe 3857649baa New translations elevation.mdx (Turkish) 2025-11-12 18:00:16 +01:00
vcoppe db32681bcf New translations elevation.mdx (Swedish) 2025-11-12 18:00:14 +01:00
vcoppe 0c551a5991 New translations elevation.mdx (Russian) 2025-11-12 18:00:13 +01:00
vcoppe be39bc80a0 New translations elevation.mdx (Portuguese) 2025-11-12 18:00:11 +01:00
vcoppe 1798cbcc1f New translations elevation.mdx (Polish) 2025-11-12 18:00:09 +01:00
vcoppe 054abd8555 New translations elevation.mdx (Norwegian) 2025-11-12 18:00:08 +01:00
vcoppe febf949e83 New translations elevation.mdx (Dutch) 2025-11-12 18:00:06 +01:00
vcoppe 712ca963d2 New translations elevation.mdx (Lithuanian) 2025-11-12 18:00:05 +01:00
vcoppe 41c06e9c98 New translations elevation.mdx (Korean) 2025-11-12 18:00:03 +01:00
vcoppe 7bf411672d New translations elevation.mdx (Italian) 2025-11-12 18:00:02 +01:00
vcoppe 3d7f98903f New translations elevation.mdx (Hungarian) 2025-11-12 18:00:00 +01:00
vcoppe ea744f4979 New translations elevation.mdx (Hebrew) 2025-11-12 17:59:59 +01:00
vcoppe 3ffec22e5e New translations elevation.mdx (Finnish) 2025-11-12 17:59:58 +01:00
vcoppe 243b6cbe79 New translations elevation.mdx (Basque) 2025-11-12 17:59:57 +01:00
vcoppe 79872ac8e4 New translations elevation.mdx (Greek) 2025-11-12 17:59:56 +01:00
vcoppe 0214cad3dd New translations elevation.mdx (German) 2025-11-12 17:59:54 +01:00
vcoppe 818b3e0d44 New translations elevation.mdx (Danish) 2025-11-12 17:59:53 +01:00
vcoppe b61313ce6c New translations elevation.mdx (Czech) 2025-11-12 17:59:52 +01:00
vcoppe f511da811b New translations elevation.mdx (Catalan) 2025-11-12 17:59:51 +01:00
vcoppe b7b1662383 New translations elevation.mdx (Belarusian) 2025-11-12 17:59:50 +01:00
vcoppe 716a28352d New translations elevation.mdx (Spanish) 2025-11-12 17:59:48 +01:00
vcoppe bf05c28e32 New translations elevation.mdx (French) 2025-11-12 17:59:47 +01:00
vcoppe 0c8f516e39 New translations elevation.mdx (Romanian) 2025-11-12 17:59:46 +01:00
vcoppe ad7355e976 New translations time.mdx (Serbian (Latin)) 2025-11-12 17:59:31 +01:00
vcoppe 9734bb251b New translations time.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:59:30 +01:00
vcoppe b1986f68d6 New translations time.mdx (Latvian) 2025-11-12 17:59:29 +01:00
vcoppe 957faced0a New translations time.mdx (Thai) 2025-11-12 17:59:28 +01:00
vcoppe 4e43867ab3 New translations time.mdx (Indonesian) 2025-11-12 17:59:26 +01:00
vcoppe 0b53016675 New translations time.mdx (Portuguese, Brazilian) 2025-11-12 17:59:25 +01:00
vcoppe 04c2411432 New translations time.mdx (Vietnamese) 2025-11-12 17:59:24 +01:00
vcoppe 237e673963 New translations time.mdx (Chinese Simplified) 2025-11-12 17:59:22 +01:00
vcoppe d48b7c1927 New translations time.mdx (Ukrainian) 2025-11-12 17:59:21 +01:00
vcoppe 39602fa0e9 New translations time.mdx (Turkish) 2025-11-12 17:59:20 +01:00
vcoppe 08704e2eff New translations time.mdx (Swedish) 2025-11-12 17:59:19 +01:00
vcoppe 79178a6514 New translations time.mdx (Russian) 2025-11-12 17:59:17 +01:00
vcoppe 05cfeca5b4 New translations time.mdx (Portuguese) 2025-11-12 17:59:16 +01:00
vcoppe 5b93749051 New translations time.mdx (Polish) 2025-11-12 17:59:15 +01:00
vcoppe 355ef01a38 New translations time.mdx (Norwegian) 2025-11-12 17:59:13 +01:00
vcoppe c382298c7e New translations time.mdx (Dutch) 2025-11-12 17:59:12 +01:00
vcoppe b083c6e0a2 New translations time.mdx (Lithuanian) 2025-11-12 17:59:11 +01:00
vcoppe 96275b9e8e New translations time.mdx (Korean) 2025-11-12 17:59:10 +01:00
vcoppe 91ae587254 New translations time.mdx (Italian) 2025-11-12 17:59:09 +01:00
vcoppe 18a8c02e7c New translations time.mdx (Hungarian) 2025-11-12 17:59:07 +01:00
vcoppe daa2d662de New translations time.mdx (Hebrew) 2025-11-12 17:59:06 +01:00
vcoppe c0d310fc1d New translations time.mdx (Finnish) 2025-11-12 17:59:05 +01:00
vcoppe 78c4553171 New translations time.mdx (Basque) 2025-11-12 17:59:03 +01:00
vcoppe 3906fe45e0 New translations time.mdx (Greek) 2025-11-12 17:59:02 +01:00
vcoppe b449397914 New translations time.mdx (German) 2025-11-12 17:59:01 +01:00
vcoppe c0fa6e34c5 New translations time.mdx (Danish) 2025-11-12 17:59:00 +01:00
vcoppe c5bb302a15 New translations time.mdx (Czech) 2025-11-12 17:58:58 +01:00
vcoppe 05012179f9 New translations time.mdx (Catalan) 2025-11-12 17:58:57 +01:00
vcoppe a53c934a1e New translations time.mdx (Belarusian) 2025-11-12 17:58:55 +01:00
vcoppe da73f9426a New translations time.mdx (Spanish) 2025-11-12 17:58:54 +01:00
vcoppe b5383122bc New translations time.mdx (French) 2025-11-12 17:58:53 +01:00
vcoppe 0a059c345b New translations time.mdx (Romanian) 2025-11-12 17:58:52 +01:00
vcoppe 07c979df5d New translations scissors.mdx (Serbian (Latin)) 2025-11-12 17:58:50 +01:00
vcoppe 52a5654923 New translations scissors.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:58:49 +01:00
vcoppe 3a86303865 New translations scissors.mdx (Latvian) 2025-11-12 17:58:48 +01:00
vcoppe 7cc06efda1 New translations scissors.mdx (Thai) 2025-11-12 17:58:47 +01:00
vcoppe b467b69478 New translations scissors.mdx (Indonesian) 2025-11-12 17:58:45 +01:00
vcoppe c289c316d7 New translations scissors.mdx (Portuguese, Brazilian) 2025-11-12 17:58:44 +01:00
vcoppe a30c9ae81a New translations scissors.mdx (Vietnamese) 2025-11-12 17:58:43 +01:00
vcoppe cb9e316a8c New translations scissors.mdx (Chinese Simplified) 2025-11-12 17:58:42 +01:00
vcoppe f6abbab561 New translations scissors.mdx (Ukrainian) 2025-11-12 17:58:40 +01:00
vcoppe aae0626a22 New translations scissors.mdx (Turkish) 2025-11-12 17:58:39 +01:00
vcoppe b39f30fec6 New translations scissors.mdx (Swedish) 2025-11-12 17:58:38 +01:00
vcoppe f323bc2e25 New translations scissors.mdx (Russian) 2025-11-12 17:58:37 +01:00
vcoppe 67abd55120 New translations scissors.mdx (Portuguese) 2025-11-12 17:58:35 +01:00
vcoppe 5f52964672 New translations scissors.mdx (Polish) 2025-11-12 17:58:34 +01:00
vcoppe 084587fcc1 New translations scissors.mdx (Norwegian) 2025-11-12 17:58:33 +01:00
vcoppe 619f129f0c New translations scissors.mdx (Dutch) 2025-11-12 17:58:32 +01:00
vcoppe acd2962427 New translations scissors.mdx (Lithuanian) 2025-11-12 17:58:31 +01:00
vcoppe c9ddb5f3d1 New translations scissors.mdx (Korean) 2025-11-12 17:58:29 +01:00
vcoppe 480056ce86 New translations scissors.mdx (Italian) 2025-11-12 17:58:28 +01:00
vcoppe cc47635a60 New translations scissors.mdx (Hungarian) 2025-11-12 17:58:27 +01:00
vcoppe 631b0cd3d3 New translations scissors.mdx (Hebrew) 2025-11-12 17:58:26 +01:00
vcoppe 447d416bbb New translations scissors.mdx (Finnish) 2025-11-12 17:58:25 +01:00
vcoppe 05b8dff0a6 New translations scissors.mdx (Basque) 2025-11-12 17:58:24 +01:00
vcoppe c44e9be42d New translations scissors.mdx (Greek) 2025-11-12 17:58:22 +01:00
vcoppe 366f26ff0b New translations scissors.mdx (German) 2025-11-12 17:58:21 +01:00
vcoppe dd1865a2cf New translations scissors.mdx (Danish) 2025-11-12 17:58:20 +01:00
vcoppe 359ce2fedc New translations scissors.mdx (Czech) 2025-11-12 17:58:19 +01:00
vcoppe dc94a33e64 New translations scissors.mdx (Catalan) 2025-11-12 17:58:17 +01:00
vcoppe 8be176b57e New translations scissors.mdx (Belarusian) 2025-11-12 17:58:16 +01:00
vcoppe 984e518163 New translations scissors.mdx (Spanish) 2025-11-12 17:58:15 +01:00
vcoppe 219afd5f1c New translations scissors.mdx (French) 2025-11-12 17:58:14 +01:00
vcoppe 4d0e0c8c95 New translations scissors.mdx (Romanian) 2025-11-12 17:58:13 +01:00
vcoppe 1bfc19cc74 New translations routing.mdx (Serbian (Latin)) 2025-11-12 17:58:11 +01:00
vcoppe 48f0466e5e New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:58:10 +01:00
vcoppe 6c004740c9 New translations routing.mdx (Latvian) 2025-11-12 17:58:09 +01:00
vcoppe 4582ddd1db New translations routing.mdx (Thai) 2025-11-12 17:58:08 +01:00
vcoppe ea254f89a5 New translations routing.mdx (Indonesian) 2025-11-12 17:58:07 +01:00
vcoppe 6d5880ee09 New translations routing.mdx (Portuguese, Brazilian) 2025-11-12 17:58:06 +01:00
vcoppe f230c5fd84 New translations routing.mdx (Vietnamese) 2025-11-12 17:58:05 +01:00
vcoppe 2326fb3809 New translations routing.mdx (Chinese Simplified) 2025-11-12 17:58:03 +01:00
vcoppe 270df17c67 New translations routing.mdx (Ukrainian) 2025-11-12 17:58:02 +01:00
vcoppe cbb386e799 New translations routing.mdx (Turkish) 2025-11-12 17:58:01 +01:00
vcoppe 6c7f4933eb New translations routing.mdx (Swedish) 2025-11-12 17:58:00 +01:00
vcoppe c572570517 New translations routing.mdx (Russian) 2025-11-12 17:57:58 +01:00
vcoppe 72264b76aa New translations routing.mdx (Portuguese) 2025-11-12 17:57:57 +01:00
vcoppe 3cfe444f8d New translations routing.mdx (Polish) 2025-11-12 17:57:56 +01:00
vcoppe d9b38bab4c New translations routing.mdx (Norwegian) 2025-11-12 17:57:55 +01:00
vcoppe b39e09bd0c New translations routing.mdx (Dutch) 2025-11-12 17:57:54 +01:00
vcoppe cd32631a71 New translations routing.mdx (Lithuanian) 2025-11-12 17:57:52 +01:00
vcoppe ec8e1455a1 New translations routing.mdx (Korean) 2025-11-12 17:57:51 +01:00
vcoppe cfd9b9946b New translations routing.mdx (Italian) 2025-11-12 17:57:50 +01:00
vcoppe 961daf76f0 New translations routing.mdx (Hungarian) 2025-11-12 17:57:49 +01:00
vcoppe 256ca75851 New translations routing.mdx (Hebrew) 2025-11-12 17:57:47 +01:00
vcoppe 6fa63cca0d New translations routing.mdx (Finnish) 2025-11-12 17:57:46 +01:00
vcoppe a8730722dd New translations routing.mdx (Basque) 2025-11-12 17:57:45 +01:00
vcoppe ededaebd39 New translations routing.mdx (Greek) 2025-11-12 17:57:44 +01:00
vcoppe 80d43a85cd New translations routing.mdx (German) 2025-11-12 17:57:42 +01:00
vcoppe 6b2a946ed7 New translations routing.mdx (Danish) 2025-11-12 17:57:41 +01:00
vcoppe 6822e3bed3 New translations routing.mdx (Catalan) 2025-11-12 17:57:40 +01:00
vcoppe 5c9ae9cde6 New translations routing.mdx (Belarusian) 2025-11-12 17:57:39 +01:00
vcoppe 335505bb6e New translations routing.mdx (Spanish) 2025-11-12 17:57:38 +01:00
vcoppe a153b3379f New translations routing.mdx (French) 2025-11-12 17:57:37 +01:00
vcoppe 04c280560e New translations routing.mdx (Romanian) 2025-11-12 17:57:35 +01:00
vcoppe 31385fb34f New translations poi.mdx (Serbian (Latin)) 2025-11-12 17:57:34 +01:00
vcoppe 3844360614 New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:57:33 +01:00
vcoppe e9f4487035 New translations poi.mdx (Latvian) 2025-11-12 17:57:32 +01:00
vcoppe b5c50fbbdf New translations poi.mdx (Thai) 2025-11-12 17:57:31 +01:00
vcoppe ace8928c99 New translations poi.mdx (Indonesian) 2025-11-12 17:57:29 +01:00
vcoppe de8c3dbcce New translations poi.mdx (Portuguese, Brazilian) 2025-11-12 17:57:28 +01:00
vcoppe b6df3ad67f New translations poi.mdx (Vietnamese) 2025-11-12 17:57:26 +01:00
vcoppe edb9950e38 New translations poi.mdx (Chinese Simplified) 2025-11-12 17:57:25 +01:00
vcoppe 2dc06ddbd4 New translations poi.mdx (Ukrainian) 2025-11-12 17:57:24 +01:00
vcoppe 2cca65730f New translations poi.mdx (Turkish) 2025-11-12 17:57:22 +01:00
vcoppe ff6090f859 New translations poi.mdx (Swedish) 2025-11-12 17:57:21 +01:00
vcoppe 105a705302 New translations poi.mdx (Russian) 2025-11-12 17:57:20 +01:00
vcoppe 2620dcd2f8 New translations poi.mdx (Portuguese) 2025-11-12 17:57:19 +01:00
vcoppe 74e0f260ae New translations poi.mdx (Polish) 2025-11-12 17:57:18 +01:00
vcoppe 64beb4cc52 New translations poi.mdx (Norwegian) 2025-11-12 17:57:16 +01:00
vcoppe b531fc5df4 New translations poi.mdx (Dutch) 2025-11-12 17:57:15 +01:00
vcoppe 300ea4b77f New translations poi.mdx (Lithuanian) 2025-11-12 17:57:14 +01:00
vcoppe 309d60fb46 New translations poi.mdx (Korean) 2025-11-12 17:57:13 +01:00
vcoppe f8606a7453 New translations poi.mdx (Italian) 2025-11-12 17:57:11 +01:00
vcoppe d7fc332d91 New translations poi.mdx (Hungarian) 2025-11-12 17:57:10 +01:00
vcoppe bc14935be3 New translations poi.mdx (Hebrew) 2025-11-12 17:57:09 +01:00
vcoppe 6b38430822 New translations poi.mdx (Finnish) 2025-11-12 17:57:08 +01:00
vcoppe 4fb2cd1805 New translations poi.mdx (Basque) 2025-11-12 17:57:07 +01:00
vcoppe e265d6b561 New translations poi.mdx (Greek) 2025-11-12 17:57:05 +01:00
vcoppe b6cb891858 New translations poi.mdx (German) 2025-11-12 17:57:04 +01:00
vcoppe 09a9c2ef17 New translations poi.mdx (Danish) 2025-11-12 17:57:03 +01:00
vcoppe 3cb9498c9b New translations poi.mdx (Czech) 2025-11-12 17:57:02 +01:00
vcoppe 1991d88c03 New translations poi.mdx (Catalan) 2025-11-12 17:57:00 +01:00
vcoppe 9e5fa15630 New translations poi.mdx (Belarusian) 2025-11-12 17:56:59 +01:00
vcoppe d3be51fdcd New translations poi.mdx (Spanish) 2025-11-12 17:56:58 +01:00
vcoppe 8ae62558be New translations poi.mdx (French) 2025-11-12 17:56:57 +01:00
vcoppe cf0d82a4d2 New translations poi.mdx (Romanian) 2025-11-12 17:56:56 +01:00
vcoppe b6aa4738cf New translations minify.mdx (Serbian (Latin)) 2025-11-12 17:56:54 +01:00
vcoppe f73de02f87 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:56:53 +01:00
vcoppe 383bf649c3 New translations minify.mdx (Latvian) 2025-11-12 17:56:52 +01:00
vcoppe 7fc4826f43 New translations minify.mdx (Thai) 2025-11-12 17:56:51 +01:00
vcoppe 4b8fd2f25c New translations minify.mdx (Indonesian) 2025-11-12 17:56:50 +01:00
vcoppe 9dee07a534 New translations minify.mdx (Portuguese, Brazilian) 2025-11-12 17:56:49 +01:00
vcoppe d476f0991e New translations minify.mdx (Vietnamese) 2025-11-12 17:56:47 +01:00
vcoppe 8da76ebf95 New translations minify.mdx (Chinese Simplified) 2025-11-12 17:56:46 +01:00
vcoppe 0db93bf1f7 New translations minify.mdx (Ukrainian) 2025-11-12 17:56:45 +01:00
vcoppe 7480d5e2d5 New translations minify.mdx (Turkish) 2025-11-12 17:56:44 +01:00
vcoppe 88da695b9f New translations minify.mdx (Swedish) 2025-11-12 17:56:43 +01:00
vcoppe 54cc725550 New translations minify.mdx (Russian) 2025-11-12 17:56:42 +01:00
vcoppe bf299220fe New translations minify.mdx (Portuguese) 2025-11-12 17:56:40 +01:00
vcoppe c726aefa87 New translations minify.mdx (Polish) 2025-11-12 17:56:39 +01:00
vcoppe 15a5d9a457 New translations minify.mdx (Norwegian) 2025-11-12 17:56:38 +01:00
vcoppe e01223f985 New translations minify.mdx (Dutch) 2025-11-12 17:56:37 +01:00
vcoppe ab6389901e New translations minify.mdx (Lithuanian) 2025-11-12 17:56:35 +01:00
vcoppe 281fec42cb New translations minify.mdx (Korean) 2025-11-12 17:56:34 +01:00
vcoppe ec0f463be9 New translations minify.mdx (Italian) 2025-11-12 17:56:33 +01:00
vcoppe a51a616181 New translations minify.mdx (Hungarian) 2025-11-12 17:56:32 +01:00
vcoppe 11a8ed3388 New translations minify.mdx (Hebrew) 2025-11-12 17:56:31 +01:00
vcoppe 1870ca9a6c New translations minify.mdx (Finnish) 2025-11-12 17:56:29 +01:00
vcoppe 4bc6ec4eea New translations minify.mdx (Basque) 2025-11-12 17:56:28 +01:00
vcoppe 82ba54e592 New translations minify.mdx (Greek) 2025-11-12 17:56:27 +01:00
vcoppe 3ef4a6d9b7 New translations minify.mdx (German) 2025-11-12 17:56:26 +01:00
vcoppe d724799258 New translations minify.mdx (Danish) 2025-11-12 17:56:24 +01:00
vcoppe e0cda2a2c0 New translations minify.mdx (Czech) 2025-11-12 17:56:23 +01:00
vcoppe 118584debb New translations minify.mdx (Catalan) 2025-11-12 17:56:22 +01:00
vcoppe fb9c242972 New translations minify.mdx (Belarusian) 2025-11-12 17:56:20 +01:00
vcoppe 448838fde4 New translations minify.mdx (Spanish) 2025-11-12 17:56:19 +01:00
vcoppe a6c4332854 New translations minify.mdx (French) 2025-11-12 17:56:18 +01:00
vcoppe f55e2eb1f3 New translations minify.mdx (Romanian) 2025-11-12 17:56:17 +01:00
vcoppe cfc5a00248 New translations merge.mdx (Serbian (Latin)) 2025-11-12 17:56:15 +01:00
vcoppe 8c508b0ca5 New translations merge.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:56:14 +01:00
vcoppe 67d6fe326f New translations merge.mdx (Latvian) 2025-11-12 17:56:12 +01:00
vcoppe eadc9a62ce New translations merge.mdx (Thai) 2025-11-12 17:56:11 +01:00
vcoppe b2b68316f2 New translations merge.mdx (Indonesian) 2025-11-12 17:56:10 +01:00
vcoppe fac8938d0a New translations merge.mdx (Portuguese, Brazilian) 2025-11-12 17:56:09 +01:00
vcoppe 0f8c37ff70 New translations merge.mdx (Vietnamese) 2025-11-12 17:56:08 +01:00
vcoppe fc004fd212 New translations merge.mdx (Chinese Simplified) 2025-11-12 17:56:06 +01:00
vcoppe 680a18f737 New translations merge.mdx (Ukrainian) 2025-11-12 17:56:05 +01:00
vcoppe 9b8533352b New translations merge.mdx (Turkish) 2025-11-12 17:56:04 +01:00
vcoppe 0761e6249d New translations merge.mdx (Swedish) 2025-11-12 17:56:03 +01:00
vcoppe 6b6636bc16 New translations merge.mdx (Russian) 2025-11-12 17:56:02 +01:00
vcoppe f2da0e9baf New translations merge.mdx (Portuguese) 2025-11-12 17:56:00 +01:00
vcoppe e82683a5e5 New translations merge.mdx (Polish) 2025-11-12 17:55:59 +01:00
vcoppe 9c59f27849 New translations merge.mdx (Norwegian) 2025-11-12 17:55:58 +01:00
vcoppe 6113af1ff5 New translations merge.mdx (Dutch) 2025-11-12 17:55:57 +01:00
vcoppe 1d535c4bba New translations merge.mdx (Lithuanian) 2025-11-12 17:55:56 +01:00
vcoppe 8cc022db94 New translations merge.mdx (Korean) 2025-11-12 17:55:54 +01:00
vcoppe 4a6e795595 New translations merge.mdx (Italian) 2025-11-12 17:55:53 +01:00
vcoppe 9453f65991 New translations merge.mdx (Hungarian) 2025-11-12 17:55:52 +01:00
vcoppe 7d5fa67aba New translations merge.mdx (Hebrew) 2025-11-12 17:55:50 +01:00
vcoppe c5488d2f9b New translations merge.mdx (Finnish) 2025-11-12 17:55:49 +01:00
vcoppe f1740d122e New translations merge.mdx (Basque) 2025-11-12 17:55:48 +01:00
vcoppe d54eb810ae New translations merge.mdx (Greek) 2025-11-12 17:55:47 +01:00
vcoppe cc77fbf7d1 New translations merge.mdx (German) 2025-11-12 17:55:46 +01:00
vcoppe 7e79a1af45 New translations merge.mdx (Danish) 2025-11-12 17:55:44 +01:00
vcoppe 0fd5929d33 New translations merge.mdx (Czech) 2025-11-12 17:55:43 +01:00
vcoppe bfc2e87cbd New translations merge.mdx (Catalan) 2025-11-12 17:55:42 +01:00
vcoppe 6de7b71e5e New translations merge.mdx (Belarusian) 2025-11-12 17:55:41 +01:00
vcoppe 6773b21e70 New translations merge.mdx (Spanish) 2025-11-12 17:55:40 +01:00
vcoppe da2538de26 New translations merge.mdx (French) 2025-11-12 17:55:38 +01:00
vcoppe 5f055d3c5e New translations merge.mdx (Romanian) 2025-11-12 17:55:37 +01:00
vcoppe a53df616be New translations extract.mdx (Serbian (Latin)) 2025-11-12 17:55:36 +01:00
vcoppe 7d55a86209 New translations extract.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:55:34 +01:00
vcoppe d0d0115ee7 New translations extract.mdx (Latvian) 2025-11-12 17:55:33 +01:00
vcoppe 630efd4ca2 New translations extract.mdx (Thai) 2025-11-12 17:55:32 +01:00
vcoppe 95f956e12c New translations extract.mdx (Indonesian) 2025-11-12 17:55:31 +01:00
vcoppe 26602696ce New translations extract.mdx (Portuguese, Brazilian) 2025-11-12 17:55:30 +01:00
vcoppe 65c5d4d950 New translations extract.mdx (Vietnamese) 2025-11-12 17:55:28 +01:00
vcoppe 765f79b7fd New translations extract.mdx (Chinese Simplified) 2025-11-12 17:55:27 +01:00
vcoppe f822939633 New translations extract.mdx (Ukrainian) 2025-11-12 17:55:26 +01:00
vcoppe d186843ba2 New translations extract.mdx (Turkish) 2025-11-12 17:55:25 +01:00
vcoppe ba7ec47a07 New translations extract.mdx (Swedish) 2025-11-12 17:55:23 +01:00
vcoppe 7ce0adc245 New translations extract.mdx (Russian) 2025-11-12 17:55:22 +01:00
vcoppe 6c9a93bc75 New translations extract.mdx (Portuguese) 2025-11-12 17:55:21 +01:00
vcoppe a09bf4dfa9 New translations extract.mdx (Polish) 2025-11-12 17:55:20 +01:00
vcoppe 6de834b55a New translations extract.mdx (Norwegian) 2025-11-12 17:55:19 +01:00
vcoppe b2447718e3 New translations extract.mdx (Dutch) 2025-11-12 17:55:18 +01:00
vcoppe e3e1746d42 New translations extract.mdx (Lithuanian) 2025-11-12 17:55:16 +01:00
vcoppe e013c3aa92 New translations extract.mdx (Korean) 2025-11-12 17:55:15 +01:00
vcoppe 8d38cc7efe New translations extract.mdx (Italian) 2025-11-12 17:55:14 +01:00
vcoppe 37f17968b7 New translations extract.mdx (Hungarian) 2025-11-12 17:55:13 +01:00
vcoppe 2a2789dcba New translations extract.mdx (Hebrew) 2025-11-12 17:55:11 +01:00
vcoppe b36e2b8d82 New translations extract.mdx (Finnish) 2025-11-12 17:55:10 +01:00
vcoppe 22dbe9d07d New translations extract.mdx (Basque) 2025-11-12 17:55:09 +01:00
vcoppe 2fb28a5a4a New translations extract.mdx (Greek) 2025-11-12 17:55:08 +01:00
vcoppe 0ea0e9992e New translations extract.mdx (German) 2025-11-12 17:55:07 +01:00
vcoppe 0f2f9703b2 New translations extract.mdx (Danish) 2025-11-12 17:55:05 +01:00
vcoppe 403f018b43 New translations extract.mdx (Czech) 2025-11-12 17:55:04 +01:00
vcoppe 35033bf439 New translations extract.mdx (Catalan) 2025-11-12 17:55:03 +01:00
vcoppe be30315718 New translations extract.mdx (Belarusian) 2025-11-12 17:55:01 +01:00
vcoppe 046e7382cf New translations extract.mdx (Spanish) 2025-11-12 17:55:00 +01:00
vcoppe ad4a0485c4 New translations extract.mdx (French) 2025-11-12 17:54:59 +01:00
vcoppe c6aface832 New translations extract.mdx (Romanian) 2025-11-12 17:54:58 +01:00
vcoppe 651e1e9203 New translations clean.mdx (Serbian (Latin)) 2025-11-12 17:54:57 +01:00
vcoppe e3dcf6eb87 New translations clean.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:54:56 +01:00
vcoppe 15d8b22025 New translations clean.mdx (Latvian) 2025-11-12 17:54:54 +01:00
vcoppe 700c8b565f New translations clean.mdx (Thai) 2025-11-12 17:54:53 +01:00
vcoppe a0a888b31f New translations clean.mdx (Indonesian) 2025-11-12 17:54:52 +01:00
vcoppe 32f99c6c73 New translations clean.mdx (Portuguese, Brazilian) 2025-11-12 17:54:51 +01:00
vcoppe d646a05222 New translations clean.mdx (Vietnamese) 2025-11-12 17:54:49 +01:00
vcoppe 3b2e49b524 New translations clean.mdx (Chinese Simplified) 2025-11-12 17:54:48 +01:00
vcoppe 5b1d415149 New translations clean.mdx (Ukrainian) 2025-11-12 17:54:47 +01:00
vcoppe 03cb33ed04 New translations clean.mdx (Turkish) 2025-11-12 17:54:46 +01:00
vcoppe 3aff104429 New translations clean.mdx (Swedish) 2025-11-12 17:54:44 +01:00
vcoppe 50c4a27ce8 New translations clean.mdx (Russian) 2025-11-12 17:54:43 +01:00
vcoppe 00ca6db8fc New translations clean.mdx (Portuguese) 2025-11-12 17:54:42 +01:00
vcoppe 1f43ccd2f2 New translations clean.mdx (Polish) 2025-11-12 17:54:40 +01:00
vcoppe d86bd0a26b New translations clean.mdx (Norwegian) 2025-11-12 17:54:39 +01:00
vcoppe f85bc96f73 New translations clean.mdx (Dutch) 2025-11-12 17:54:38 +01:00
vcoppe 96f89e5329 New translations clean.mdx (Lithuanian) 2025-11-12 17:54:36 +01:00
vcoppe d2fea652e4 New translations clean.mdx (Korean) 2025-11-12 17:54:35 +01:00
vcoppe ffb1e23ac0 New translations clean.mdx (Italian) 2025-11-12 17:54:34 +01:00
vcoppe 5ef0316099 New translations clean.mdx (Hungarian) 2025-11-12 17:54:33 +01:00
vcoppe 42168c2cd5 New translations clean.mdx (Hebrew) 2025-11-12 17:54:31 +01:00
vcoppe 5133bf3768 New translations clean.mdx (Finnish) 2025-11-12 17:54:30 +01:00
vcoppe f1497a0caa New translations clean.mdx (Basque) 2025-11-12 17:54:29 +01:00
vcoppe 0fd7cd5c07 New translations clean.mdx (Greek) 2025-11-12 17:54:28 +01:00
vcoppe 48c35dcf17 New translations clean.mdx (German) 2025-11-12 17:54:27 +01:00
vcoppe c5982f725c New translations clean.mdx (Danish) 2025-11-12 17:54:26 +01:00
vcoppe 063e567a09 New translations clean.mdx (Czech) 2025-11-12 17:54:24 +01:00
vcoppe a0b067d92e New translations clean.mdx (Catalan) 2025-11-12 17:54:23 +01:00
vcoppe 47d4c9cb20 New translations clean.mdx (Belarusian) 2025-11-12 17:54:22 +01:00
vcoppe 585ef679f4 New translations clean.mdx (Spanish) 2025-11-12 17:54:21 +01:00
vcoppe 62f5851e25 New translations clean.mdx (French) 2025-11-12 17:54:19 +01:00
vcoppe a6f81c03d2 New translations clean.mdx (Romanian) 2025-11-12 17:54:18 +01:00
vcoppe 70192b2cb9 New translations view.mdx (Serbian (Latin)) 2025-11-12 17:54:03 +01:00
vcoppe 9578aee988 New translations view.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:54:02 +01:00
vcoppe f91dee0074 New translations view.mdx (Latvian) 2025-11-12 17:54:01 +01:00
vcoppe fd2ebd10f5 New translations view.mdx (Thai) 2025-11-12 17:53:59 +01:00
vcoppe d8db4d2a73 New translations view.mdx (Indonesian) 2025-11-12 17:53:58 +01:00
vcoppe ebccd487be New translations view.mdx (Portuguese, Brazilian) 2025-11-12 17:53:57 +01:00
vcoppe 42a1a73dbf New translations view.mdx (Vietnamese) 2025-11-12 17:53:56 +01:00
vcoppe 7b543b65a6 New translations view.mdx (Chinese Simplified) 2025-11-12 17:53:54 +01:00
vcoppe 9d870faa1e New translations view.mdx (Ukrainian) 2025-11-12 17:53:53 +01:00
vcoppe b514595b24 New translations view.mdx (Turkish) 2025-11-12 17:53:52 +01:00
vcoppe ea6d9783fd New translations view.mdx (Swedish) 2025-11-12 17:53:50 +01:00
vcoppe 6686d48249 New translations view.mdx (Russian) 2025-11-12 17:53:49 +01:00
vcoppe 94a243b3b9 New translations view.mdx (Portuguese) 2025-11-12 17:53:48 +01:00
vcoppe 284fec9857 New translations view.mdx (Polish) 2025-11-12 17:53:47 +01:00
vcoppe 36b0b82625 New translations view.mdx (Norwegian) 2025-11-12 17:53:45 +01:00
vcoppe 0f0628024f New translations view.mdx (Dutch) 2025-11-12 17:53:44 +01:00
vcoppe 3633448af5 New translations view.mdx (Lithuanian) 2025-11-12 17:53:43 +01:00
vcoppe ef500ac6f7 New translations view.mdx (Korean) 2025-11-12 17:53:41 +01:00
vcoppe 2678ce6948 New translations view.mdx (Italian) 2025-11-12 17:53:40 +01:00
vcoppe 1f2bca7271 New translations view.mdx (Hungarian) 2025-11-12 17:53:39 +01:00
vcoppe ff31897603 New translations view.mdx (Hebrew) 2025-11-12 17:53:38 +01:00
vcoppe 930930fc43 New translations view.mdx (Finnish) 2025-11-12 17:53:37 +01:00
vcoppe 158120e396 New translations view.mdx (Basque) 2025-11-12 17:53:35 +01:00
vcoppe ae61d76e3f New translations view.mdx (Greek) 2025-11-12 17:53:34 +01:00
vcoppe 3df66d5653 New translations view.mdx (German) 2025-11-12 17:53:33 +01:00
vcoppe f10a27f882 New translations view.mdx (Danish) 2025-11-12 17:53:32 +01:00
vcoppe 88b776b415 New translations view.mdx (Czech) 2025-11-12 17:53:31 +01:00
vcoppe d3e0f65e9e New translations view.mdx (Catalan) 2025-11-12 17:53:29 +01:00
vcoppe 666554bf18 New translations view.mdx (Belarusian) 2025-11-12 17:53:28 +01:00
vcoppe fc722177e8 New translations view.mdx (Spanish) 2025-11-12 17:53:27 +01:00
vcoppe d5a3f32811 New translations view.mdx (French) 2025-11-12 17:53:26 +01:00
vcoppe 7bbbe8d861 New translations view.mdx (Romanian) 2025-11-12 17:53:25 +01:00
vcoppe ddc6f2e10b New translations settings.mdx (Serbian (Latin)) 2025-11-12 17:53:24 +01:00
vcoppe ff78e98ae2 New translations settings.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:53:22 +01:00
vcoppe ecf54ea6da New translations settings.mdx (Latvian) 2025-11-12 17:53:21 +01:00
vcoppe ca0e1110ca New translations settings.mdx (Thai) 2025-11-12 17:53:20 +01:00
vcoppe ca128d332f New translations settings.mdx (Indonesian) 2025-11-12 17:53:19 +01:00
vcoppe ad027cb58c New translations settings.mdx (Portuguese, Brazilian) 2025-11-12 17:53:18 +01:00
vcoppe ff7f1d7f67 New translations settings.mdx (Vietnamese) 2025-11-12 17:53:16 +01:00
vcoppe ded9f33313 New translations settings.mdx (Chinese Simplified) 2025-11-12 17:53:15 +01:00
vcoppe ccbc9211ac New translations settings.mdx (Ukrainian) 2025-11-12 17:53:14 +01:00
vcoppe 85684fe415 New translations settings.mdx (Turkish) 2025-11-12 17:53:13 +01:00
vcoppe aeb60c50ee New translations settings.mdx (Swedish) 2025-11-12 17:53:12 +01:00
vcoppe f81405e409 New translations settings.mdx (Russian) 2025-11-12 17:53:11 +01:00
vcoppe 294d85e33b New translations settings.mdx (Portuguese) 2025-11-12 17:53:09 +01:00
vcoppe 2d52865487 New translations settings.mdx (Polish) 2025-11-12 17:53:08 +01:00
vcoppe cdba2179ff New translations settings.mdx (Norwegian) 2025-11-12 17:53:07 +01:00
vcoppe 60a521373d New translations settings.mdx (Dutch) 2025-11-12 17:53:06 +01:00
vcoppe 1bb65ea66f New translations settings.mdx (Lithuanian) 2025-11-12 17:53:05 +01:00
vcoppe d0ceecbf51 New translations settings.mdx (Korean) 2025-11-12 17:53:03 +01:00
vcoppe 57af9b9ece New translations settings.mdx (Italian) 2025-11-12 17:53:02 +01:00
vcoppe 27d10bf9fe New translations settings.mdx (Hungarian) 2025-11-12 17:53:01 +01:00
vcoppe 0ee0814779 New translations settings.mdx (Hebrew) 2025-11-12 17:53:00 +01:00
vcoppe 384af64599 New translations settings.mdx (Finnish) 2025-11-12 17:52:58 +01:00
vcoppe a4f06e5196 New translations settings.mdx (Basque) 2025-11-12 17:52:57 +01:00
vcoppe 150b7878fc New translations settings.mdx (Greek) 2025-11-12 17:52:56 +01:00
vcoppe da83cc1649 New translations settings.mdx (German) 2025-11-12 17:52:54 +01:00
vcoppe 5be4e6816d New translations settings.mdx (Danish) 2025-11-12 17:52:53 +01:00
vcoppe 9769d7f9ec New translations settings.mdx (Czech) 2025-11-12 17:52:52 +01:00
vcoppe d7b843db26 New translations settings.mdx (Catalan) 2025-11-12 17:52:50 +01:00
vcoppe 23ee01a8e9 New translations settings.mdx (Belarusian) 2025-11-12 17:52:49 +01:00
vcoppe adc2197d98 New translations settings.mdx (Spanish) 2025-11-12 17:52:48 +01:00
vcoppe f5c95fe0f4 New translations settings.mdx (French) 2025-11-12 17:52:47 +01:00
vcoppe 38523f1c42 New translations settings.mdx (Romanian) 2025-11-12 17:52:45 +01:00
vcoppe 747d3b28ce New translations file.mdx (Serbian (Latin)) 2025-11-12 17:52:44 +01:00
vcoppe a63683d894 New translations file.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:52:43 +01:00
vcoppe 13907f2b5c New translations file.mdx (Latvian) 2025-11-12 17:52:41 +01:00
vcoppe 9d448cb8d4 New translations file.mdx (Thai) 2025-11-12 17:52:40 +01:00
vcoppe 7647ca0762 New translations file.mdx (Indonesian) 2025-11-12 17:52:39 +01:00
vcoppe da9021c3ff New translations file.mdx (Portuguese, Brazilian) 2025-11-12 17:52:38 +01:00
vcoppe 5a8232167a New translations file.mdx (Vietnamese) 2025-11-12 17:52:37 +01:00
vcoppe f937c9750d New translations file.mdx (Chinese Simplified) 2025-11-12 17:52:35 +01:00
vcoppe 65a10b2daa New translations file.mdx (Ukrainian) 2025-11-12 17:52:34 +01:00
vcoppe 20c87a14aa New translations file.mdx (Turkish) 2025-11-12 17:52:33 +01:00
vcoppe f77571b7ef New translations file.mdx (Swedish) 2025-11-12 17:52:32 +01:00
vcoppe 9e95c4b3e1 New translations file.mdx (Russian) 2025-11-12 17:52:30 +01:00
vcoppe 92e46ec540 New translations file.mdx (Portuguese) 2025-11-12 17:52:29 +01:00
vcoppe 1713437813 New translations file.mdx (Polish) 2025-11-12 17:52:28 +01:00
vcoppe 5bf88a34f7 New translations file.mdx (Norwegian) 2025-11-12 17:52:26 +01:00
vcoppe 75c6a2facf New translations file.mdx (Dutch) 2025-11-12 17:52:25 +01:00
vcoppe 3c728115b8 New translations file.mdx (Lithuanian) 2025-11-12 17:52:24 +01:00
vcoppe 51d56dcf7a New translations file.mdx (Korean) 2025-11-12 17:52:23 +01:00
vcoppe 6fe0aa3ee6 New translations file.mdx (Italian) 2025-11-12 17:52:21 +01:00
vcoppe 670bccd2aa New translations file.mdx (Hungarian) 2025-11-12 17:52:20 +01:00
vcoppe 7cb253565e New translations file.mdx (Hebrew) 2025-11-12 17:52:19 +01:00
vcoppe 54e1900e6d New translations file.mdx (Finnish) 2025-11-12 17:52:18 +01:00
vcoppe d83194b225 New translations file.mdx (Basque) 2025-11-12 17:52:16 +01:00
vcoppe 82eb9a1dc9 New translations file.mdx (Greek) 2025-11-12 17:52:15 +01:00
vcoppe d298e51a6c New translations file.mdx (German) 2025-11-12 17:52:14 +01:00
vcoppe 2b7e3a2b5d New translations file.mdx (Danish) 2025-11-12 17:52:12 +01:00
vcoppe 0644952db3 New translations file.mdx (Czech) 2025-11-12 17:52:11 +01:00
vcoppe 08e5dc382f New translations file.mdx (Catalan) 2025-11-12 17:52:10 +01:00
vcoppe afd3d5e8dc New translations file.mdx (Belarusian) 2025-11-12 17:52:09 +01:00
vcoppe b2fa2d8685 New translations file.mdx (Spanish) 2025-11-12 17:52:07 +01:00
vcoppe f8e5ccc5bb New translations file.mdx (French) 2025-11-12 17:52:06 +01:00
vcoppe 63b56166cb New translations file.mdx (Romanian) 2025-11-12 17:52:05 +01:00
vcoppe 357791d17c New translations edit.mdx (Serbian (Latin)) 2025-11-12 17:52:04 +01:00
vcoppe d095f6734d New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:52:03 +01:00
vcoppe ea8771d6f5 New translations edit.mdx (Latvian) 2025-11-12 17:52:01 +01:00
vcoppe c8e28127c3 New translations edit.mdx (Thai) 2025-11-12 17:52:00 +01:00
vcoppe cae013a68d New translations edit.mdx (Indonesian) 2025-11-12 17:51:59 +01:00
vcoppe caa56ee16b New translations edit.mdx (Portuguese, Brazilian) 2025-11-12 17:51:57 +01:00
vcoppe 80e11a7723 New translations edit.mdx (Vietnamese) 2025-11-12 17:51:56 +01:00
vcoppe 87b3126eb0 New translations edit.mdx (Chinese Simplified) 2025-11-12 17:51:55 +01:00
vcoppe 8388c8db23 New translations edit.mdx (Ukrainian) 2025-11-12 17:51:54 +01:00
vcoppe 2bce1dca84 New translations edit.mdx (Turkish) 2025-11-12 17:51:52 +01:00
vcoppe 749be1eb73 New translations edit.mdx (Swedish) 2025-11-12 17:51:51 +01:00
vcoppe 23b74a1d63 New translations edit.mdx (Russian) 2025-11-12 17:51:50 +01:00
vcoppe b2f10c2f68 New translations edit.mdx (Portuguese) 2025-11-12 17:51:49 +01:00
vcoppe 2aa7f12514 New translations edit.mdx (Polish) 2025-11-12 17:51:47 +01:00
vcoppe cea8cfbfa6 New translations edit.mdx (Norwegian) 2025-11-12 17:51:46 +01:00
vcoppe e15275d96d New translations edit.mdx (Dutch) 2025-11-12 17:51:44 +01:00
vcoppe 3b6fb4d170 New translations edit.mdx (Lithuanian) 2025-11-12 17:51:43 +01:00
vcoppe 05c83c63e7 New translations edit.mdx (Korean) 2025-11-12 17:51:42 +01:00
vcoppe 470ca982a1 New translations edit.mdx (Italian) 2025-11-12 17:51:41 +01:00
vcoppe ec62477937 New translations edit.mdx (Hungarian) 2025-11-12 17:51:40 +01:00
vcoppe fed8474dce New translations edit.mdx (Hebrew) 2025-11-12 17:51:38 +01:00
vcoppe 7dc93302bf New translations edit.mdx (Finnish) 2025-11-12 17:51:37 +01:00
vcoppe 3105c444ce New translations edit.mdx (Basque) 2025-11-12 17:51:36 +01:00
vcoppe a2cae2e086 New translations edit.mdx (Greek) 2025-11-12 17:51:35 +01:00
vcoppe cd4320fe08 New translations edit.mdx (German) 2025-11-12 17:51:33 +01:00
vcoppe 32b07b0e32 New translations edit.mdx (Danish) 2025-11-12 17:51:32 +01:00
vcoppe 3d6234d5b9 New translations edit.mdx (Czech) 2025-11-12 17:51:31 +01:00
vcoppe 2463c8b40a New translations edit.mdx (Catalan) 2025-11-12 17:51:30 +01:00
vcoppe 0a9206b1d3 New translations edit.mdx (Belarusian) 2025-11-12 17:51:28 +01:00
vcoppe 7cd1c0dd69 New translations edit.mdx (Spanish) 2025-11-12 17:51:27 +01:00
vcoppe 3111927f02 New translations edit.mdx (French) 2025-11-12 17:51:26 +01:00
vcoppe c471eb160c New translations edit.mdx (Romanian) 2025-11-12 17:51:24 +01:00
vcoppe c983abb49f New translations map-controls.mdx (Serbian (Latin)) 2025-11-12 17:51:10 +01:00
vcoppe 5195e4f591 New translations map-controls.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:51:08 +01:00
vcoppe 538d04824d New translations map-controls.mdx (Latvian) 2025-11-12 17:51:06 +01:00
vcoppe 4666fa418d New translations map-controls.mdx (Thai) 2025-11-12 17:51:05 +01:00
vcoppe 87ce67c326 New translations map-controls.mdx (Indonesian) 2025-11-12 17:51:04 +01:00
vcoppe 6eaec189e6 New translations map-controls.mdx (Portuguese, Brazilian) 2025-11-12 17:51:03 +01:00
vcoppe 6f342951ce New translations map-controls.mdx (Vietnamese) 2025-11-12 17:51:01 +01:00
vcoppe aacab6e62c New translations map-controls.mdx (Chinese Simplified) 2025-11-12 17:51:00 +01:00
vcoppe cc7a07e872 New translations map-controls.mdx (Ukrainian) 2025-11-12 17:50:59 +01:00
vcoppe 9127031ac9 New translations map-controls.mdx (Turkish) 2025-11-12 17:50:58 +01:00
vcoppe 9aa654e69d New translations map-controls.mdx (Swedish) 2025-11-12 17:50:56 +01:00
vcoppe 573d4e58bb New translations map-controls.mdx (Russian) 2025-11-12 17:50:55 +01:00
vcoppe e7c96e3b19 New translations map-controls.mdx (Portuguese) 2025-11-12 17:50:54 +01:00
vcoppe c9bd41e58e New translations map-controls.mdx (Polish) 2025-11-12 17:50:53 +01:00
vcoppe 5027903820 New translations map-controls.mdx (Norwegian) 2025-11-12 17:50:51 +01:00
vcoppe 767ebc573a New translations map-controls.mdx (Dutch) 2025-11-12 17:50:50 +01:00
vcoppe 5a3d66c885 New translations map-controls.mdx (Lithuanian) 2025-11-12 17:50:49 +01:00
vcoppe e84e6bd6c5 New translations map-controls.mdx (Korean) 2025-11-12 17:50:48 +01:00
vcoppe bee6a7dacc New translations map-controls.mdx (Italian) 2025-11-12 17:50:47 +01:00
vcoppe 9876c7506c New translations map-controls.mdx (Hungarian) 2025-11-12 17:50:45 +01:00
vcoppe afb477d155 New translations map-controls.mdx (Hebrew) 2025-11-12 17:50:44 +01:00
vcoppe 2c9a8491ca New translations map-controls.mdx (Finnish) 2025-11-12 17:50:43 +01:00
vcoppe f6a9b37cc2 New translations map-controls.mdx (Basque) 2025-11-12 17:50:41 +01:00
vcoppe 1852214758 New translations map-controls.mdx (Greek) 2025-11-12 17:50:40 +01:00
vcoppe a404591c57 New translations map-controls.mdx (German) 2025-11-12 17:50:39 +01:00
vcoppe 406f8012fc New translations map-controls.mdx (Danish) 2025-11-12 17:50:38 +01:00
vcoppe 9de5fa1819 New translations map-controls.mdx (Czech) 2025-11-12 17:50:36 +01:00
vcoppe da7862546c New translations map-controls.mdx (Catalan) 2025-11-12 17:50:35 +01:00
vcoppe 8314ee0c30 New translations map-controls.mdx (Belarusian) 2025-11-12 17:50:34 +01:00
vcoppe ba0da25c6c New translations map-controls.mdx (Spanish) 2025-11-12 17:50:32 +01:00
vcoppe 9b432cef1b New translations map-controls.mdx (French) 2025-11-12 17:50:31 +01:00
vcoppe ada6adaadf New translations map-controls.mdx (Romanian) 2025-11-12 17:50:30 +01:00
vcoppe 4fe7d72e65 New translations translation.mdx (Serbian (Latin)) 2025-11-12 17:50:16 +01:00
vcoppe aafc5df915 New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:50:15 +01:00
vcoppe d4cbaeb1e7 New translations translation.mdx (Latvian) 2025-11-12 17:50:13 +01:00
vcoppe 36300aaa45 New translations translation.mdx (Thai) 2025-11-12 17:50:12 +01:00
vcoppe 693800e0b8 New translations translation.mdx (Indonesian) 2025-11-12 17:50:11 +01:00
vcoppe 245c0eecc6 New translations translation.mdx (Portuguese, Brazilian) 2025-11-12 17:50:10 +01:00
vcoppe 25fe02602f New translations translation.mdx (Vietnamese) 2025-11-12 17:50:09 +01:00
vcoppe 5524ddc6a0 New translations translation.mdx (Chinese Simplified) 2025-11-12 17:50:07 +01:00
vcoppe 2fabbec217 New translations translation.mdx (Ukrainian) 2025-11-12 17:50:06 +01:00
vcoppe 3c080873e5 New translations translation.mdx (Turkish) 2025-11-12 17:50:05 +01:00
vcoppe f252afb29f New translations translation.mdx (Swedish) 2025-11-12 17:50:04 +01:00
vcoppe 3109c0002f New translations translation.mdx (Russian) 2025-11-12 17:50:02 +01:00
vcoppe 059d7d6ae5 New translations translation.mdx (Portuguese) 2025-11-12 17:50:01 +01:00
vcoppe 58f159c0c9 New translations translation.mdx (Polish) 2025-11-12 17:50:00 +01:00
vcoppe 5a6c115121 New translations translation.mdx (Norwegian) 2025-11-12 17:49:59 +01:00
vcoppe 4cb85a543c New translations translation.mdx (Dutch) 2025-11-12 17:49:58 +01:00
vcoppe d99c3e8f43 New translations translation.mdx (Lithuanian) 2025-11-12 17:49:56 +01:00
vcoppe a2908b00ef New translations translation.mdx (Korean) 2025-11-12 17:49:55 +01:00
vcoppe 7b88e161e4 New translations translation.mdx (Italian) 2025-11-12 17:49:54 +01:00
vcoppe 96ea0d6ec0 New translations translation.mdx (Hungarian) 2025-11-12 17:49:53 +01:00
vcoppe bbfed01367 New translations translation.mdx (Hebrew) 2025-11-12 17:49:52 +01:00
vcoppe 65d81a0d58 New translations translation.mdx (Finnish) 2025-11-12 17:49:50 +01:00
vcoppe 186908fd3b New translations translation.mdx (Basque) 2025-11-12 17:49:49 +01:00
vcoppe ab43314a53 New translations translation.mdx (Greek) 2025-11-12 17:49:48 +01:00
vcoppe ed1c76879d New translations translation.mdx (German) 2025-11-12 17:49:47 +01:00
vcoppe 38af47f2eb New translations translation.mdx (Danish) 2025-11-12 17:49:46 +01:00
vcoppe 302d84baf7 New translations translation.mdx (Czech) 2025-11-12 17:49:45 +01:00
vcoppe 2b43f5f74f New translations translation.mdx (Catalan) 2025-11-12 17:49:43 +01:00
vcoppe e4630052d1 New translations translation.mdx (Belarusian) 2025-11-12 17:49:42 +01:00
vcoppe 8711f942c0 New translations translation.mdx (Spanish) 2025-11-12 17:49:41 +01:00
vcoppe 5282a6e045 New translations translation.mdx (French) 2025-11-12 17:49:40 +01:00
vcoppe 55cbbfa920 New translations translation.mdx (Romanian) 2025-11-12 17:49:39 +01:00
vcoppe 68371d716b New translations funding.mdx (Serbian (Latin)) 2025-11-12 17:49:25 +01:00
vcoppe 027c0b856f New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:49:24 +01:00
vcoppe 422978e404 New translations funding.mdx (Latvian) 2025-11-12 17:49:22 +01:00
vcoppe 8b5ce045e4 New translations funding.mdx (Thai) 2025-11-12 17:49:21 +01:00
vcoppe 86f126e735 New translations funding.mdx (Indonesian) 2025-11-12 17:49:20 +01:00
vcoppe 0e3898c065 New translations funding.mdx (Portuguese, Brazilian) 2025-11-12 17:49:19 +01:00
vcoppe 1e3a74643e New translations funding.mdx (Vietnamese) 2025-11-12 17:49:18 +01:00
vcoppe 4792689998 New translations funding.mdx (Chinese Simplified) 2025-11-12 17:49:17 +01:00
vcoppe 0eec7f0813 New translations funding.mdx (Ukrainian) 2025-11-12 17:49:15 +01:00
vcoppe 2c1bf56638 New translations funding.mdx (Turkish) 2025-11-12 17:49:14 +01:00
vcoppe 57a4279647 New translations funding.mdx (Swedish) 2025-11-12 17:49:13 +01:00
vcoppe f9146d62cf New translations funding.mdx (Russian) 2025-11-12 17:49:12 +01:00
vcoppe 694528a294 New translations funding.mdx (Portuguese) 2025-11-12 17:49:11 +01:00
vcoppe f11968d2c6 New translations funding.mdx (Polish) 2025-11-12 17:49:09 +01:00
vcoppe b64b354f36 New translations funding.mdx (Norwegian) 2025-11-12 17:49:08 +01:00
vcoppe 0cb7b61043 New translations funding.mdx (Dutch) 2025-11-12 17:49:07 +01:00
vcoppe b45fb37b02 New translations funding.mdx (Lithuanian) 2025-11-12 17:49:06 +01:00
vcoppe bfbb25c71e New translations funding.mdx (Korean) 2025-11-12 17:49:04 +01:00
vcoppe 8a0e89c14d New translations funding.mdx (Italian) 2025-11-12 17:49:03 +01:00
vcoppe 8c7573b669 New translations funding.mdx (Hungarian) 2025-11-12 17:49:02 +01:00
vcoppe 0cae837e07 New translations funding.mdx (Hebrew) 2025-11-12 17:49:01 +01:00
vcoppe ef36fb567a New translations funding.mdx (Finnish) 2025-11-12 17:48:59 +01:00
vcoppe 5431fe1eac New translations funding.mdx (Basque) 2025-11-12 17:48:58 +01:00
vcoppe 07b4e4b59d New translations funding.mdx (Greek) 2025-11-12 17:48:57 +01:00
vcoppe fa06da7f03 New translations funding.mdx (German) 2025-11-12 17:48:56 +01:00
vcoppe 6cc6a34b74 New translations funding.mdx (Danish) 2025-11-12 17:48:55 +01:00
vcoppe 7187c9ce71 New translations funding.mdx (Czech) 2025-11-12 17:48:54 +01:00
vcoppe b209ce2524 New translations funding.mdx (Catalan) 2025-11-12 17:48:52 +01:00
vcoppe c37dd605cc New translations funding.mdx (Belarusian) 2025-11-12 17:48:51 +01:00
vcoppe d0a3ea99bd New translations funding.mdx (Spanish) 2025-11-12 17:48:50 +01:00
vcoppe 80077b92c4 New translations funding.mdx (French) 2025-11-12 17:48:49 +01:00
vcoppe dcf919e440 New translations funding.mdx (Romanian) 2025-11-12 17:48:48 +01:00
vcoppe 48569e05e8 New translations gpx.mdx (Serbian (Latin)) 2025-11-12 17:48:46 +01:00
vcoppe 60b24e9c3b New translations gpx.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:48:45 +01:00
vcoppe fd4a97f547 New translations gpx.mdx (Latvian) 2025-11-12 17:48:44 +01:00
vcoppe 8b348aeea9 New translations gpx.mdx (Thai) 2025-11-12 17:48:43 +01:00
vcoppe 6d0400e85e New translations gpx.mdx (Indonesian) 2025-11-12 17:48:41 +01:00
vcoppe d60cb68430 New translations gpx.mdx (Portuguese, Brazilian) 2025-11-12 17:48:40 +01:00
vcoppe 5834940965 New translations gpx.mdx (Vietnamese) 2025-11-12 17:48:39 +01:00
vcoppe 2680d3eb5c New translations gpx.mdx (Chinese Simplified) 2025-11-12 17:48:38 +01:00
vcoppe 149a1e5b0d New translations gpx.mdx (Ukrainian) 2025-11-12 17:48:36 +01:00
vcoppe 1e4a0bfb8e New translations gpx.mdx (Turkish) 2025-11-12 17:48:35 +01:00
vcoppe ba4c29c44d New translations gpx.mdx (Swedish) 2025-11-12 17:48:33 +01:00
vcoppe a632bedb4c New translations gpx.mdx (Russian) 2025-11-12 17:48:32 +01:00
vcoppe fc8deeb0c5 New translations gpx.mdx (Portuguese) 2025-11-12 17:48:31 +01:00
vcoppe e9e462b020 New translations gpx.mdx (Polish) 2025-11-12 17:48:30 +01:00
vcoppe 713d2cd7ee New translations gpx.mdx (Norwegian) 2025-11-12 17:48:28 +01:00
vcoppe dea37b2e83 New translations gpx.mdx (Dutch) 2025-11-12 17:48:27 +01:00
vcoppe 95e02956fb New translations gpx.mdx (Lithuanian) 2025-11-12 17:48:26 +01:00
vcoppe 87ef568de0 New translations gpx.mdx (Korean) 2025-11-12 17:48:25 +01:00
vcoppe a4c407e3ab New translations gpx.mdx (Italian) 2025-11-12 17:48:23 +01:00
vcoppe 543af45daa New translations gpx.mdx (Hungarian) 2025-11-12 17:48:22 +01:00
vcoppe aca0442928 New translations gpx.mdx (Hebrew) 2025-11-12 17:48:21 +01:00
vcoppe af1cc42b0d New translations gpx.mdx (Finnish) 2025-11-12 17:48:19 +01:00
vcoppe 1bca40eb6a New translations gpx.mdx (Basque) 2025-11-12 17:48:18 +01:00
vcoppe 2c099c3a2a New translations gpx.mdx (Greek) 2025-11-12 17:48:17 +01:00
vcoppe 1afd9fdab2 New translations gpx.mdx (German) 2025-11-12 17:48:16 +01:00
vcoppe 522eea1371 New translations gpx.mdx (Danish) 2025-11-12 17:48:15 +01:00
vcoppe e9fc115bdd New translations gpx.mdx (Czech) 2025-11-12 17:48:13 +01:00
vcoppe 711e4a84f8 New translations gpx.mdx (Catalan) 2025-11-12 17:48:12 +01:00
vcoppe ac6b9c855a New translations gpx.mdx (Belarusian) 2025-11-12 17:48:11 +01:00
vcoppe eb1a9f8b9f New translations gpx.mdx (Spanish) 2025-11-12 17:48:09 +01:00
vcoppe 4fc08faa48 New translations gpx.mdx (French) 2025-11-12 17:48:08 +01:00
vcoppe 4dcf6b672c New translations gpx.mdx (Romanian) 2025-11-12 17:48:07 +01:00
vcoppe 36c8d8668a New translations files-and-stats.mdx (Serbian (Latin)) 2025-11-12 17:47:53 +01:00
vcoppe ffe3d1f5bb New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:47:51 +01:00
vcoppe 260cad776e New translations files-and-stats.mdx (Latvian) 2025-11-12 17:47:50 +01:00
vcoppe 3cb6fd1f44 New translations files-and-stats.mdx (Thai) 2025-11-12 17:47:49 +01:00
vcoppe 8f0d0b24c6 New translations files-and-stats.mdx (Indonesian) 2025-11-12 17:47:48 +01:00
vcoppe f80a06b258 New translations files-and-stats.mdx (Portuguese, Brazilian) 2025-11-12 17:47:46 +01:00
vcoppe de1ce49ba3 New translations files-and-stats.mdx (Vietnamese) 2025-11-12 17:47:45 +01:00
vcoppe f20b26809d New translations files-and-stats.mdx (Chinese Simplified) 2025-11-12 17:47:44 +01:00
vcoppe dbf788976f New translations files-and-stats.mdx (Ukrainian) 2025-11-12 17:47:43 +01:00
vcoppe 2f8cd71c51 New translations files-and-stats.mdx (Turkish) 2025-11-12 17:47:41 +01:00
vcoppe ea9757a288 New translations files-and-stats.mdx (Swedish) 2025-11-12 17:47:40 +01:00
vcoppe 782ca0c11a New translations files-and-stats.mdx (Russian) 2025-11-12 17:47:39 +01:00
vcoppe 56d5474bd7 New translations files-and-stats.mdx (Portuguese) 2025-11-12 17:47:37 +01:00
vcoppe 683c3a6122 New translations files-and-stats.mdx (Polish) 2025-11-12 17:47:36 +01:00
vcoppe b89dc25959 New translations files-and-stats.mdx (Norwegian) 2025-11-12 17:47:35 +01:00
vcoppe ab09fe19d8 New translations routing.mdx (Czech) 2025-11-12 17:47:33 +01:00
vcoppe d16cd4bf8e New translations files-and-stats.mdx (Dutch) 2025-11-12 17:47:32 +01:00
vcoppe 2e853006a5 New translations files-and-stats.mdx (Lithuanian) 2025-11-12 17:47:31 +01:00
vcoppe fbfaf858e6 New translations files-and-stats.mdx (Korean) 2025-11-12 17:47:30 +01:00
vcoppe 70511c3ece New translations files-and-stats.mdx (Italian) 2025-11-12 17:47:28 +01:00
vcoppe 7c529eefb3 New translations files-and-stats.mdx (Hungarian) 2025-11-12 17:47:27 +01:00
vcoppe bb794cf8f3 New translations files-and-stats.mdx (Hebrew) 2025-11-12 17:47:26 +01:00
vcoppe 20ae712719 New translations files-and-stats.mdx (Finnish) 2025-11-12 17:47:25 +01:00
vcoppe 607cd21f41 New translations files-and-stats.mdx (Basque) 2025-11-12 17:47:24 +01:00
vcoppe 754394e1a8 New translations files-and-stats.mdx (Greek) 2025-11-12 17:47:22 +01:00
vcoppe 7a8fcb46c7 New translations files-and-stats.mdx (German) 2025-11-12 17:47:21 +01:00
vcoppe 59b5a5068d New translations files-and-stats.mdx (Danish) 2025-11-12 17:47:20 +01:00
vcoppe d65bdc103d New translations files-and-stats.mdx (Czech) 2025-11-12 17:47:19 +01:00
vcoppe aefd924f05 New translations files-and-stats.mdx (Catalan) 2025-11-12 17:47:17 +01:00
vcoppe 96018b9e6a New translations files-and-stats.mdx (Belarusian) 2025-11-12 17:47:16 +01:00
vcoppe 3865d52c29 New translations files-and-stats.mdx (Spanish) 2025-11-12 17:47:15 +01:00
vcoppe 20c8133301 New translations files-and-stats.mdx (French) 2025-11-12 17:47:14 +01:00
vcoppe 9705f56e2f New translations files-and-stats.mdx (Romanian) 2025-11-12 17:47:12 +01:00
vcoppe f4d2459a10 New translations elevation.mdx (Serbian (Latin)) 2025-11-12 17:43:59 +01:00
vcoppe 7073baea6e New translations elevation.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:43:58 +01:00
vcoppe 7834e6294c New translations elevation.mdx (Latvian) 2025-11-12 17:43:56 +01:00
vcoppe a2d2d79ba4 New translations elevation.mdx (Thai) 2025-11-12 17:43:55 +01:00
vcoppe 087132a8b3 New translations elevation.mdx (Indonesian) 2025-11-12 17:43:54 +01:00
vcoppe e62eca1fdf New translations elevation.mdx (Portuguese, Brazilian) 2025-11-12 17:43:53 +01:00
vcoppe f0333d0f68 New translations elevation.mdx (Vietnamese) 2025-11-12 17:43:52 +01:00
vcoppe 08e39ae6e0 New translations elevation.mdx (Chinese Simplified) 2025-11-12 17:43:50 +01:00
vcoppe 128484dd73 New translations elevation.mdx (Ukrainian) 2025-11-12 17:43:49 +01:00
vcoppe 1e2d14af4c New translations elevation.mdx (Turkish) 2025-11-12 17:43:48 +01:00
vcoppe 37e3b86a36 New translations elevation.mdx (Swedish) 2025-11-12 17:43:47 +01:00
vcoppe 13e49b7f2c New translations elevation.mdx (Russian) 2025-11-12 17:43:45 +01:00
vcoppe 8c3e556071 New translations elevation.mdx (Portuguese) 2025-11-12 17:43:44 +01:00
vcoppe 7a24f95566 New translations elevation.mdx (Polish) 2025-11-12 17:43:43 +01:00
vcoppe dbc14f071a New translations elevation.mdx (Norwegian) 2025-11-12 17:43:42 +01:00
vcoppe b75b5dc107 New translations elevation.mdx (Dutch) 2025-11-12 17:43:41 +01:00
vcoppe f1f6f3b0a8 New translations elevation.mdx (Lithuanian) 2025-11-12 17:43:39 +01:00
vcoppe a3f556a136 New translations elevation.mdx (Korean) 2025-11-12 17:43:38 +01:00
vcoppe 04ec1d3df4 New translations elevation.mdx (Italian) 2025-11-12 17:43:37 +01:00
vcoppe da8000787d New translations elevation.mdx (Hungarian) 2025-11-12 17:43:36 +01:00
vcoppe faa4e917f1 New translations elevation.mdx (Hebrew) 2025-11-12 17:43:34 +01:00
vcoppe 99dc05f55b New translations elevation.mdx (Finnish) 2025-11-12 17:43:33 +01:00
vcoppe 0f2c0b8cc7 New translations elevation.mdx (Basque) 2025-11-12 17:43:32 +01:00
vcoppe c4b1e75b0c New translations elevation.mdx (Greek) 2025-11-12 17:43:31 +01:00
vcoppe b2eb932a06 New translations elevation.mdx (German) 2025-11-12 17:43:29 +01:00
vcoppe aac6b15c77 New translations elevation.mdx (Danish) 2025-11-12 17:43:28 +01:00
vcoppe 2b4f1e3203 New translations elevation.mdx (Czech) 2025-11-12 17:43:27 +01:00
vcoppe 386320e12c New translations elevation.mdx (Catalan) 2025-11-12 17:43:26 +01:00
vcoppe e775e7918d New translations elevation.mdx (Belarusian) 2025-11-12 17:43:24 +01:00
vcoppe e0f60ddf28 New translations elevation.mdx (Spanish) 2025-11-12 17:43:23 +01:00
vcoppe 7e9b9500d5 New translations elevation.mdx (French) 2025-11-12 17:43:22 +01:00
vcoppe 29b143fb50 New translations elevation.mdx (Romanian) 2025-11-12 17:43:21 +01:00
vcoppe 0a6f454bb4 New translations time.mdx (Serbian (Latin)) 2025-11-12 17:43:06 +01:00
vcoppe ad21ab0a45 New translations time.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:43:05 +01:00
vcoppe 7958d32b90 New translations time.mdx (Latvian) 2025-11-12 17:43:04 +01:00
vcoppe 81bfb6184e New translations time.mdx (Thai) 2025-11-12 17:43:03 +01:00
vcoppe 79bc30cbe0 New translations time.mdx (Indonesian) 2025-11-12 17:43:01 +01:00
vcoppe 076d741453 New translations time.mdx (Portuguese, Brazilian) 2025-11-12 17:43:00 +01:00
vcoppe dc36e2e9d7 New translations time.mdx (Vietnamese) 2025-11-12 17:42:58 +01:00
vcoppe 0a3da42af9 New translations time.mdx (Chinese Simplified) 2025-11-12 17:42:57 +01:00
vcoppe 019d2b0c1b New translations time.mdx (Ukrainian) 2025-11-12 17:42:56 +01:00
vcoppe cf5c4bcd32 New translations time.mdx (Turkish) 2025-11-12 17:42:54 +01:00
vcoppe 8c813103f2 New translations time.mdx (Swedish) 2025-11-12 17:42:53 +01:00
vcoppe ed97c83a4c New translations time.mdx (Russian) 2025-11-12 17:42:52 +01:00
vcoppe a7dfb0b6d3 New translations time.mdx (Portuguese) 2025-11-12 17:42:51 +01:00
vcoppe be544e6051 New translations time.mdx (Polish) 2025-11-12 17:42:49 +01:00
vcoppe e457526e8f New translations time.mdx (Norwegian) 2025-11-12 17:42:48 +01:00
vcoppe 539e0a2045 New translations time.mdx (Dutch) 2025-11-12 17:42:47 +01:00
vcoppe fdcfca7633 New translations time.mdx (Lithuanian) 2025-11-12 17:42:46 +01:00
vcoppe bd0f0cabca New translations time.mdx (Korean) 2025-11-12 17:42:44 +01:00
vcoppe a94edc6fe2 New translations time.mdx (Italian) 2025-11-12 17:42:43 +01:00
vcoppe 295ea93340 New translations time.mdx (Hungarian) 2025-11-12 17:42:42 +01:00
vcoppe 4af58fa882 New translations time.mdx (Hebrew) 2025-11-12 17:42:41 +01:00
vcoppe 9b4b10e3b6 New translations time.mdx (Finnish) 2025-11-12 17:42:40 +01:00
vcoppe 24f27fcfb4 New translations time.mdx (Basque) 2025-11-12 17:42:38 +01:00
vcoppe 725a8b7959 New translations time.mdx (Greek) 2025-11-12 17:42:37 +01:00
vcoppe c7b5b4b7dd New translations time.mdx (German) 2025-11-12 17:42:36 +01:00
vcoppe 04ffafb3b7 New translations time.mdx (Danish) 2025-11-12 17:42:34 +01:00
vcoppe 485b6903e5 New translations time.mdx (Czech) 2025-11-12 17:42:33 +01:00
vcoppe b55d79564c New translations time.mdx (Catalan) 2025-11-12 17:42:32 +01:00
vcoppe e23de3adbc New translations time.mdx (Belarusian) 2025-11-12 17:42:31 +01:00
vcoppe b97aa933a2 New translations time.mdx (Spanish) 2025-11-12 17:42:29 +01:00
vcoppe f0f7d5ea2f New translations time.mdx (French) 2025-11-12 17:42:28 +01:00
vcoppe 30af13cf67 New translations time.mdx (Romanian) 2025-11-12 17:42:27 +01:00
vcoppe 45e8a2ffd8 New translations scissors.mdx (Serbian (Latin)) 2025-11-12 17:42:26 +01:00
vcoppe b41bb21fba New translations scissors.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:42:25 +01:00
vcoppe e828d82a5a New translations scissors.mdx (Latvian) 2025-11-12 17:42:23 +01:00
vcoppe 563ac5e44c New translations scissors.mdx (Thai) 2025-11-12 17:42:22 +01:00
vcoppe fbc46cf7bc New translations scissors.mdx (Indonesian) 2025-11-12 17:42:21 +01:00
vcoppe 5efc0dc11b New translations scissors.mdx (Portuguese, Brazilian) 2025-11-12 17:42:20 +01:00
vcoppe d16ce01efd New translations scissors.mdx (Vietnamese) 2025-11-12 17:42:18 +01:00
vcoppe 8ff0ca1855 New translations scissors.mdx (Chinese Simplified) 2025-11-12 17:42:17 +01:00
vcoppe 6e49714e80 New translations scissors.mdx (Ukrainian) 2025-11-12 17:42:16 +01:00
vcoppe f402ef278f New translations scissors.mdx (Turkish) 2025-11-12 17:42:15 +01:00
vcoppe 4fb32bc11b New translations scissors.mdx (Swedish) 2025-11-12 17:42:13 +01:00
vcoppe 684b29d479 New translations scissors.mdx (Russian) 2025-11-12 17:42:12 +01:00
vcoppe 5b2f253f10 New translations scissors.mdx (Portuguese) 2025-11-12 17:42:11 +01:00
vcoppe c88ad2c97b New translations scissors.mdx (Polish) 2025-11-12 17:42:10 +01:00
vcoppe 3e0f670e02 New translations scissors.mdx (Norwegian) 2025-11-12 17:42:08 +01:00
vcoppe 737e8167b4 New translations scissors.mdx (Dutch) 2025-11-12 17:42:07 +01:00
vcoppe 23be635323 New translations scissors.mdx (Lithuanian) 2025-11-12 17:42:06 +01:00
vcoppe 8d7d63a7fa New translations scissors.mdx (Korean) 2025-11-12 17:42:05 +01:00
vcoppe 8ad6c77c92 New translations scissors.mdx (Italian) 2025-11-12 17:42:03 +01:00
vcoppe 7d1f862185 New translations scissors.mdx (Hungarian) 2025-11-12 17:42:02 +01:00
vcoppe 2d9561de38 New translations scissors.mdx (Hebrew) 2025-11-12 17:42:01 +01:00
vcoppe 1c09d67846 New translations scissors.mdx (Finnish) 2025-11-12 17:42:00 +01:00
vcoppe 5f5775d201 New translations scissors.mdx (Basque) 2025-11-12 17:41:59 +01:00
vcoppe db64d97ef1 New translations scissors.mdx (Greek) 2025-11-12 17:41:57 +01:00
vcoppe 19b0f7e572 New translations scissors.mdx (German) 2025-11-12 17:41:56 +01:00
vcoppe 2fb0ccc752 New translations scissors.mdx (Danish) 2025-11-12 17:41:54 +01:00
vcoppe e6f1abad32 New translations scissors.mdx (Czech) 2025-11-12 17:41:53 +01:00
vcoppe 816f36de50 New translations scissors.mdx (Catalan) 2025-11-12 17:41:52 +01:00
vcoppe 193056531c New translations scissors.mdx (Belarusian) 2025-11-12 17:41:51 +01:00
vcoppe ce356ce3ff New translations scissors.mdx (Spanish) 2025-11-12 17:41:50 +01:00
vcoppe 334b6d7588 New translations scissors.mdx (French) 2025-11-12 17:41:48 +01:00
vcoppe 2c38082cec New translations scissors.mdx (Romanian) 2025-11-12 17:41:47 +01:00
vcoppe 5dfc5d35ea New translations routing.mdx (Serbian (Latin)) 2025-11-12 17:41:46 +01:00
vcoppe 9d6782e4eb New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:41:45 +01:00
vcoppe 81cf903e5f New translations routing.mdx (Latvian) 2025-11-12 17:41:43 +01:00
vcoppe 3d02a377ba New translations routing.mdx (Thai) 2025-11-12 17:41:42 +01:00
vcoppe e80659cf38 New translations routing.mdx (Indonesian) 2025-11-12 17:41:41 +01:00
vcoppe 3c0eeba130 New translations routing.mdx (Portuguese, Brazilian) 2025-11-12 17:41:39 +01:00
vcoppe 6911bc624f New translations routing.mdx (Vietnamese) 2025-11-12 17:41:38 +01:00
vcoppe d732745e2d New translations routing.mdx (Chinese Simplified) 2025-11-12 17:41:37 +01:00
vcoppe 9cd0fb8420 New translations routing.mdx (Ukrainian) 2025-11-12 17:41:35 +01:00
vcoppe e3ed704119 New translations routing.mdx (Turkish) 2025-11-12 17:41:34 +01:00
vcoppe 7ff575de97 New translations routing.mdx (Swedish) 2025-11-12 17:41:33 +01:00
vcoppe c0083be420 New translations routing.mdx (Russian) 2025-11-12 17:41:32 +01:00
vcoppe 4272a25ffd New translations routing.mdx (Portuguese) 2025-11-12 17:41:30 +01:00
vcoppe 3e39e93999 New translations routing.mdx (Polish) 2025-11-12 17:41:29 +01:00
vcoppe b0dce71d1e New translations routing.mdx (Norwegian) 2025-11-12 17:41:28 +01:00
vcoppe fd66246ec5 New translations routing.mdx (Dutch) 2025-11-12 17:41:26 +01:00
vcoppe 0dc1598eca New translations routing.mdx (Lithuanian) 2025-11-12 17:41:25 +01:00
vcoppe d5108acc99 New translations routing.mdx (Korean) 2025-11-12 17:41:24 +01:00
vcoppe d87facb618 New translations routing.mdx (Italian) 2025-11-12 17:41:22 +01:00
vcoppe e4c4b3f07d New translations routing.mdx (Hungarian) 2025-11-12 17:41:21 +01:00
vcoppe 70c4a52321 New translations routing.mdx (Hebrew) 2025-11-12 17:41:20 +01:00
vcoppe 7102ef0d26 New translations routing.mdx (Finnish) 2025-11-12 17:41:18 +01:00
vcoppe e91383d558 New translations routing.mdx (Basque) 2025-11-12 17:41:17 +01:00
vcoppe a4708a5fce New translations routing.mdx (Greek) 2025-11-12 17:41:16 +01:00
vcoppe 3d06200b71 New translations routing.mdx (German) 2025-11-12 17:41:14 +01:00
vcoppe 4d173d7d76 New translations routing.mdx (Danish) 2025-11-12 17:41:13 +01:00
vcoppe 1fb2f03835 New translations routing.mdx (Catalan) 2025-11-12 17:41:11 +01:00
vcoppe 15d010d895 New translations routing.mdx (Belarusian) 2025-11-12 17:41:10 +01:00
vcoppe 7367b19515 New translations routing.mdx (Spanish) 2025-11-12 17:41:09 +01:00
vcoppe 9202ee5289 New translations routing.mdx (French) 2025-11-12 17:41:07 +01:00
vcoppe a4bb04786d New translations routing.mdx (Romanian) 2025-11-12 17:41:06 +01:00
vcoppe 94591fdd4b New translations poi.mdx (Serbian (Latin)) 2025-11-12 17:41:04 +01:00
vcoppe 3df7b27d9b New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:41:03 +01:00
vcoppe bbf1bcbf58 New translations poi.mdx (Latvian) 2025-11-12 17:41:02 +01:00
vcoppe 383c3ec00c New translations poi.mdx (Thai) 2025-11-12 17:41:00 +01:00
vcoppe e328cdc21a New translations poi.mdx (Indonesian) 2025-11-12 17:40:59 +01:00
vcoppe c80b48e9c8 New translations poi.mdx (Portuguese, Brazilian) 2025-11-12 17:40:58 +01:00
vcoppe 4128ae3aed New translations poi.mdx (Vietnamese) 2025-11-12 17:40:57 +01:00
vcoppe e34f5b01bb New translations poi.mdx (Chinese Simplified) 2025-11-12 17:40:55 +01:00
vcoppe ef80df69ca New translations poi.mdx (Ukrainian) 2025-11-12 17:40:54 +01:00
vcoppe 4a34d6c599 New translations poi.mdx (Turkish) 2025-11-12 17:40:53 +01:00
vcoppe 55bb191268 New translations poi.mdx (Swedish) 2025-11-12 17:40:52 +01:00
vcoppe 36ae02a44d New translations poi.mdx (Russian) 2025-11-12 17:40:51 +01:00
vcoppe 43f282dded New translations poi.mdx (Portuguese) 2025-11-12 17:40:49 +01:00
vcoppe 639418692e New translations poi.mdx (Polish) 2025-11-12 17:40:48 +01:00
vcoppe 12e3a0cddd New translations poi.mdx (Norwegian) 2025-11-12 17:40:47 +01:00
vcoppe c6a6e9511f New translations poi.mdx (Dutch) 2025-11-12 17:40:45 +01:00
vcoppe 5f34726b22 New translations poi.mdx (Lithuanian) 2025-11-12 17:40:44 +01:00
vcoppe f9f16ec609 New translations poi.mdx (Korean) 2025-11-12 17:40:42 +01:00
vcoppe ae7f14a975 New translations poi.mdx (Italian) 2025-11-12 17:40:41 +01:00
vcoppe 1a2652855a New translations poi.mdx (Hungarian) 2025-11-12 17:40:40 +01:00
vcoppe bfb52060e7 New translations poi.mdx (Hebrew) 2025-11-12 17:40:39 +01:00
vcoppe 371998cef4 New translations poi.mdx (Finnish) 2025-11-12 17:40:38 +01:00
vcoppe af379947b8 New translations poi.mdx (Basque) 2025-11-12 17:40:36 +01:00
vcoppe 09bddc0958 New translations poi.mdx (Greek) 2025-11-12 17:40:35 +01:00
vcoppe b4e971596d New translations poi.mdx (German) 2025-11-12 17:40:34 +01:00
vcoppe 57bc7251cf New translations poi.mdx (Danish) 2025-11-12 17:40:32 +01:00
vcoppe 576142a25a New translations poi.mdx (Czech) 2025-11-12 17:40:31 +01:00
vcoppe 76a7f168af New translations poi.mdx (Catalan) 2025-11-12 17:40:30 +01:00
vcoppe 5b1ae587c0 New translations poi.mdx (Belarusian) 2025-11-12 17:40:29 +01:00
vcoppe c9cdcaeb94 New translations poi.mdx (Spanish) 2025-11-12 17:40:27 +01:00
vcoppe 39581801c6 New translations poi.mdx (French) 2025-11-12 17:40:26 +01:00
vcoppe 3983d39f0c New translations poi.mdx (Romanian) 2025-11-12 17:40:25 +01:00
vcoppe 4fc41816ca New translations minify.mdx (Serbian (Latin)) 2025-11-12 17:40:24 +01:00
vcoppe d102ff4de7 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:40:22 +01:00
vcoppe adf34ee85a New translations minify.mdx (Latvian) 2025-11-12 17:40:21 +01:00
vcoppe 870f1c11da New translations minify.mdx (Thai) 2025-11-12 17:40:20 +01:00
vcoppe 11aedd1274 New translations minify.mdx (Indonesian) 2025-11-12 17:40:18 +01:00
vcoppe ef04c874f0 New translations minify.mdx (Portuguese, Brazilian) 2025-11-12 17:40:17 +01:00
vcoppe d478f74252 New translations minify.mdx (Vietnamese) 2025-11-12 17:40:16 +01:00
vcoppe 3d140ae9c2 New translations minify.mdx (Chinese Simplified) 2025-11-12 17:40:15 +01:00
vcoppe 9dc652322f New translations minify.mdx (Ukrainian) 2025-11-12 17:40:13 +01:00
vcoppe 0ae7f59444 New translations minify.mdx (Turkish) 2025-11-12 17:40:12 +01:00
vcoppe e215d473c6 New translations minify.mdx (Swedish) 2025-11-12 17:40:11 +01:00
vcoppe 96cbd24d4f New translations minify.mdx (Russian) 2025-11-12 17:40:10 +01:00
vcoppe 665f12785d New translations minify.mdx (Portuguese) 2025-11-12 17:40:08 +01:00
vcoppe 7a933ff297 New translations minify.mdx (Polish) 2025-11-12 17:40:07 +01:00
vcoppe 9f59cce7c9 New translations minify.mdx (Norwegian) 2025-11-12 17:40:05 +01:00
vcoppe 2c04528c12 New translations minify.mdx (Dutch) 2025-11-12 17:40:04 +01:00
vcoppe aad22e2698 New translations minify.mdx (Lithuanian) 2025-11-12 17:40:03 +01:00
vcoppe 3714deac89 New translations minify.mdx (Korean) 2025-11-12 17:40:02 +01:00
vcoppe 37c4c9538b New translations minify.mdx (Italian) 2025-11-12 17:40:00 +01:00
vcoppe 5ca5473c1b New translations minify.mdx (Hungarian) 2025-11-12 17:39:59 +01:00
vcoppe 4b5bc7b2d7 New translations minify.mdx (Hebrew) 2025-11-12 17:39:57 +01:00
vcoppe 72aa9fe46f New translations minify.mdx (Finnish) 2025-11-12 17:39:56 +01:00
vcoppe ce08c5165b New translations minify.mdx (Basque) 2025-11-12 17:39:55 +01:00
vcoppe 61e6b9476d New translations minify.mdx (Greek) 2025-11-12 17:39:54 +01:00
vcoppe 4466787c94 New translations minify.mdx (German) 2025-11-12 17:39:53 +01:00
vcoppe 0df0e891cb New translations minify.mdx (Danish) 2025-11-12 17:39:51 +01:00
vcoppe 89d608b2ac New translations minify.mdx (Czech) 2025-11-12 17:39:50 +01:00
vcoppe 5b0c09f75d New translations minify.mdx (Catalan) 2025-11-12 17:39:49 +01:00
vcoppe a9e114388f New translations minify.mdx (Belarusian) 2025-11-12 17:39:47 +01:00
vcoppe 31c428ccbc New translations minify.mdx (Spanish) 2025-11-12 17:39:46 +01:00
vcoppe 9d4d70ee76 New translations minify.mdx (French) 2025-11-12 17:39:45 +01:00
vcoppe 5258327694 New translations minify.mdx (Romanian) 2025-11-12 17:39:44 +01:00
vcoppe e80875e3b0 New translations merge.mdx (Serbian (Latin)) 2025-11-12 17:39:42 +01:00
vcoppe cb42dcf2c4 New translations merge.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:39:41 +01:00
vcoppe fe9f3f0a55 New translations merge.mdx (Latvian) 2025-11-12 17:39:40 +01:00
vcoppe 712d193f7c New translations merge.mdx (Thai) 2025-11-12 17:39:39 +01:00
vcoppe c546185dda New translations merge.mdx (Indonesian) 2025-11-12 17:39:37 +01:00
vcoppe 5945a1c063 New translations merge.mdx (Portuguese, Brazilian) 2025-11-12 17:39:36 +01:00
vcoppe 79a17b4720 New translations merge.mdx (Vietnamese) 2025-11-12 17:39:35 +01:00
vcoppe 0c69eb2910 New translations merge.mdx (Chinese Simplified) 2025-11-12 17:39:34 +01:00
vcoppe 9f5b865949 New translations merge.mdx (Ukrainian) 2025-11-12 17:39:33 +01:00
vcoppe fccbe1c700 New translations merge.mdx (Turkish) 2025-11-12 17:39:31 +01:00
vcoppe a67f503b15 New translations merge.mdx (Swedish) 2025-11-12 17:39:30 +01:00
vcoppe b9d37a87d3 New translations merge.mdx (Russian) 2025-11-12 17:39:29 +01:00
vcoppe 103580954b New translations merge.mdx (Portuguese) 2025-11-12 17:39:28 +01:00
vcoppe a46a5cb723 New translations merge.mdx (Polish) 2025-11-12 17:39:27 +01:00
vcoppe 7c9bb0786f New translations merge.mdx (Norwegian) 2025-11-12 17:39:25 +01:00
vcoppe a10e365fe3 New translations merge.mdx (Dutch) 2025-11-12 17:39:24 +01:00
vcoppe bb11d417aa New translations merge.mdx (Lithuanian) 2025-11-12 17:39:23 +01:00
vcoppe 1014c5b8fc New translations merge.mdx (Korean) 2025-11-12 17:39:22 +01:00
vcoppe 0f52beeda0 New translations merge.mdx (Italian) 2025-11-12 17:39:21 +01:00
vcoppe b34c87a4a8 New translations merge.mdx (Hungarian) 2025-11-12 17:39:19 +01:00
vcoppe c4ea8c8902 New translations merge.mdx (Hebrew) 2025-11-12 17:39:18 +01:00
vcoppe 67f79e447e New translations merge.mdx (Finnish) 2025-11-12 17:39:17 +01:00
vcoppe c55e2b5223 New translations merge.mdx (Basque) 2025-11-12 17:39:16 +01:00
vcoppe 58020a2ddd New translations merge.mdx (Greek) 2025-11-12 17:39:15 +01:00
vcoppe 263fb75e40 New translations merge.mdx (German) 2025-11-12 17:39:13 +01:00
vcoppe e2253b07f8 New translations merge.mdx (Danish) 2025-11-12 17:39:12 +01:00
vcoppe fd9a23a2b4 New translations merge.mdx (Czech) 2025-11-12 17:39:11 +01:00
vcoppe 1f175e3693 New translations merge.mdx (Catalan) 2025-11-12 17:39:09 +01:00
vcoppe cb36ceff9e New translations merge.mdx (Belarusian) 2025-11-12 17:39:08 +01:00
vcoppe 3634071686 New translations merge.mdx (Spanish) 2025-11-12 17:39:07 +01:00
vcoppe 5e09d7cafd New translations merge.mdx (French) 2025-11-12 17:39:06 +01:00
vcoppe 9fc574a1da New translations merge.mdx (Romanian) 2025-11-12 17:39:04 +01:00
vcoppe f57a3770e6 New translations extract.mdx (Serbian (Latin)) 2025-11-12 17:39:03 +01:00
vcoppe f3de70d69e New translations extract.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:39:02 +01:00
vcoppe 5bf5ebc279 New translations extract.mdx (Latvian) 2025-11-12 17:39:01 +01:00
vcoppe 0ffbf02ba0 New translations extract.mdx (Thai) 2025-11-12 17:38:59 +01:00
vcoppe 73ffaaecd7 New translations extract.mdx (Indonesian) 2025-11-12 17:38:58 +01:00
vcoppe b68e5f8b03 New translations extract.mdx (Portuguese, Brazilian) 2025-11-12 17:38:56 +01:00
vcoppe 79586b2d3f New translations extract.mdx (Vietnamese) 2025-11-12 17:38:55 +01:00
vcoppe 5307e9e4a6 New translations extract.mdx (Chinese Simplified) 2025-11-12 17:38:54 +01:00
vcoppe 900225fb8d New translations extract.mdx (Ukrainian) 2025-11-12 17:38:53 +01:00
vcoppe 0d2993ea16 New translations extract.mdx (Turkish) 2025-11-12 17:38:52 +01:00
vcoppe 019857bc61 New translations extract.mdx (Swedish) 2025-11-12 17:38:50 +01:00
vcoppe aec642d6f3 New translations extract.mdx (Russian) 2025-11-12 17:38:49 +01:00
vcoppe 0ce25946d4 New translations extract.mdx (Portuguese) 2025-11-12 17:38:48 +01:00
vcoppe d4d897b53a New translations extract.mdx (Polish) 2025-11-12 17:38:47 +01:00
vcoppe e7967c441c New translations extract.mdx (Norwegian) 2025-11-12 17:38:45 +01:00
vcoppe 9122e8bf0e New translations extract.mdx (Dutch) 2025-11-12 17:38:44 +01:00
vcoppe 3ac4e30a5f New translations extract.mdx (Lithuanian) 2025-11-12 17:38:43 +01:00
vcoppe 668b77c91f New translations extract.mdx (Korean) 2025-11-12 17:38:42 +01:00
vcoppe 5dae658d4b New translations extract.mdx (Italian) 2025-11-12 17:38:41 +01:00
vcoppe adc132609b New translations extract.mdx (Hungarian) 2025-11-12 17:38:39 +01:00
vcoppe c9853a4058 New translations extract.mdx (Hebrew) 2025-11-12 17:38:38 +01:00
vcoppe 336ad925aa New translations extract.mdx (Finnish) 2025-11-12 17:38:37 +01:00
vcoppe 624dd408c1 New translations extract.mdx (Basque) 2025-11-12 17:38:36 +01:00
vcoppe aa7ffbe88d New translations extract.mdx (Greek) 2025-11-12 17:38:35 +01:00
vcoppe bb6ae3ec00 New translations extract.mdx (German) 2025-11-12 17:38:33 +01:00
vcoppe e568f77de3 New translations extract.mdx (Danish) 2025-11-12 17:38:32 +01:00
vcoppe ec23dfba68 New translations extract.mdx (Czech) 2025-11-12 17:38:31 +01:00
vcoppe 817a67a118 New translations extract.mdx (Catalan) 2025-11-12 17:38:30 +01:00
vcoppe bf5aa1321c New translations extract.mdx (Belarusian) 2025-11-12 17:38:29 +01:00
vcoppe 85d1be85a3 New translations extract.mdx (Spanish) 2025-11-12 17:38:27 +01:00
vcoppe ecc15cb462 New translations extract.mdx (French) 2025-11-12 17:38:26 +01:00
vcoppe 4bbf813617 New translations extract.mdx (Romanian) 2025-11-12 17:38:25 +01:00
vcoppe 28fd8b104c New translations clean.mdx (Serbian (Latin)) 2025-11-12 17:38:24 +01:00
vcoppe 578c3732b3 New translations clean.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:38:22 +01:00
vcoppe 7568f1581f New translations clean.mdx (Latvian) 2025-11-12 17:38:21 +01:00
vcoppe 0d07ef8eab New translations clean.mdx (Thai) 2025-11-12 17:38:20 +01:00
vcoppe 301bbf4206 New translations clean.mdx (Indonesian) 2025-11-12 17:38:19 +01:00
vcoppe 14057bf3b4 New translations clean.mdx (Portuguese, Brazilian) 2025-11-12 17:38:17 +01:00
vcoppe 01527c1dba New translations clean.mdx (Vietnamese) 2025-11-12 17:38:16 +01:00
vcoppe 3350b7e8e8 New translations clean.mdx (Chinese Simplified) 2025-11-12 17:38:15 +01:00
vcoppe 673d6d776e New translations clean.mdx (Ukrainian) 2025-11-12 17:38:13 +01:00
vcoppe 4169f3880c New translations clean.mdx (Turkish) 2025-11-12 17:38:12 +01:00
vcoppe a21b17be89 New translations clean.mdx (Swedish) 2025-11-12 17:38:11 +01:00
vcoppe dad9f4f790 New translations clean.mdx (Russian) 2025-11-12 17:38:10 +01:00
vcoppe f6b5fa83af New translations clean.mdx (Portuguese) 2025-11-12 17:38:09 +01:00
vcoppe a3bcef09d4 New translations clean.mdx (Polish) 2025-11-12 17:38:07 +01:00
vcoppe 545fb1368a New translations clean.mdx (Norwegian) 2025-11-12 17:38:06 +01:00
vcoppe d737131f27 New translations clean.mdx (Dutch) 2025-11-12 17:38:05 +01:00
vcoppe ead2477400 New translations clean.mdx (Lithuanian) 2025-11-12 17:38:04 +01:00
vcoppe aa7cc66a81 New translations clean.mdx (Korean) 2025-11-12 17:38:02 +01:00
vcoppe d82882c98a New translations clean.mdx (Italian) 2025-11-12 17:38:01 +01:00
vcoppe 4e0584177f New translations clean.mdx (Hungarian) 2025-11-12 17:38:00 +01:00
vcoppe bca62140d0 New translations clean.mdx (Hebrew) 2025-11-12 17:37:59 +01:00
vcoppe 1e80cfe7b3 New translations clean.mdx (Finnish) 2025-11-12 17:37:57 +01:00
vcoppe a27863e67a New translations clean.mdx (Basque) 2025-11-12 17:37:56 +01:00
vcoppe d491eeb314 New translations clean.mdx (Greek) 2025-11-12 17:37:55 +01:00
vcoppe c943d0c796 New translations clean.mdx (German) 2025-11-12 17:37:54 +01:00
vcoppe bf749e02e2 New translations clean.mdx (Danish) 2025-11-12 17:37:53 +01:00
vcoppe ea7a17fbc6 New translations clean.mdx (Czech) 2025-11-12 17:37:51 +01:00
vcoppe 7da23271b9 New translations clean.mdx (Catalan) 2025-11-12 17:37:50 +01:00
vcoppe 49204ac5b0 New translations clean.mdx (Belarusian) 2025-11-12 17:37:49 +01:00
vcoppe d38fb3c30b New translations clean.mdx (Spanish) 2025-11-12 17:37:47 +01:00
vcoppe 9a2f75d33e New translations clean.mdx (French) 2025-11-12 17:37:46 +01:00
vcoppe ce64e201ce New translations clean.mdx (Romanian) 2025-11-12 17:37:45 +01:00
vcoppe 49ac3859a6 New translations view.mdx (Serbian (Latin)) 2025-11-12 17:37:31 +01:00
vcoppe 5e63f53b4e New translations view.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:37:30 +01:00
vcoppe a24a1d55fa New translations view.mdx (Latvian) 2025-11-12 17:37:28 +01:00
vcoppe 7e69310202 New translations view.mdx (Thai) 2025-11-12 17:37:27 +01:00
vcoppe 9d65bd006d New translations view.mdx (Indonesian) 2025-11-12 17:37:26 +01:00
vcoppe 552cb854e6 New translations view.mdx (Portuguese, Brazilian) 2025-11-12 17:37:25 +01:00
vcoppe 7cc9f23792 New translations view.mdx (Vietnamese) 2025-11-12 17:37:23 +01:00
vcoppe 8b3a852508 New translations view.mdx (Chinese Simplified) 2025-11-12 17:37:22 +01:00
vcoppe 1bbbbffd8d New translations view.mdx (Ukrainian) 2025-11-12 17:37:21 +01:00
vcoppe 52906e8041 New translations view.mdx (Turkish) 2025-11-12 17:37:20 +01:00
vcoppe 6e402f3c97 New translations view.mdx (Swedish) 2025-11-12 17:37:19 +01:00
vcoppe 549d8debb8 New translations view.mdx (Russian) 2025-11-12 17:37:17 +01:00
vcoppe 52027b028a New translations view.mdx (Portuguese) 2025-11-12 17:37:16 +01:00
vcoppe 8d922e55d9 New translations view.mdx (Polish) 2025-11-12 17:37:15 +01:00
vcoppe 5eea175b76 New translations view.mdx (Norwegian) 2025-11-12 17:37:14 +01:00
vcoppe be843d056e New translations view.mdx (Dutch) 2025-11-12 17:37:12 +01:00
vcoppe 7a58268c25 New translations view.mdx (Lithuanian) 2025-11-12 17:37:11 +01:00
vcoppe eb23baed5d New translations view.mdx (Korean) 2025-11-12 17:37:09 +01:00
vcoppe afb699bc12 New translations view.mdx (Italian) 2025-11-12 17:37:08 +01:00
vcoppe 3112f4158b New translations view.mdx (Hungarian) 2025-11-12 17:37:07 +01:00
vcoppe 2cce3eb452 New translations view.mdx (Hebrew) 2025-11-12 17:37:06 +01:00
vcoppe bf58de7c1f New translations view.mdx (Finnish) 2025-11-12 17:37:05 +01:00
vcoppe fa797f219f New translations view.mdx (Basque) 2025-11-12 17:37:03 +01:00
vcoppe f6f859a343 New translations view.mdx (Greek) 2025-11-12 17:37:02 +01:00
vcoppe baf43cefe9 New translations view.mdx (German) 2025-11-12 17:37:01 +01:00
vcoppe 7c4e3cfe32 New translations view.mdx (Danish) 2025-11-12 17:37:00 +01:00
vcoppe ed6b41823d New translations view.mdx (Czech) 2025-11-12 17:36:59 +01:00
vcoppe 32f141aa2b New translations view.mdx (Catalan) 2025-11-12 17:36:57 +01:00
vcoppe 9f76ece5cf New translations view.mdx (Belarusian) 2025-11-12 17:36:56 +01:00
vcoppe ba08346083 New translations view.mdx (Spanish) 2025-11-12 17:36:55 +01:00
vcoppe fe2d418b3a New translations view.mdx (French) 2025-11-12 17:36:54 +01:00
vcoppe e4e6e0f4ad New translations view.mdx (Romanian) 2025-11-12 17:36:52 +01:00
vcoppe cb2214ef9b New translations settings.mdx (Serbian (Latin)) 2025-11-12 17:36:51 +01:00
vcoppe ca1289460d New translations settings.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:36:50 +01:00
vcoppe 0540b9eb12 New translations settings.mdx (Latvian) 2025-11-12 17:36:48 +01:00
vcoppe 0a6cf92f54 New translations settings.mdx (Thai) 2025-11-12 17:36:47 +01:00
vcoppe 835ff9ec19 New translations settings.mdx (Indonesian) 2025-11-12 17:36:46 +01:00
vcoppe 4ba0f4bba3 New translations settings.mdx (Portuguese, Brazilian) 2025-11-12 17:36:45 +01:00
vcoppe 654769b67b New translations settings.mdx (Vietnamese) 2025-11-12 17:36:43 +01:00
vcoppe 4d64ae5944 New translations settings.mdx (Chinese Simplified) 2025-11-12 17:36:41 +01:00
vcoppe c6f1890055 New translations settings.mdx (Ukrainian) 2025-11-12 17:36:40 +01:00
vcoppe 55f4a6f021 New translations settings.mdx (Turkish) 2025-11-12 17:36:39 +01:00
vcoppe f8c8b4d368 New translations settings.mdx (Swedish) 2025-11-12 17:36:38 +01:00
vcoppe be5b248f21 New translations settings.mdx (Russian) 2025-11-12 17:36:37 +01:00
vcoppe 6a11f8bd41 New translations settings.mdx (Portuguese) 2025-11-12 17:36:35 +01:00
vcoppe 2cc6d5ad03 New translations settings.mdx (Polish) 2025-11-12 17:36:34 +01:00
vcoppe a21c80f843 New translations settings.mdx (Norwegian) 2025-11-12 17:36:33 +01:00
vcoppe 90060a36c6 New translations settings.mdx (Dutch) 2025-11-12 17:36:32 +01:00
vcoppe ac5c2372fa New translations settings.mdx (Lithuanian) 2025-11-12 17:36:31 +01:00
vcoppe f6efcd160f New translations settings.mdx (Korean) 2025-11-12 17:36:29 +01:00
vcoppe 83ec2b97d6 New translations settings.mdx (Italian) 2025-11-12 17:36:28 +01:00
vcoppe 3220550633 New translations settings.mdx (Hungarian) 2025-11-12 17:36:26 +01:00
vcoppe bafafc281a New translations settings.mdx (Hebrew) 2025-11-12 17:36:25 +01:00
vcoppe b58a0d22c6 New translations settings.mdx (Finnish) 2025-11-12 17:36:24 +01:00
vcoppe 55c25efca7 New translations settings.mdx (Basque) 2025-11-12 17:36:23 +01:00
vcoppe 967a5bf612 New translations settings.mdx (Greek) 2025-11-12 17:36:21 +01:00
vcoppe fcb762ee8f New translations settings.mdx (German) 2025-11-12 17:36:20 +01:00
vcoppe 38a941e986 New translations settings.mdx (Danish) 2025-11-12 17:36:18 +01:00
vcoppe 3544e1bac3 New translations settings.mdx (Czech) 2025-11-12 17:36:17 +01:00
vcoppe 9c81714568 New translations settings.mdx (Catalan) 2025-11-12 17:36:16 +01:00
vcoppe dff93c491c New translations settings.mdx (Belarusian) 2025-11-12 17:36:14 +01:00
vcoppe 0b7ffdc45a New translations settings.mdx (Spanish) 2025-11-12 17:36:13 +01:00
vcoppe 71cfc65f68 New translations settings.mdx (French) 2025-11-12 17:36:12 +01:00
vcoppe 005548bdd0 New translations settings.mdx (Romanian) 2025-11-12 17:36:11 +01:00
vcoppe aa285d8409 New translations file.mdx (Serbian (Latin)) 2025-11-12 17:36:09 +01:00
vcoppe a801166692 New translations file.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:36:08 +01:00
vcoppe 8259c57fad New translations file.mdx (Latvian) 2025-11-12 17:36:06 +01:00
vcoppe 0b47fadef7 New translations file.mdx (Thai) 2025-11-12 17:36:05 +01:00
vcoppe 4453001b30 New translations file.mdx (Indonesian) 2025-11-12 17:36:04 +01:00
vcoppe 58999c28fe New translations file.mdx (Portuguese, Brazilian) 2025-11-12 17:36:03 +01:00
vcoppe 74d06e5aad New translations file.mdx (Vietnamese) 2025-11-12 17:36:01 +01:00
vcoppe 662c8f5ca2 New translations file.mdx (Chinese Simplified) 2025-11-12 17:36:00 +01:00
vcoppe b5b5f82464 New translations file.mdx (Ukrainian) 2025-11-12 17:35:59 +01:00
vcoppe 7593ec5587 New translations file.mdx (Turkish) 2025-11-12 17:35:58 +01:00
vcoppe 94ac0ef90c New translations file.mdx (Swedish) 2025-11-12 17:35:56 +01:00
vcoppe 36093288f7 New translations file.mdx (Russian) 2025-11-12 17:35:55 +01:00
vcoppe d226b3c3c1 New translations file.mdx (Portuguese) 2025-11-12 17:35:54 +01:00
vcoppe 13215e5c16 New translations file.mdx (Polish) 2025-11-12 17:35:53 +01:00
vcoppe f6c6eb441f New translations file.mdx (Norwegian) 2025-11-12 17:35:51 +01:00
vcoppe e883ce7c43 New translations file.mdx (Dutch) 2025-11-12 17:35:50 +01:00
vcoppe 4836a99fcd New translations file.mdx (Lithuanian) 2025-11-12 17:35:49 +01:00
vcoppe a7075067bf New translations file.mdx (Korean) 2025-11-12 17:35:48 +01:00
vcoppe c8edea25ae New translations file.mdx (Italian) 2025-11-12 17:35:46 +01:00
vcoppe 07731fc2de New translations file.mdx (Hungarian) 2025-11-12 17:35:45 +01:00
vcoppe 8c436d5c0e New translations file.mdx (Hebrew) 2025-11-12 17:35:44 +01:00
vcoppe 16a2b1979e New translations file.mdx (Finnish) 2025-11-12 17:35:43 +01:00
vcoppe 7753613cf4 New translations file.mdx (Basque) 2025-11-12 17:35:42 +01:00
vcoppe d156a6ffee New translations file.mdx (Greek) 2025-11-12 17:35:41 +01:00
vcoppe f54b614ece New translations file.mdx (German) 2025-11-12 17:35:39 +01:00
vcoppe 24ed769eac New translations file.mdx (Danish) 2025-11-12 17:35:38 +01:00
vcoppe ef8387f7c7 New translations file.mdx (Czech) 2025-11-12 17:35:37 +01:00
vcoppe c76c37c874 New translations file.mdx (Catalan) 2025-11-12 17:35:36 +01:00
vcoppe bf41cb70f8 New translations file.mdx (Belarusian) 2025-11-12 17:35:34 +01:00
vcoppe f7e759c69f New translations file.mdx (Spanish) 2025-11-12 17:35:33 +01:00
vcoppe 376360caac New translations file.mdx (French) 2025-11-12 17:35:32 +01:00
vcoppe 66fa02c90a New translations file.mdx (Romanian) 2025-11-12 17:35:31 +01:00
vcoppe 26cc2e3c9e New translations edit.mdx (Serbian (Latin)) 2025-11-12 17:35:29 +01:00
vcoppe 09ceb1b5ce New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:35:28 +01:00
vcoppe 1204e971c2 New translations edit.mdx (Latvian) 2025-11-12 17:35:27 +01:00
vcoppe 9833df2110 New translations edit.mdx (Thai) 2025-11-12 17:35:26 +01:00
vcoppe 7843170978 New translations edit.mdx (Indonesian) 2025-11-12 17:35:25 +01:00
vcoppe 93c1535b76 New translations edit.mdx (Portuguese, Brazilian) 2025-11-12 17:35:23 +01:00
vcoppe c3c6e2e757 New translations edit.mdx (Vietnamese) 2025-11-12 17:35:22 +01:00
vcoppe 2d4b3514bc New translations edit.mdx (Chinese Simplified) 2025-11-12 17:35:21 +01:00
vcoppe a27fce18ed New translations edit.mdx (Ukrainian) 2025-11-12 17:35:20 +01:00
vcoppe 1adc1833dc New translations edit.mdx (Turkish) 2025-11-12 17:35:19 +01:00
vcoppe 9a1c721a46 New translations edit.mdx (Swedish) 2025-11-12 17:35:17 +01:00
vcoppe 9bd7d49fe7 New translations edit.mdx (Russian) 2025-11-12 17:35:16 +01:00
vcoppe e94f128de8 New translations edit.mdx (Portuguese) 2025-11-12 17:35:15 +01:00
vcoppe 9005a58e43 New translations edit.mdx (Polish) 2025-11-12 17:35:14 +01:00
vcoppe 12f07c57f2 New translations edit.mdx (Norwegian) 2025-11-12 17:35:12 +01:00
vcoppe 3c6066f047 New translations edit.mdx (Dutch) 2025-11-12 17:35:11 +01:00
vcoppe 99d39c6d71 New translations edit.mdx (Lithuanian) 2025-11-12 17:35:09 +01:00
vcoppe 24d088ac79 New translations edit.mdx (Korean) 2025-11-12 17:35:08 +01:00
vcoppe 268ddd9aae New translations edit.mdx (Italian) 2025-11-12 17:35:07 +01:00
vcoppe ace28b2051 New translations edit.mdx (Hungarian) 2025-11-12 17:35:06 +01:00
vcoppe beb9b8c19c New translations edit.mdx (Hebrew) 2025-11-12 17:35:04 +01:00
vcoppe 9f1c23ea67 New translations edit.mdx (Finnish) 2025-11-12 17:35:03 +01:00
vcoppe 901a41dab1 New translations edit.mdx (Basque) 2025-11-12 17:35:02 +01:00
vcoppe 8777cb3a72 New translations edit.mdx (Greek) 2025-11-12 17:35:01 +01:00
vcoppe c34a9719d9 New translations edit.mdx (German) 2025-11-12 17:34:59 +01:00
vcoppe 54a3535373 New translations edit.mdx (Danish) 2025-11-12 17:34:58 +01:00
vcoppe 7e2cecc838 New translations edit.mdx (Czech) 2025-11-12 17:34:57 +01:00
vcoppe 33a61c41d4 New translations edit.mdx (Catalan) 2025-11-12 17:34:56 +01:00
vcoppe e21d5ea4ef New translations edit.mdx (Belarusian) 2025-11-12 17:34:54 +01:00
vcoppe 96c12b38ed New translations edit.mdx (Spanish) 2025-11-12 17:34:53 +01:00
vcoppe 2faa17a608 New translations edit.mdx (French) 2025-11-12 17:34:52 +01:00
vcoppe 5386d3b19f New translations edit.mdx (Romanian) 2025-11-12 17:34:50 +01:00
vcoppe 56b219ed0d New translations map-controls.mdx (Serbian (Latin)) 2025-11-12 17:34:36 +01:00
vcoppe 5bfd82272d New translations map-controls.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:34:35 +01:00
vcoppe 9af0480418 New translations map-controls.mdx (Latvian) 2025-11-12 17:34:33 +01:00
vcoppe bcbd9535e5 New translations map-controls.mdx (Thai) 2025-11-12 17:34:32 +01:00
vcoppe e4bf263fe2 New translations map-controls.mdx (Indonesian) 2025-11-12 17:34:31 +01:00
vcoppe a5da416a54 New translations map-controls.mdx (Portuguese, Brazilian) 2025-11-12 17:34:30 +01:00
vcoppe 1dbc9c968f New translations map-controls.mdx (Vietnamese) 2025-11-12 17:34:29 +01:00
vcoppe b5b6b293ca New translations map-controls.mdx (Chinese Simplified) 2025-11-12 17:34:27 +01:00
vcoppe b7df027c05 New translations map-controls.mdx (Ukrainian) 2025-11-12 17:34:26 +01:00
vcoppe 6890c2b182 New translations map-controls.mdx (Turkish) 2025-11-12 17:34:25 +01:00
vcoppe 2f90728b82 New translations map-controls.mdx (Swedish) 2025-11-12 17:34:23 +01:00
vcoppe d8e445315b New translations map-controls.mdx (Russian) 2025-11-12 17:34:22 +01:00
vcoppe 26d99cc790 New translations map-controls.mdx (Portuguese) 2025-11-12 17:34:21 +01:00
vcoppe 52c273d47f New translations map-controls.mdx (Polish) 2025-11-12 17:34:20 +01:00
vcoppe 9ebca23a96 New translations map-controls.mdx (Norwegian) 2025-11-12 17:34:19 +01:00
vcoppe 0775c745dd New translations map-controls.mdx (Dutch) 2025-11-12 17:34:17 +01:00
vcoppe f4111a0185 New translations map-controls.mdx (Lithuanian) 2025-11-12 17:34:16 +01:00
vcoppe 6ad88accf4 New translations map-controls.mdx (Korean) 2025-11-12 17:34:15 +01:00
vcoppe 9669b7bd16 New translations map-controls.mdx (Italian) 2025-11-12 17:34:14 +01:00
vcoppe a37e27a82f New translations map-controls.mdx (Hungarian) 2025-11-12 17:34:13 +01:00
vcoppe 85e347dfe2 New translations map-controls.mdx (Hebrew) 2025-11-12 17:34:11 +01:00
vcoppe afcaa89cc3 New translations map-controls.mdx (Finnish) 2025-11-12 17:34:10 +01:00
vcoppe c87c8952c4 New translations map-controls.mdx (Basque) 2025-11-12 17:34:08 +01:00
vcoppe d08d8dacb4 New translations map-controls.mdx (Greek) 2025-11-12 17:34:07 +01:00
vcoppe 2fbad4fa91 New translations map-controls.mdx (German) 2025-11-12 17:34:06 +01:00
vcoppe 08731d2708 New translations map-controls.mdx (Danish) 2025-11-12 17:34:04 +01:00
vcoppe a684b7c101 New translations map-controls.mdx (Czech) 2025-11-12 17:34:03 +01:00
vcoppe 6000d8c939 New translations map-controls.mdx (Catalan) 2025-11-12 17:34:02 +01:00
vcoppe 4a30157638 New translations map-controls.mdx (Belarusian) 2025-11-12 17:34:00 +01:00
vcoppe 16fc317543 New translations map-controls.mdx (Spanish) 2025-11-12 17:33:59 +01:00
vcoppe 40e6b801ef New translations map-controls.mdx (French) 2025-11-12 17:33:58 +01:00
vcoppe e908b4d996 New translations map-controls.mdx (Romanian) 2025-11-12 17:33:56 +01:00
vcoppe a4230df2be New translations translation.mdx (Serbian (Latin)) 2025-11-12 17:33:42 +01:00
vcoppe 37894a26e7 New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:33:41 +01:00
vcoppe d9ee36076c New translations translation.mdx (Latvian) 2025-11-12 17:33:40 +01:00
vcoppe f9f07de8ef New translations translation.mdx (Thai) 2025-11-12 17:33:39 +01:00
vcoppe 141560e159 New translations translation.mdx (Indonesian) 2025-11-12 17:33:38 +01:00
vcoppe f7a9697779 New translations translation.mdx (Portuguese, Brazilian) 2025-11-12 17:33:36 +01:00
vcoppe e778e3e85f New translations translation.mdx (Vietnamese) 2025-11-12 17:33:35 +01:00
vcoppe c227b5c37b New translations translation.mdx (Chinese Simplified) 2025-11-12 17:33:34 +01:00
vcoppe bdf03ab962 New translations translation.mdx (Ukrainian) 2025-11-12 17:33:32 +01:00
vcoppe 07e2cdbac1 New translations translation.mdx (Turkish) 2025-11-12 17:33:31 +01:00
vcoppe ec0c27c421 New translations translation.mdx (Swedish) 2025-11-12 17:33:30 +01:00
vcoppe 5670d47c5f New translations translation.mdx (Russian) 2025-11-12 17:33:28 +01:00
vcoppe c8a3bf2d8a New translations translation.mdx (Portuguese) 2025-11-12 17:33:27 +01:00
vcoppe cc28f877fd New translations translation.mdx (Polish) 2025-11-12 17:33:26 +01:00
vcoppe 39c746068a New translations translation.mdx (Norwegian) 2025-11-12 17:33:25 +01:00
vcoppe f37ad668be New translations translation.mdx (Dutch) 2025-11-12 17:33:24 +01:00
vcoppe e87561fb17 New translations translation.mdx (Lithuanian) 2025-11-12 17:33:22 +01:00
vcoppe 96084ca57f New translations translation.mdx (Korean) 2025-11-12 17:33:21 +01:00
vcoppe 186b60dcfe New translations translation.mdx (Italian) 2025-11-12 17:33:20 +01:00
vcoppe aa10fbaf9f New translations translation.mdx (Hungarian) 2025-11-12 17:33:18 +01:00
vcoppe 2954b353c9 New translations translation.mdx (Hebrew) 2025-11-12 17:33:17 +01:00
vcoppe 63614e5e0c New translations translation.mdx (Finnish) 2025-11-12 17:33:15 +01:00
vcoppe 899ac29ec1 New translations translation.mdx (Basque) 2025-11-12 17:33:14 +01:00
vcoppe 722683fae2 New translations translation.mdx (Greek) 2025-11-12 17:33:13 +01:00
vcoppe 5eb82b5ef9 New translations translation.mdx (German) 2025-11-12 17:33:11 +01:00
vcoppe 89e8f0ba2d New translations translation.mdx (Danish) 2025-11-12 17:33:10 +01:00
vcoppe f6cfde92c4 New translations translation.mdx (Czech) 2025-11-12 17:33:08 +01:00
vcoppe 35b7063ca8 New translations translation.mdx (Catalan) 2025-11-12 17:33:06 +01:00
vcoppe 23357de4db New translations translation.mdx (Belarusian) 2025-11-12 17:33:05 +01:00
vcoppe 0850db9f0d New translations translation.mdx (Spanish) 2025-11-12 17:33:03 +01:00
vcoppe 0206bb94be New translations translation.mdx (French) 2025-11-12 17:33:02 +01:00
vcoppe d25bbd216d New translations translation.mdx (Romanian) 2025-11-12 17:33:01 +01:00
vcoppe aedd584fbc New translations funding.mdx (Serbian (Latin)) 2025-11-12 17:32:46 +01:00
vcoppe 4a68e2c8be New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:32:45 +01:00
vcoppe 926fbc7fe7 New translations funding.mdx (Latvian) 2025-11-12 17:32:44 +01:00
vcoppe 7cf4ced17d New translations funding.mdx (Thai) 2025-11-12 17:32:43 +01:00
vcoppe b415c8b3da New translations funding.mdx (Indonesian) 2025-11-12 17:32:41 +01:00
vcoppe b58273b98c New translations funding.mdx (Portuguese, Brazilian) 2025-11-12 17:32:40 +01:00
vcoppe bd5f66b5d5 New translations funding.mdx (Vietnamese) 2025-11-12 17:32:39 +01:00
vcoppe 3abe640640 New translations funding.mdx (Chinese Simplified) 2025-11-12 17:32:37 +01:00
vcoppe 95bc685459 New translations funding.mdx (Ukrainian) 2025-11-12 17:32:36 +01:00
vcoppe e65c8ceace New translations funding.mdx (Turkish) 2025-11-12 17:32:35 +01:00
vcoppe d41080c4e3 New translations funding.mdx (Swedish) 2025-11-12 17:32:34 +01:00
vcoppe 95e4169108 New translations funding.mdx (Russian) 2025-11-12 17:32:33 +01:00
vcoppe 09ed3ffd3f New translations funding.mdx (Portuguese) 2025-11-12 17:32:31 +01:00
vcoppe 0cd8ac88d6 New translations funding.mdx (Polish) 2025-11-12 17:32:30 +01:00
vcoppe 99a022f647 New translations funding.mdx (Norwegian) 2025-11-12 17:32:29 +01:00
vcoppe 6aab3d842b New translations funding.mdx (Dutch) 2025-11-12 17:32:28 +01:00
vcoppe 430e004c60 New translations funding.mdx (Lithuanian) 2025-11-12 17:32:27 +01:00
vcoppe 022a2a9f78 New translations funding.mdx (Korean) 2025-11-12 17:32:25 +01:00
vcoppe 821657c447 New translations funding.mdx (Italian) 2025-11-12 17:32:24 +01:00
vcoppe c03e7517a5 New translations funding.mdx (Hungarian) 2025-11-12 17:32:23 +01:00
vcoppe a89032b5e4 New translations funding.mdx (Hebrew) 2025-11-12 17:32:21 +01:00
vcoppe d6c55cf555 New translations funding.mdx (Finnish) 2025-11-12 17:32:20 +01:00
vcoppe 2da2f63503 New translations funding.mdx (Basque) 2025-11-12 17:32:19 +01:00
vcoppe ab1bb2ee5d New translations funding.mdx (Greek) 2025-11-12 17:32:17 +01:00
vcoppe 326e9a82c9 New translations funding.mdx (German) 2025-11-12 17:32:16 +01:00
vcoppe fbdba3ef69 New translations funding.mdx (Danish) 2025-11-12 17:32:15 +01:00
vcoppe 312c4e7211 New translations funding.mdx (Czech) 2025-11-12 17:32:13 +01:00
vcoppe 3386e1e6b0 New translations funding.mdx (Catalan) 2025-11-12 17:32:12 +01:00
vcoppe a11565e94c New translations funding.mdx (Belarusian) 2025-11-12 17:32:10 +01:00
vcoppe 0c0e6d5017 New translations funding.mdx (Spanish) 2025-11-12 17:32:05 +01:00
vcoppe 4c33e88c63 New translations funding.mdx (French) 2025-11-12 17:32:04 +01:00
vcoppe aa582fb0d8 New translations funding.mdx (Romanian) 2025-11-12 17:32:03 +01:00
vcoppe c38fdf8ccc New translations gpx.mdx (Serbian (Latin)) 2025-11-12 17:32:01 +01:00
vcoppe 305c2743ba New translations gpx.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:32:00 +01:00
vcoppe 5cc085c6a6 New translations gpx.mdx (Latvian) 2025-11-12 17:31:59 +01:00
vcoppe 3859779f0a New translations gpx.mdx (Thai) 2025-11-12 17:31:58 +01:00
vcoppe 9857469cd1 New translations gpx.mdx (Indonesian) 2025-11-12 17:31:57 +01:00
vcoppe ff805d32fb New translations gpx.mdx (Portuguese, Brazilian) 2025-11-12 17:31:55 +01:00
vcoppe 5a94e58236 New translations gpx.mdx (Vietnamese) 2025-11-12 17:31:54 +01:00
vcoppe 9ff7da58ea New translations gpx.mdx (Chinese Simplified) 2025-11-12 17:31:52 +01:00
vcoppe 30d974ff4a New translations gpx.mdx (Ukrainian) 2025-11-12 17:31:51 +01:00
vcoppe 11709dc594 New translations gpx.mdx (Turkish) 2025-11-12 17:31:50 +01:00
vcoppe 246c31bac4 New translations gpx.mdx (Swedish) 2025-11-12 17:31:49 +01:00
vcoppe 2a8cc9ad70 New translations gpx.mdx (Russian) 2025-11-12 17:31:47 +01:00
vcoppe 098844bb4a New translations gpx.mdx (Portuguese) 2025-11-12 17:31:46 +01:00
vcoppe 11a064a05b New translations gpx.mdx (Polish) 2025-11-12 17:31:45 +01:00
vcoppe b2adf99c2a New translations gpx.mdx (Norwegian) 2025-11-12 17:31:44 +01:00
vcoppe 937d9fab36 New translations gpx.mdx (Dutch) 2025-11-12 17:31:42 +01:00
vcoppe 3c4d5a8324 New translations gpx.mdx (Lithuanian) 2025-11-12 17:31:41 +01:00
vcoppe d66ae036e5 New translations gpx.mdx (Korean) 2025-11-12 17:31:40 +01:00
vcoppe 28dcb92f4c New translations gpx.mdx (Italian) 2025-11-12 17:31:38 +01:00
vcoppe 162059df91 New translations gpx.mdx (Hungarian) 2025-11-12 17:31:37 +01:00
vcoppe 439085c873 New translations gpx.mdx (Hebrew) 2025-11-12 17:31:36 +01:00
vcoppe e3c34b6e8b New translations gpx.mdx (Finnish) 2025-11-12 17:31:35 +01:00
vcoppe 8a6e60f961 New translations gpx.mdx (Basque) 2025-11-12 17:31:33 +01:00
vcoppe eedcf5b2e8 New translations gpx.mdx (Greek) 2025-11-12 17:31:32 +01:00
vcoppe bad3cb5c7e New translations gpx.mdx (German) 2025-11-12 17:31:31 +01:00
vcoppe 2351a16fae New translations gpx.mdx (Danish) 2025-11-12 17:31:29 +01:00
vcoppe 5779c11097 New translations gpx.mdx (Czech) 2025-11-12 17:31:28 +01:00
vcoppe ab34ab3429 New translations gpx.mdx (Catalan) 2025-11-12 17:31:27 +01:00
vcoppe 98487a18e5 New translations gpx.mdx (Belarusian) 2025-11-12 17:31:26 +01:00
vcoppe fae46c2af0 New translations gpx.mdx (Spanish) 2025-11-12 17:31:24 +01:00
vcoppe 2725d3a386 New translations gpx.mdx (French) 2025-11-12 17:31:23 +01:00
vcoppe 4601d61385 New translations gpx.mdx (Romanian) 2025-11-12 17:31:21 +01:00
vcoppe 2b1081b523 New translations files-and-stats.mdx (Serbian (Latin)) 2025-11-12 17:31:05 +01:00
vcoppe c2bcbd3597 New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:31:03 +01:00
vcoppe d4367475fb New translations files-and-stats.mdx (Latvian) 2025-11-12 17:31:02 +01:00
vcoppe e62d7b2132 New translations files-and-stats.mdx (Thai) 2025-11-12 17:31:00 +01:00
vcoppe 13486e3dfa New translations files-and-stats.mdx (Indonesian) 2025-11-12 17:30:59 +01:00
vcoppe 34e4069b2f New translations files-and-stats.mdx (Portuguese, Brazilian) 2025-11-12 17:30:58 +01:00
vcoppe 5ed921d8dc New translations files-and-stats.mdx (Vietnamese) 2025-11-12 17:30:56 +01:00
vcoppe 6e9c2191eb New translations files-and-stats.mdx (Chinese Simplified) 2025-11-12 17:30:55 +01:00
vcoppe afb9f1ba26 New translations files-and-stats.mdx (Ukrainian) 2025-11-12 17:30:54 +01:00
vcoppe acc9fd8101 New translations files-and-stats.mdx (Turkish) 2025-11-12 17:30:53 +01:00
vcoppe 60f9b4ae33 New translations files-and-stats.mdx (Swedish) 2025-11-12 17:30:51 +01:00
vcoppe 8d3e241869 New translations files-and-stats.mdx (Russian) 2025-11-12 17:30:50 +01:00
vcoppe 0cfd35e74f New translations files-and-stats.mdx (Portuguese) 2025-11-12 17:30:49 +01:00
vcoppe dba8fa3a42 New translations files-and-stats.mdx (Polish) 2025-11-12 17:30:47 +01:00
vcoppe bf4d7101b8 New translations files-and-stats.mdx (Norwegian) 2025-11-12 17:30:46 +01:00
vcoppe bff1d5fa6f New translations routing.mdx (Czech) 2025-11-12 17:30:45 +01:00
vcoppe f8f5a59520 New translations files-and-stats.mdx (Dutch) 2025-11-12 17:30:43 +01:00
vcoppe dc2bd3a5d6 New translations files-and-stats.mdx (Lithuanian) 2025-11-12 17:30:42 +01:00
vcoppe 5b7cf32314 New translations files-and-stats.mdx (Korean) 2025-11-12 17:30:41 +01:00
vcoppe 5f99680b01 New translations files-and-stats.mdx (Italian) 2025-11-12 17:30:39 +01:00
vcoppe f76c4b0405 New translations files-and-stats.mdx (Hungarian) 2025-11-12 17:30:37 +01:00
vcoppe d8425ce831 New translations files-and-stats.mdx (Hebrew) 2025-11-12 17:30:36 +01:00
vcoppe 7e42e5bb54 New translations files-and-stats.mdx (Finnish) 2025-11-12 17:30:34 +01:00
vcoppe 7c2533b6fb New translations files-and-stats.mdx (Basque) 2025-11-12 17:30:33 +01:00
vcoppe ae05034927 New translations files-and-stats.mdx (Greek) 2025-11-12 17:30:31 +01:00
vcoppe 1c331d57b3 New translations files-and-stats.mdx (German) 2025-11-12 17:30:30 +01:00
vcoppe db5cda70ad New translations files-and-stats.mdx (Danish) 2025-11-12 17:30:28 +01:00
vcoppe 8196ac451d New translations files-and-stats.mdx (Czech) 2025-11-12 17:30:27 +01:00
vcoppe e2cfeec10a New translations files-and-stats.mdx (Catalan) 2025-11-12 17:30:26 +01:00
vcoppe 9923c1ae65 New translations files-and-stats.mdx (Belarusian) 2025-11-12 17:30:24 +01:00
vcoppe 957634f90c New translations files-and-stats.mdx (Spanish) 2025-11-12 17:30:23 +01:00
vcoppe 6be78b2a54 New translations files-and-stats.mdx (French) 2025-11-12 17:30:21 +01:00
vcoppe f04972666c New translations files-and-stats.mdx (Romanian) 2025-11-12 17:30:20 +01:00
vcoppe e25caa6e03 New translations elevation.mdx (Serbian (Latin)) 2025-11-12 17:27:56 +01:00
vcoppe 1525bc447d New translations elevation.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:27:55 +01:00
vcoppe 700dc9e81d New translations elevation.mdx (Latvian) 2025-11-12 17:27:54 +01:00
vcoppe 6aafd74ddf New translations elevation.mdx (Thai) 2025-11-12 17:27:53 +01:00
vcoppe f519ff427b New translations elevation.mdx (Indonesian) 2025-11-12 17:27:52 +01:00
vcoppe a89ec4df27 New translations elevation.mdx (Portuguese, Brazilian) 2025-11-12 17:27:51 +01:00
vcoppe c4cb648a2a New translations elevation.mdx (Vietnamese) 2025-11-12 17:27:50 +01:00
vcoppe 0c8c3feb0c New translations elevation.mdx (Chinese Simplified) 2025-11-12 17:27:49 +01:00
vcoppe 5529eeaa72 New translations elevation.mdx (Ukrainian) 2025-11-12 17:27:48 +01:00
vcoppe 8b1055bdda New translations elevation.mdx (Turkish) 2025-11-12 17:27:47 +01:00
vcoppe df23d9fe63 New translations elevation.mdx (Swedish) 2025-11-12 17:27:46 +01:00
vcoppe 18328711b2 New translations elevation.mdx (Russian) 2025-11-12 17:27:45 +01:00
vcoppe dc996ed44c New translations elevation.mdx (Portuguese) 2025-11-12 17:27:44 +01:00
vcoppe e2bc6e44e2 New translations elevation.mdx (Polish) 2025-11-12 17:27:43 +01:00
vcoppe b667718fc2 New translations elevation.mdx (Norwegian) 2025-11-12 17:27:43 +01:00
vcoppe 4ae5707068 New translations elevation.mdx (Dutch) 2025-11-12 17:27:42 +01:00
vcoppe a14e8dbc52 New translations elevation.mdx (Lithuanian) 2025-11-12 17:27:41 +01:00
vcoppe a9344e0069 New translations elevation.mdx (Korean) 2025-11-12 17:27:40 +01:00
vcoppe 9cdf831dd1 New translations elevation.mdx (Italian) 2025-11-12 17:27:39 +01:00
vcoppe 0857e2fee3 New translations elevation.mdx (Hungarian) 2025-11-12 17:27:38 +01:00
vcoppe 69553c3bf3 New translations elevation.mdx (Hebrew) 2025-11-12 17:27:37 +01:00
vcoppe 5ce5da1443 New translations elevation.mdx (Finnish) 2025-11-12 17:27:36 +01:00
vcoppe ede95247c7 New translations elevation.mdx (Basque) 2025-11-12 17:27:35 +01:00
vcoppe 9b7294c34c New translations elevation.mdx (Greek) 2025-11-12 17:27:34 +01:00
vcoppe 58ebe8c5eb New translations elevation.mdx (German) 2025-11-12 17:27:33 +01:00
vcoppe 67ed742858 New translations elevation.mdx (Danish) 2025-11-12 17:27:32 +01:00
vcoppe 538a6680e7 New translations elevation.mdx (Czech) 2025-11-12 17:27:31 +01:00
vcoppe e16d42af87 New translations elevation.mdx (Catalan) 2025-11-12 17:27:30 +01:00
vcoppe 6ee62a7e1f New translations elevation.mdx (Belarusian) 2025-11-12 17:27:29 +01:00
vcoppe 3adfe54788 New translations elevation.mdx (Spanish) 2025-11-12 17:27:28 +01:00
vcoppe 673ac6503f New translations elevation.mdx (French) 2025-11-12 17:27:27 +01:00
vcoppe b226b87b4b New translations elevation.mdx (Romanian) 2025-11-12 17:27:26 +01:00
vcoppe a62c0c623a New translations time.mdx (Serbian (Latin)) 2025-11-12 17:27:11 +01:00
vcoppe e7981f61bb New translations time.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:27:10 +01:00
vcoppe cfe131815e New translations time.mdx (Latvian) 2025-11-12 17:27:09 +01:00
vcoppe bee1b32f4c New translations time.mdx (Thai) 2025-11-12 17:27:08 +01:00
vcoppe 5861d61444 New translations time.mdx (Indonesian) 2025-11-12 17:27:07 +01:00
vcoppe 9852f41683 New translations time.mdx (Portuguese, Brazilian) 2025-11-12 17:27:06 +01:00
vcoppe d1f8ae2c3d New translations time.mdx (Vietnamese) 2025-11-12 17:27:05 +01:00
vcoppe bdc1c9d6c2 New translations time.mdx (Chinese Simplified) 2025-11-12 17:27:04 +01:00
vcoppe 2b7a7b1da8 New translations time.mdx (Ukrainian) 2025-11-12 17:27:02 +01:00
vcoppe 9ef115b74f New translations time.mdx (Turkish) 2025-11-12 17:27:01 +01:00
vcoppe 3e1395466c New translations time.mdx (Swedish) 2025-11-12 17:27:00 +01:00
vcoppe 54c667cbed New translations time.mdx (Russian) 2025-11-12 17:26:59 +01:00
vcoppe 1e224c588d New translations time.mdx (Portuguese) 2025-11-12 17:26:58 +01:00
vcoppe 5bd410cc4c New translations time.mdx (Polish) 2025-11-12 17:26:57 +01:00
vcoppe 67ad2676a1 New translations time.mdx (Norwegian) 2025-11-12 17:26:56 +01:00
vcoppe 0910f4c002 New translations time.mdx (Dutch) 2025-11-12 17:26:55 +01:00
vcoppe ea8e91c0cc New translations time.mdx (Lithuanian) 2025-11-12 17:26:54 +01:00
vcoppe db5517e0c9 New translations time.mdx (Korean) 2025-11-12 17:26:53 +01:00
vcoppe 77d787d5b3 New translations time.mdx (Italian) 2025-11-12 17:26:52 +01:00
vcoppe 69f3180b63 New translations time.mdx (Hungarian) 2025-11-12 17:26:51 +01:00
vcoppe ee2abb0517 New translations time.mdx (Hebrew) 2025-11-12 17:26:50 +01:00
vcoppe d7a938a90b New translations time.mdx (Finnish) 2025-11-12 17:26:49 +01:00
vcoppe 4d292a077a New translations time.mdx (Basque) 2025-11-12 17:26:48 +01:00
vcoppe 526c9d674d New translations time.mdx (Greek) 2025-11-12 17:26:47 +01:00
vcoppe 36f624da04 New translations time.mdx (German) 2025-11-12 17:26:46 +01:00
vcoppe 9d2a77599e New translations time.mdx (Danish) 2025-11-12 17:26:45 +01:00
vcoppe e9fbe7d303 New translations time.mdx (Czech) 2025-11-12 17:26:44 +01:00
vcoppe 47757bde94 New translations time.mdx (Catalan) 2025-11-12 17:26:43 +01:00
vcoppe f71c410d36 New translations time.mdx (Belarusian) 2025-11-12 17:26:42 +01:00
vcoppe 8f8a3f8de6 New translations time.mdx (Spanish) 2025-11-12 17:26:41 +01:00
vcoppe a338e87b2e New translations time.mdx (French) 2025-11-12 17:26:40 +01:00
vcoppe 5801c5ddd0 New translations time.mdx (Romanian) 2025-11-12 17:26:40 +01:00
vcoppe 5b71da0223 New translations scissors.mdx (Serbian (Latin)) 2025-11-12 17:26:39 +01:00
vcoppe ed85728729 New translations scissors.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:26:38 +01:00
vcoppe 9b5b661f1c New translations scissors.mdx (Latvian) 2025-11-12 17:26:37 +01:00
vcoppe a5790aaba3 New translations scissors.mdx (Thai) 2025-11-12 17:26:36 +01:00
vcoppe 698b149271 New translations scissors.mdx (Indonesian) 2025-11-12 17:26:35 +01:00
vcoppe 15c28b0ecb New translations scissors.mdx (Portuguese, Brazilian) 2025-11-12 17:26:34 +01:00
vcoppe b38f533ccc New translations scissors.mdx (Vietnamese) 2025-11-12 17:26:33 +01:00
vcoppe 5d61376486 New translations scissors.mdx (Chinese Simplified) 2025-11-12 17:26:32 +01:00
vcoppe 28c9e5ff78 New translations scissors.mdx (Ukrainian) 2025-11-12 17:26:31 +01:00
vcoppe a35f978a08 New translations scissors.mdx (Turkish) 2025-11-12 17:26:30 +01:00
vcoppe 8fb8ecf9f9 New translations scissors.mdx (Swedish) 2025-11-12 17:26:29 +01:00
vcoppe c8be4d1e4a New translations scissors.mdx (Russian) 2025-11-12 17:26:27 +01:00
vcoppe 8a13c908f8 New translations scissors.mdx (Portuguese) 2025-11-12 17:26:26 +01:00
vcoppe 0ab9cab46d New translations scissors.mdx (Polish) 2025-11-12 17:26:26 +01:00
vcoppe ae9e31a127 New translations scissors.mdx (Norwegian) 2025-11-12 17:26:24 +01:00
vcoppe ea74d066ac New translations scissors.mdx (Dutch) 2025-11-12 17:26:23 +01:00
vcoppe 3a8da3a818 New translations scissors.mdx (Lithuanian) 2025-11-12 17:26:23 +01:00
vcoppe 7b2273d180 New translations scissors.mdx (Korean) 2025-11-12 17:26:22 +01:00
vcoppe 2bba112bc0 New translations scissors.mdx (Italian) 2025-11-12 17:26:21 +01:00
vcoppe f747ac2fc9 New translations scissors.mdx (Hungarian) 2025-11-12 17:26:19 +01:00
vcoppe 50112ef89f New translations scissors.mdx (Hebrew) 2025-11-12 17:26:18 +01:00
vcoppe c5cce0a87b New translations scissors.mdx (Finnish) 2025-11-12 17:26:17 +01:00
vcoppe bae21c87f4 New translations scissors.mdx (Basque) 2025-11-12 17:26:17 +01:00
vcoppe edf77a6c0f New translations scissors.mdx (Greek) 2025-11-12 17:26:15 +01:00
vcoppe 232a0b2acf New translations scissors.mdx (German) 2025-11-12 17:26:14 +01:00
vcoppe 06381cb094 New translations scissors.mdx (Danish) 2025-11-12 17:26:13 +01:00
vcoppe 33a16bdd51 New translations scissors.mdx (Czech) 2025-11-12 17:26:12 +01:00
vcoppe 1b1276cc90 New translations scissors.mdx (Catalan) 2025-11-12 17:26:11 +01:00
vcoppe 8d2d7a8884 New translations scissors.mdx (Belarusian) 2025-11-12 17:26:10 +01:00
vcoppe 3894c12505 New translations scissors.mdx (Spanish) 2025-11-12 17:26:10 +01:00
vcoppe 504d952581 New translations scissors.mdx (French) 2025-11-12 17:26:09 +01:00
vcoppe e3777ac18a New translations scissors.mdx (Romanian) 2025-11-12 17:26:08 +01:00
vcoppe 4b087d8943 New translations merge.mdx (Serbian (Latin)) 2025-11-12 17:25:26 +01:00
vcoppe d383db5874 New translations merge.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:25:24 +01:00
vcoppe fa75a76ed8 New translations merge.mdx (Latvian) 2025-11-12 17:25:23 +01:00
vcoppe bc28507ea6 New translations merge.mdx (Thai) 2025-11-12 17:25:22 +01:00
vcoppe e470df06ec New translations merge.mdx (Indonesian) 2025-11-12 17:25:21 +01:00
vcoppe 9b81157d5d New translations merge.mdx (Portuguese, Brazilian) 2025-11-12 17:25:20 +01:00
vcoppe 40bf919931 New translations merge.mdx (Vietnamese) 2025-11-12 17:25:19 +01:00
vcoppe e55f7af9b3 New translations merge.mdx (Chinese Simplified) 2025-11-12 17:25:18 +01:00
vcoppe 40495dc86f New translations merge.mdx (Ukrainian) 2025-11-12 17:25:17 +01:00
vcoppe 75f01ca497 New translations merge.mdx (Turkish) 2025-11-12 17:25:16 +01:00
vcoppe 6ebdf327e3 New translations merge.mdx (Swedish) 2025-11-12 17:25:15 +01:00
vcoppe 9741c45e52 New translations merge.mdx (Russian) 2025-11-12 17:25:14 +01:00
vcoppe d193929dd7 New translations merge.mdx (Portuguese) 2025-11-12 17:25:13 +01:00
vcoppe 19cdc8b3cf New translations merge.mdx (Polish) 2025-11-12 17:25:12 +01:00
vcoppe 8ac97c332d New translations merge.mdx (Norwegian) 2025-11-12 17:25:10 +01:00
vcoppe 1b61bbafa9 New translations merge.mdx (Dutch) 2025-11-12 17:25:09 +01:00
vcoppe 9745b308d2 New translations merge.mdx (Lithuanian) 2025-11-12 17:25:08 +01:00
vcoppe 3f2e162523 New translations merge.mdx (Korean) 2025-11-12 17:25:07 +01:00
vcoppe 9669afb772 New translations merge.mdx (Italian) 2025-11-12 17:25:06 +01:00
vcoppe 105842023b New translations merge.mdx (Hungarian) 2025-11-12 17:25:05 +01:00
vcoppe 224ce99d79 New translations merge.mdx (Hebrew) 2025-11-12 17:25:05 +01:00
vcoppe 1d696edf56 New translations merge.mdx (Finnish) 2025-11-12 17:25:03 +01:00
vcoppe ae8e244418 New translations merge.mdx (Basque) 2025-11-12 17:25:02 +01:00
vcoppe a1f51e9400 New translations merge.mdx (Greek) 2025-11-12 17:25:02 +01:00
vcoppe d0c5392cf9 New translations merge.mdx (German) 2025-11-12 17:25:01 +01:00
vcoppe 998c597e17 New translations merge.mdx (Danish) 2025-11-12 17:25:00 +01:00
vcoppe c943ffd30a New translations merge.mdx (Czech) 2025-11-12 17:24:59 +01:00
vcoppe 1f54884a38 New translations merge.mdx (Catalan) 2025-11-12 17:24:58 +01:00
vcoppe df0c91a448 New translations merge.mdx (Belarusian) 2025-11-12 17:24:57 +01:00
vcoppe cceaaa5106 New translations merge.mdx (Spanish) 2025-11-12 17:24:56 +01:00
vcoppe 380aa3d178 New translations merge.mdx (French) 2025-11-12 17:24:55 +01:00
vcoppe e1624b4fe6 New translations merge.mdx (Romanian) 2025-11-12 17:24:54 +01:00
vcoppe e3bf1e8ca1 New translations extract.mdx (Serbian (Latin)) 2025-11-12 17:24:53 +01:00
vcoppe 6cb2553cc3 New translations extract.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:24:52 +01:00
vcoppe 5ad4e86b8a New translations extract.mdx (Latvian) 2025-11-12 17:24:51 +01:00
vcoppe 4d392ff48e New translations extract.mdx (Thai) 2025-11-12 17:24:50 +01:00
vcoppe 16e542d302 New translations extract.mdx (Indonesian) 2025-11-12 17:24:50 +01:00
vcoppe 3a252c911c New translations extract.mdx (Portuguese, Brazilian) 2025-11-12 17:24:49 +01:00
vcoppe 128ce34a41 New translations extract.mdx (Vietnamese) 2025-11-12 17:24:48 +01:00
vcoppe b65f7231f8 New translations extract.mdx (Chinese Simplified) 2025-11-12 17:24:47 +01:00
vcoppe 7412faf534 New translations extract.mdx (Ukrainian) 2025-11-12 17:24:46 +01:00
vcoppe ac4dd28927 New translations extract.mdx (Turkish) 2025-11-12 17:24:45 +01:00
vcoppe 1672451def New translations extract.mdx (Swedish) 2025-11-12 17:24:44 +01:00
vcoppe 96269ba437 New translations extract.mdx (Russian) 2025-11-12 17:24:43 +01:00
vcoppe 320a6311c5 New translations extract.mdx (Portuguese) 2025-11-12 17:24:42 +01:00
vcoppe 7c2a96b04a New translations extract.mdx (Polish) 2025-11-12 17:24:41 +01:00
vcoppe 704359cdba New translations extract.mdx (Norwegian) 2025-11-12 17:24:40 +01:00
vcoppe 87a1a893bf New translations extract.mdx (Dutch) 2025-11-12 17:24:39 +01:00
vcoppe 66421250b4 New translations extract.mdx (Lithuanian) 2025-11-12 17:24:38 +01:00
vcoppe 374f2b22b5 New translations extract.mdx (Korean) 2025-11-12 17:24:37 +01:00
vcoppe 5b3a287bf4 New translations extract.mdx (Italian) 2025-11-12 17:24:36 +01:00
vcoppe 56bc9ca01a New translations extract.mdx (Hungarian) 2025-11-12 17:24:35 +01:00
vcoppe 18dcbf3568 New translations extract.mdx (Hebrew) 2025-11-12 17:24:34 +01:00
vcoppe bfc8dd681d New translations extract.mdx (Finnish) 2025-11-12 17:24:33 +01:00
vcoppe 86412f34f3 New translations extract.mdx (Basque) 2025-11-12 17:24:32 +01:00
vcoppe fc416db742 New translations extract.mdx (Greek) 2025-11-12 17:24:31 +01:00
vcoppe a6a5092abd New translations extract.mdx (German) 2025-11-12 17:24:30 +01:00
vcoppe 522360bbe7 New translations extract.mdx (Danish) 2025-11-12 17:24:29 +01:00
vcoppe ed5b6e4b1d New translations extract.mdx (Czech) 2025-11-12 17:24:28 +01:00
vcoppe 4224fe3211 New translations extract.mdx (Catalan) 2025-11-12 17:24:28 +01:00
vcoppe cb99d5b007 New translations extract.mdx (Belarusian) 2025-11-12 17:24:27 +01:00
vcoppe be1ba18f7f New translations extract.mdx (Spanish) 2025-11-12 17:24:26 +01:00
vcoppe 52b54d8584 New translations extract.mdx (French) 2025-11-12 17:24:25 +01:00
vcoppe 665822d2f8 New translations extract.mdx (Romanian) 2025-11-12 17:24:24 +01:00
vcoppe fbfa3b2974 New translations clean.mdx (Serbian (Latin)) 2025-11-12 17:24:23 +01:00
vcoppe b3b0ef8f0b New translations clean.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:24:22 +01:00
vcoppe d255b29832 New translations clean.mdx (Latvian) 2025-11-12 17:24:21 +01:00
vcoppe 5a0198d10d New translations clean.mdx (Thai) 2025-11-12 17:24:20 +01:00
vcoppe 3781a118a1 New translations clean.mdx (Indonesian) 2025-11-12 17:24:19 +01:00
vcoppe ca3262d554 New translations clean.mdx (Portuguese, Brazilian) 2025-11-12 17:24:18 +01:00
vcoppe c5194b254a New translations clean.mdx (Vietnamese) 2025-11-12 17:24:17 +01:00
vcoppe 4a369b8af7 New translations clean.mdx (Chinese Simplified) 2025-11-12 17:24:16 +01:00
vcoppe 93bddd469d New translations clean.mdx (Ukrainian) 2025-11-12 17:24:15 +01:00
vcoppe 8cf3d028b7 New translations clean.mdx (Turkish) 2025-11-12 17:24:14 +01:00
vcoppe 23af660dc0 New translations clean.mdx (Swedish) 2025-11-12 17:24:13 +01:00
vcoppe 11119654eb New translations clean.mdx (Russian) 2025-11-12 17:24:12 +01:00
vcoppe 6f9d3b7fa9 New translations clean.mdx (Portuguese) 2025-11-12 17:24:11 +01:00
vcoppe 0c64ffd362 New translations clean.mdx (Polish) 2025-11-12 17:24:10 +01:00
vcoppe 5d316d4d94 New translations clean.mdx (Norwegian) 2025-11-12 17:24:09 +01:00
vcoppe aa3df50a30 New translations clean.mdx (Dutch) 2025-11-12 17:24:08 +01:00
vcoppe 8308a78834 New translations clean.mdx (Lithuanian) 2025-11-12 17:24:07 +01:00
vcoppe c02a0f27ca New translations clean.mdx (Korean) 2025-11-12 17:24:06 +01:00
vcoppe d36d798c1b New translations clean.mdx (Italian) 2025-11-12 17:24:05 +01:00
vcoppe 7eb5ed7bcd New translations clean.mdx (Hungarian) 2025-11-12 17:24:04 +01:00
vcoppe 584aa6bf5f New translations clean.mdx (Hebrew) 2025-11-12 17:24:03 +01:00
vcoppe d90915e25b New translations clean.mdx (Finnish) 2025-11-12 17:24:02 +01:00
vcoppe e787abb6de New translations clean.mdx (Basque) 2025-11-12 17:24:01 +01:00
vcoppe 7e4c2f2cf3 New translations clean.mdx (Greek) 2025-11-12 17:24:00 +01:00
vcoppe fb709389c4 New translations clean.mdx (German) 2025-11-12 17:23:59 +01:00
vcoppe f22fe9eec1 New translations clean.mdx (Danish) 2025-11-12 17:23:58 +01:00
vcoppe 4352309f9f New translations clean.mdx (Czech) 2025-11-12 17:23:57 +01:00
vcoppe 687f3533c8 New translations clean.mdx (Catalan) 2025-11-12 17:23:56 +01:00
vcoppe 2b99240be3 New translations clean.mdx (Belarusian) 2025-11-12 17:23:55 +01:00
vcoppe e03fa42d63 New translations clean.mdx (Spanish) 2025-11-12 17:23:54 +01:00
vcoppe 8639ed4d00 New translations clean.mdx (French) 2025-11-12 17:23:53 +01:00
vcoppe cf9dca228a New translations clean.mdx (Romanian) 2025-11-12 17:23:52 +01:00
vcoppe be66de7584 New translations view.mdx (Serbian (Latin)) 2025-11-12 17:23:37 +01:00
vcoppe f47b73ee01 New translations view.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:23:36 +01:00
vcoppe d5703b0e43 New translations view.mdx (Latvian) 2025-11-12 17:23:35 +01:00
vcoppe 293e10e944 New translations view.mdx (Thai) 2025-11-12 17:23:34 +01:00
vcoppe 328baea33b New translations view.mdx (Indonesian) 2025-11-12 17:23:34 +01:00
vcoppe 6c0b38d946 New translations view.mdx (Portuguese, Brazilian) 2025-11-12 17:23:32 +01:00
vcoppe c0221fc0e5 New translations view.mdx (Vietnamese) 2025-11-12 17:23:32 +01:00
vcoppe f1418ee99d New translations view.mdx (Chinese Simplified) 2025-11-12 17:23:31 +01:00
vcoppe 111bd40b94 New translations view.mdx (Ukrainian) 2025-11-12 17:23:30 +01:00
vcoppe ef477f5642 New translations view.mdx (Turkish) 2025-11-12 17:23:28 +01:00
vcoppe c785fc957c New translations view.mdx (Swedish) 2025-11-12 17:23:28 +01:00
vcoppe eeaeed9d83 New translations view.mdx (Russian) 2025-11-12 17:23:27 +01:00
vcoppe 483d66b2cd New translations view.mdx (Portuguese) 2025-11-12 17:23:26 +01:00
vcoppe e2332ee760 New translations view.mdx (Polish) 2025-11-12 17:23:25 +01:00
vcoppe 14e3cb049e New translations view.mdx (Norwegian) 2025-11-12 17:23:24 +01:00
vcoppe c021d2fa5b New translations view.mdx (Dutch) 2025-11-12 17:23:23 +01:00
vcoppe 20e365b0c0 New translations view.mdx (Lithuanian) 2025-11-12 17:23:22 +01:00
vcoppe 1ef4c9878d New translations view.mdx (Korean) 2025-11-12 17:23:21 +01:00
vcoppe 7e47a68acf New translations view.mdx (Italian) 2025-11-12 17:23:20 +01:00
vcoppe fe5502e0dd New translations view.mdx (Hungarian) 2025-11-12 17:23:19 +01:00
vcoppe 392cc2ba43 New translations view.mdx (Hebrew) 2025-11-12 17:23:18 +01:00
vcoppe b93c15bc38 New translations view.mdx (Finnish) 2025-11-12 17:23:17 +01:00
vcoppe 5d86c6f0e1 New translations view.mdx (Basque) 2025-11-12 17:23:16 +01:00
vcoppe dbfca7c677 New translations view.mdx (Greek) 2025-11-12 17:23:15 +01:00
vcoppe 714c8d2e1c New translations view.mdx (German) 2025-11-12 17:23:14 +01:00
vcoppe e6e0f259f4 New translations view.mdx (Danish) 2025-11-12 17:23:13 +01:00
vcoppe a03580a64d New translations view.mdx (Czech) 2025-11-12 17:23:12 +01:00
vcoppe 588f80c282 New translations view.mdx (Catalan) 2025-11-12 17:23:11 +01:00
vcoppe 4e3bedac31 New translations view.mdx (Belarusian) 2025-11-12 17:23:10 +01:00
vcoppe 953c1eb639 New translations view.mdx (Spanish) 2025-11-12 17:23:09 +01:00
vcoppe aa4f14c461 New translations view.mdx (French) 2025-11-12 17:23:08 +01:00
vcoppe 6937c86e66 New translations view.mdx (Romanian) 2025-11-12 17:23:07 +01:00
vcoppe c1ea4741eb New translations settings.mdx (Serbian (Latin)) 2025-11-12 17:23:05 +01:00
vcoppe d4d5edd0f3 New translations settings.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:23:04 +01:00
vcoppe 3d8dd63fad New translations settings.mdx (Latvian) 2025-11-12 17:23:03 +01:00
vcoppe 8bc6477e88 New translations settings.mdx (Thai) 2025-11-12 17:23:02 +01:00
vcoppe 5743814d90 New translations settings.mdx (Indonesian) 2025-11-12 17:23:01 +01:00
vcoppe d631b3d799 New translations settings.mdx (Portuguese, Brazilian) 2025-11-12 17:23:00 +01:00
vcoppe 6b4cf58454 New translations settings.mdx (Vietnamese) 2025-11-12 17:22:59 +01:00
vcoppe e864e7c2bf New translations settings.mdx (Chinese Simplified) 2025-11-12 17:22:58 +01:00
vcoppe ebca3f0f5f New translations settings.mdx (Ukrainian) 2025-11-12 17:22:57 +01:00
vcoppe 8216ba11b8 New translations settings.mdx (Turkish) 2025-11-12 17:22:56 +01:00
vcoppe ae67f84476 New translations settings.mdx (Swedish) 2025-11-12 17:22:56 +01:00
vcoppe 59f1313bb2 New translations settings.mdx (Russian) 2025-11-12 17:22:54 +01:00
vcoppe 04341c8c3c New translations settings.mdx (Portuguese) 2025-11-12 17:22:53 +01:00
vcoppe 0a315b50b4 New translations settings.mdx (Polish) 2025-11-12 17:22:52 +01:00
vcoppe 36a453f6c0 New translations settings.mdx (Norwegian) 2025-11-12 17:22:51 +01:00
vcoppe 991f079deb New translations settings.mdx (Dutch) 2025-11-12 17:22:50 +01:00
vcoppe 5110b4449c New translations settings.mdx (Lithuanian) 2025-11-12 17:22:49 +01:00
vcoppe 0e8fb9d56d New translations settings.mdx (Korean) 2025-11-12 17:22:48 +01:00
vcoppe 868dd01952 New translations settings.mdx (Italian) 2025-11-12 17:22:47 +01:00
vcoppe 47be4ed8dc New translations settings.mdx (Hungarian) 2025-11-12 17:22:46 +01:00
vcoppe 392c00a635 New translations settings.mdx (Hebrew) 2025-11-12 17:22:45 +01:00
vcoppe aaa99ded43 New translations settings.mdx (Finnish) 2025-11-12 17:22:44 +01:00
vcoppe 838a6be13d New translations settings.mdx (Basque) 2025-11-12 17:22:43 +01:00
vcoppe b546e130bc New translations settings.mdx (Greek) 2025-11-12 17:22:42 +01:00
vcoppe 3254afce29 New translations settings.mdx (German) 2025-11-12 17:22:41 +01:00
vcoppe 417a113ca7 New translations settings.mdx (Danish) 2025-11-12 17:22:40 +01:00
vcoppe aef814b094 New translations settings.mdx (Czech) 2025-11-12 17:22:39 +01:00
vcoppe 47075dadec New translations settings.mdx (Catalan) 2025-11-12 17:22:38 +01:00
vcoppe c1e9e0c55f New translations settings.mdx (Belarusian) 2025-11-12 17:22:37 +01:00
vcoppe 70b0698504 New translations settings.mdx (Spanish) 2025-11-12 17:22:36 +01:00
vcoppe 3f00534549 New translations settings.mdx (French) 2025-11-12 17:22:34 +01:00
vcoppe 392fbf6af4 New translations settings.mdx (Romanian) 2025-11-12 17:22:33 +01:00
vcoppe 552b18148f New translations file.mdx (Spanish) 2025-11-12 17:22:20 +01:00
vcoppe 08007ad3b2 New translations edit.mdx (Serbian (Latin)) 2025-11-12 17:22:18 +01:00
vcoppe e8890e6ba3 New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:22:17 +01:00
vcoppe b10e4fc487 New translations edit.mdx (Latvian) 2025-11-12 17:22:15 +01:00
vcoppe 1ce92bcfbc New translations edit.mdx (Thai) 2025-11-12 17:22:15 +01:00
vcoppe 7e9cd2f672 New translations edit.mdx (Indonesian) 2025-11-12 17:22:14 +01:00
vcoppe bc1c06b01f New translations edit.mdx (Portuguese, Brazilian) 2025-11-12 17:22:13 +01:00
vcoppe 2be6d36fc6 New translations edit.mdx (Vietnamese) 2025-11-12 17:22:11 +01:00
vcoppe 75f7aeb588 New translations edit.mdx (Chinese Simplified) 2025-11-12 17:22:10 +01:00
vcoppe 2944694492 New translations edit.mdx (Ukrainian) 2025-11-12 17:22:09 +01:00
vcoppe 53b29b5bdd New translations edit.mdx (Turkish) 2025-11-12 17:22:08 +01:00
vcoppe 0d4f73b269 New translations edit.mdx (Swedish) 2025-11-12 17:22:07 +01:00
vcoppe f5f2d2734d New translations edit.mdx (Russian) 2025-11-12 17:22:06 +01:00
vcoppe 4b5507333c New translations edit.mdx (Portuguese) 2025-11-12 17:22:05 +01:00
vcoppe 7d6d4d4cec New translations edit.mdx (Polish) 2025-11-12 17:22:04 +01:00
vcoppe 3c86cb0e69 New translations edit.mdx (Norwegian) 2025-11-12 17:22:03 +01:00
vcoppe 73f5d46e76 New translations edit.mdx (Dutch) 2025-11-12 17:22:01 +01:00
vcoppe 8cd02ac3ed New translations edit.mdx (Lithuanian) 2025-11-12 17:22:00 +01:00
vcoppe 8b208f9dbd New translations edit.mdx (Korean) 2025-11-12 17:21:59 +01:00
vcoppe 11065519aa New translations edit.mdx (Italian) 2025-11-12 17:21:58 +01:00
vcoppe 5124f9a054 New translations edit.mdx (Hungarian) 2025-11-12 17:21:57 +01:00
vcoppe 6c0646acc6 New translations edit.mdx (Hebrew) 2025-11-12 17:21:56 +01:00
vcoppe 60bfc8f856 New translations edit.mdx (Finnish) 2025-11-12 17:21:55 +01:00
vcoppe c57f0f84af New translations edit.mdx (Basque) 2025-11-12 17:21:54 +01:00
vcoppe cd1a5bc169 New translations edit.mdx (Greek) 2025-11-12 17:21:53 +01:00
vcoppe 27f0fe30e7 New translations edit.mdx (German) 2025-11-12 17:21:52 +01:00
vcoppe c24e008865 New translations edit.mdx (Danish) 2025-11-12 17:21:51 +01:00
vcoppe a852ed88f7 New translations edit.mdx (Czech) 2025-11-12 17:21:50 +01:00
vcoppe 57236e7010 New translations edit.mdx (Catalan) 2025-11-12 17:21:49 +01:00
vcoppe 1f7389d1e1 New translations edit.mdx (Belarusian) 2025-11-12 17:21:48 +01:00
vcoppe 3a213a273d New translations edit.mdx (Spanish) 2025-11-12 17:21:47 +01:00
vcoppe 7602c70d98 New translations edit.mdx (French) 2025-11-12 17:21:46 +01:00
vcoppe 54fda40fcf New translations edit.mdx (Romanian) 2025-11-12 17:21:45 +01:00
vcoppe ce038fde8c New translations map-controls.mdx (Serbian (Latin)) 2025-11-12 17:21:30 +01:00
vcoppe dbe2c2be9b New translations map-controls.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:21:29 +01:00
vcoppe 85c66f1b1c New translations map-controls.mdx (Latvian) 2025-11-12 17:21:28 +01:00
vcoppe 4b72468f0c New translations map-controls.mdx (Thai) 2025-11-12 17:21:27 +01:00
vcoppe ecaedf15d0 New translations map-controls.mdx (Indonesian) 2025-11-12 17:21:26 +01:00
vcoppe 1d9bee9fe7 New translations map-controls.mdx (Portuguese, Brazilian) 2025-11-12 17:21:25 +01:00
vcoppe 22a30cef11 New translations map-controls.mdx (Vietnamese) 2025-11-12 17:21:24 +01:00
vcoppe def7a36f70 New translations map-controls.mdx (Chinese Simplified) 2025-11-12 17:21:23 +01:00
vcoppe 064adcfc2a New translations map-controls.mdx (Ukrainian) 2025-11-12 17:21:22 +01:00
vcoppe c84eedbce0 New translations map-controls.mdx (Turkish) 2025-11-12 17:21:21 +01:00
vcoppe 10babaa9e6 New translations map-controls.mdx (Swedish) 2025-11-12 17:21:20 +01:00
vcoppe 2f8f3264df New translations map-controls.mdx (Russian) 2025-11-12 17:21:19 +01:00
vcoppe 5f58762dda New translations map-controls.mdx (Portuguese) 2025-11-12 17:21:18 +01:00
vcoppe e293000db9 New translations map-controls.mdx (Polish) 2025-11-12 17:21:17 +01:00
vcoppe cff344cda5 New translations map-controls.mdx (Norwegian) 2025-11-12 17:21:16 +01:00
vcoppe ba9c622d7a New translations map-controls.mdx (Dutch) 2025-11-12 17:21:15 +01:00
vcoppe 8b44770072 New translations map-controls.mdx (Lithuanian) 2025-11-12 17:21:14 +01:00
vcoppe 371c5c8c32 New translations map-controls.mdx (Korean) 2025-11-12 17:21:13 +01:00
vcoppe ddac691e8d New translations map-controls.mdx (Italian) 2025-11-12 17:21:12 +01:00
vcoppe 71b658612c New translations map-controls.mdx (Hungarian) 2025-11-12 17:21:11 +01:00
vcoppe 6f3bba910e New translations map-controls.mdx (Hebrew) 2025-11-12 17:21:10 +01:00
vcoppe 4e197885c8 New translations map-controls.mdx (Finnish) 2025-11-12 17:21:09 +01:00
vcoppe 8802375dc0 New translations map-controls.mdx (Basque) 2025-11-12 17:21:08 +01:00
vcoppe 5298879240 New translations map-controls.mdx (Greek) 2025-11-12 17:21:07 +01:00
vcoppe 2f8f40df0d New translations map-controls.mdx (German) 2025-11-12 17:21:06 +01:00
vcoppe 3c3216f384 New translations map-controls.mdx (Danish) 2025-11-12 17:21:04 +01:00
vcoppe 346c9308f2 New translations map-controls.mdx (Czech) 2025-11-12 17:21:03 +01:00
vcoppe 7468c52865 New translations map-controls.mdx (Catalan) 2025-11-12 17:21:02 +01:00
vcoppe 49c7fb5d67 New translations map-controls.mdx (Belarusian) 2025-11-12 17:21:01 +01:00
vcoppe ad3d11daf4 New translations map-controls.mdx (Spanish) 2025-11-12 17:21:00 +01:00
vcoppe 4e5d485157 New translations map-controls.mdx (French) 2025-11-12 17:20:59 +01:00
vcoppe e9097670f8 New translations map-controls.mdx (Romanian) 2025-11-12 17:20:58 +01:00
vcoppe 53ba24a087 New translations gpx.mdx (Serbian (Latin)) 2025-11-12 17:20:03 +01:00
vcoppe 271d046248 New translations gpx.mdx (Chinese Traditional, Hong Kong) 2025-11-12 17:20:02 +01:00
vcoppe beffe9cdcc New translations gpx.mdx (Latvian) 2025-11-12 17:20:01 +01:00
vcoppe e7a375d912 New translations gpx.mdx (Thai) 2025-11-12 17:20:00 +01:00
vcoppe 663dd96019 New translations gpx.mdx (Indonesian) 2025-11-12 17:19:59 +01:00
vcoppe 7eeb33a973 New translations gpx.mdx (Portuguese, Brazilian) 2025-11-12 17:19:58 +01:00
vcoppe e95195151d New translations gpx.mdx (Vietnamese) 2025-11-12 17:19:57 +01:00
vcoppe d81c2e190a New translations gpx.mdx (Chinese Simplified) 2025-11-12 17:19:56 +01:00
vcoppe 6628a52b66 New translations gpx.mdx (Ukrainian) 2025-11-12 17:19:55 +01:00
vcoppe e5ff306200 New translations gpx.mdx (Turkish) 2025-11-12 17:19:54 +01:00
vcoppe 43f4023370 New translations gpx.mdx (Swedish) 2025-11-12 17:19:53 +01:00
vcoppe 5f6956dd47 New translations gpx.mdx (Russian) 2025-11-12 17:19:52 +01:00
vcoppe 91247b1bb5 New translations gpx.mdx (Portuguese) 2025-11-12 17:19:51 +01:00
vcoppe f4056986fa New translations gpx.mdx (Polish) 2025-11-12 17:19:50 +01:00
vcoppe 30a752928f New translations gpx.mdx (Norwegian) 2025-11-12 17:19:49 +01:00
vcoppe bff66cf0dd New translations gpx.mdx (Dutch) 2025-11-12 17:19:48 +01:00
vcoppe 1abf0bc157 New translations gpx.mdx (Lithuanian) 2025-11-12 17:19:47 +01:00
vcoppe 6e845e765d New translations gpx.mdx (Korean) 2025-11-12 17:19:46 +01:00
vcoppe ef18c1a673 New translations gpx.mdx (Italian) 2025-11-12 17:19:45 +01:00
vcoppe a32460b8d0 New translations gpx.mdx (Hungarian) 2025-11-12 17:19:44 +01:00
vcoppe aedfaa05ca New translations gpx.mdx (Hebrew) 2025-11-12 17:19:43 +01:00
vcoppe 78a30a7f51 New translations gpx.mdx (Finnish) 2025-11-12 17:19:42 +01:00
vcoppe a59215e082 New translations gpx.mdx (Basque) 2025-11-12 17:19:41 +01:00
vcoppe a41f084fad New translations gpx.mdx (Greek) 2025-11-12 17:19:40 +01:00
vcoppe 28edec8158 New translations gpx.mdx (German) 2025-11-12 17:19:39 +01:00
vcoppe 9870a631c4 New translations gpx.mdx (Danish) 2025-11-12 17:19:38 +01:00
vcoppe 5027f188c9 New translations gpx.mdx (Czech) 2025-11-12 17:19:37 +01:00
vcoppe df157a5940 New translations gpx.mdx (Catalan) 2025-11-12 17:19:36 +01:00
vcoppe 1a50843ee0 New translations gpx.mdx (Belarusian) 2025-11-12 17:19:35 +01:00
vcoppe fb1b365fcf New translations gpx.mdx (Spanish) 2025-11-12 17:19:34 +01:00
vcoppe 393568499d New translations gpx.mdx (French) 2025-11-12 17:19:33 +01:00
vcoppe b24bf11192 New translations gpx.mdx (Romanian) 2025-11-12 17:19:32 +01:00
vcoppe 1e5db9dc6c fix import 2025-11-12 16:17:25 +01:00
vcoppe 252dc10e61 Merge remote-tracking branch 'origin/l10n' into dev 2025-11-12 16:08:18 +01:00
vcoppe f9e2315ba1 only show layer if it has been activated before 2025-11-12 15:50:05 +01:00
vcoppe 0eca588280 update extension api 2025-11-12 14:48:17 +01:00
vcoppe 33523bbfb9 New translations files-and-stats.mdx (Lithuanian) 2025-11-12 12:56:32 +01:00
vcoppe 110f23bdf1 update extension api 2025-11-12 12:47:26 +01:00
vcoppe 50a5cb23f5 remove unused imports 2025-11-12 11:39:51 +01:00
vcoppe 30e72db5ea hide horizontal scroll bar 2025-11-12 09:05:20 +01:00
vcoppe c4c64c8fe8 load files from urls/ids once local ones are loaded 2025-11-12 09:02:09 +01:00
vcoppe df39350d7d New translations file.mdx (Czech) 2025-11-11 18:33:32 +01:00
vcoppe 5daacd3ed4 New translations en.json (Czech) 2025-11-11 18:33:27 +01:00
vcoppe 0f7f64fb2f migrate component 2025-11-11 17:30:06 +01:00
vcoppe b09a1fdcb7 migrate component 2025-11-11 17:23:24 +01:00
vcoppe e5d45dee3a fix hidden computation for new files 2025-11-11 14:03:07 +01:00
vcoppe f3270e19df New translations file.mdx (Dutch) 2025-11-11 13:57:07 +01:00
vcoppe 1b9ad41c87 New translations en.json (Dutch) 2025-11-11 13:57:05 +01:00
vcoppe 8c3365ef24 update nz basemap 2025-11-11 13:00:34 +01:00
vcoppe db5cbffb70 api for adding overlays from extensions 2025-11-11 12:11:38 +01:00
vcoppe c6586f0eed New translations file.mdx (Spanish) 2025-11-11 12:11:02 +01:00
vcoppe f40bdc8ed9 New translations en.json (Spanish) 2025-11-11 12:11:00 +01:00
vcoppe 683ac4e118 clean custom layer logic 2025-11-11 10:37:06 +01:00
vcoppe 88c9abb78e open collapsible if an item item is selected 2025-11-11 09:31:08 +01:00
vcoppe 1729a2f734 remove dead code 2025-11-11 09:29:07 +01:00
vcoppe e5ad8bbb70 New translations file.mdx (French) 2025-11-10 20:34:51 +01:00
vcoppe 7f6acbfdbc Update source file file.mdx 2025-11-10 20:33:50 +01:00
vcoppe 2e070529e0 New translations routing.mdx (Portuguese, Brazilian) 2025-11-10 19:08:15 +01:00
vcoppe f4b31e5f0a New translations routing.mdx (Chinese Simplified) 2025-11-10 19:08:14 +01:00
vcoppe f7f093a464 New translations routing.mdx (Italian) 2025-11-10 19:08:09 +01:00
vcoppe 95cc340de5 New translations routing.mdx (Basque) 2025-11-10 19:08:06 +01:00
vcoppe 51a003c816 New translations routing.mdx (German) 2025-11-10 19:08:04 +01:00
vcoppe 977152139f New translations routing.mdx (Catalan) 2025-11-10 19:08:03 +01:00
vcoppe c6798dbcd5 delete empty file 2025-11-10 19:06:56 +01:00
vcoppe 78833df95e New translations file.mdx (Serbian (Latin)) 2025-11-10 19:06:14 +01:00
vcoppe 099d941d2e New translations file.mdx (Chinese Traditional, Hong Kong) 2025-11-10 19:06:13 +01:00
vcoppe ea58f378a9 New translations file.mdx (Latvian) 2025-11-10 19:06:11 +01:00
vcoppe 4060884909 New translations file.mdx (Thai) 2025-11-10 19:06:10 +01:00
vcoppe d9277c11d2 New translations file.mdx (Indonesian) 2025-11-10 19:06:09 +01:00
vcoppe bcbc90820a New translations file.mdx (Portuguese, Brazilian) 2025-11-10 19:06:07 +01:00
vcoppe e9caa95673 New translations file.mdx (Vietnamese) 2025-11-10 19:06:06 +01:00
vcoppe 9cd6703b05 New translations file.mdx (Chinese Simplified) 2025-11-10 19:06:05 +01:00
vcoppe 4233bd7771 New translations file.mdx (Ukrainian) 2025-11-10 19:06:03 +01:00
vcoppe 0e2db441f2 New translations file.mdx (Turkish) 2025-11-10 19:06:02 +01:00
vcoppe 571b101ea4 New translations file.mdx (Swedish) 2025-11-10 19:06:01 +01:00
vcoppe 0b9dca61ab New translations file.mdx (Russian) 2025-11-10 19:06:00 +01:00
vcoppe d8fa76d076 New translations file.mdx (Portuguese) 2025-11-10 19:05:58 +01:00
vcoppe 6116eef513 New translations file.mdx (Polish) 2025-11-10 19:05:57 +01:00
vcoppe fabe987f2c New translations file.mdx (Norwegian) 2025-11-10 19:05:56 +01:00
vcoppe af20880f37 New translations file.mdx (Dutch) 2025-11-10 19:05:55 +01:00
vcoppe 44eeab0d4b New translations file.mdx (Lithuanian) 2025-11-10 19:05:53 +01:00
vcoppe b331900158 New translations file.mdx (Korean) 2025-11-10 19:05:52 +01:00
vcoppe d74380404c New translations file.mdx (Italian) 2025-11-10 19:05:51 +01:00
vcoppe 3833a9cd6b New translations file.mdx (Hungarian) 2025-11-10 19:05:50 +01:00
vcoppe 74fdd943c9 New translations file.mdx (Hebrew) 2025-11-10 19:05:49 +01:00
vcoppe ad5b772502 New translations file.mdx (Finnish) 2025-11-10 19:05:47 +01:00
vcoppe 9bc941aa31 New translations file.mdx (Basque) 2025-11-10 19:05:46 +01:00
vcoppe 705df43047 New translations file.mdx (Greek) 2025-11-10 19:05:45 +01:00
vcoppe b31e3bb710 New translations file.mdx (German) 2025-11-10 19:05:44 +01:00
vcoppe 4082d0a368 New translations file.mdx (Danish) 2025-11-10 19:05:42 +01:00
vcoppe ce85286cdf New translations file.mdx (Czech) 2025-11-10 19:05:41 +01:00
vcoppe 415cf1a777 New translations file.mdx (Catalan) 2025-11-10 19:05:40 +01:00
vcoppe f249919ec8 New translations file.mdx (Belarusian) 2025-11-10 19:05:39 +01:00
vcoppe 054c9787d3 New translations file.mdx (Spanish) 2025-11-10 19:05:37 +01:00
vcoppe c1e88e2b5a New translations file.mdx (French) 2025-11-10 19:05:36 +01:00
vcoppe f5efeb16c4 New translations file.mdx (Romanian) 2025-11-10 19:05:35 +01:00
vcoppe 59afae7bca New translations translation.mdx (Portuguese, Brazilian) 2025-11-10 19:04:37 +01:00
vcoppe 92c6339064 New translations translation.mdx (Chinese Simplified) 2025-11-10 19:04:35 +01:00
vcoppe 582ae233f2 New translations translation.mdx (Turkish) 2025-11-10 19:04:34 +01:00
vcoppe 3c3016a211 New translations translation.mdx (Dutch) 2025-11-10 19:04:31 +01:00
vcoppe e24e1d9d3c New translations translation.mdx (Italian) 2025-11-10 19:04:28 +01:00
vcoppe 85fb564be7 New translations translation.mdx (Basque) 2025-11-10 19:04:26 +01:00
vcoppe 18f7db9eee New translations translation.mdx (German) 2025-11-10 19:04:24 +01:00
vcoppe ea0770fd11 New translations translation.mdx (Czech) 2025-11-10 19:04:23 +01:00
vcoppe 9c28fab3f9 New translations translation.mdx (Catalan) 2025-11-10 19:04:22 +01:00
vcoppe bac332a8c4 New translations translation.mdx (Spanish) 2025-11-10 19:04:20 +01:00
vcoppe 86d2542bd9 New translations funding.mdx (Portuguese, Brazilian) 2025-11-10 19:04:02 +01:00
vcoppe 5f63ec884f New translations funding.mdx (Chinese Simplified) 2025-11-10 19:04:00 +01:00
vcoppe 0f87b33354 New translations funding.mdx (Turkish) 2025-11-10 19:03:58 +01:00
vcoppe c7dc99a12f New translations funding.mdx (Dutch) 2025-11-10 19:03:55 +01:00
vcoppe 05c3c3f8f3 New translations funding.mdx (Italian) 2025-11-10 19:03:53 +01:00
vcoppe 2437a43471 New translations funding.mdx (Basque) 2025-11-10 19:03:50 +01:00
vcoppe 16cd812ba0 New translations funding.mdx (German) 2025-11-10 19:03:49 +01:00
vcoppe c036128720 New translations funding.mdx (Czech) 2025-11-10 19:03:47 +01:00
vcoppe 645db15848 New translations funding.mdx (Catalan) 2025-11-10 19:03:45 +01:00
vcoppe 21e142ffc3 New translations funding.mdx (Spanish) 2025-11-10 19:03:44 +01:00
vcoppe 23a6b3db72 New translations funding.mdx (French) 2025-11-10 19:03:43 +01:00
vcoppe b87d109625 New translations routing.mdx (Czech) 2025-11-10 19:03:04 +01:00
vcoppe 651bd295b9 New translations en.json (French) 2025-11-10 19:02:37 +01:00
vcoppe 01607e92b9 Update source file routing.mdx 2025-11-10 19:02:30 +01:00
vcoppe f40d54adbb Update source file poi.mdx 2025-11-10 19:02:29 +01:00
vcoppe 60fb387495 Update source file minify.mdx 2025-11-10 19:02:28 +01:00
vcoppe a7df18723c Update source file toolbar.mdx 2025-11-10 19:02:26 +01:00
vcoppe 4e39cc937a Update source file translation.mdx 2025-11-10 19:02:24 +01:00
vcoppe fe513c17ab Update source file funding.mdx 2025-11-10 19:02:23 +01:00
vcoppe f7fb88ed3d Update source file files-and-stats.mdx 2025-11-10 19:02:22 +01:00
vcoppe b3247c7cbe Update source file en.json 2025-11-10 19:02:21 +01:00
vcoppe d490dc0a8b match updated wording 2025-11-10 19:02:03 +01:00
vcoppe 97e79c23f6 New translations routing.mdx (Serbian (Latin)) 2025-11-10 18:51:41 +01:00
vcoppe 7803a29875 New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:51:40 +01:00
vcoppe 08093beed1 New translations routing.mdx (Latvian) 2025-11-10 18:51:38 +01:00
vcoppe ae46ae89bc New translations routing.mdx (Thai) 2025-11-10 18:51:37 +01:00
vcoppe f7977dca39 New translations routing.mdx (Indonesian) 2025-11-10 18:51:36 +01:00
vcoppe 0b344f3e08 New translations routing.mdx (Portuguese, Brazilian) 2025-11-10 18:51:35 +01:00
vcoppe 6058154eca New translations routing.mdx (Vietnamese) 2025-11-10 18:51:34 +01:00
vcoppe 941016f04b New translations routing.mdx (Chinese Simplified) 2025-11-10 18:51:33 +01:00
vcoppe 7a15898087 New translations routing.mdx (Ukrainian) 2025-11-10 18:51:31 +01:00
vcoppe 6b4effa90f New translations routing.mdx (Turkish) 2025-11-10 18:51:30 +01:00
vcoppe 77d56e4a4c New translations routing.mdx (Swedish) 2025-11-10 18:51:29 +01:00
vcoppe c4dd622e7b New translations routing.mdx (Russian) 2025-11-10 18:51:28 +01:00
vcoppe 9f24535142 New translations routing.mdx (Portuguese) 2025-11-10 18:51:26 +01:00
vcoppe a45161fcb8 New translations routing.mdx (Polish) 2025-11-10 18:51:25 +01:00
vcoppe 4379763a73 New translations routing.mdx (Norwegian) 2025-11-10 18:51:23 +01:00
vcoppe a25b376dfd New translations routing.mdx (Dutch) 2025-11-10 18:51:21 +01:00
vcoppe fb429f6777 New translations routing.mdx (Lithuanian) 2025-11-10 18:51:20 +01:00
vcoppe ae33227c3a New translations routing.mdx (Korean) 2025-11-10 18:51:19 +01:00
vcoppe 2fedc61d1e New translations routing.mdx (Italian) 2025-11-10 18:51:18 +01:00
vcoppe 6988a36dd1 New translations routing.mdx (Hungarian) 2025-11-10 18:51:17 +01:00
vcoppe 60e1124970 New translations routing.mdx (Hebrew) 2025-11-10 18:51:15 +01:00
vcoppe a007684006 New translations routing.mdx (Finnish) 2025-11-10 18:51:13 +01:00
vcoppe 090a709522 New translations routing.mdx (Basque) 2025-11-10 18:51:12 +01:00
vcoppe 9e06655214 New translations routing.mdx (Greek) 2025-11-10 18:51:11 +01:00
vcoppe 3651825e79 New translations routing.mdx (German) 2025-11-10 18:51:09 +01:00
vcoppe dcf3160b58 New translations routing.mdx (Danish) 2025-11-10 18:51:08 +01:00
vcoppe fd014f42cd New translations routing.mdx (Catalan) 2025-11-10 18:51:07 +01:00
vcoppe a760f2f7fc New translations routing.mdx (Belarusian) 2025-11-10 18:51:06 +01:00
vcoppe 262c114b7d New translations routing.mdx (Spanish) 2025-11-10 18:51:05 +01:00
vcoppe c90bdd83bb New translations routing.mdx (French) 2025-11-10 18:51:03 +01:00
vcoppe fdb2d37b12 New translations routing.mdx (Romanian) 2025-11-10 18:51:02 +01:00
vcoppe 7951837ecf New translations poi.mdx (Serbian (Latin)) 2025-11-10 18:51:00 +01:00
vcoppe d2e112a672 New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:50:59 +01:00
vcoppe 90e86077ba New translations poi.mdx (Latvian) 2025-11-10 18:50:58 +01:00
vcoppe f69dfcdefe New translations poi.mdx (Thai) 2025-11-10 18:50:57 +01:00
vcoppe 5cff7c4e72 New translations poi.mdx (Indonesian) 2025-11-10 18:50:56 +01:00
vcoppe 0eaa833cdf New translations poi.mdx (Portuguese, Brazilian) 2025-11-10 18:50:55 +01:00
vcoppe db015d925e New translations poi.mdx (Vietnamese) 2025-11-10 18:50:54 +01:00
vcoppe 69fd47601e New translations poi.mdx (Chinese Simplified) 2025-11-10 18:50:52 +01:00
vcoppe 17521574f0 New translations poi.mdx (Ukrainian) 2025-11-10 18:50:51 +01:00
vcoppe 56650a76ae New translations poi.mdx (Turkish) 2025-11-10 18:50:49 +01:00
vcoppe 984a98e792 New translations poi.mdx (Swedish) 2025-11-10 18:50:48 +01:00
vcoppe 49eb0cf202 New translations poi.mdx (Russian) 2025-11-10 18:50:47 +01:00
vcoppe 8e24a6b036 New translations poi.mdx (Portuguese) 2025-11-10 18:50:46 +01:00
vcoppe d07bf6d699 New translations poi.mdx (Polish) 2025-11-10 18:50:45 +01:00
vcoppe 877d12c676 New translations poi.mdx (Norwegian) 2025-11-10 18:50:44 +01:00
vcoppe ca629f625a New translations poi.mdx (Dutch) 2025-11-10 18:50:43 +01:00
vcoppe 087dd9a4b6 New translations poi.mdx (Lithuanian) 2025-11-10 18:50:41 +01:00
vcoppe d9a967c072 New translations poi.mdx (Korean) 2025-11-10 18:50:40 +01:00
vcoppe 6d522c82c3 New translations poi.mdx (Italian) 2025-11-10 18:50:39 +01:00
vcoppe 867af98083 New translations poi.mdx (Hungarian) 2025-11-10 18:50:38 +01:00
vcoppe 1be058d831 New translations poi.mdx (Hebrew) 2025-11-10 18:50:37 +01:00
vcoppe 71bc044ae5 New translations poi.mdx (Finnish) 2025-11-10 18:50:36 +01:00
vcoppe d660c50ade New translations poi.mdx (Basque) 2025-11-10 18:50:35 +01:00
vcoppe 3fd733d903 New translations poi.mdx (Greek) 2025-11-10 18:50:33 +01:00
vcoppe 7703b2361d New translations poi.mdx (German) 2025-11-10 18:50:32 +01:00
vcoppe 68fdb9ebc6 New translations poi.mdx (Danish) 2025-11-10 18:50:31 +01:00
vcoppe b6513343be New translations poi.mdx (Czech) 2025-11-10 18:50:30 +01:00
vcoppe cc95ff1c55 New translations poi.mdx (Catalan) 2025-11-10 18:50:29 +01:00
vcoppe c954ee0fde New translations poi.mdx (Belarusian) 2025-11-10 18:50:28 +01:00
vcoppe 1ada5e5d18 New translations poi.mdx (Spanish) 2025-11-10 18:50:27 +01:00
vcoppe f5244c3d93 New translations poi.mdx (French) 2025-11-10 18:50:26 +01:00
vcoppe 1f17776cd4 New translations poi.mdx (Romanian) 2025-11-10 18:50:24 +01:00
vcoppe ebd4f36f94 New translations minify.mdx (Serbian (Latin)) 2025-11-10 18:50:23 +01:00
vcoppe dbb9b5f254 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:50:22 +01:00
vcoppe 4301472cb2 New translations minify.mdx (Latvian) 2025-11-10 18:50:20 +01:00
vcoppe 15e7954321 New translations minify.mdx (Thai) 2025-11-10 18:50:19 +01:00
vcoppe 32d1de08e9 New translations minify.mdx (Indonesian) 2025-11-10 18:50:18 +01:00
vcoppe ea88663dce New translations minify.mdx (Portuguese, Brazilian) 2025-11-10 18:50:16 +01:00
vcoppe 97aecaf890 New translations minify.mdx (Vietnamese) 2025-11-10 18:50:15 +01:00
vcoppe b16688e1b7 New translations minify.mdx (Chinese Simplified) 2025-11-10 18:50:13 +01:00
vcoppe 5ac856251b New translations minify.mdx (Ukrainian) 2025-11-10 18:50:12 +01:00
vcoppe 62a9aacd85 New translations minify.mdx (Turkish) 2025-11-10 18:50:10 +01:00
vcoppe dff0b55a8c New translations minify.mdx (Swedish) 2025-11-10 18:50:09 +01:00
vcoppe 3b21c75b13 New translations minify.mdx (Russian) 2025-11-10 18:50:08 +01:00
vcoppe 2c7cc4b8e5 New translations minify.mdx (Portuguese) 2025-11-10 18:50:07 +01:00
vcoppe bcf8c0e35c New translations minify.mdx (Polish) 2025-11-10 18:50:05 +01:00
vcoppe 11d2936fca New translations minify.mdx (Norwegian) 2025-11-10 18:50:04 +01:00
vcoppe 5e84429e24 New translations minify.mdx (Dutch) 2025-11-10 18:50:02 +01:00
vcoppe 48c88b2d7e New translations minify.mdx (Lithuanian) 2025-11-10 18:50:00 +01:00
vcoppe e5185c0b77 New translations minify.mdx (Korean) 2025-11-10 18:49:59 +01:00
vcoppe 15773d3aba New translations minify.mdx (Italian) 2025-11-10 18:49:58 +01:00
vcoppe b577837446 New translations minify.mdx (Hungarian) 2025-11-10 18:49:56 +01:00
vcoppe 324e234b2a New translations minify.mdx (Hebrew) 2025-11-10 18:49:55 +01:00
vcoppe d40fefb0ea New translations minify.mdx (Finnish) 2025-11-10 18:49:54 +01:00
vcoppe 7e05568549 New translations minify.mdx (Basque) 2025-11-10 18:49:52 +01:00
vcoppe 0249a52d1c New translations minify.mdx (Greek) 2025-11-10 18:49:50 +01:00
vcoppe 5df1c5b09b New translations minify.mdx (German) 2025-11-10 18:49:49 +01:00
vcoppe 953ec8fe31 New translations minify.mdx (Danish) 2025-11-10 18:49:48 +01:00
vcoppe 6054afebdc New translations minify.mdx (Czech) 2025-11-10 18:49:46 +01:00
vcoppe 04f356e119 New translations minify.mdx (Catalan) 2025-11-10 18:49:45 +01:00
vcoppe a6ebefbb30 New translations minify.mdx (Belarusian) 2025-11-10 18:49:44 +01:00
vcoppe 9a1edbe1fa New translations minify.mdx (Spanish) 2025-11-10 18:49:43 +01:00
vcoppe c46d74be54 New translations minify.mdx (French) 2025-11-10 18:49:42 +01:00
vcoppe 72e949586a New translations minify.mdx (Romanian) 2025-11-10 18:49:41 +01:00
vcoppe 68dacad741 New translations toolbar.mdx (Serbian (Latin)) 2025-11-10 18:48:33 +01:00
vcoppe 7e6505ca73 New translations toolbar.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:48:32 +01:00
vcoppe 362d83f504 New translations toolbar.mdx (Latvian) 2025-11-10 18:48:31 +01:00
vcoppe e4879736b7 New translations toolbar.mdx (Thai) 2025-11-10 18:48:29 +01:00
vcoppe 68fe6628c7 New translations toolbar.mdx (Indonesian) 2025-11-10 18:48:28 +01:00
vcoppe f39ef569be New translations toolbar.mdx (Portuguese, Brazilian) 2025-11-10 18:48:27 +01:00
vcoppe 42c199376a New translations toolbar.mdx (Vietnamese) 2025-11-10 18:48:26 +01:00
vcoppe cd6b774a8c New translations toolbar.mdx (Chinese Simplified) 2025-11-10 18:48:25 +01:00
vcoppe 62598f94f8 New translations toolbar.mdx (Ukrainian) 2025-11-10 18:48:24 +01:00
vcoppe aa3b46141f New translations toolbar.mdx (Turkish) 2025-11-10 18:48:21 +01:00
vcoppe dcaa2aaeab New translations toolbar.mdx (Swedish) 2025-11-10 18:48:20 +01:00
vcoppe e36f5e47da New translations toolbar.mdx (Russian) 2025-11-10 18:48:19 +01:00
vcoppe 578d7b41b4 New translations toolbar.mdx (Portuguese) 2025-11-10 18:48:18 +01:00
vcoppe c4f81ce279 New translations toolbar.mdx (Polish) 2025-11-10 18:48:16 +01:00
vcoppe 02a7dbea85 New translations toolbar.mdx (Norwegian) 2025-11-10 18:48:15 +01:00
vcoppe 0305d3fe36 New translations toolbar.mdx (Dutch) 2025-11-10 18:48:13 +01:00
vcoppe 84fd034197 New translations toolbar.mdx (Lithuanian) 2025-11-10 18:48:11 +01:00
vcoppe 61b48e2048 New translations toolbar.mdx (Korean) 2025-11-10 18:48:10 +01:00
vcoppe c91f85389c New translations toolbar.mdx (Italian) 2025-11-10 18:48:09 +01:00
vcoppe f3683355a9 New translations toolbar.mdx (Hungarian) 2025-11-10 18:48:08 +01:00
vcoppe a86da886f4 New translations toolbar.mdx (Hebrew) 2025-11-10 18:48:06 +01:00
vcoppe 7bf8d42eb8 New translations toolbar.mdx (Finnish) 2025-11-10 18:48:05 +01:00
vcoppe 88533e29b9 New translations toolbar.mdx (Basque) 2025-11-10 18:48:03 +01:00
vcoppe 8299dcc881 New translations toolbar.mdx (Greek) 2025-11-10 18:48:01 +01:00
vcoppe 0d0c250fea New translations toolbar.mdx (German) 2025-11-10 18:48:00 +01:00
vcoppe eb8d86616b New translations toolbar.mdx (Danish) 2025-11-10 18:47:59 +01:00
vcoppe 1edc90810f New translations toolbar.mdx (Czech) 2025-11-10 18:47:58 +01:00
vcoppe c011d84a35 New translations toolbar.mdx (Catalan) 2025-11-10 18:47:56 +01:00
vcoppe b97f62ac12 New translations toolbar.mdx (Belarusian) 2025-11-10 18:47:55 +01:00
vcoppe 200a38bdc0 New translations toolbar.mdx (Spanish) 2025-11-10 18:47:54 +01:00
vcoppe e12c53a90e New translations toolbar.mdx (French) 2025-11-10 18:47:53 +01:00
vcoppe 7892916f56 New translations toolbar.mdx (Romanian) 2025-11-10 18:47:51 +01:00
vcoppe 5d681beab3 New translations translation.mdx (Serbian (Latin)) 2025-11-10 18:45:10 +01:00
vcoppe a54a2affb3 New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:45:09 +01:00
vcoppe 120062bb85 New translations translation.mdx (Latvian) 2025-11-10 18:45:07 +01:00
vcoppe a0754d2bd7 New translations translation.mdx (Thai) 2025-11-10 18:45:06 +01:00
vcoppe f4b13a84d4 New translations translation.mdx (Indonesian) 2025-11-10 18:45:05 +01:00
vcoppe 1c98e27e4d New translations translation.mdx (Portuguese, Brazilian) 2025-11-10 18:45:04 +01:00
vcoppe 6067e25df8 New translations translation.mdx (Vietnamese) 2025-11-10 18:45:02 +01:00
vcoppe 304c50a247 New translations translation.mdx (Chinese Simplified) 2025-11-10 18:45:00 +01:00
vcoppe 274ad8eac2 New translations translation.mdx (Ukrainian) 2025-11-10 18:44:59 +01:00
vcoppe 9946a3fc0e New translations translation.mdx (Turkish) 2025-11-10 18:44:58 +01:00
vcoppe 622d6db968 New translations translation.mdx (Swedish) 2025-11-10 18:44:56 +01:00
vcoppe d756ce0656 New translations translation.mdx (Russian) 2025-11-10 18:44:55 +01:00
vcoppe 316e776355 New translations translation.mdx (Portuguese) 2025-11-10 18:44:54 +01:00
vcoppe f861b7ad99 New translations translation.mdx (Polish) 2025-11-10 18:44:53 +01:00
vcoppe 01382e98f3 New translations translation.mdx (Norwegian) 2025-11-10 18:44:52 +01:00
vcoppe 3371442bbc New translations translation.mdx (Dutch) 2025-11-10 18:44:51 +01:00
vcoppe a81a804364 New translations translation.mdx (Lithuanian) 2025-11-10 18:44:50 +01:00
vcoppe b24cf5c946 New translations translation.mdx (Korean) 2025-11-10 18:44:48 +01:00
vcoppe 3d60213644 New translations translation.mdx (Italian) 2025-11-10 18:44:47 +01:00
vcoppe a23822c9df New translations translation.mdx (Hungarian) 2025-11-10 18:44:46 +01:00
vcoppe 9fa69758f1 New translations translation.mdx (Hebrew) 2025-11-10 18:44:45 +01:00
vcoppe c892d3f134 New translations translation.mdx (Finnish) 2025-11-10 18:44:44 +01:00
vcoppe 73271501dc New translations translation.mdx (Basque) 2025-11-10 18:44:43 +01:00
vcoppe a62ea520e7 New translations translation.mdx (Greek) 2025-11-10 18:44:42 +01:00
vcoppe a2a0b3c71e New translations translation.mdx (German) 2025-11-10 18:44:40 +01:00
vcoppe 51bedbe003 New translations translation.mdx (Danish) 2025-11-10 18:44:39 +01:00
vcoppe 7062a3e657 New translations translation.mdx (Czech) 2025-11-10 18:44:38 +01:00
vcoppe 209d95d5da New translations translation.mdx (Catalan) 2025-11-10 18:44:37 +01:00
vcoppe 95ee74ab2b New translations translation.mdx (Belarusian) 2025-11-10 18:44:36 +01:00
vcoppe 9f6b268dce New translations translation.mdx (Spanish) 2025-11-10 18:44:35 +01:00
vcoppe 96e8ebcc10 New translations translation.mdx (French) 2025-11-10 18:44:33 +01:00
vcoppe ca261c7037 New translations translation.mdx (Romanian) 2025-11-10 18:44:32 +01:00
vcoppe a95fe13b31 New translations funding.mdx (Serbian (Latin)) 2025-11-10 18:44:09 +01:00
vcoppe c3fe76adf9 New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:44:07 +01:00
vcoppe e9b9d51a6e New translations funding.mdx (Latvian) 2025-11-10 18:44:06 +01:00
vcoppe aff66205e9 New translations funding.mdx (Thai) 2025-11-10 18:44:05 +01:00
vcoppe aa0c9d65ae New translations funding.mdx (Indonesian) 2025-11-10 18:44:04 +01:00
vcoppe 423bd2122f New translations funding.mdx (Portuguese, Brazilian) 2025-11-10 18:44:02 +01:00
vcoppe 7533910114 New translations funding.mdx (Vietnamese) 2025-11-10 18:44:01 +01:00
vcoppe 0d4d377c22 New translations funding.mdx (Chinese Simplified) 2025-11-10 18:43:59 +01:00
vcoppe 3a576b3ea5 New translations funding.mdx (Ukrainian) 2025-11-10 18:43:58 +01:00
vcoppe 27f37744a0 New translations funding.mdx (Turkish) 2025-11-10 18:43:57 +01:00
vcoppe 012ecad0bb New translations funding.mdx (Swedish) 2025-11-10 18:43:55 +01:00
vcoppe 49b5d5e961 New translations funding.mdx (Russian) 2025-11-10 18:43:54 +01:00
vcoppe 01ca48fe96 New translations funding.mdx (Portuguese) 2025-11-10 18:43:52 +01:00
vcoppe 5abe5045b2 New translations funding.mdx (Polish) 2025-11-10 18:43:51 +01:00
vcoppe 8c79a577b9 New translations funding.mdx (Norwegian) 2025-11-10 18:43:50 +01:00
vcoppe 5b18f1016f New translations funding.mdx (Dutch) 2025-11-10 18:43:49 +01:00
vcoppe bda58312bb New translations funding.mdx (Lithuanian) 2025-11-10 18:43:47 +01:00
vcoppe eff352a003 New translations funding.mdx (Korean) 2025-11-10 18:43:46 +01:00
vcoppe 3109d4ff82 New translations funding.mdx (Italian) 2025-11-10 18:43:45 +01:00
vcoppe c44eda3af6 New translations funding.mdx (Hungarian) 2025-11-10 18:43:44 +01:00
vcoppe 33e8c41c6f New translations funding.mdx (Hebrew) 2025-11-10 18:43:43 +01:00
vcoppe e0b515ba2b New translations funding.mdx (Finnish) 2025-11-10 18:43:42 +01:00
vcoppe c6b6ad41fb New translations funding.mdx (Basque) 2025-11-10 18:43:40 +01:00
vcoppe 99576238cd New translations funding.mdx (Greek) 2025-11-10 18:43:39 +01:00
vcoppe db712650ef New translations funding.mdx (German) 2025-11-10 18:43:38 +01:00
vcoppe d77e7b07eb New translations funding.mdx (Danish) 2025-11-10 18:43:37 +01:00
vcoppe 47b35fad3f New translations funding.mdx (Czech) 2025-11-10 18:43:36 +01:00
vcoppe 720105ca92 New translations funding.mdx (Catalan) 2025-11-10 18:43:35 +01:00
vcoppe ce656068e3 New translations funding.mdx (Belarusian) 2025-11-10 18:43:34 +01:00
vcoppe e69ba76e7d New translations funding.mdx (Spanish) 2025-11-10 18:43:32 +01:00
vcoppe d1bab213a3 New translations funding.mdx (French) 2025-11-10 18:43:31 +01:00
vcoppe 016a9341ad New translations funding.mdx (Romanian) 2025-11-10 18:43:30 +01:00
vcoppe 81f407ad5f New translations files-and-stats.mdx (Serbian (Latin)) 2025-11-10 18:42:44 +01:00
vcoppe 37ff2f6bdb New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:42:43 +01:00
vcoppe 13cf1a9551 New translations files-and-stats.mdx (Latvian) 2025-11-10 18:42:41 +01:00
vcoppe 799aaac449 New translations files-and-stats.mdx (Thai) 2025-11-10 18:42:40 +01:00
vcoppe fbe430e4cf New translations files-and-stats.mdx (Indonesian) 2025-11-10 18:42:39 +01:00
vcoppe 511746620e New translations files-and-stats.mdx (Portuguese, Brazilian) 2025-11-10 18:42:38 +01:00
vcoppe fed5b648f8 New translations files-and-stats.mdx (Vietnamese) 2025-11-10 18:42:37 +01:00
vcoppe 1f601743f6 New translations files-and-stats.mdx (Chinese Simplified) 2025-11-10 18:42:36 +01:00
vcoppe db3ccf5da7 New translations files-and-stats.mdx (Ukrainian) 2025-11-10 18:42:34 +01:00
vcoppe 038898d580 New translations files-and-stats.mdx (Turkish) 2025-11-10 18:42:33 +01:00
vcoppe dafbc6bc51 New translations files-and-stats.mdx (Swedish) 2025-11-10 18:42:32 +01:00
vcoppe 3d8271e7a1 New translations files-and-stats.mdx (Russian) 2025-11-10 18:42:31 +01:00
vcoppe e2ade95219 New translations files-and-stats.mdx (Portuguese) 2025-11-10 18:42:30 +01:00
vcoppe 604971a576 New translations files-and-stats.mdx (Polish) 2025-11-10 18:42:28 +01:00
vcoppe 76173103af New translations files-and-stats.mdx (Norwegian) 2025-11-10 18:42:27 +01:00
vcoppe 3fcbc60232 New translations routing.mdx (Czech) 2025-11-10 18:42:26 +01:00
vcoppe 835bdb39d0 New translations files-and-stats.mdx (Dutch) 2025-11-10 18:42:25 +01:00
vcoppe 200f09c5d2 New translations files-and-stats.mdx (Lithuanian) 2025-11-10 18:42:23 +01:00
vcoppe 99a008f122 New translations files-and-stats.mdx (Korean) 2025-11-10 18:42:22 +01:00
vcoppe 9b71e89ba0 New translations files-and-stats.mdx (Italian) 2025-11-10 18:42:20 +01:00
vcoppe 9955369cfa New translations files-and-stats.mdx (Hungarian) 2025-11-10 18:42:18 +01:00
vcoppe 6ef6ffab66 New translations files-and-stats.mdx (Hebrew) 2025-11-10 18:42:17 +01:00
vcoppe 0dd46f0f20 New translations files-and-stats.mdx (Finnish) 2025-11-10 18:42:15 +01:00
vcoppe cce0d85dd4 New translations files-and-stats.mdx (Basque) 2025-11-10 18:42:14 +01:00
vcoppe 1ed3eae038 New translations files-and-stats.mdx (Greek) 2025-11-10 18:42:12 +01:00
vcoppe 2854c24197 New translations files-and-stats.mdx (German) 2025-11-10 18:42:10 +01:00
vcoppe 4b90ddda2a New translations files-and-stats.mdx (Danish) 2025-11-10 18:42:09 +01:00
vcoppe 552c39e583 New translations files-and-stats.mdx (Czech) 2025-11-10 18:42:08 +01:00
vcoppe 5176e459b5 New translations files-and-stats.mdx (Catalan) 2025-11-10 18:42:06 +01:00
vcoppe 39515d0600 New translations files-and-stats.mdx (Belarusian) 2025-11-10 18:42:05 +01:00
vcoppe 5314949394 New translations files-and-stats.mdx (Spanish) 2025-11-10 18:42:02 +01:00
vcoppe 59e5be749c New translations files-and-stats.mdx (French) 2025-11-10 18:42:01 +01:00
vcoppe da252a0070 New translations files-and-stats.mdx (Romanian) 2025-11-10 18:42:00 +01:00
vcoppe b6199e430c New translations en.json (Serbian (Latin)) 2025-11-10 18:41:58 +01:00
vcoppe e260a68c26 New translations en.json (Chinese Traditional, Hong Kong) 2025-11-10 18:41:57 +01:00
vcoppe 570cb2deaf New translations en.json (Latvian) 2025-11-10 18:41:55 +01:00
vcoppe 7a36f03fb5 New translations en.json (Thai) 2025-11-10 18:41:54 +01:00
vcoppe 6c3058ba97 New translations en.json (Indonesian) 2025-11-10 18:41:53 +01:00
vcoppe 48a1034c12 New translations en.json (Portuguese, Brazilian) 2025-11-10 18:41:52 +01:00
vcoppe aaddb50ab9 New translations en.json (Vietnamese) 2025-11-10 18:41:51 +01:00
vcoppe a947586cfe New translations en.json (Chinese Simplified) 2025-11-10 18:41:49 +01:00
vcoppe f3cfa14a59 New translations en.json (Ukrainian) 2025-11-10 18:41:48 +01:00
vcoppe a2c0a77c53 New translations en.json (Turkish) 2025-11-10 18:41:47 +01:00
vcoppe c078f9d5cb New translations en.json (Swedish) 2025-11-10 18:41:46 +01:00
vcoppe 08eab8a157 New translations en.json (Russian) 2025-11-10 18:41:45 +01:00
vcoppe 9c36f234bc New translations en.json (Portuguese) 2025-11-10 18:41:44 +01:00
vcoppe 36b81d0e2a New translations en.json (Polish) 2025-11-10 18:41:42 +01:00
vcoppe 4432c14377 New translations en.json (Norwegian) 2025-11-10 18:41:41 +01:00
vcoppe 99e6dd5ca3 New translations en.json (Dutch) 2025-11-10 18:41:40 +01:00
vcoppe 6327a25aec New translations en.json (Lithuanian) 2025-11-10 18:41:39 +01:00
vcoppe 40989de7f5 New translations en.json (Korean) 2025-11-10 18:41:38 +01:00
vcoppe 58af44e795 New translations en.json (Italian) 2025-11-10 18:41:36 +01:00
vcoppe 4910cc05f8 New translations en.json (Hungarian) 2025-11-10 18:41:35 +01:00
vcoppe 164ee24d16 New translations en.json (Hebrew) 2025-11-10 18:41:34 +01:00
vcoppe 0ffda4ab7c New translations en.json (Finnish) 2025-11-10 18:41:33 +01:00
vcoppe 4694a6271d New translations en.json (Basque) 2025-11-10 18:41:32 +01:00
vcoppe 2f50bc747a New translations en.json (Greek) 2025-11-10 18:41:30 +01:00
vcoppe a13f621a81 New translations en.json (German) 2025-11-10 18:41:29 +01:00
vcoppe 0cc520cc67 New translations en.json (Czech) 2025-11-10 18:41:28 +01:00
vcoppe c9451c3f2d New translations en.json (Catalan) 2025-11-10 18:41:26 +01:00
vcoppe 8da53ffda2 New translations en.json (Belarusian) 2025-11-10 18:41:25 +01:00
vcoppe 4319761687 New translations en.json (Spanish) 2025-11-10 18:41:24 +01:00
vcoppe a1f3227cd9 New translations en.json (Romanian) 2025-11-10 18:41:22 +01:00
vcoppe b07f87c920 New translations en.json (Danish) 2025-11-10 18:41:21 +01:00
vcoppe 9c8f23eb64 New translations en.json (French) 2025-11-10 18:41:19 +01:00
vcoppe 36c6c623de fix crawling 2025-11-10 18:37:31 +01:00
vcoppe e334419e24 fix import 2025-11-10 16:54:09 +01:00
vcoppe 01240c4f2a fix spacing 2025-11-10 16:47:16 +01:00
vcoppe 431a9ce827 migrate component 2025-11-10 16:45:50 +01:00
vcoppe 20ab41c3b4 update lucid icon name 2025-11-10 16:23:35 +01:00
vcoppe 3f4ea27be2 update gitignore 2025-11-10 16:07:06 +01:00
vcoppe 5bb5b2f8c8 fix destroy 2025-11-10 16:03:03 +01:00
vcoppe e9bb9e27bb fix spacing 2025-11-10 16:02:54 +01:00
vcoppe ee1dd1fae7 migrate component 2025-11-10 15:47:43 +01:00
vcoppe 738530a960 remove active layers when removed from selection 2025-11-10 15:26:12 +01:00
vcoppe 16023b0688 fix some typescript errors 2025-11-10 13:11:44 +01:00
vcoppe bce7b5984f fix footer spacing 2025-11-10 11:56:28 +01:00
vcoppe 4e5d7d391a small style fixes 2025-11-10 11:51:16 +01:00
vcoppe 0554a85e01 fix toolbar animation 2025-11-10 11:11:37 +01:00
vcoppe 2d232b3c4b New translations routing.mdx (Czech) 2025-11-09 23:00:54 +01:00
vcoppe b2a5462372 improve website link 2025-11-09 20:14:52 +01:00
vcoppe 9d61f51270 fix spacing 2025-11-09 20:11:35 +01:00
vcoppe a0eb7d61cc remove swedish map 2025-11-09 20:05:00 +01:00
vcoppe 9861b319f4 fix popup max height 2025-11-09 19:52:02 +01:00
vcoppe b04e0f10b2 resize map on load 2025-11-09 19:20:10 +01:00
vcoppe e6d089b34b close -> delete 2025-11-09 19:09:16 +01:00
vcoppe 9df014e986 fix sortable 2025-11-09 19:00:33 +01:00
vcoppe 39b8d2e70d initialize missing settings 2025-11-09 18:45:20 +01:00
vcoppe 59710d2e1a fix embedding + playground 2025-11-09 18:03:27 +01:00
vcoppe 712dc9bb34 New translations en.json (Danish) 2025-11-04 06:53:16 +01:00
vcoppe 5c338d53ae New translations en.json (Danish) 2025-11-04 05:49:17 +01:00
vcoppe ec3eb387e5 sortable file list, to be fixed 2025-11-02 16:01:17 +01:00
vcoppe 722cf58486 progress 2025-10-26 12:12:23 +01:00
vcoppe 17e5347d55 update date 2025-10-26 11:04:23 +01:00
vcoppe 2e828dfde3 migrate custom layers sortable list from sortablejs to svelte-dnd-action 2025-10-25 18:34:24 +02:00
vcoppe 1b035bcde3 fix metadata and style dialogs 2025-10-25 17:44:41 +02:00
vcoppe 30981130c9 fix menu not closing 2025-10-25 15:05:11 +02:00
vcoppe 6db8696a36 small fixes for tools 2025-10-24 20:07:15 +02:00
vcoppe 9c83dcafa7 fix gpx markers 2025-10-24 20:06:54 +02:00
vcoppe 1db9ecafef fix coordinates popup 2025-10-23 19:07:32 +02:00
vcoppe aa624e2c60 renaming 2025-10-23 18:58:33 +02:00
vcoppe bde7e3e8aa rename files 2025-10-23 18:56:04 +02:00
vcoppe b2b3e1b153 clean scissors logic 2025-10-23 18:54:01 +02:00
vcoppe 76b3d09320 fix layer control 2025-10-23 18:42:10 +02:00
vcoppe 899dcddd2e fix custom layer creation 2025-10-22 21:54:22 +02:00
vcoppe 9edae7e1b8 fix elevation profile 2025-10-22 19:05:20 +02:00
vcoppe 8d26842aab New translations en.json (Russian) 2025-10-21 12:25:32 +02:00
vcoppe 76e654304b New translations en.json (Russian) 2025-10-21 10:35:36 +02:00
vcoppe d73b684999 move file 2025-10-20 20:17:47 +02:00
vcoppe a78bd8d7ca minor fixes 2025-10-20 19:53:42 +02:00
vcoppe 2ca53c1004 fix 2025-10-19 16:51:30 +02:00
vcoppe d621516d59 fix separator 2025-10-19 16:47:23 +02:00
vcoppe ef310cc3cc fix elevation profile toggle 2025-10-19 16:45:12 +02:00
vcoppe 776c867c0b fix xs selector 2025-10-19 16:44:15 +02:00
vcoppe 8abe0ec333 fix fill 2025-10-19 16:19:22 +02:00
vcoppe e57ced0ce7 bounds management 2025-10-19 16:14:05 +02:00
vcoppe 117c46341b add utagawaVTT layer 2025-10-19 14:19:44 +02:00
vcoppe ba1ac69151 start/end & distance markers 2025-10-19 14:15:52 +02:00
vcoppe a8d3af35de fix gap 2025-10-19 13:55:01 +02:00
vcoppe 307eed86e3 fix export 2025-10-19 13:51:56 +02:00
vcoppe 3f103323c7 fix hiding 2025-10-19 13:45:05 +02:00
vcoppe 05df3ca064 start fixing elevation profile 2025-10-18 20:12:19 +02:00
vcoppe 356884cf58 starting to fix time tool 2025-10-18 19:21:10 +02:00
vcoppe e68da7354e update shadcn components 2025-10-18 18:51:11 +02:00
vcoppe c59cd66141 fix tools 2025-10-18 16:10:08 +02:00
vcoppe 9fa8fe5767 fix copied & cut stores 2025-10-18 09:36:55 +02:00
vcoppe 4ae0bc25c2 update selection on file removal 2025-10-18 00:46:59 +02:00
vcoppe de81a8940e statistics 2025-10-18 00:31:14 +02:00
vcoppe a73da0d81d progress 2025-10-17 23:54:45 +02:00
vcoppe 32ba679719 New translations en.json (Korean) 2025-10-17 09:13:10 +02:00
vcoppe cac0fefcdb New translations en.json (Chinese Traditional, Hong Kong) 2025-10-14 02:30:52 +02:00
vcoppe 498c76dd96 New translations en.json (Chinese Simplified) 2025-10-14 02:30:51 +02:00
vcoppe 7526182304 New translations en.json (Romanian) 2025-10-12 09:39:34 +02:00
vcoppe d46bbd9cbf New translations en.json (Romanian) 2025-10-12 08:41:16 +02:00
vcoppe e98b537499 New translations en.json (Ukrainian) 2025-10-09 12:12:30 +02:00
vcoppe fc9d8509e5 New translations en.json (Ukrainian) 2025-10-09 10:21:48 +02:00
vcoppe 7c6bbb61b5 New translations en.json (Ukrainian) 2025-10-08 21:12:54 +02:00
vcoppe 8501ddc87f New translations faq.mdx (Polish) 2025-10-07 19:48:49 +02:00
vcoppe 7d9b94525e New translations time.mdx (Polish) 2025-10-07 19:48:48 +02:00
vcoppe eb02f0eadf New translations scissors.mdx (Polish) 2025-10-07 19:48:47 +02:00
vcoppe 69a8ba5aec New translations routing.mdx (Polish) 2025-10-07 19:48:46 +02:00
vcoppe fe49b8e618 New translations merge.mdx (Polish) 2025-10-07 19:48:45 +02:00
vcoppe 26bf4dde5f New translations extract.mdx (Polish) 2025-10-07 19:48:44 +02:00
vcoppe e9b73050ba New translations toolbar.mdx (Polish) 2025-10-07 19:48:43 +02:00
vcoppe bacd0ab43f New translations view.mdx (Polish) 2025-10-07 19:48:42 +02:00
vcoppe e438051371 New translations file.mdx (Polish) 2025-10-07 19:48:40 +02:00
vcoppe 314155593d New translations edit.mdx (Polish) 2025-10-07 19:48:39 +02:00
vcoppe 787f819ce0 New translations menu.mdx (Polish) 2025-10-07 19:48:38 +02:00
vcoppe 3632a62ea3 New translations integration.mdx (Polish) 2025-10-07 19:48:37 +02:00
vcoppe c7294df007 New translations gpx.mdx (Polish) 2025-10-07 19:48:36 +02:00
vcoppe e3ad7fe3c0 New translations getting-started.mdx (Polish) 2025-10-07 19:48:34 +02:00
vcoppe 6213683ddf New translations files-and-stats.mdx (Polish) 2025-10-07 19:48:33 +02:00
vcoppe a4ddfc9970 New translations en.json (Polish) 2025-10-07 19:48:32 +02:00
vcoppe 7ff271f9b9 New translations view.mdx (Spanish) 2025-10-07 02:54:04 +02:00
vcoppe d75cdd63a9 New translations file.mdx (Spanish) 2025-10-07 02:54:02 +02:00
vcoppe 0a7575d1e4 New translations integration.mdx (Spanish) 2025-10-07 02:54:01 +02:00
vcoppe ec3022d8ad New translations en.json (Spanish) 2025-10-07 01:50:49 +02:00
vcoppe 0733562c0d progress 2025-10-05 19:34:05 +02:00
vcoppe d42103b91b New translations en.json (Ukrainian) 2025-10-03 23:57:07 +02:00
vcoppe 00f7d08b04 New translations en.json (Ukrainian) 2025-10-03 22:36:21 +02:00
vcoppe 408cc383cb New translations en.json (Portuguese) 2025-10-03 17:56:00 +02:00
vcoppe 5c926d0ac6 New translations en.json (Ukrainian) 2025-09-23 18:44:01 +02:00
vcoppe 5cb88782fc New translations en.json (Ukrainian) 2025-09-23 15:59:34 +02:00
vcoppe 5eef4e9ece New translations en.json (Russian) 2025-09-20 17:13:17 +02:00
vcoppe 04a2124141 New translations en.json (Italian) 2025-09-20 17:13:15 +02:00
vcoppe 1b6229b2a1 New translations elevation.mdx (Italian) 2025-09-20 16:10:28 +02:00
vcoppe bca6db50a7 New translations en.json (Italian) 2025-09-20 16:10:27 +02:00
vcoppe f3aae26996 New translations settings.mdx (Chinese Simplified) 2025-09-10 10:34:12 +02:00
vcoppe f3c17a8e0f New translations en.json (Indonesian) 2025-09-05 04:13:37 +02:00
vcoppe d6b24f8753 New translations en.json (Indonesian) 2025-09-05 03:11:41 +02:00
vcoppe 253db0a303 New translations en.json (Norwegian) 2025-09-04 18:13:32 +02:00
vcoppe 8499e52461 New translations en.json (Dutch) 2025-09-01 08:57:32 +02:00
vcoppe d0153179a9 New translations en.json (Indonesian) 2025-08-29 18:50:34 +02:00
vcoppe 264d03727e New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-08-13 18:27:34 +02:00
vcoppe 544405d9b9 New translations en.json (Chinese Traditional, Hong Kong) 2025-08-13 16:32:09 +02:00
vcoppe 24488a3b67 New translations en.json (Indonesian) 2025-08-08 14:15:35 +02:00
vcoppe ae78185b29 New translations en.json (Indonesian) 2025-08-08 12:50:22 +02:00
vcoppe 7f682b24ef New translations en.json (Indonesian) 2025-08-04 04:32:52 +02:00
vcoppe d42a52d8cf New translations en.json (Norwegian) 2025-08-03 16:13:45 +02:00
vcoppe b85df15890 New translations en.json (Norwegian) 2025-08-03 15:00:56 +02:00
vcoppe 393499f34f New translations en.json (Indonesian) 2025-08-02 13:58:57 +02:00
vcoppe c656d0f9b5 New translations en.json (Indonesian) 2025-08-02 12:40:02 +02:00
vcoppe 32017a8859 New translations en.json (Indonesian) 2025-08-02 11:35:56 +02:00
vcoppe d87c5b1140 New translations en.json (Norwegian) 2025-08-01 22:28:15 +02:00
vcoppe f59f783d3f New translations en.json (Norwegian) 2025-08-01 21:18:36 +02:00
vcoppe ec298eac61 New translations elevation.mdx (Indonesian) 2025-08-01 16:14:37 +02:00
vcoppe 81a25bb4ee New translations faq.mdx (Indonesian) 2025-08-01 16:14:36 +02:00
vcoppe e99f044e45 New translations time.mdx (Indonesian) 2025-08-01 16:14:35 +02:00
vcoppe 5ae25a5fd9 New translations scissors.mdx (Indonesian) 2025-08-01 16:14:34 +02:00
vcoppe e9d1cb4907 New translations routing.mdx (Indonesian) 2025-08-01 16:14:33 +02:00
vcoppe 99f8ca2dca New translations poi.mdx (Indonesian) 2025-08-01 16:14:31 +02:00
vcoppe ddea5d38b5 New translations minify.mdx (Indonesian) 2025-08-01 16:14:30 +02:00
vcoppe 31d2b83550 New translations merge.mdx (Indonesian) 2025-08-01 16:14:29 +02:00
vcoppe 5535e56ed2 New translations extract.mdx (Indonesian) 2025-08-01 16:14:28 +02:00
vcoppe d740b95dbc New translations clean.mdx (Indonesian) 2025-08-01 16:14:26 +02:00
vcoppe ae92e9a945 New translations toolbar.mdx (Indonesian) 2025-08-01 16:14:25 +02:00
vcoppe 29730c3896 New translations view.mdx (Indonesian) 2025-08-01 16:14:24 +02:00
vcoppe a5ae8270f0 New translations settings.mdx (Indonesian) 2025-08-01 16:14:23 +02:00
vcoppe 54f5fa6432 New translations file.mdx (Indonesian) 2025-08-01 16:14:22 +02:00
vcoppe 0260644063 New translations edit.mdx (Indonesian) 2025-08-01 16:14:20 +02:00
vcoppe 267fc03a82 New translations menu.mdx (Indonesian) 2025-08-01 16:14:19 +02:00
vcoppe bf1537584c New translations map-controls.mdx (Indonesian) 2025-08-01 16:14:18 +02:00
vcoppe 9ee7825022 New translations integration.mdx (Indonesian) 2025-08-01 16:14:17 +02:00
vcoppe 2be0c42dd1 New translations translation.mdx (Indonesian) 2025-08-01 16:14:16 +02:00
vcoppe 3423c053a2 New translations mapbox.mdx (Indonesian) 2025-08-01 16:14:15 +02:00
vcoppe 26923cca00 New translations funding.mdx (Indonesian) 2025-08-01 16:14:14 +02:00
vcoppe 36e027659c New translations gpx.mdx (Indonesian) 2025-08-01 16:14:13 +02:00
vcoppe f447dccdb4 New translations getting-started.mdx (Indonesian) 2025-08-01 16:14:11 +02:00
vcoppe 69eae32851 New translations files-and-stats.mdx (Indonesian) 2025-08-01 16:14:10 +02:00
vcoppe aa2fcfb8cb New translations en.json (Indonesian) 2025-08-01 16:14:09 +02:00
vcoppe fae5ef2a41 New translations en.json (Norwegian) 2025-07-31 23:43:31 +02:00
vcoppe 7251ca7d2d New translations toolbar.mdx (Norwegian) 2025-07-31 22:34:29 +02:00
vcoppe 7cdbd919bf New translations en.json (Norwegian) 2025-07-31 22:34:27 +02:00
vcoppe d450f95602 New translations en.json (Dutch) 2025-07-31 14:26:59 +02:00
vcoppe 5a65201971 New translations en.json (Thai) 2025-07-30 18:35:07 +02:00
vcoppe d303b8db3e New translations gpx.mdx (Portuguese) 2025-07-20 19:33:06 +02:00
vcoppe 06baa33827 New translations gpx.mdx (Portuguese) 2025-07-20 18:31:13 +02:00
vcoppe 42743e637e New translations en.json (French) 2025-07-18 16:38:10 +02:00
vcoppe 9969fd7dec New translations edit.mdx (Swedish) 2025-07-17 23:06:28 +02:00
vcoppe fc6d5c2a1d New translations en.json (Basque) 2025-07-16 07:51:58 +02:00
vcoppe f8abb1ca24 New translations elevation.mdx (Thai) 2025-07-15 14:10:56 +02:00
vcoppe a5af38ae3d New translations faq.mdx (Thai) 2025-07-15 14:10:55 +02:00
vcoppe aab70951dc New translations time.mdx (Thai) 2025-07-15 14:10:54 +02:00
vcoppe 334cacf93c New translations scissors.mdx (Thai) 2025-07-15 14:10:52 +02:00
vcoppe 53024012fc New translations routing.mdx (Thai) 2025-07-15 14:10:51 +02:00
vcoppe 86a72f77c1 New translations poi.mdx (Thai) 2025-07-15 14:10:50 +02:00
vcoppe bc11a5ad0a New translations minify.mdx (Thai) 2025-07-15 14:10:49 +02:00
vcoppe 8f2d217fd4 New translations merge.mdx (Thai) 2025-07-15 14:10:47 +02:00
vcoppe 183727cd50 New translations extract.mdx (Thai) 2025-07-15 14:10:46 +02:00
vcoppe 676e87591a New translations clean.mdx (Thai) 2025-07-15 14:10:44 +02:00
vcoppe 8c05fc4da0 New translations toolbar.mdx (Thai) 2025-07-15 14:10:43 +02:00
vcoppe 2bab06561e New translations view.mdx (Thai) 2025-07-15 14:10:42 +02:00
vcoppe dfa7e2f5bb New translations settings.mdx (Thai) 2025-07-15 14:10:41 +02:00
vcoppe 78bece5616 New translations file.mdx (Thai) 2025-07-15 14:10:39 +02:00
vcoppe eeea15e373 New translations edit.mdx (Thai) 2025-07-15 14:10:38 +02:00
vcoppe 80cd513ab7 New translations menu.mdx (Thai) 2025-07-15 14:10:37 +02:00
vcoppe 942ef1615e New translations map-controls.mdx (Thai) 2025-07-15 14:10:35 +02:00
vcoppe a354698022 New translations integration.mdx (Thai) 2025-07-15 14:10:34 +02:00
vcoppe 0cdea488c9 New translations translation.mdx (Thai) 2025-07-15 14:10:33 +02:00
vcoppe 4f4291ac47 New translations mapbox.mdx (Thai) 2025-07-15 14:10:32 +02:00
vcoppe bf0cf03091 New translations funding.mdx (Thai) 2025-07-15 14:10:30 +02:00
vcoppe f7da09f20f New translations gpx.mdx (Thai) 2025-07-15 14:10:28 +02:00
vcoppe be1529331c New translations getting-started.mdx (Thai) 2025-07-15 14:10:27 +02:00
vcoppe 301d658a29 New translations files-and-stats.mdx (Thai) 2025-07-15 14:10:26 +02:00
vcoppe 1cc54e5b2c New translations en.json (Thai) 2025-07-15 14:10:25 +02:00
vcoppe 65a7fd21e7 New translations en.json (Italian) 2025-07-14 12:56:13 +02:00
vcoppe 856537c0cd New translations en.json (Ukrainian) 2025-07-10 01:33:15 +02:00
vcoppe b2a88e0063 New translations en.json (Ukrainian) 2025-07-10 00:32:33 +02:00
vcoppe 85a7068785 New translations en.json (Ukrainian) 2025-07-09 12:14:44 +02:00
vcoppe cbb733d99a New translations settings.mdx (Ukrainian) 2025-07-07 18:53:29 +02:00
vcoppe ce88c94a19 New translations edit.mdx (Ukrainian) 2025-07-07 18:53:28 +02:00
vcoppe 16516915d8 New translations translation.mdx (Ukrainian) 2025-07-07 18:53:27 +02:00
vcoppe 6addb8da23 New translations mapbox.mdx (Ukrainian) 2025-07-07 18:53:26 +02:00
vcoppe bc7f664fd8 New translations funding.mdx (Ukrainian) 2025-07-07 17:28:09 +02:00
vcoppe aac17aa33c New translations en.json (Ukrainian) 2025-07-07 17:28:08 +02:00
vcoppe 825500e207 New translations en.json (Ukrainian) 2025-07-07 15:46:23 +02:00
vcoppe 4d42016c72 New translations en.json (Italian) 2025-06-28 14:49:34 +02:00
vcoppe 9d665df602 New translations poi.mdx (Polish) 2025-06-23 23:44:31 +02:00
vcoppe 9087f69fb0 New translations minify.mdx (Polish) 2025-06-23 23:44:30 +02:00
vcoppe 2a06f6a214 New translations clean.mdx (Polish) 2025-06-23 23:44:29 +02:00
vcoppe 78a8428bd0 New translations toolbar.mdx (Polish) 2025-06-23 23:44:28 +02:00
vcoppe 0d235768fa New translations menu.mdx (Polish) 2025-06-23 23:44:26 +02:00
vcoppe af092bbdec New translations edit.mdx (Polish) 2025-06-23 22:23:55 +02:00
vcoppe 1cc07901f6 progress 2025-06-21 21:07:36 +02:00
vcoppe 4961630d62 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 05:53:38 +02:00
vcoppe 81920b9ab9 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 04:39:50 +02:00
vcoppe 9e031d3b5b New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 03:33:14 +02:00
vcoppe 7ae3ed6d2a New translations elevation.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:44 +02:00
vcoppe 05d79f2b51 New translations faq.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:43 +02:00
vcoppe 274e591354 New translations time.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:42 +02:00
vcoppe 95fd152b3d New translations scissors.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:41 +02:00
vcoppe ffc91ed6d8 New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:40 +02:00
vcoppe de0b759875 New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:38 +02:00
vcoppe f041dcf944 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:37 +02:00
vcoppe 946b9bd9d1 New translations merge.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:36 +02:00
vcoppe db77a69838 New translations extract.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:35 +02:00
vcoppe d10f4d26e2 New translations clean.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:34 +02:00
vcoppe 6b62d686ba New translations toolbar.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:33 +02:00
vcoppe 065826e64d New translations view.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:32 +02:00
vcoppe a3b096343f New translations settings.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:31 +02:00
vcoppe b33be91b06 New translations file.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:29 +02:00
vcoppe a94a1816c5 New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:28 +02:00
vcoppe 9a9e7fea07 New translations menu.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:27 +02:00
vcoppe 9a03042077 New translations map-controls.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:26 +02:00
vcoppe 704d3b2d6b New translations integration.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:24 +02:00
vcoppe e5c2be238d New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:23 +02:00
vcoppe 9feea07527 New translations mapbox.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:22 +02:00
vcoppe b0967d03b8 New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:21 +02:00
vcoppe d33fd71f93 New translations gpx.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:20 +02:00
vcoppe 226b5b2682 New translations getting-started.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:18 +02:00
vcoppe f8879b0223 New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:17 +02:00
vcoppe ada09d96c4 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-19 18:26:16 +02:00
vcoppe f0230d4634 commit before upgrading to tailwind 4 2025-06-08 16:32:41 +02:00
vcoppe 228ad1044e remove svelte-i18n dependency, replace with minimalistic implementation 2025-06-08 13:49:39 +02:00
vcoppe a9ea0e223d add turkish language 2025-06-07 11:30:06 +02:00
vcoppe 37e8237f78 New Crowdin updates (#225)
* New translations settings.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Chinese Simplified)
2025-06-07 11:29:19 +02:00
vcoppe 40deb19837 New Crowdin updates (#224)
* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations funding.mdx (Turkish)

* New translations mapbox.mdx (Turkish)

* New translations translation.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations clean.mdx (Turkish)

* New translations extract.mdx (Turkish)

* New translations merge.mdx (Turkish)

* New translations minify.mdx (Turkish)

* New translations poi.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations elevation.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations time.mdx (Turkish)

* New translations faq.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)
2025-06-07 10:36:02 +02:00
vcoppe a9deb681e0 add Overpass POI website as wpt link 2025-06-04 18:55:14 +02:00
vcoppe 018e638ae3 New Crowdin updates (#220)
* New translations en.json (Chinese Simplified)

* New translations poi.mdx (Basque)

* New translations en.json (Swedish)

* New translations en.json (Polish)

* New translations en.json (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations integration.mdx (Swedish)

* New translations getting-started.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations elevation.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations faq.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations settings.mdx (Swedish)

* New translations view.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations menu.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations getting-started.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations integration.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations menu.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations time.mdx (Swedish)

* New translations extract.mdx (Swedish)

* New translations merge.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations poi.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations elevation.mdx (Swedish)

* New translations en.json (Swedish)

* New translations edit.mdx (Swedish)

* New translations file.mdx (Swedish)

* New translations view.mdx (Swedish)

* New translations toolbar.mdx (Swedish)

* New translations clean.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations poi.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations faq.mdx (Swedish)
2025-06-04 18:41:56 +02:00
vcoppe 967d271667 simplify shift key detection 2025-06-04 18:41:28 +02:00
vcoppe 4e92a16d8d fix zip creation when multiple files have the same name 2025-05-23 09:12:02 +02:00
vcoppe b4bcda12c2 New Crowdin updates (#219)
* New translations en.json (Basque)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* Update source file files-and-stats.mdx

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Portuguese, Brazilian)
2025-05-20 00:11:43 +02:00
Vianney Coppé e5bb902cd4 Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2025-05-19 23:36:54 +02:00
Vianney Coppé a3817fd5cd add elevation profile component to documentation, closes #217 2025-05-19 23:35:56 +02:00
vcoppe 3a6338e9fb add basque language 2025-05-14 21:54:01 +02:00
vcoppe 34d2342bdb New Crowdin updates (#218)
* New translations files-and-stats.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations file.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations faq.mdx (Basque)
2025-05-14 21:46:32 +02:00
vcoppe 5623a6b662 New Crowdin updates (#216)
* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations funding.mdx (Basque)

* New translations mapbox.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations en.json (Basque)

* New translations edit.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations file.mdx (Basque)

* New translations funding.mdx (Polish)

* New translations file.mdx (Basque)

* New translations file.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations view.mdx (Basque)

* New translations clean.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations en.json (Portuguese, Brazilian)

* New translations routing.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations minify.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations time.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations view.mdx (Basque)

* New translations en.json (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations getting-started.mdx (Basque)
2025-05-14 20:51:49 +02:00
vcoppe de0cc63d53 move ts-node to devDependencies 2025-05-09 19:45:21 +02:00
vcoppe cb4892ace3 rename file 2025-05-09 19:39:25 +02:00
vcoppe 3cd17d7409 New translations mapbox.mdx (Czech) (#215) 2025-05-08 19:49:19 +02:00
vcoppe 2848402e97 add apple touch icon 2025-05-08 19:48:13 +02:00
vcoppe 7e8f1acf67 remove apple touch icon 2025-05-08 18:54:46 +02:00
vcoppe 8aa8a77260 change import 2025-05-08 18:44:20 +02:00
vcoppe ea4e078c92 New translations en.json (Dutch) (#213) 2025-05-08 18:39:23 +02:00
vcoppe 21261f732f improve icons 2025-05-08 18:39:12 +02:00
Jon Herman 4e9d65089c Add basic PWA support (#38) (#210)
* Add basic PWA support

See: https://github.com/gpxstudio/gpx.studio/issues/38

This will add a basic manifest.json file to the response that creates an
installable PWA.

It still needs to be internationalized.

* Refactor PWA integration and update dependencies

- Removed @vite-pwa/sveltekit dependency and related configurations from package.json and vite.config.ts.
- Added prebuild script to generate PWA manifest before build.
- Moved the manifest link to hooks.server.js and included Apple touch icon.
-Added images for various platforms. Images were generated with https://www.pwabuilder.com/imageGenerator

* use svg icon, fix urls, and remove generated manifest files

---------

Co-authored-by: jonherman <jonherman@gmail.com>
Co-authored-by: vcoppe <vianney.coppe@gmail.com>
2025-05-08 18:23:07 +02:00
vcoppe e4f6dfbf78 fix logo 2025-05-08 15:16:47 +02:00
vcoppe 9083784ffd Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2025-05-08 14:55:35 +02:00
vcoppe 6c4c4dbac9 add czech 2025-05-08 14:54:48 +02:00
vcoppe ea0abf15ce New translations view.mdx (Czech) (#212) 2025-05-08 14:54:14 +02:00
vcoppe 71cccc5bdc fix config, closes #211 2025-05-08 14:16:07 +02:00
vcoppe 4964928ac0 New Crowdin updates (#202)
* New translations en.json (German)

* New translations en.json (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (Portuguese)

* New translations en.json (Ukrainian)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations menu.mdx (Portuguese)

* New translations toolbar.mdx (Portuguese)

* New translations en.json (German)

* New translations translation.mdx (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations toolbar.mdx (Polish)

* New translations menu.mdx (Portuguese)

* New translations getting-started.mdx (German)

* New translations edit.mdx (German)

* New translations view.mdx (German)

* New translations funding.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations file.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations view.mdx (Czech)

* New translations extract.mdx (Czech)

* New translations merge.mdx (Czech)

* New translations scissors.mdx (Czech)

* New translations time.mdx (Czech)

* New translations elevation.mdx (Czech)

* New translations extract.mdx (Czech)

* New translations clean.mdx (Czech)

* New translations en.json (Danish)

* New translations faq.mdx (Czech)

* New translations faq.mdx (Czech)

* New translations en.json (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations getting-started.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations menu.mdx (Czech)

* New translations toolbar.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations view.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations gpx.mdx (Czech)

* New translations gpx.mdx (Czech)

* New translations integration.mdx (Czech)

* New translations view.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations integration.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations toolbar.mdx (Czech)

* New translations en.json (Czech)

* New translations settings.mdx (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations funding.mdx (Basque)

* New translations mapbox.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations file.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations clean.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations minify.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations time.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations settings.mdx (Portuguese)
2025-05-08 13:03:53 +02:00
vcoppe 7fde60a267 upgrade mapbox gl js 2025-04-24 19:42:46 +02:00
vcoppe 306ed2ae0e use correct OSM type for edit link 2025-04-11 18:40:16 +02:00
vcoppe a7cfe36b2e safer float parsing 2025-04-04 08:56:53 +02:00
vcoppe f4879a9e8a update routing server url 2025-03-30 20:30:22 +02:00
vcoppe c8e09fcd90 better handle time updates with weird original data 2025-03-22 15:54:13 +01:00
vcoppe c5f20d323c copy coordinates button for POIs, closes #195 2025-03-22 14:46:16 +01:00
vcoppe e3dcdf2f41 New Crowdin updates, closes #198 (#193)
* New translations en.json (Turkish)

* New translations en.json (Polish)

* New translations en.json (Polish)

* New translations gpx.mdx (Hungarian)

* New translations poi.mdx (Hungarian)

* New translations clean.mdx (Polish)

* New translations translation.mdx (Danish)
2025-03-22 14:18:45 +01:00
vcoppe 82d8b5d61e revert 2025-03-22 13:38:07 +01:00
vcoppe 47692656e4 use new routing server 2025-03-22 13:32:45 +01:00
vcoppe b5bf06b37a format file 2025-02-15 13:09:41 +01:00
vcoppe bc3b1e5f7c New Crowdin updates (#184)
* New translations en.json (Spanish)

* New translations funding.mdx (Danish)

* New translations mapbox.mdx (Danish)

* New translations translation.mdx (Danish)

* New translations settings.mdx (Danish)
2025-02-15 12:56:16 +01:00
vcoppe 63eae15191 New translations map-controls.mdx (Italian) (#182) 2025-02-07 18:38:06 +01:00
vcoppe 848b6dcef3 catch invalid dates on export, closes #180 2025-02-07 18:13:46 +01:00
vcoppe dfcdd71057 New Crowdin updates (#179)
* New translations en.json (Catalan)

* New translations en.json (Polish)

* New translations en.json (German)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations map-controls.mdx (Italian)

* New translations faq.mdx (Italian)

* New translations en.json (Italian)

* New translations getting-started.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations file.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations view.mdx (Italian)

* New translations routing.mdx (Italian)
2025-02-07 18:06:49 +01:00
vcoppe 7368945bf3 New Crowdin updates (#178)
* New translations files-and-stats.mdx (Catalan)

* New translations map-controls.mdx (Catalan)
2025-02-02 20:58:49 +01:00
vcoppe de52203e89 add catalan 2025-02-02 20:36:38 +01:00
vcoppe 62d1a3e01f New Crowdin updates (#177)
* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations gpx.mdx (Catalan)

* New translations en.json (Serbian (Latin))

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations getting-started.mdx (Serbian (Latin))

* New translations gpx.mdx (Serbian (Latin))

* New translations map-controls.mdx (Serbian (Latin))

* New translations menu.mdx (Serbian (Latin))

* New translations edit.mdx (Serbian (Latin))

* New translations view.mdx (Serbian (Latin))

* New translations toolbar.mdx (Serbian (Latin))

* New translations integration.mdx (Catalan)

* New translations integration.mdx (Catalan)

* New translations map-controls.mdx (Catalan)

* New translations view.mdx (Catalan)
2025-02-02 20:36:13 +01:00
vcoppe 68b4ecadf5 New Crowdin updates (#174)
* New translations en.json (Hungarian)

* New translations en.json (Catalan)

* New translations mapbox.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations en.json (Catalan)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations file.mdx (Catalan)

* New translations settings.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations extract.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations elevation.mdx (Catalan)

* New translations menu.mdx (Catalan)

* New translations file.mdx (Catalan)

* New translations merge.mdx (Catalan)

* New translations minify.mdx (Catalan)

* New translations poi.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations poi.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations scissors.mdx (Catalan)

* New translations time.mdx (Catalan)

* New translations en.json (Catalan)

* New translations toolbar.mdx (Catalan)

* New translations faq.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations getting-started.mdx (Catalan)

* New translations map-controls.mdx (Catalan)

* New translations menu.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations toolbar.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))
2025-02-02 11:53:12 +01:00
vcoppe 7831774703 handle files with uniform timestamps 2025-02-02 11:43:56 +01:00
vcoppe 52984d4b70 handle files with missing timestamps 2025-02-02 11:27:06 +01:00
vcoppe 0b457f9a1e prettier config + format all, closes #175 2025-02-02 11:17:22 +01:00
vcoppe 01cfd448f0 avoid embedding anything else than /embed 2025-01-30 19:25:58 +01:00
vcoppe c189ebd8ca fix attribution link for OSM layer, part of #176 2025-01-29 18:31:11 +01:00
vcoppe b35d11c9ed add missing ign fr attribution to custom styles 2025-01-29 18:24:03 +01:00
vcoppe 5fa5908072 use waypoint symbol in file tree 2025-01-26 12:48:23 +01:00
vcoppe a89f2754d3 New Crowdin updates (#173)
* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Italian)

* New translations en.json (Dutch)
2025-01-25 20:20:13 +01:00
vcoppe 453ae55db0 remember split type, part of #145 2025-01-25 13:35:21 +01:00
vcoppe c1a5bdd7ae New Crowdin updates (#172)
* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* New translations view.mdx (Chinese Simplified)

* Update source file en.json

* New translations en.json (French)
2025-01-25 13:03:57 +01:00
vcoppe d19e702084 map context menu with coordinates, closes #149 2025-01-25 12:31:12 +01:00
vcoppe e02a22eaea fix export for gpx files with no attributes 2025-01-25 10:47:47 +01:00
vcoppe 63f3d63518 use filesaver 2025-01-24 20:42:45 +01:00
Anthony 0d03ebfe96 Export multiple files as zip (#153) 2025-01-24 19:57:08 +01:00
vcoppe 074da855c1 New Crowdin updates (#167)
* New translations en.json (Russian)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (Dutch)

* New translations getting-started.mdx (Dutch)

* New translations gpx.mdx (Dutch)

* New translations funding.mdx (Dutch)

* New translations translation.mdx (Dutch)

* New translations integration.mdx (Dutch)

* New translations map-controls.mdx (Dutch)

* New translations edit.mdx (Dutch)

* New translations file.mdx (Dutch)

* New translations settings.mdx (Dutch)

* New translations view.mdx (Dutch)

* New translations extract.mdx (Dutch)

* New translations merge.mdx (Dutch)

* New translations minify.mdx (Dutch)

* New translations scissors.mdx (Dutch)

* New translations time.mdx (Dutch)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations integration.mdx (Italian)

* New translations toolbar.mdx (Italian)

* New translations integration.mdx (Italian)

* New translations en.json (German)

* New translations files-and-stats.mdx (German)

* New translations view.mdx (German)

* New translations files-and-stats.mdx (German)
2025-01-24 17:42:58 +01:00
vcoppe 9a028b9d5d support all xml namespaces 2025-01-09 20:45:11 +01:00
vcoppe a502980a39 New Crowdin updates (#165)
* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations integration.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-09 09:03:18 +01:00
vcoppe 1a1f6e5131 New Crowdin updates (#164)
* New translations files-and-stats.mdx (Italian)

* New translations elevation.mdx (Italian)
2025-01-05 19:36:38 +01:00
vcoppe a67501e6de add italian 2025-01-05 19:13:07 +01:00
vcoppe 1f4164aca3 New Crowdin updates (#163)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Czech)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-05 19:12:23 +01:00
vcoppe 25e05f9855 fix home link for english 2025-01-04 12:48:58 +01:00
vcoppe 1555799533 New Crowdin updates (#162)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)
2025-01-04 12:38:08 +01:00
vcoppe 93d5211d27 fix home link for languages other than english 2025-01-03 18:23:48 +01:00
vcoppe de38fea917 New Crowdin updates (#161)
* New translations clean.mdx (Chinese Simplified)

* New translations en.json (German)
2025-01-03 17:35:31 +01:00
vcoppe 3a3bc1c0db add language 2025-01-03 14:28:34 +01:00
vcoppe b5871d974e New Crowdin updates (#160)
* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Ukrainian)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-03 14:19:47 +01:00
vcoppe 2985c3201b fix localized home link 2025-01-03 14:18:55 +01:00
vcoppe cf2fcd8221 remove unused color strings 2025-01-03 09:27:50 +01:00
vcoppe 2cb21a43c5 fix tab color when all tracks have a different one 2025-01-02 22:04:14 +01:00
vcoppe bae0a3f93b handle gpx style attributes without the namespace 2025-01-01 20:01:46 +01:00
vcoppe a853c45ec7 New Crowdin updates (#158)
* New translations files-and-stats.mdx (Italian)

* New translations map-controls.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations edit.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations en.json (German)

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Dutch)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Dutch)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Dutch)

* New translations view.mdx (Spanish)

* New translations view.mdx (Dutch)

* New translations funding.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)
2025-01-01 14:41:42 +01:00
vcoppe 6cb6c88cd1 fix line weight attribute with correct one: line width 2025-01-01 14:40:28 +01:00
vcoppe 077f2b4435 New Crowdin updates (#157)
* Update source file files-and-stats.mdx

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* Update source file files-and-stats.mdx

* New translations files-and-stats.mdx (French)
2024-12-28 19:44:27 +01:00
vcoppe 8c3c4860f8 fix link 2024-12-28 19:30:58 +01:00
vcoppe 143592f724 New Crowdin updates (#156)
* New translations map-controls.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations integration.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations en.json (Ukrainian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations getting-started.mdx (Romanian)

* New translations getting-started.mdx (French)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Belarusian)

* New translations getting-started.mdx (Catalan)

* New translations getting-started.mdx (Czech)

* New translations getting-started.mdx (Danish)

* New translations getting-started.mdx (German)

* New translations getting-started.mdx (Greek)

* New translations getting-started.mdx (Finnish)

* New translations getting-started.mdx (Hebrew)

* New translations getting-started.mdx (Hungarian)

* New translations getting-started.mdx (Italian)

* New translations getting-started.mdx (Korean)

* New translations getting-started.mdx (Lithuanian)

* New translations getting-started.mdx (Dutch)

* New translations getting-started.mdx (Norwegian)

* New translations getting-started.mdx (Polish)

* New translations getting-started.mdx (Portuguese)

* New translations getting-started.mdx (Russian)

* New translations getting-started.mdx (Swedish)

* New translations getting-started.mdx (Turkish)

* New translations getting-started.mdx (Ukrainian)

* New translations getting-started.mdx (Chinese Simplified)

* New translations getting-started.mdx (Vietnamese)

* New translations getting-started.mdx (Portuguese, Brazilian)

* New translations getting-started.mdx (Latvian)

* New translations getting-started.mdx (Serbian (Latin))

* New translations edit.mdx (Romanian)

* New translations edit.mdx (French)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Belarusian)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations edit.mdx (Danish)

* New translations edit.mdx (German)

* New translations edit.mdx (Greek)

* New translations edit.mdx (Finnish)

* New translations edit.mdx (Hebrew)

* New translations edit.mdx (Hungarian)

* New translations edit.mdx (Italian)

* New translations edit.mdx (Korean)

* New translations edit.mdx (Lithuanian)

* New translations edit.mdx (Dutch)

* New translations edit.mdx (Norwegian)

* New translations edit.mdx (Polish)

* New translations edit.mdx (Portuguese)

* New translations edit.mdx (Russian)

* New translations edit.mdx (Swedish)

* New translations edit.mdx (Turkish)

* New translations edit.mdx (Ukrainian)

* New translations edit.mdx (Chinese Simplified)

* New translations edit.mdx (Vietnamese)

* New translations edit.mdx (Portuguese, Brazilian)

* New translations edit.mdx (Latvian)

* New translations edit.mdx (Serbian (Latin))

* New translations view.mdx (Romanian)

* New translations view.mdx (French)

* New translations view.mdx (Spanish)

* New translations view.mdx (Belarusian)

* New translations view.mdx (Catalan)

* New translations view.mdx (Czech)

* New translations view.mdx (Danish)

* New translations view.mdx (German)

* New translations view.mdx (Greek)

* New translations view.mdx (Finnish)

* New translations view.mdx (Hebrew)

* New translations view.mdx (Hungarian)

* New translations view.mdx (Italian)

* New translations view.mdx (Korean)

* New translations view.mdx (Lithuanian)

* New translations view.mdx (Dutch)

* New translations view.mdx (Norwegian)

* New translations view.mdx (Polish)

* New translations view.mdx (Portuguese)

* New translations view.mdx (Russian)

* New translations view.mdx (Swedish)

* New translations view.mdx (Turkish)

* New translations view.mdx (Ukrainian)

* New translations view.mdx (Vietnamese)

* New translations view.mdx (Portuguese, Brazilian)

* New translations view.mdx (Latvian)

* New translations view.mdx (Serbian (Latin))

* Update source file en.json

* Update source file files-and-stats.mdx

* Update source file getting-started.mdx

* Update source file edit.mdx

* Update source file view.mdx

* New translations en.json (French)

* New translations files-and-stats.mdx (French)

* New translations getting-started.mdx (French)

* New translations edit.mdx (French)

* New translations view.mdx (French)

* New translations view.mdx (Italian)
2024-12-28 19:22:36 +01:00
vcoppe dc404706c5 detect and ignore duplicate POIs when merging 2024-12-28 16:14:36 +01:00
vcoppe 7a80e9e104 rename "vertical file list" to "file tree" 2024-12-28 15:52:29 +01:00
vcoppe d7aae81c41 fix style extension handling 2024-12-28 15:16:32 +01:00
vcoppe 745c7e8470 New translations map-controls.mdx (Chinese Simplified) (#155) 2024-12-24 17:07:22 +01:00
vcoppe 52623350bd New Crowdin updates (#140)
* New translations en.json (German)

* New translations en.json (German)

* New translations minify.mdx (German)

* New translations en.json (Belarusian)

* New translations en.json (Belarusian)

* New translations en.json (Belarusian)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Serbian (Latin))

* New translations en.json (Serbian (Latin))

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Serbian (Latin))

* New translations translation.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations en.json (Catalan)

* New translations en.json (Belarusian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations mapbox.mdx (Catalan)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

* New translations files-and-stats.mdx (Hungarian)

* New translations getting-started.mdx (Hungarian)

* New translations integration.mdx (Hungarian)

* New translations map-controls.mdx (Hungarian)

* New translations menu.mdx (Hungarian)

* New translations edit.mdx (Hungarian)

* New translations settings.mdx (Hungarian)

* New translations view.mdx (Hungarian)

* New translations toolbar.mdx (Hungarian)

* New translations routing.mdx (Hungarian)

* New translations scissors.mdx (Hungarian)

* New translations time.mdx (Hungarian)

* New translations en.json (Vietnamese)

* New translations translation.mdx (Vietnamese)

* New translations settings.mdx (Vietnamese)

* New translations funding.mdx (Vietnamese)

* New translations map-controls.mdx (Belarusian)

* New translations view.mdx (Belarusian)

* New translations map-controls.mdx (Belarusian)

* New translations integration.mdx (Belarusian)

* New translations integration.mdx (Belarusian)

* New translations gpx.mdx (Belarusian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations faq.mdx (Italian)
2024-12-24 16:51:29 +01:00
vcoppe 7d2c030ebd update Mapbox GL JS, closes #129 2024-12-24 16:50:52 +01:00
vcoppe b841326e19 finer tolerances for minify tool, closes #150 2024-12-24 16:40:48 +01:00
vcoppe 23c41f18de use new toilet icon, closes #100 2024-11-18 21:08:30 +01:00
vcoppe 44e11e1a51 back to globe projection 2024-10-23 13:50:28 +02:00
vcoppe 798d8e7a14 add german 2024-10-21 23:27:19 +02:00
vcoppe 29748cf114 New translations integration.mdx (German) (#139) 2024-10-21 23:26:57 +02:00
vcoppe f5794b1355 New Crowdin updates (#132)
* New translations en.json (Italian)

* New translations funding.mdx (Czech)

* New translations mapbox.mdx (Czech)

* New translations translation.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations edit.mdx (Polish)

* New translations funding.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations elevation.mdx (Italian)

* New translations en.json (Catalan)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations file.mdx (Catalan)

* New translations file.mdx (Italian)

* New translations extract.mdx (Catalan)

* New translations routing.mdx (Italian)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations getting-started.mdx (Catalan)

* New translations mapbox.mdx (Catalan)

* New translations settings.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations faq.mdx (Catalan)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations funding.mdx (Italian)

* New translations view.mdx (Catalan)

* New translations view.mdx (Italian)

* New translations en.json (Catalan)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations en.json (Hebrew)

* New translations files-and-stats.mdx (Catalan)

* New translations en.json (Hebrew)

* New translations gpx.mdx (German)

* New translations en.json (German)

* New translations en.json (German)

* New translations funding.mdx (Hebrew)

* New translations en.json (Dutch)

* New translations edit.mdx (German)

* New translations extract.mdx (German)

* New translations elevation.mdx (German)

* New translations en.json (German)

* New translations extract.mdx (German)

* New translations en.json (Italian)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations minify.mdx (Italian)

* New translations poi.mdx (Italian)

* New translations routing.mdx (Italian)

* New translations en.json (Czech)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (Czech)

* New translations file.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations routing.mdx (Czech)

* New translations en.json (German)

* New translations mapbox.mdx (German)

* New translations en.json (German)

* New translations en.json (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations funding.mdx (Turkish)

* New translations mapbox.mdx (Turkish)

* New translations translation.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations clean.mdx (Turkish)

* New translations extract.mdx (Turkish)

* New translations merge.mdx (Turkish)

* New translations minify.mdx (Turkish)

* New translations poi.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations time.mdx (Turkish)

* New translations faq.mdx (Turkish)

* New translations elevation.mdx (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations files-and-stats.mdx (Ukrainian)

* New translations getting-started.mdx (Ukrainian)

* New translations gpx.mdx (Ukrainian)

* New translations funding.mdx (Ukrainian)

* New translations mapbox.mdx (Ukrainian)

* New translations translation.mdx (Ukrainian)

* New translations integration.mdx (Ukrainian)

* New translations map-controls.mdx (Ukrainian)

* New translations menu.mdx (Ukrainian)

* New translations edit.mdx (Ukrainian)

* New translations file.mdx (Ukrainian)

* New translations settings.mdx (Ukrainian)

* New translations view.mdx (Ukrainian)

* New translations toolbar.mdx (Ukrainian)

* New translations clean.mdx (Ukrainian)

* New translations extract.mdx (Ukrainian)

* New translations merge.mdx (Ukrainian)

* New translations minify.mdx (Ukrainian)

* New translations poi.mdx (Ukrainian)

* New translations routing.mdx (Ukrainian)

* New translations scissors.mdx (Ukrainian)

* New translations time.mdx (Ukrainian)

* New translations faq.mdx (Ukrainian)

* New translations elevation.mdx (Ukrainian)

* New translations en.json (Ukrainian)

* New translations merge.mdx (German)

* New translations en.json (German)

* New translations gpx.mdx (German)

* New translations edit.mdx (German)

* New translations extract.mdx (German)

* New translations en.json (Ukrainian)

* New translations merge.mdx (German)

* New translations files-and-stats.mdx (German)

* New translations getting-started.mdx (German)

* New translations funding.mdx (German)

* New translations integration.mdx (German)

* New translations map-controls.mdx (German)

* New translations view.mdx (German)

* New translations toolbar.mdx (German)

* New translations minify.mdx (German)

* New translations poi.mdx (German)

* New translations routing.mdx (German)

* New translations scissors.mdx (German)

* New translations faq.mdx (German)

* New translations en.json (German)

* New translations funding.mdx (German)
2024-10-21 23:08:45 +02:00
vcoppe 4ada271ad3 try to detect 512px custom tiles 2024-10-17 11:55:13 +02:00
vcoppe 1bda957778 scrollable poi content, closes #124 2024-10-14 19:00:05 +02:00
vcoppe 95328db1ee fix poi empty name 2024-10-14 18:42:14 +02:00
vcoppe 65cbf5e751 fix updating overlay opacity 2024-10-14 16:42:05 +02:00
vcoppe 60f24f8757 add h1's 2024-10-10 11:39:14 +02:00
vcoppe d1e4588813 distance markers hierarchy 2024-10-08 16:58:04 +02:00
vcoppe 711825f5a3 refactor map popups and add inspect trackpoint feature 2024-10-08 15:49:14 +02:00
vcoppe d823f44558 more precise polyfill 2024-10-08 12:39:00 +02:00
vcoppe 193c77e51a fix xml output for osm attributes 2024-10-08 12:04:07 +02:00
vcoppe 45f6c405c0 fix elevation profile drag on retina screens 2024-10-06 20:03:35 +02:00
vcoppe 78e1ea1e59 New Crowdin updates (#131)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations en.json (Portuguese, Brazilian)
2024-10-05 17:43:44 +02:00
vcoppe ca51e1d788 fix derive file style in constructor 2024-10-05 17:40:25 +02:00
vcoppe 72b0b5a706 add spanish satellite layer 2024-10-05 17:06:17 +02:00
vcoppe 4d1de97ba5 New Crowdin updates (#130)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations en.json (Italian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations files-and-stats.mdx (Korean)

* New translations en.json (Hebrew)

* New translations files-and-stats.mdx (Hebrew)

* New translations en.json (Finnish)

* New translations files-and-stats.mdx (Finnish)

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations files-and-stats.mdx (Belarusian)

* New translations en.json (Danish)

* New translations files-and-stats.mdx (Danish)

* New translations en.json (Latvian)

* New translations files-and-stats.mdx (Latvian)

* Update source file en.json

* Update source file files-and-stats.mdx

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations files-and-stats.mdx (Spanish)

* New translations en.json (French)

* New translations en.json (Dutch)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* New translations en.json (French)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Dutch)
2024-10-04 19:53:31 +02:00
vcoppe 04769002d0 update screenshot 2024-10-04 19:53:03 +02:00
vcoppe 8190284148 correct highway naming 2024-10-04 18:10:16 +02:00
vcoppe a668b4be62 improve highway names 2024-10-04 18:04:20 +02:00
vcoppe 87534468d2 highway, sac_scale and mtb:scale coloring 2024-10-04 17:32:05 +02:00
vcoppe d7a02f714a add highway info to elevation profile, closes #65 2024-10-03 18:14:01 +02:00
vcoppe 0c16ddd534 fix z-indices 2024-10-03 14:19:51 +02:00
vcoppe a96f989199 small ui improvements around elevation profile 2024-10-03 14:16:41 +02:00
vcoppe 35c7c9d965 compact elevation profile dataset control 2024-10-03 13:24:23 +02:00
vcoppe efa05b93ce remove additional scales 2024-10-03 11:41:08 +02:00
vcoppe 0e298cd0e4 same surface tags as brouter 2024-10-03 10:12:10 +02:00
vcoppe 3ef98b2110 improve mapillary integration, closes #127 2024-10-02 18:52:02 +02:00
vcoppe fbf93ed6f9 increase wpt popup max width 2024-10-02 17:03:59 +02:00
vcoppe 1bd56b6505 New Crowdin updates (#121)
* New translations en.json (Hungarian)

* New translations files-and-stats.mdx (Hungarian)

* New translations en.json (Hungarian)

* New translations funding.mdx (Hungarian)

* New translations integration.mdx (Hungarian)

* New translations faq.mdx (Hungarian)

* New translations en.json (Hungarian)

* New translations file.mdx (Italian)

* New translations file.mdx (Italian)

* New translations en.json (Italian)

* New translations file.mdx (Italian)

* New translations en.json (German)

* New translations en.json (German)

* New translations getting-started.mdx (German)

* New translations map-controls.mdx (German)

* New translations menu.mdx (German)

* New translations toolbar.mdx (German)

* New translations extract.mdx (German)

* New translations en.json (German)

* Update source file en.json

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* New translations en.json (Hungarian)

* New translations menu.mdx (Hungarian)

* New translations toolbar.mdx (Hungarian)

* New translations en.json (Italian)
2024-10-02 12:53:50 +02:00
vcoppe acf0750ccb temporary fix for #129 2024-10-02 12:49:08 +02:00
vcoppe 48eaa344e4 put ui elements below popups 2024-10-01 16:59:20 +02:00
vcoppe 3262dec7d3 show popup after content has been rendered, fixes popup placement, see #124 2024-10-01 16:56:13 +02:00
vcoppe 572d206c2c reduce hiding distance for popups 2024-10-01 14:18:23 +02:00
vcoppe 11934e5825 option to remove time gaps when merging 2024-10-01 13:17:39 +02:00
vcoppe 5cca106d18 fix button event propagation 2024-09-30 22:08:54 +02:00
1102 changed files with 43849 additions and 43380 deletions
@@ -2,3 +2,5 @@
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
yarn.lock yarn.lock
src/lib/components/ui
*.mdx
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"overrides": [
{
"files": "**/*.svelte",
"options": {
"plugins": ["prettier-plugin-svelte"],
"parser": "svelte"
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
}
}
+2 -2
View File
@@ -26,6 +26,7 @@ Any help is greatly appreciated!
## Development ## Development
The code is split into two parts: The code is split into two parts:
- `gpx`: a Typescript library for parsing and manipulating GPX files, - `gpx`: a Typescript library for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. - `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
@@ -57,10 +58,9 @@ This project has been made possible thanks to the following open source projects
- Development: - Development:
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience - [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation - [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
- [svelte-i18n](https://github.com/kaisermann/svelte-i18n) — easy localization
- Design: - Design:
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components - [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
- [lucide-svelte](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-svelte) — beautiful icons - [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling - [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts - [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
- Logic: - Logic:
+1602 -15
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -12,16 +12,20 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"immer": "^10.1.1", "immer": "^10.1.1"
"ts-node": "^10.9.2"
}, },
"devDependencies": { "devDependencies": {
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.14",
"@types/node": "^20.16.10", "@types/node": "^20.16.10",
"@typescript-eslint/parser": "^8.22.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.6.2" "typescript": "^5.6.2"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postinstall": "npm run build" "postinstall": "npm run build",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
} }
} }
+776 -261
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,4 +2,3 @@ export * from './gpx';
export { Coordinates, LineStyleExtension, WaypointType } from './types'; export { Coordinates, LineStyleExtension, WaypointType } from './types';
export { parseGPX, buildGPX } from './io'; export { parseGPX, buildGPX } from './io';
export * from './simplify'; export * from './simplify';
+86 -24
View File
@@ -1,25 +1,68 @@
import { XMLParser, XMLBuilder } from "fast-xml-parser"; import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { GPXFileType } from "./types"; import { GPXFileType } from './types';
import { GPXFile } from "./gpx"; import { GPXFile } from './gpx';
const attributesWithNamespace = {
RoutePointExtension: 'gpxx:RoutePointExtension',
rpt: 'gpxx:rpt',
TrackPointExtension: 'gpxtpx:TrackPointExtension',
PowerExtension: 'gpxpx:PowerExtension',
atemp: 'gpxtpx:atemp',
hr: 'gpxtpx:hr',
cad: 'gpxtpx:cad',
Extensions: 'gpxtpx:Extensions',
PowerInWatts: 'gpxpx:PowerInWatts',
power: 'gpxpx:PowerExtension',
line: 'gpx_style:line',
color: 'gpx_style:color',
opacity: 'gpx_style:opacity',
width: 'gpx_style:width',
};
const floatPatterns = [
/[-+]?\d*\.\d+$/, // decimal
/[-+]?\d+$/, // integer
];
function safeParseFloat(value: string): number {
const parsed = parseFloat(value);
if (!isNaN(parsed)) {
return parsed;
}
for (const pattern of floatPatterns) {
const match = value.match(pattern);
if (match) {
return parseFloat(match[0]);
}
}
return 0.0;
}
export function parseGPX(gpxData: string): GPXFile { export function parseGPX(gpxData: string): GPXFile {
const parser = new XMLParser({ const parser = new XMLParser({
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
removeNSPrefix: true,
isArray(name: string) { isArray(name: string) {
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt'; return (
name === 'trk' ||
name === 'trkseg' ||
name === 'trkpt' ||
name === 'wpt' ||
name === 'rte' ||
name === 'rtept' ||
name === 'gpxx:rpt'
);
}, },
attributeValueProcessor(attrName, attrValue, jPath) { attributeValueProcessor(attrName, attrValue, jPath) {
if (attrName === 'lat' || attrName === 'lon') { if (attrName === 'lat' || attrName === 'lon') {
return parseFloat(attrValue); return safeParseFloat(attrValue);
} }
return attrValue; return attrValue;
}, },
transformTagName(tagName: string) { transformTagName(tagName: string) {
if (tagName === 'power') { if (attributesWithNamespace[tagName]) {
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor return attributesWithNamespace[tagName];
return 'gpxpx:PowerExtension';
} }
return tagName; return tagName;
}, },
@@ -27,22 +70,29 @@ export function parseGPX(gpxData: string): GPXFile {
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) { tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
if (isLeafNode) { if (isLeafNode) {
if (tagName === 'ele') { if (tagName === 'ele') {
return parseFloat(tagValue); return safeParseFloat(tagValue);
} }
if (tagName === 'time') { if (tagName === 'time') {
return new Date(tagValue); return new Date(tagValue);
} }
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') { if (
return parseFloat(tagValue); tagName === 'gpxtpx:atemp' ||
tagName === 'gpxtpx:hr' ||
tagName === 'gpxtpx:cad' ||
tagName === 'gpxpx:PowerInWatts' ||
tagName === 'gpx_style:opacity' ||
tagName === 'gpx_style:width'
) {
return safeParseFloat(tagValue);
} }
if (tagName === 'gpxpx:PowerExtension') { if (tagName === 'gpxpx:PowerExtension') {
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag // Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
// Note that this only targets the transformed <power> tag, since it must be a leaf node // Note that this only targets the transformed <power> tag, since it must be a leaf node
return { return {
'gpxpx:PowerInWatts': parseFloat(tagValue) 'gpxpx:PowerInWatts': safeParseFloat(tagValue),
}; };
} }
} }
@@ -54,7 +104,7 @@ export function parseGPX(gpxData: string): GPXFile {
const parsed: GPXFileType = parser.parse(gpxData).gpx; const parsed: GPXFileType = parser.parse(gpxData).gpx;
// @ts-ignore // @ts-ignore
if (parsed.metadata === "") { if (parsed.metadata === '') {
parsed.metadata = {}; parsed.metadata = {};
} }
@@ -64,25 +114,32 @@ export function parseGPX(gpxData: string): GPXFile {
export function buildGPX(file: GPXFile, exclude: string[]): string { export function buildGPX(file: GPXFile, exclude: string[]): string {
const gpx = file.toGPXFileType(exclude); const gpx = file.toGPXFileType(exclude);
let lastDate = undefined;
const builder = new XMLBuilder({ const builder = new XMLBuilder({
format: true, format: true,
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
suppressEmptyNode: true, suppressEmptyNode: true,
tagValueProcessor: (tagName: string, tagValue: unknown): string => { tagValueProcessor: (tagName: string, tagValue: unknown): string | undefined => {
if (tagValue instanceof Date) { if (tagValue instanceof Date) {
if (isNaN(tagValue.getTime())) {
return lastDate?.toISOString();
}
lastDate = tagValue;
return tagValue.toISOString(); return tagValue.toISOString();
} }
return tagValue.toString(); return tagValue.toString();
}, },
}); });
gpx.attributes.creator = gpx.attributes.creator ?? 'https://gpx.studio'; if (!gpx.attributes) gpx.attributes = {};
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
gpx.attributes['version'] = '1.1'; gpx.attributes['version'] = '1.1';
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1'; gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'; gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
gpx.attributes['xsi:schemaLocation'] = 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd'; gpx.attributes['xsi:schemaLocation'] =
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1'; gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3'; gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1'; gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
@@ -93,19 +150,24 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
} }
return builder.build({ return builder.build({
"?xml": { '?xml': {
attributes: { attributes: {
version: "1.0", version: '1.0',
encoding: "UTF-8", encoding: 'UTF-8',
}
}, },
gpx: removeEmptyElements(gpx) },
gpx: removeEmptyElements(gpx),
}); });
} }
function removeEmptyElements(obj: GPXFileType): GPXFileType { function removeEmptyElements(obj: GPXFileType): GPXFileType {
for (const key in obj) { for (const key in obj) {
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) { if (
obj[key] === null ||
obj[key] === undefined ||
obj[key] === '' ||
(Array.isArray(obj[key]) && obj[key].length === 0)
) {
delete obj[key]; delete obj[key];
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) { } else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
removeEmptyElements(obj[key]); removeEmptyElements(obj[key]);
+73 -27
View File
@@ -1,33 +1,48 @@
import { TrackPoint } from "./gpx"; import { TrackPoint } from './gpx';
import { Coordinates } from "./types"; import { Coordinates } from './types';
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number }; export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] { export function ramerDouglasPeucker(
points: TrackPoint[],
epsilon: number = 50,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
): SimplifiedTrackPoint[] {
if (points.length == 0) { if (points.length == 0) {
return []; return [];
} else if (points.length == 1) { } else if (points.length == 1) {
return [{ return [
point: points[0] {
}]; point: points[0],
},
];
} }
let simplified = [{ let simplified = [
point: points[0] {
}]; point: points[0],
},
];
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified); ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
simplified.push({ simplified.push({
point: points[points.length - 1] point: points[points.length - 1],
}); });
return simplified; return simplified;
} }
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) { function ramerDouglasPeuckerRecursive(
points: TrackPoint[],
epsilon: number,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
start: number,
end: number,
simplified: SimplifiedTrackPoint[]
) {
let largest = { let largest = {
index: 0, index: 0,
distance: 0 distance: 0,
}; };
for (let i = start + 1; i < end; i++) { for (let i = start + 1; i < end; i++) {
@@ -45,8 +60,16 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, mea
} }
} }
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number { export function crossarcDistance(
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3); point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): number {
return crossarc(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
} }
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
@@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
} }
// Is relative bearing obtuse? // Is relative bearing obtuse?
if (diff > (Math.PI / 2)) { if (diff > Math.PI / 2) {
return dis13; return dis13;
} }
@@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
// Is p4 beyond the arc? // Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2); let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) { if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3); return distance(lat2, lon2, lat3, lon3);
} else { } else {
@@ -93,18 +117,32 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
function distance(latA: number, lonA: number, latB: number, lonB: number): number { function distance(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the distance between two lat / lon points. // Finds the distance between two lat / lon points.
return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius; return (
Math.acos(
Math.sin(latA) * Math.sin(latB) +
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
) * earthRadius
);
} }
function bearing(latA: number, lonA: number, latB: number, lonB: number): number { function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the bearing from one lat / lon point to another. // Finds the bearing from one lat / lon point to another.
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB), return Math.atan2(
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); Math.sin(lonB - lonA) * Math.cos(latB),
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
);
} }
export function projectedPoint(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): Coordinates { export function projectedPoint(
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3); point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): Coordinates {
return projected(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
} }
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates { function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
@@ -132,7 +170,7 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
} }
// Is relative bearing obtuse? // Is relative bearing obtuse?
if (diff > (Math.PI / 2)) { if (diff > Math.PI / 2) {
return coord1; return coord1;
} }
@@ -141,14 +179,22 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
// Is p4 beyond the arc? // Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2); let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) { if (dis14 > dis12) {
return coord2; return coord2;
} else { } else {
// Determine the closest point (p4) on the great circle // Determine the closest point (p4) on the great circle
const f = dis14 / earthRadius; const f = dis14 / earthRadius;
const lat4 = Math.asin(Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)); const lat4 = Math.asin(
const lon4 = lon1 + Math.atan2(Math.sin(bear12) * Math.sin(f) * Math.cos(lat1), Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)); Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
);
const lon4 =
lon1 +
Math.atan2(
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
);
return { lat: lat4 / rad, lon: lon4 / rad }; return { lat: lat4 / rad, lon: lon4 / rad };
} }
+9 -11
View File
@@ -67,9 +67,9 @@ export type TrackExtensions = {
}; };
export type LineStyleExtension = { export type LineStyleExtension = {
color?: string; 'gpx_style:color'?: string;
opacity?: number; 'gpx_style:opacity'?: number;
weight?: number; 'gpx_style:width'?: number;
}; };
export type TrackSegmentType = { export type TrackSegmentType = {
@@ -92,14 +92,12 @@ export type TrackPointExtension = {
'gpxtpx:atemp'?: number; 'gpxtpx:atemp'?: number;
'gpxtpx:hr'?: number; 'gpxtpx:hr'?: number;
'gpxtpx:cad'?: number; 'gpxtpx:cad'?: number;
'gpxtpx:Extensions'?: { 'gpxtpx:Extensions'?: Record<string, string>;
surface?: string; };
};
}
export type PowerExtension = { export type PowerExtension = {
'gpxpx:PowerInWatts'?: number; 'gpxpx:PowerInWatts'?: number;
} };
export type Author = { export type Author = {
name?: string; name?: string;
@@ -116,12 +114,12 @@ export type RouteType = {
type?: string; type?: string;
extensions?: TrackExtensions; extensions?: TrackExtensions;
rtept: WaypointType[]; rtept: WaypointType[];
} };
export type RoutePointExtension = { export type RoutePointExtension = {
'gpxx:rpt'?: GPXXRoutePoint[]; 'gpxx:rpt'?: GPXXRoutePoint[];
} };
export type GPXXRoutePoint = { export type GPXXRoutePoint = {
attributes: Coordinates; attributes: Coordinates;
} };
+3 -3
View File
@@ -16,9 +16,9 @@
<type>Cycling</type> <type>Cycling</type>
<extensions> <extensions>
<gpx_style:line> <gpx_style:line>
<color>#2d3ee9</color> <gpx_style:color>2d3ee9</gpx_style:color>
<opacity>0.5</opacity> <gpx_style:opacity>0.5</gpx_style:opacity>
<weight>6</weight> <gpx_style:width>6</gpx_style:width>
</gpx_style:line> </gpx_style:line>
</extensions> </extensions>
<trkseg> <trkseg>
+2 -4
View File
@@ -4,9 +4,7 @@
"target": "ES2015", "target": "ES2015",
"declaration": true, "declaration": true,
"outDir": "./dist", "outDir": "./dist",
"moduleResolution": "node", "moduleResolution": "node"
}, },
"include": [ "include": ["src"]
"src"
],
} }
+6
View File
@@ -0,0 +1,6 @@
{
"name": "gpx.studio",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+7 -7
View File
@@ -5,27 +5,27 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended', 'plugin:svelte/recommended',
'prettier' 'prettier',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020, ecmaVersion: 2020,
extraFileExtensions: ['.svelte'] extraFileExtensions: ['.svelte'],
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
}, },
overrides: [ overrides: [
{ {
files: ['*.svelte'], files: ['*.svelte'],
parser: 'svelte-eslint-parser', parser: 'svelte-eslint-parser',
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser',
} },
} },
] ],
}; };
+2
View File
@@ -8,3 +8,5 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
static/*.webmanifest
!static/en.manifest.webmanifest
-8
View File
@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+6 -3
View File
@@ -2,13 +2,16 @@
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default", "style": "default",
"tailwind": { "tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css", "css": "src/app.css",
"baseColor": "slate" "baseColor": "slate"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils" "utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
}, },
"typescript": true "typescript": true,
"registry": "https://shadcn-svelte.com/registry"
} }
+3220 -3500
View File
File diff suppressed because it is too large Load Diff
+51 -46
View File
@@ -5,7 +5,8 @@
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"postbuild": "npx tsx src/lib/sitemap.ts", "prebuild": "npx tsx src/lib/scripts/pwa-manifest.ts",
"postbuild": "npx tsx src/lib/scripts/sitemap.ts",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -13,67 +14,71 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.2.5", "@lucide/svelte": "^0.544.0",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.3.8", "@sveltejs/enhanced-img": "^0.6.0",
"@sveltejs/kit": "^2.6.1", "@sveltejs/kit": "^2.21.2",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^5.1.0",
"@types/eslint": "^8.56.12", "@tailwindcss/vite": "^4.1.8",
"@types/eslint": "^9.6.1",
"@types/events": "^3.0.3", "@types/events": "^3.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/file-saver": "^2.0.7",
"@types/mapbox__tilebelt": "^1.0.4", "@types/mapbox__tilebelt": "^1.0.4",
"@types/mapbox-gl": "^3.4.0", "@types/mapbox-gl": "^3.4.1",
"@types/node": "^20.16.10", "@types/node": "^22.15.30",
"@types/png.js": "^0.2.3", "@types/png.js": "^0.2.3",
"@types/sanitize-html": "^2.13.0", "@types/sanitize-html": "^2.16.0",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^8.33.1",
"autoprefixer": "^10.4.20", "bits-ui": "^2.12.0",
"eslint": "^8.57.1", "eslint": "^9.28.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^2.44.1", "eslint-plugin-svelte": "^3.9.1",
"events": "^3.3.0", "events": "^3.3.0",
"glob": "^10.4.5", "glob": "^11.0.2",
"mdsvex": "^0.11.2", "lucide-static": "^0.513.0",
"mdsvex": "^0.12.6",
"mode-watcher": "^1.1.0",
"paneforge": "^1.0.0-next.5",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.2.7", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^4.2.19", "svelte": "^5.33.18",
"svelte-check": "^3.8.6", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.13", "svelte-dnd-action": "^0.9.65",
"tslib": "^2.7.0", "svelte-sonner": "^1.0.5",
"tailwind-variants": "^3.1.1",
"tailwindcss": "^4.1.8",
"tslib": "^2.8.1",
"tsx": "^4.19.1", "tsx": "^4.19.1",
"typescript": "^5.6.2", "tw-animate-css": "^1.3.4",
"vite": "^5.4.8", "typescript": "^5.8.3",
"vite-plugin-node-polyfills": "^0.22.0" "vaul-svelte": "^1.0.0-next.7",
"vite": "^6.3.5",
"vite-plugin-node-polyfills": "^0.23.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@docsearch/js": "^3.6.2", "@docsearch/js": "^3.9.0",
"@internationalized/date": "^3.5.5", "@internationalized/date": "^3.8.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mapbox/sphericalmercator": "^1.2.0", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^1.0.2", "@mapbox/tilebelt": "^2.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__sphericalmercator": "^1.2.3",
"bits-ui": "^0.21.15", "chart.js": "^4.4.9",
"chart.js": "^4.4.4", "chartjs-plugin-zoom": "^2.2.0",
"chartjs-plugin-zoom": "^2.0.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dexie": "^4.0.8", "dexie": "^4.0.11",
"file-saver": "^2.0.5",
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"lucide-static": "^0.427.0", "jszip": "^3.10.1",
"lucide-svelte": "^0.427.0", "mapbox-gl": "^3.12.0",
"mapbox-gl": "^3.7.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"mode-watcher": "^0.3.1",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.13.0", "sanitize-html": "^2.17.0",
"sortablejs": "^1.15.3", "sortablejs": "^1.15.6",
"svelte-i18n": "^4.0.0", "tailwind-merge": "^3.3.0"
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.2",
"tailwind-variants": "^0.2.1"
} }
} }
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+124
View File
@@ -0,0 +1,124 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: hsl(0 0% 100%) /* <- Wrap in HSL */;
--foreground: hsl(240 10% 3.9%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 10% 3.9%);
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(220 15 130);
--link: rgb(0 110 180);
--radius: 0.5rem;
}
.dark {
--background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 4.9% 83.9%);
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(255 110 190);
--link: rgb(80 190 255);
}
@theme inline {
/* Radius (for rounded-*) */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Colors */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-support: var(--support);
--color-link: var(--link);
--breakpoint-xs: 540px;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+6 -7
View File
@@ -1,15 +1,14 @@
<!doctype html> <!doctype html>
<html> <html>
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>
-86
View File
@@ -1,86 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 45%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 92%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--support: 220 15 130;
--link: 0 110 180;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 30%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--support: 255 110 190;
--link: 80 190 255;
--ring: hsl(212.7,26.8%,83.9);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+6 -17
View File
@@ -38,31 +38,20 @@ export async function handle({ event, resolve }) {
<meta name="twitter:url" content="https://gpx.studio/" /> <meta name="twitter:url" content="https://gpx.studio/" />
<meta name="twitter:site" content="@gpxstudio" /> <meta name="twitter:site" content="@gpxstudio" />
<meta name="twitter:creator" content="@gpxstudio" /> <meta name="twitter:creator" content="@gpxstudio" />
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />`; <link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />
<link rel="manifest" href="/${language}.manifest.webmanifest" />`;
if (page !== '404') {
for (let lang of Object.keys(languages)) { for (let lang of Object.keys(languages)) {
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" /> headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" />
`; `;
} }
}
const stringsHTML = page === 'app' ? stringsToHTML(strings) : '';
const response = await resolve(event, { const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag).replace('</body>', `<div class="fixed -z-10 text-transparent">${stringsHTML}</div></body>`) transformPageChunk: ({ html }) =>
html.replace('<html>', htmlTag).replace('<head>', headTag),
}); });
return response; return response;
} }
function stringsToHTML(dictionary, strings = new Set(), root = true) {
Object.values(dictionary).forEach((value) => {
if (typeof value === 'object') {
stringsToHTML(value, strings, false);
} else {
strings.add(value);
}
});
if (root) {
return Array.from(strings).map((string) => `<p>${string}</p>`).join('');
}
}
+171
View File
@@ -0,0 +1,171 @@
export const surfaceColors: { [key: string]: string } = {
missing: '#d1d1d1',
paved: '#8c8c8c',
unpaved: '#6b443a',
asphalt: '#8c8c8c',
concrete: '#8c8c8c',
cobblestone: '#ffd991',
paving_stones: '#8c8c8c',
sett: '#ffd991',
metal: '#8c8c8c',
wood: '#6b443a',
compacted: '#ffffa8',
fine_gravel: '#ffffa8',
gravel: '#ffffa8',
pebblestone: '#ffffa8',
rock: '#ffd991',
dirt: '#ffffa8',
ground: '#6b443a',
earth: '#6b443a',
mud: '#6b443a',
sand: '#ffffc4',
grass: '#61b55c',
grass_paver: '#61b55c',
clay: '#6b443a',
stone: '#ffd991',
};
export function getSurfaceColor(surface: string): string {
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
}
export const highwayColors: { [key: string]: string } = {
missing: '#d1d1d1',
motorway: '#ff4d33',
motorway_link: '#ff4d33',
trunk: '#ff5e4d',
trunk_link: '#ff947f',
primary: '#ff6e5c',
primary_link: '#ff6e5c',
secondary: '#ff8d7b',
secondary_link: '#ff8d7b',
tertiary: '#ffd75f',
tertiary_link: '#ffd75f',
unclassified: '#f1f2a5',
road: '#f1f2a5',
residential: '#73b2ff',
living_street: '#73b2ff',
service: '#9c9cd9',
track: '#a8e381',
footway: '#a8e381',
path: '#a8e381',
pedestrian: '#a8e381',
cycleway: '#9de2ff',
construction: '#e09a4a',
bridleway: '#946f43',
raceway: '#ff0000',
rest_area: '#9c9cd9',
services: '#9c9cd9',
corridor: '#474747',
elevator: '#474747',
steps: '#474747',
bus_stop: '#8545a3',
busway: '#8545a3',
via_ferrata: '#474747',
};
export const sacScaleColors: { [key: string]: string } = {
hiking: '#007700',
mountain_hiking: '#1843ad',
demanding_mountain_hiking: '#ffff00',
alpine_hiking: '#ff9233',
demanding_alpine_hiking: '#ff0000',
difficult_alpine_hiking: '#000000',
};
export const mtbScaleColors: { [key: string]: string } = {
'0-': '#007700',
'0': '#007700',
'0+': '#007700',
'1-': '#1843ad',
'1': '#1843ad',
'1+': '#1843ad',
'2-': '#ffff00',
'2': '#ffff00',
'2+': '#ffff00',
'3': '#ff0000',
'4': '#00ff00',
'5': '#000000',
'6': '#b105eb',
};
function createPattern(
backgroundColor: string,
sacScaleColor: string | undefined,
mtbScaleColor: string | undefined,
size: number = 16,
lineWidth: number = 4
) {
let canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
let ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, size, size);
ctx.lineWidth = lineWidth;
const halfSize = size / 2;
const halfLineWidth = lineWidth / 2;
if (sacScaleColor) {
ctx.strokeStyle = sacScaleColor;
ctx.beginPath();
ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
ctx.stroke();
}
if (mtbScaleColor) {
ctx.strokeStyle = mtbScaleColor;
ctx.beginPath();
ctx.moveTo(halfSize - halfLineWidth, size + halfLineWidth);
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
ctx.stroke();
}
}
return ctx?.createPattern(canvas, 'repeat') || backgroundColor;
}
const patterns: Record<string, string | CanvasPattern> = {};
export function getHighwayColor(
highway: string,
sacScale: string | undefined,
mtbScale: string | undefined
) {
let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
if (sacScale || mtbScale) {
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter((x) => x).join('-')}`;
if (!patterns[patternId]) {
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
}
return patterns[patternId];
}
return backgroundColor;
}
const maxSlope = 20;
export function getSlopeColor(slope: number): string {
if (slope > maxSlope) {
slope = maxSlope;
} else if (slope < -maxSlope) {
slope = -maxSlope;
}
let v = slope / maxSlope;
v = 1 / (1 + Math.exp(-6 * v));
v = v - 0.5;
let hue = ((0.5 - v) * 120).toString(10);
let lightness = 90 - Math.abs(v) * 70;
return `hsl(${hue},70%,${lightness}%)`;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
export const surfaceColors: { [key: string]: string } = {
'missing': '#d1d1d1',
'paved': '#8c8c8c',
'unpaved': '#6b443a',
'asphalt': '#8c8c8c',
'concrete': '#8c8c8c',
'chipseal': '#8c8c8c',
'cobblestone': '#ffd991',
'unhewn_cobblestone': '#ffd991',
'paving_stones': '#8c8c8c',
'stepping_stones': '#c7b2db',
'sett': '#ffd991',
'metal': '#8c8c8c',
'wood': '#6b443a',
'compacted': '#ffffa8',
'fine_gravel': '#ffffa8',
'gravel': '#ffffa8',
'pebblestone': '#ffffa8',
'rock': '#ffd991',
'dirt': '#ffffa8',
'ground': '#6b443a',
'earth': '#6b443a',
'snow': '#bdfffc',
'ice': '#bdfffc',
'salt': '#b6c0f2',
'mud': '#6b443a',
'sand': '#ffffc4',
'woodchips': '#6b443a',
'grass': '#61b55c',
'grass_paver': '#61b55c'
}
+85 -11
View File
@@ -1,10 +1,72 @@
import { Landmark, Icon, Shell, Bike, Building, Tent, Car, Wrench, ShoppingBasket, Droplet, DoorOpen, Trees, Fuel, Home, Info, TreeDeciduous, CircleParking, Cross, Utensils, Construction, BrickWall, ShowerHead, Mountain, Phone, TrainFront, Bed, Binoculars, TriangleAlert, Anchor } from "lucide-svelte"; import {
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg } from "lucide-static"; Landmark,
import type { ComponentType } from "svelte"; Icon,
Shell,
Bike,
Building,
Tent,
Car,
Wrench,
ShoppingBasket,
Droplet,
DoorOpen,
Trees,
Fuel,
House,
Info,
TreeDeciduous,
CircleParking,
Cross,
Utensils,
Construction,
BrickWall,
ShowerHead,
Mountain,
Phone,
TrainFront,
Bed,
Binoculars,
TriangleAlert,
Anchor,
Toilet,
type IconProps,
} from '@lucide/svelte';
import {
Landmark as LandmarkSvg,
Shell as ShellSvg,
Bike as BikeSvg,
Building as BuildingSvg,
Tent as TentSvg,
Car as CarSvg,
Wrench as WrenchSvg,
ShoppingBasket as ShoppingBasketSvg,
Droplet as DropletSvg,
DoorOpen as DoorOpenSvg,
Trees as TreesSvg,
Fuel as FuelSvg,
House as HouseSvg,
Info as InfoSvg,
TreeDeciduous as TreeDeciduousSvg,
CircleParking as CircleParkingSvg,
Cross as CrossSvg,
Utensils as UtensilsSvg,
Construction as ConstructionSvg,
BrickWall as BrickWallSvg,
ShowerHead as ShowerHeadSvg,
Mountain as MountainSvg,
Phone as PhoneSvg,
TrainFront as TrainFrontSvg,
Bed as BedSvg,
Binoculars as BinocularsSvg,
TriangleAlert as TriangleAlertSvg,
Anchor as AnchorSvg,
Toilet as ToiletSvg,
} from 'lucide-static';
import type { Component } from 'svelte';
export type Symbol = { export type Symbol = {
value: string; value: string;
icon?: ComponentType<Icon>; icon?: Component<IconProps>;
iconSvg?: string; iconSvg?: string;
}; };
@@ -20,18 +82,30 @@ export const symbols: { [key: string]: Symbol } = {
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg }, campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
car: { value: 'Car', icon: Car, iconSvg: CarSvg }, car: { value: 'Car', icon: Car, iconSvg: CarSvg },
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg }, car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg }, convenience_store: {
value: 'Convenience Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
crossing: { value: 'Crossing' }, crossing: { value: 'Crossing' },
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg }, department_store: {
value: 'Department Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg }, drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg }, exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg }, lodge: { value: 'Lodge', icon: House, iconSvg: HouseSvg },
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg }, lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg }, forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg }, gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg }, ground_transportation: {
value: 'Ground Transportation',
icon: TrainFront,
iconSvg: TrainFrontSvg,
},
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg }, hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
house: { value: 'House', icon: Home, iconSvg: HomeSvg }, house: { value: 'House', icon: House, iconSvg: HouseSvg },
information: { value: 'Information', icon: Info, iconSvg: InfoSvg }, information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg }, park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg },
parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg }, parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg },
@@ -39,7 +113,7 @@ export const symbols: { [key: string]: Symbol } = {
picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg }, picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg }, restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg }, restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg },
restroom: { value: 'Restroom' }, restroom: { value: 'Restroom', icon: Toilet, iconSvg: ToiletSvg },
road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg }, road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg }, scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg }, shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
@@ -55,6 +129,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} else { } else {
return Object.keys(symbols).find(key => symbols[key].value === value); return Object.keys(symbols).find((key) => symbols[key].value === value);
} }
} }
@@ -2,7 +2,11 @@
import docsearch from '@docsearch/js'; import docsearch from '@docsearch/js';
import '@docsearch/css'; import '@docsearch/css';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { _, locale, waitLocale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
let props: {
class?: string;
} = $props();
let mounted = false; let mounted = false;
@@ -13,34 +17,34 @@
indexName: 'gpx', indexName: 'gpx',
container: '#docsearch', container: '#docsearch',
searchParameters: { searchParameters: {
facetFilters: ['lang:' + ($locale ?? 'en')] facetFilters: ['lang:' + i18n.lang],
}, },
placeholder: $_('docs.search.search'), placeholder: i18n._('docs.search.search'),
disableUserPersonalization: true, disableUserPersonalization: true,
translations: { translations: {
button: { button: {
buttonText: $_('docs.search.search'), buttonText: i18n._('docs.search.search'),
buttonAriaLabel: $_('docs.search.search') buttonAriaLabel: i18n._('docs.search.search'),
}, },
modal: { modal: {
searchBox: { searchBox: {
resetButtonTitle: $_('docs.search.clear'), resetButtonTitle: i18n._('docs.search.clear'),
resetButtonAriaLabel: $_('docs.search.clear'), resetButtonAriaLabel: i18n._('docs.search.clear'),
cancelButtonText: $_('docs.search.cancel'), cancelButtonText: i18n._('docs.search.cancel'),
cancelButtonAriaLabel: $_('docs.search.cancel'), cancelButtonAriaLabel: i18n._('docs.search.cancel'),
searchInputLabel: $_('docs.search.search') searchInputLabel: i18n._('docs.search.search'),
}, },
footer: { footer: {
selectText: $_('docs.search.to_select'), selectText: i18n._('docs.search.to_select'),
navigateText: $_('docs.search.to_navigate'), navigateText: i18n._('docs.search.to_navigate'),
closeText: $_('docs.search.to_close') closeText: i18n._('docs.search.to_close'),
}, },
noResultsScreen: { noResultsScreen: {
noResultsText: $_('docs.search.no_results'), noResultsText: i18n._('docs.search.no_results'),
suggestedQueryText: $_('docs.search.no_results_suggestion') suggestedQueryText: i18n._('docs.search.no_results_suggestion'),
} },
} },
} },
}); });
} }
@@ -48,13 +52,15 @@
mounted = true; mounted = true;
}); });
$: if (mounted && $locale) { $effect(() => {
waitLocale().then(initDocsearch); if (mounted && i18n.lang && !i18n.isLoading) {
initDocsearch();
} }
});
</script> </script>
<svelte:head> <svelte:head>
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin /> <link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
</svelte:head> </svelte:head>
<div id="docsearch" {...$$restProps}></div> <div id="docsearch" class={props.class ?? ''}></div>
@@ -1,26 +1,38 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Snippet } from 'svelte';
export let variant: const {
| 'default' variant = 'default',
| 'secondary' label,
| 'link' side = 'top',
| 'destructive' disabled = false,
| 'outline' class: className = '',
| 'ghost' children,
| undefined = 'default'; onclick,
export let label: string; }: {
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top'; variant?: 'default' | 'secondary' | 'link' | 'destructive' | 'outline' | 'ghost';
label: string;
side?: 'top' | 'right' | 'bottom' | 'left';
disabled?: boolean;
class?: string;
children: Snippet;
onclick?: (event: MouseEvent) => void;
} = $props();
</script> </script>
<Tooltip.Root> <Tooltip.Provider>
<Tooltip.Trigger asChild let:builder> <Tooltip.Root>
<Button builders={[builder]} {variant} {...$$restProps}> <Tooltip.Trigger>
<slot /> {#snippet child({ props })}
<Button {...props} {variant} class={className} {onclick}>
{@render children()}
</Button> </Button>
{/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content {side}> <Tooltip.Content {side}>
<span>{label}</span> <span>{label}</span>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider>
@@ -1,685 +0,0 @@
<script lang="ts">
import * as ToggleGroup from '$lib/components/ui/toggle-group';
import Tooltip from '$lib/components/Tooltip.svelte';
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import {
BrickWall,
TriangleRight,
HeartPulse,
Orbit,
SquareActivity,
Thermometer,
Zap
} from 'lucide-svelte';
import { surfaceColors } from '$lib/assets/surfaces';
import { _, locale } from 'svelte-i18n';
import {
getCadenceUnits,
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
getConvertedTemperature,
getConvertedVelocity,
getDistanceUnits,
getDistanceWithUnits,
getElevationWithUnits,
getHeartRateUnits,
getHeartRateWithUnits,
getPowerUnits,
getPowerWithUnits,
getTemperatureUnits,
getTemperatureWithUnits,
getVelocityUnits,
getVelocityWithUnits,
secondsToHHMMSS
} from '$lib/units';
import type { Writable } from 'svelte/store';
import { DateFormatter } from '@internationalized/date';
import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/db';
import { mode } from 'mode-watcher';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let panelSize: number;
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined;
export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
let df: DateFormatter;
$: if ($locale) {
df = new DateFormatter($locale, {
dateStyle: 'medium',
timeStyle: 'medium'
});
}
let canvas: HTMLCanvasElement;
let showAdditionalScales = true;
let updateShowAdditionalScales = () => {
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
};
let overlay: HTMLCanvasElement;
let chart: Chart;
Chart.defaults.font.family =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
let marker: mapboxgl.Marker | null = null;
let dragging = false;
let panning = false;
let options = {
animation: false,
parsing: false,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
ticks: {
callback: function (value: number, index: number, ticks: { value: number }[]) {
if (index === ticks.length - 1) {
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
}
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
}
}
},
y: {
type: 'linear',
ticks: {
callback: function (value: number) {
return getElevationWithUnits(value, false);
}
}
}
},
datasets: {
line: {
pointRadius: 0,
tension: 0.4,
borderWidth: 2,
cubicInterpolationMode: 'monotone'
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
plugins: {
legend: {
display: false
},
decimation: {
enabled: true
},
tooltip: {
enabled: () => !dragging && !panning,
callbacks: {
title: function () {
return '';
},
label: function (context: Chart.TooltipContext) {
let point = context.raw;
if (context.datasetIndex === 0) {
if ($map && marker) {
if (dragging) {
marker.remove();
} else {
marker.setLngLat(point.coordinates);
marker.addTo($map);
}
}
return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) {
return `${$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) {
return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) {
return `${$_('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
} else if (context.datasetIndex === 4) {
return `${$_('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 5) {
return `${$_('quantities.power')}: ${getPowerWithUnits(point.y)}`;
}
},
afterBody: function (contexts: Chart.TooltipContext[]) {
let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return;
let point = context[0].raw;
let slope = {
at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length)
};
let surface = point.surface ? point.surface : 'unknown';
let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`
];
if (elevationFill === 'surface') {
labels.push(
` ${$_('quantities.surface')}: ${$_(`toolbar.routing.surface.${surface}`)}`
);
}
if (point.time) {
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
}
return labels;
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'shift',
onPanStart: function () {
// hide tooltip
panning = true;
$slicedGPXStatistics = undefined;
},
onPanComplete: function () {
panning = false;
}
},
zoom: {
wheel: {
enabled: true
},
mode: 'x',
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
if (
event.deltaY < 0 &&
Math.abs(
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange -
chart.getZoomLevel()
) < 0.01
) {
// Disable wheel pan if zoomed in to the max, and zooming in
return false;
}
$slicedGPXStatistics = undefined;
}
},
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1
}
}
}
},
stacked: false,
onResize: function () {
updateOverlay();
updateShowAdditionalScales();
}
};
let datasets: {
[key: string]: {
id: string;
getLabel: () => string;
getUnits: () => string;
};
} = {
speed: {
id: 'speed',
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
getUnits: () => getVelocityUnits()
},
hr: {
id: 'hr',
getLabel: () => $_('quantities.heartrate'),
getUnits: () => getHeartRateUnits()
},
cad: {
id: 'cad',
getLabel: () => $_('quantities.cadence'),
getUnits: () => getCadenceUnits()
},
atemp: {
id: 'atemp',
getLabel: () => $_('quantities.temperature'),
getUnits: () => getTemperatureUnits()
},
power: {
id: 'power',
getLabel: () => $_('quantities.power'),
getUnits: () => getPowerUnits()
}
};
for (let [id, dataset] of Object.entries(datasets)) {
options.scales[`y${id}`] = {
type: 'linear',
position: 'right',
title: {
display: true,
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
padding: {
top: 6,
bottom: 0
}
},
grid: {
display: false
},
reverse: () => id === 'speed' && $velocityUnits === 'pace',
display: false
};
}
options.scales.yspeed['ticks'] = {
callback: function (value: number) {
if ($velocityUnits === 'speed') {
return value;
} else {
return secondsToHHMMSS(value);
}
}
};
onMount(async () => {
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
chart = new Chart(canvas, {
type: 'line',
data: {
datasets: []
},
options,
plugins: [
{
id: 'toggleMarker',
events: ['mouseout'],
afterEvent: function (chart: Chart, args: { event: Chart.ChartEvent }) {
if (args.event.type === 'mouseout') {
if ($map && marker) {
marker.remove();
}
}
}
}
]
});
// Map marker to show on hover
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
marker = new mapboxgl.Marker({
element
});
updateShowAdditionalScales();
let startIndex = 0;
let endIndex = 0;
function getIndex(evt) {
const points = chart.getElementsAtEventForMode(
evt,
'x',
{
intersect: false
},
true
);
if (points.length === 0) {
const rect = canvas.getBoundingClientRect();
if (evt.x - rect.left <= chart.chartArea.left) {
return 0;
} else if (evt.x - rect.left >= chart.chartArea.right) {
return $gpxStatistics.local.points.length - 1;
} else {
return undefined;
}
}
let point = points.find((point) => point.element.raw);
if (point) {
return point.element.raw.index;
} else {
return points[0].index;
}
}
let dragStarted = false;
function onMouseDown(evt) {
if (evt.shiftKey) {
// Panning interaction
return;
}
dragStarted = true;
canvas.style.cursor = 'col-resize';
startIndex = getIndex(evt);
}
function onMouseMove(evt) {
if (dragStarted) {
dragging = true;
endIndex = getIndex(evt);
if (endIndex !== undefined) {
if (startIndex === undefined) {
startIndex = endIndex;
} else if (startIndex !== endIndex) {
$slicedGPXStatistics = [
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
];
}
}
}
}
function onMouseUp(evt) {
dragStarted = false;
dragging = false;
canvas.style.cursor = '';
endIndex = getIndex(evt);
if (startIndex === endIndex) {
$slicedGPXStatistics = undefined;
}
}
canvas.addEventListener('pointerdown', onMouseDown);
canvas.addEventListener('pointermove', onMouseMove);
canvas.addEventListener('pointerup', onMouseUp);
});
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
let data = $gpxStatistics;
// update data
chart.data.datasets[0] = {
label: $_('quantities.elevation'),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index]
},
surface: point.getSurface(),
coordinates: point.getCoordinates(),
index: index
};
}),
normalized: true,
fill: 'start',
order: 1
};
chart.data.datasets[1] = {
label: datasets.speed.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.speed.id}`,
hidden: true
};
chart.data.datasets[2] = {
label: datasets.hr.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.hr.id}`,
hidden: true
};
chart.data.datasets[3] = {
label: datasets.cad.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.cad.id}`,
hidden: true
};
chart.data.datasets[4] = {
label: datasets.atemp.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.atemp.id}`,
hidden: true
};
chart.data.datasets[5] = {
label: datasets.power.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.power.id}`,
hidden: true
};
chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
// update units
for (let [id, dataset] of Object.entries(datasets)) {
chart.options.scales[`y${id}`].title.text =
dataset.getLabel() + ' (' + dataset.getUnits() + ')';
}
chart.update();
}
let maxSlope = 20;
function slopeFillCallback(context) {
let slope = context.p0.raw.slope.segment;
if (slope > maxSlope) {
slope = maxSlope;
} else if (slope < -maxSlope) {
slope = -maxSlope;
}
let v = slope / maxSlope;
v = 1 / (1 + Math.exp(-6 * v));
v = v - 0.5;
let hue = ((0.5 - v) * 120).toString(10);
let lightness = 90 - Math.abs(v) * 70;
return ['hsl(', hue, ',70%,', lightness, '%)'].join('');
}
function surfaceFillCallback(context) {
let surface = context.p0.raw.surface;
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
}
$: if (chart) {
if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback
};
} else if (elevationFill === 'surface') {
chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback
};
} else {
chart.data.datasets[0]['segment'] = {};
}
chart.update();
}
$: if (additionalDatasets && chart) {
let includeSpeed = additionalDatasets.includes('speed');
let includeHeartRate = additionalDatasets.includes('hr');
let includeCadence = additionalDatasets.includes('cad');
let includeTemperature = additionalDatasets.includes('atemp');
let includePower = additionalDatasets.includes('power');
if (chart.data.datasets.length > 0) {
chart.data.datasets[1].hidden = !includeSpeed;
chart.data.datasets[2].hidden = !includeHeartRate;
chart.data.datasets[3].hidden = !includeCadence;
chart.data.datasets[4].hidden = !includeTemperature;
chart.data.datasets[5].hidden = !includePower;
}
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
chart.options.scales[`y${datasets.atemp.id}`].display =
includeTemperature && showAdditionalScales;
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
chart.update();
}
function updateOverlay() {
if (!canvas) {
return;
}
overlay.width = canvas.width / window.devicePixelRatio;
overlay.height = canvas.height / window.devicePixelRatio;
if ($slicedGPXStatistics) {
let startIndex = $slicedGPXStatistics[1];
let endIndex = $slicedGPXStatistics[2];
// Draw selection rectangle
let selectionContext = overlay.getContext('2d');
if (selectionContext) {
selectionContext.fillStyle = $mode === 'dark' ? 'white' : 'black';
selectionContext.globalAlpha = $mode === 'dark' ? 0.2 : 0.1;
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
let startPixel = chart.scales.x.getPixelForValue(
getConvertedDistance($gpxStatistics.local.distance.total[startIndex])
);
let endPixel = chart.scales.x.getPixelForValue(
getConvertedDistance($gpxStatistics.local.distance.total[endIndex])
);
selectionContext.fillRect(
startPixel,
chart.chartArea.top,
endPixel - startPixel,
chart.chartArea.bottom - chart.chartArea.top
);
}
} else if (overlay) {
let selectionContext = overlay.getContext('2d');
if (selectionContext) {
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
}
}
}
$: $slicedGPXStatistics, $mode, updateOverlay();
onDestroy(() => {
if (chart) {
chart.destroy();
}
});
</script>
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
<div class="grow h-full min-w-0 relative">
<canvas bind:this={overlay} class=" w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full"></canvas>
</div>
{#if showControls}
<div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
type="single"
bind:value={elevationFill}
>
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope" aria-label={$_('chart.show_slope')}>
<Tooltip side="left" label={$_('chart.show_slope')}>
<TriangleRight size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface" aria-label={$_('chart.show_surface')}>
<Tooltip side="left" label={$_('chart.show_surface')}>
<BrickWall size="15" />
</Tooltip>
</ToggleGroup.Item>
</ToggleGroup.Root>
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
type="multiple"
bind:value={additionalDatasets}
>
<ToggleGroup.Item
class="p-0 w-5 h-5"
value="speed"
aria-label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
>
<Tooltip
side="left"
label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
>
<Zap size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr" aria-label={$_('chart.show_heartrate')}>
<Tooltip side="left" label={$_('chart.show_heartrate')}>
<HeartPulse size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad" aria-label={$_('chart.show_cadence')}>
<Tooltip side="left" label={$_('chart.show_cadence')}>
<Orbit size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 w-5 h-5"
value="atemp"
aria-label={$_('chart.show_temperature')}
>
<Tooltip side="left" label={$_('chart.show_temperature')}>
<Thermometer size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="power" aria-label={$_('chart.show_power')}>
<Tooltip side="left" label={$_('chart.show_power')}>
<SquareActivity size="15" />
</Tooltip>
</ToggleGroup.Item>
</ToggleGroup.Root>
</div>
{/if}
</div>
-186
View File
@@ -1,186 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Separator } from '$lib/components/ui/separator';
import { Dialog } from 'bits-ui';
import {
currentTool,
exportAllFiles,
exportSelectedFiles,
ExportState,
exportState,
gpxStatistics
} from '$lib/stores';
import { fileObservers } from '$lib/db';
import {
Download,
Zap,
BrickWall,
HeartPulse,
Orbit,
Thermometer,
SquareActivity
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { selection } from './file-list/Selection';
import { get } from 'svelte/store';
import { GPXStatistics } from 'gpx';
import { ListRootItem } from './file-list/FileList';
let open = false;
let exportOptions: Record<string, boolean> = {
time: true,
surface: true,
hr: true,
cad: true,
atemp: true,
power: true
};
let hide: Record<string, boolean> = {
time: false,
surface: false,
hr: false,
cad: false,
atemp: false,
power: false
};
$: if ($exportState !== ExportState.NONE) {
open = true;
$currentTool = null;
let statistics = $gpxStatistics;
if ($exportState === ExportState.ALL) {
statistics = Array.from($fileObservers.values())
.map((file) => get(file)?.statistics)
.reduce((acc, cur) => {
if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
}
return acc;
}, new GPXStatistics());
}
hide.time = statistics.global.time.total === 0;
hide.surface = !Object.keys(statistics.global.surface).some((key) => key !== 'unknown');
hide.hr = statistics.global.hr.count === 0;
hide.cad = statistics.global.cad.count === 0;
hide.atemp = statistics.global.atemp.count === 0;
hide.power = statistics.global.power.count === 0;
}
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
</script>
<Dialog.Root
bind:open
onOpenChange={(isOpen) => {
if (!isOpen) {
$exportState = ExportState.NONE;
}
}}
>
<Dialog.Trigger class="hidden" />
<Dialog.Portal>
<Dialog.Content
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
>
<div
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
>
<span>⚠️</span>
<span class="max-w-[80%] text-sm">
{$_('menu.support_message')}
</span>
</div>
<div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{$_('menu.support_button')}
<span class="ml-2">🙏</span>
</Button>
<Button
variant="outline"
class="grow"
on:click={() => {
if ($exportState === ExportState.SELECTION) {
exportSelectedFiles(exclude);
} else if ($exportState === ExportState.ALL) {
exportAllFiles(exclude);
}
open = false;
$exportState = ExportState.NONE;
}}
>
<Download size="16" class="mr-1" />
{#if $fileObservers.size === 1 || ($exportState === ExportState.SELECTION && $selection.size === 1)}
{$_('menu.download_file')}
{:else}
{$_('menu.download_files')}
{/if}
</Button>
</div>
<div
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some((v) => !v)
? ''
: 'hidden'}"
>
<div class="w-full flex flex-row items-center gap-3">
<div class="grow">
<Separator />
</div>
<Label class="shrink-0">
{$_('menu.export_options')}
</Label>
<div class="grow">
<Separator />
</div>
</div>
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
<Checkbox id="export-time" bind:checked={exportOptions.time} />
<Label for="export-time" class="flex flex-row items-center gap-1">
<Zap size="16" />
{$_('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
<Label for="export-surface" class="flex flex-row items-center gap-1">
<BrickWall size="16" />
{$_('quantities.surface')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{$_('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
<Label for="export-cadence" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{$_('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
<Label for="export-temperature" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{$_('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
<Checkbox id="export-power" bind:checked={exportOptions.power} />
<Label for="export-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{$_('quantities.power')}
</Label>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
+40 -40
View File
@@ -2,8 +2,8 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import LanguageSelect from '$lib/components/LanguageSelect.svelte'; import LanguageSelect from '$lib/components/LanguageSelect.svelte';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte'; import { AtSign, BookOpenText, Heart, House, Map } from '@lucide/svelte';
import { _, locale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
</script> </script>
@@ -14,109 +14,109 @@
<Logo class="h-8" width="153" /> <Logo class="h-8" width="153" />
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE" href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank" target="_blank"
> >
MIT © 2024 gpx.studio MIT © 2025 gpx.studio
</Button> </Button>
<LanguageSelect class="w-40 mt-3" /> <LanguageSelect class="w-40 mt-3" />
</div> </div>
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6"> <div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
<div class="flex flex-col items-start gap-1"> <div class="flex flex-col items-start gap-1">
<span class="font-semibold">{$_('homepage.website')}</span> <span class="font-semibold">{i18n._('homepage.website')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/')} href={getURLForLanguage(i18n.lang, '/')}
> >
<Home size="16" class="mr-1" /> <House size="16" />
{$_('homepage.home')} {i18n._('homepage.home')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/app')} href={getURLForLanguage(i18n.lang, '/app')}
> >
<Map size="16" class="mr-1" /> <Map size="16" />
{$_('homepage.app')} {i18n._('homepage.app')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/help')} href={getURLForLanguage(i18n.lang, '/help')}
> >
<BookOpenText size="16" class="mr-1" /> <BookOpenText size="16" />
{$_('menu.help')} {i18n._('menu.help')}
</Button> </Button>
</div> </div>
<div class="flex flex-col items-start gap-1" id="contact"> <div class="flex flex-col items-start gap-1" id="contact">
<span class="font-semibold">{$_('homepage.contact')}</span> <span class="font-semibold">{i18n._('homepage.contact')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://www.reddit.com/r/gpxstudio/" href="https://www.reddit.com/r/gpxstudio/"
target="_blank" target="_blank"
> >
<Logo company="reddit" class="h-4 mr-1 fill-muted-foreground" /> <Logo company="reddit" class="h-4 fill-muted-foreground" />
{$_('homepage.reddit')} {i18n._('homepage.reddit')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://facebook.com/gpx.studio" href="https://facebook.com/gpx.studio"
target="_blank" target="_blank"
> >
<Logo company="facebook" class="h-4 mr-1 fill-muted-foreground" /> <Logo company="facebook" class="h-4 fill-muted-foreground" />
{$_('homepage.facebook')} {i18n._('homepage.facebook')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://x.com/gpxstudio" href="https://x.com/gpxstudio"
target="_blank" target="_blank"
> >
<Logo company="x" class="h-4 mr-1 fill-muted-foreground" /> <Logo company="x" class="h-4 fill-muted-foreground" />
{$_('homepage.x')} {i18n._('homepage.x')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="mailto:hello@gpx.studio" href="mailto:hello@gpx.studio"
target="_blank" target="_blank"
> >
<AtSign size="16" class="mr-1" /> <AtSign size="16" />
{$_('homepage.email')} {i18n._('homepage.email')}
</Button> </Button>
</div> </div>
<div class="flex flex-col items-start gap-1"> <div class="flex flex-col items-start gap-1">
<span class="font-semibold">{$_('homepage.contribute')}</span> <span class="font-semibold">{i18n._('homepage.contribute')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://ko-fi.com/gpxstudio" href="https://ko-fi.com/gpxstudio"
target="_blank" target="_blank"
> >
<Heart size="16" class="mr-1" /> <Heart size="16" />
{$_('menu.donate')} {i18n._('menu.donate')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://crowdin.com/project/gpxstudio" href="https://crowdin.com/project/gpxstudio"
target="_blank" target="_blank"
> >
<Logo company="crowdin" class="h-4 mr-1 fill-muted-foreground" /> <Logo company="crowdin" class="h-4 fill-muted-foreground" />
{$_('homepage.crowdin')} {i18n._('homepage.crowdin')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio" href="https://github.com/gpxstudio/gpx.studio"
target="_blank" target="_blank"
> >
<Logo company="github" class="h-4 mr-1 fill-muted-foreground" /> <Logo company="github" class="h-4 fill-muted-foreground" />
{$_('homepage.github')} {i18n._('homepage.github')}
</Button> </Button>
</div> </div>
</div> </div>
+40 -29
View File
@@ -3,63 +3,72 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import WithUnits from '$lib/components/WithUnits.svelte'; import WithUnits from '$lib/components/WithUnits.svelte';
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte'; import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import type { GPXStatistics } from 'gpx'; import type { GPXStatistics } from 'gpx';
import type { Writable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { settings } from '$lib/db'; import { settings } from '$lib/logic/settings';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let orientation: 'horizontal' | 'vertical';
export let panelSize: number;
const { velocityUnits } = settings; const { velocityUnits } = settings;
let statistics: GPXStatistics; let {
gpxStatistics,
slicedGPXStatistics,
orientation,
panelSize,
}: {
gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
orientation: 'horizontal' | 'vertical';
panelSize: number;
} = $props();
$: if ($slicedGPXStatistics !== undefined) { let statistics = $derived(
statistics = $slicedGPXStatistics[0]; $slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
} else { );
statistics = $gpxStatistics;
}
</script> </script>
<Card.Root <Card.Root
class="h-full {orientation === 'vertical' class="h-full {orientation === 'vertical'
? 'min-w-44 sm:min-w-52 text-sm sm:text-base' ? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
: 'w-full'} border-none shadow-none" : 'w-full'} border-none shadow-none p-0"
> >
<Card.Content <Card.Content
class="h-full flex {orientation === 'vertical' class="h-full flex {orientation === 'vertical'
? 'flex-col justify-center' ? 'flex-col justify-center'
: 'flex-row w-full justify-between'} gap-4 p-0" : 'flex-row w-full justify-between'} gap-4 p-0"
> >
<Tooltip label={$_('quantities.distance')}> <Tooltip label={i18n._('quantities.distance')}>
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Ruler size="18" class="mr-1" /> <Ruler size="16" class="mr-1" />
<WithUnits value={statistics.global.distance.total} type="distance" /> <WithUnits value={statistics.global.distance.total} type="distance" />
</span> </span>
</Tooltip> </Tooltip>
<Tooltip label={$_('quantities.elevation_gain_loss')}> <Tooltip label={i18n._('quantities.elevation_gain_loss')}>
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<MoveUpRight size="18" class="mr-1" /> <MoveUpRight size="16" class="mr-1" />
<WithUnits value={statistics.global.elevation.gain} type="elevation" /> <WithUnits value={statistics.global.elevation.gain} type="elevation" />
<MoveDownRight size="18" class="mx-1" /> <MoveDownRight size="16" class="mx-1" />
<WithUnits value={statistics.global.elevation.loss} type="elevation" /> <WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span> </span>
</Tooltip> </Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'} {#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip <Tooltip
class={orientation === 'horizontal' ? 'hidden xs:block' : ''} class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_( label="{$velocityUnits === 'speed'
'quantities.moving' ? i18n._('quantities.speed')
)} / {$_('quantities.total')})" : i18n._('quantities.pace')} ({i18n._('quantities.moving')} / {i18n._(
'quantities.total'
)})"
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Zap size="18" class="mr-1" /> <Zap size="16" class="mr-1" />
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} /> <WithUnits
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
@@ -68,10 +77,12 @@
{#if panelSize > 160 || orientation === 'horizontal'} {#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip <Tooltip
class={orientation === 'horizontal' ? 'hidden md:block' : ''} class={orientation === 'horizontal' ? 'hidden md:block' : ''}
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})" label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._(
'quantities.total'
)})"
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Timer size="18" class="mr-1" /> <Timer size="16" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" /> <WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" /> <WithUnits value={statistics.global.time.total} type="time" />
+16 -9
View File
@@ -1,19 +1,26 @@
<script lang="ts"> <script lang="ts">
import { CircleHelp } from 'lucide-svelte'; import { CircleQuestionMark } from '@lucide/svelte';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import type { Snippet } from 'svelte';
export let link: string | undefined = undefined; let {
link,
class: className = '',
children,
}: {
link: string;
class?: string;
children: Snippet;
} = $props();
</script> </script>
<div <div class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {className}">
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}" <CircleQuestionMark size="16" class="w-4 mr-2 shrink-0 grow-0" />
>
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div> <div>
<slot /> {@render children()}
{#if link} {#if link}
<a href={link} target="_blank" class="text-sm text-link hover:underline"> <a href={link} target="_blank" class="text-sm text-link hover:underline">
{$_('menu.more')} {i18n._('menu.more')}
</a> </a>
{/if} {/if}
</div> </div>
@@ -1,51 +1,36 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Languages } from 'lucide-svelte'; import { Languages } from '@lucide/svelte';
import { _, locale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
let selected = { let {
value: '', class: className = '',
label: '' }: {
}; class?: string;
} = $props();
$: if ($locale) {
selected = {
value: $locale,
label: languages[$locale]
};
}
</script> </script>
<Select.Root bind:selected> <Select.Root type="single" value={i18n.lang}>
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}> <Select.Trigger class="min-w-[180px] {className}" aria-label={i18n._('menu.language')}>
<Languages size="16" /> <Languages size="16" />
<Select.Value class="ml-2 mr-auto" /> <span class="mr-auto">
{languages[i18n.lang]}
</span>
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
{#if $page.url.pathname.includes('404')} {#if page.url.pathname.includes('404')}
<a href={getURLForLanguage(lang, '/')}> <a href={getURLForLanguage(lang, '/')}>
<Select.Item value={lang}>{label}</Select.Item> <Select.Item value={lang}>{label}</Select.Item>
</a> </a>
{:else} {:else}
<a href={getURLForLanguage(lang, $page.url.pathname)}> <a href={getURLForLanguage(lang, page.url.pathname)}>
<Select.Item value={lang}>{label}</Select.Item> <Select.Item value={lang}>{label}</Select.Item>
</a> </a>
{/if} {/if}
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<!-- hidden links for svelte crawling -->
<div class="hidden">
{#if !$page.url.pathname.includes('404')}
{#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang, $page.url.pathname)}>
{label}
</a>
{/each}
{/if}
</div>
+19 -14
View File
@@ -1,31 +1,36 @@
<script lang="ts"> <script lang="ts">
import { mode } from 'mode-watcher';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { mode, systemPrefersMode } from 'mode-watcher';
export let iconOnly = false; let {
export let company = 'gpx.studio'; iconOnly = false,
company = 'gpx.studio',
$: effectiveMode = $mode ?? $systemPrefersMode ?? 'light'; ...others
}: {
iconOnly?: boolean;
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
[key: string]: any;
} = $props();
</script> </script>
{#if company === 'gpx.studio'} {#if company === 'gpx.studio'}
<img <img
src="{base}/{iconOnly ? 'icon' : 'logo'}{effectiveMode === 'dark' ? '-dark' : ''}.svg" src="{base}/{iconOnly ? 'icon' : 'logo'}{mode.current === 'dark' ? '-dark' : ''}.svg"
alt="Logo of gpx.studio." alt="Logo of gpx.studio."
{...$$restProps} {...others}
/> />
{:else if company === 'mapbox'} {:else if company === 'mapbox'}
<img <img
src="{base}/mapbox-logo-{effectiveMode === 'dark' ? 'white' : 'black'}.svg" src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Mapbox." alt="Logo of Mapbox."
{...$$restProps} {...others}
/> />
{:else if company === 'github'} {:else if company === 'github'}
<svg <svg
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>GitHub</title><path ><title>GitHub</title><path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/></svg /></svg
@@ -35,7 +40,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Crowdin</title><path ><title>Crowdin</title><path
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z" d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
/></svg /></svg
@@ -45,7 +50,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Facebook</title><path ><title>Facebook</title><path
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z" d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
/></svg /></svg
@@ -55,7 +60,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>X</title><path ><title>X</title><path
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/></svg /></svg
@@ -65,7 +70,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Reddit</title><path ><title>Reddit</title><path
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
/></svg /></svg
-380
View File
@@ -1,380 +0,0 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { Button } from '$lib/components/ui/button';
import { map } from '$lib/stores';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { page } from '$app/stores';
export let accessToken = PUBLIC_MAPBOX_TOKEN;
export let geolocate = true;
export let geocoder = true;
export let hash = true;
mapboxgl.accessToken = accessToken;
let webgl2Supported = true;
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15,
linear: true,
easing: () => 1
};
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
settings;
let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits
});
onMount(() => {
let gl = document.createElement('canvas').getContext('webgl2');
if (!gl) {
webgl2Supported = false;
return;
}
let language = $page.params.language;
if (language === 'zh') {
language = 'zh-Hans';
} else if (language?.includes('-')) {
language = language.split('-')[0];
} else if (language === '' || language === undefined) {
language = 'en';
}
let newMap = new mapboxgl.Map({
container: 'map',
style: {
version: 8,
sources: {},
layers: [],
imports: [
{
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
url: '',
data: {
version: 8,
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
}
},
{
id: 'basemap',
url: ''
},
{
id: 'overlays',
url: '',
data: {
version: 8,
sources: {},
layers: []
}
}
]
},
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false
});
newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded
window._map = newMap; // entry point for extensions
scaleControl.setUnit($distanceUnits);
});
newMap.addControl(
new mapboxgl.AttributionControl({
compact: true
})
);
newMap.addControl(
new mapboxgl.NavigationControl({
visualizePitch: true
})
);
if (geocoder) {
let geocoder = new MapboxGeocoder({
mapboxgl: mapboxgl,
enableEventLogging: false,
collapsed: true,
flyTo: fitBoundsOptions,
language,
localGeocoder: () => [],
localGeocoderOnly: true,
externalGeocoder: (query: string) =>
fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
)
.then((response) => response.json())
.then((data) => {
return data.map((result: any) => {
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [result.lon, result.lat]
},
place_name: result.display_name
};
});
})
});
let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => {
// Trigger search on Enter key only
if (e.key === 'Enter') {
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
} else if (geocoder._typeahead.data.length > 0) {
geocoder._typeahead.clear();
}
};
newMap.addControl(geocoder);
}
if (geolocate) {
newMap.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
fitBoundsOptions,
trackUserLocation: true,
showUserHeading: true
})
);
}
newMap.addControl(scaleControl);
newMap.on('style.load', () => {
newMap.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14
});
if (newMap.getPitch() > 0) {
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: 1
});
}
newMap.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)'
});
newMap.on('pitch', () => {
if (newMap.getPitch() > 0) {
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: 1
});
} else {
newMap.setTerrain(null);
}
});
});
});
onDestroy(() => {
if ($map) {
$map.remove();
$map = null;
}
});
$: if (
$map &&
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
) {
$map.resize();
}
</script>
<div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
<div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
>
<p>{$_('webgl2_required')}</p>
<Button href="https://get.webgl.org/webgl2/" target="_blank">
{$_('enable_webgl2')}
</Button>
</div>
</div>
<style lang="postcss">
div :global(.mapboxgl-map) {
@apply font-sans;
}
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-icon) {
@apply dark:brightness-[4.7];
}
div :global(.mapboxgl-ctrl-geocoder) {
@apply flex;
@apply flex-row;
@apply w-fit;
@apply min-w-fit;
@apply items-center;
@apply shadow-md;
}
div :global(.suggestions) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
@apply text-foreground;
@apply hover:text-accent-foreground;
@apply hover:bg-accent;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
@apply bg-background;
}
div :global(.mapboxgl-ctrl-geocoder--button) {
@apply bg-transparent;
@apply hover:bg-transparent;
}
div :global(.mapboxgl-ctrl-geocoder--icon) {
@apply fill-foreground;
@apply hover:fill-accent-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
@apply relative;
@apply top-0;
@apply left-0;
@apply my-2;
@apply w-[29px];
}
div :global(.mapboxgl-ctrl-geocoder--input) {
@apply relative;
@apply w-64;
@apply py-0;
@apply pl-2;
@apply focus:outline-none;
@apply transition-[width];
@apply duration-200;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
@apply w-0;
@apply p-0;
}
div :global(.mapboxgl-ctrl-top-right) {
@apply z-40;
@apply flex;
@apply flex-col;
@apply items-end;
@apply h-full;
@apply overflow-hidden;
}
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px];
}
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px];
}
div :global(.mapboxgl-ctrl-attrib) {
@apply dark:bg-transparent;
}
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
@apply dark:bg-background;
}
div :global(.mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-ctrl-attrib a) {
@apply text-foreground;
}
div :global(.mapboxgl-popup) {
@apply w-fit;
@apply z-20;
}
div :global(.mapboxgl-popup-content) {
@apply p-0;
@apply bg-transparent;
@apply shadow-none;
}
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
@apply border-r-background;
}
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
@apply border-l-background;
}
</style>
+252 -211
View File
@@ -22,7 +22,7 @@
Sun, Sun,
Moon, Moon,
Layers, Layers,
GalleryVertical, ListTree,
Languages, Languages,
Settings, Settings,
Info, Info,
@@ -42,48 +42,43 @@
FileX, FileX,
BookOpenText, BookOpenText,
ChartArea, ChartArea,
Maximize Maximize,
} from 'lucide-svelte'; } from '@lucide/svelte';
import { map } from '$lib/components/map/map';
import { import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
map, import { editStyle } from '$lib/components/file-list/style/utils.svelte';
triggerFileInput, import { exportState, ExportState } from '$lib/components/export/utils.svelte';
createFile, import { anySelectedLayer } from '$lib/components/map/layer-control/utils';
loadFiles,
updateSelectionFromKey,
allHidden,
editMetadata,
editStyle,
exportState,
ExportState,
centerMapOnSelection
} from '$lib/stores';
import {
copied,
copySelection,
cutSelection,
pasteSelection,
selectAll,
selection
} from '$lib/components/file-list/Selection';
import { derived } from 'svelte/store';
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
import { anySelectedLayer } from '$lib/components/layer-control/utils';
import { defaultOverlays } from '$lib/assets/layers'; import { defaultOverlays } from '$lib/assets/layers';
import LayerControlSettings from '$lib/components/layer-control/LayerControlSettings.svelte'; import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte';
import { allowedPastes, ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList'; import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
import Export from '$lib/components/Export.svelte'; import Export from '$lib/components/export/Export.svelte';
import { mode, setMode, systemPrefersMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { _, locale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { settings } from '$lib/logic/settings';
import {
createFile,
fileActions,
loadFiles,
pasteSelection,
triggerFileInput,
} from '$lib/logic/file-actions';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActionManager } from '$lib/logic/file-action-manager';
import { copied, selection } from '$lib/logic/selection';
import { allHidden } from '$lib/logic/hidden';
import { boundsManager } from '$lib/logic/bounds';
import { tick } from 'svelte';
import { allowedPastes } from '$lib/components/file-list/sortable-file-list';
const { const {
distanceUnits, distanceUnits,
velocityUnits, velocityUnits,
temperatureUnits, temperatureUnits,
elevationProfile, elevationProfile,
verticalFileView, treeFileView,
currentBasemap, currentBasemap,
previousBasemap, previousBasemap,
currentOverlays, currentOverlays,
@@ -91,115 +86,112 @@
distanceMarkers, distanceMarkers,
directionMarkers, directionMarkers,
streetViewSource, streetViewSource,
routing routing,
} = settings; } = settings;
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo); const canUndo = fileActionManager.canUndo;
let redoDisabled = derived(canRedo, ($canRedo) => !$canRedo); const canRedo = fileActionManager.canRedo;
function switchBasemaps() { function switchBasemaps() {
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap]; [$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
} }
function toggleOverlays() { function toggleOverlays() {
if (anySelectedLayer($currentOverlays)) { if ($currentOverlays && anySelectedLayer($currentOverlays)) {
[$currentOverlays, $previousOverlays] = [defaultOverlays, $currentOverlays]; [$currentOverlays, $previousOverlays] = [defaultOverlays, $currentOverlays];
} else { } else {
[$currentOverlays, $previousOverlays] = [$previousOverlays, defaultOverlays]; [$currentOverlays, $previousOverlays] = [$previousOverlays, defaultOverlays];
} }
} }
function toggle3D() { let layerSettingsOpen = $state(false);
if ($map) {
if ($map.getPitch() === 0) {
$map.easeTo({ pitch: 70 });
} else {
$map.easeTo({ pitch: 0 });
}
}
}
let layerSettingsOpen = false;
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
</script> </script>
<div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none"> <div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
<div <div
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md" class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
> >
<a href="./" target="_blank" class="shrink-0"> <a href={getURLForLanguage(i18n.lang, '/')} target="_blank" class="shrink-0">
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" /> <Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" /> <Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
</a> </a>
<Menubar.Root class="border-none h-fit p-0"> <Menubar.Root class="border-none shadow-none h-fit p-0">
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger aria-label={$_('gpx.file')}> <Menubar.Trigger aria-label={i18n._('gpx.file')}>
<File size="18" class="md:hidden" /> <File size="18" class="md:hidden" />
<span class="hidden md:block">{$_('gpx.file')}</span> <span class="hidden md:block">{i18n._('gpx.file')}</span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Item on:click={createFile}> <Menubar.Item onclick={createFile}>
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new')} {i18n._('menu.new')}
<Shortcut key="+" ctrl={true} /> <Shortcut key="+" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={triggerFileInput}> <Menubar.Item onclick={triggerFileInput}>
<FolderOpen size="16" class="mr-1" /> <FolderOpen size="16" />
{$_('menu.open')} {i18n._('menu.open')}
<Shortcut key="O" ctrl={true} /> <Shortcut key="O" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.duplicateSelection} disabled={$selection.size == 0}> <Menubar.Item
<Copy size="16" class="mr-1" /> onclick={fileActions.duplicateSelection}
{$_('menu.duplicate')} disabled={$selection.size == 0}
>
<Copy size="16" />
{i18n._('menu.duplicate')}
<Shortcut key="D" ctrl={true} /> <Shortcut key="D" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelectedFiles} disabled={$selection.size == 0}> <Menubar.Item
<FileX size="16" class="mr-1" /> onclick={() => tick().then(fileActions.deleteSelectedFiles)}
{$_('menu.close')} disabled={$selection.size == 0}
>
<FileX size="16" />
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item on:click={dbUtils.deleteAllFiles} disabled={$fileObservers.size == 0}> <Menubar.Item
<FileX size="16" class="mr-1" /> onclick={fileActions.deleteAllFiles}
{$_('menu.close_all')} disabled={fileStateCollection.size == 0}
>
<FileX size="16" />
{i18n._('menu.delete_all')}
<Shortcut key="⌫" ctrl={true} shift={true} /> <Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
on:click={() => ($exportState = ExportState.SELECTION)} onclick={() => (exportState.current = ExportState.SELECTION)}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" />
{$_('menu.export')} {i18n._('menu.export')}
<Shortcut key="S" ctrl={true} /> <Shortcut key="S" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
on:click={() => ($exportState = ExportState.ALL)} onclick={() => (exportState.current = ExportState.ALL)}
disabled={$fileObservers.size == 0} disabled={fileStateCollection.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" />
{$_('menu.export_all')} {i18n._('menu.export_all')}
<Shortcut key="S" ctrl={true} shift={true} /> <Shortcut key="S" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger aria-label={$_('menu.edit')}> <Menubar.Trigger aria-label={i18n._('menu.edit')}>
<FilePen size="18" class="md:hidden" /> <FilePen size="18" class="md:hidden" />
<span class="hidden md:block">{$_('menu.edit')}</span> <span class="hidden md:block">{i18n._('menu.edit')}</span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Item on:click={dbUtils.undo} disabled={$undoDisabled}> <Menubar.Item onclick={() => fileActionManager.undo()} disabled={!$canUndo}>
<Undo2 size="16" class="mr-1" /> <Undo2 size="16" />
{$_('menu.undo')} {i18n._('menu.undo')}
<Shortcut key="Z" ctrl={true} /> <Shortcut key="Z" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item on:click={dbUtils.redo} disabled={$redoDisabled}> <Menubar.Item onclick={() => fileActionManager.redo()} disabled={!$canRedo}>
<Redo2 size="16" class="mr-1" /> <Redo2 size="16" />
{$_('menu.redo')} {i18n._('menu.redo')}
<Shortcut key="Z" ctrl={true} shift={true} /> <Shortcut key="Z" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
@@ -207,203 +199,250 @@
disabled={$selection.size !== 1 || disabled={$selection.size !== 1 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
on:click={() => ($editMetadata = true)} (item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
onclick={() => (editMetadata.current = true)}
> >
<Info size="16" class="mr-1" /> <Info size="16" />
{$_('menu.metadata.button')} {i18n._('menu.metadata.button')}
<Shortcut key="I" ctrl={true} /> <Shortcut key="I" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
disabled={$selection.size === 0 || disabled={$selection.size === 0 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
on:click={() => ($editStyle = true)} (item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
onclick={() => (editStyle.current = true)}
> >
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" />
{$_('menu.style.button')} {i18n._('menu.style.button')}
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
on:click={() => { onclick={() => {
if ($allHidden) { if ($allHidden) {
dbUtils.setHiddenToSelection(false); fileActions.setHiddenToSelection(false);
} else { } else {
dbUtils.setHiddenToSelection(true); fileActions.setHiddenToSelection(true);
} }
}} }}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
{#if $allHidden} {#if $allHidden}
<Eye size="16" class="mr-1" /> <Eye size="16" />
{$_('menu.unhide')} {i18n._('menu.unhide')}
{:else} {:else}
<EyeOff size="16" class="mr-1" /> <EyeOff size="16" />
{$_('menu.hide')} {i18n._('menu.hide')}
{/if} {/if}
<Shortcut key="H" ctrl={true} /> <Shortcut key="H" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{#if $verticalFileView} {#if $treeFileView}
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)} {#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())} onclick={() =>
fileActions.addNewTrack(
$selection.getSelected()[0].getFileId()
)}
disabled={$selection.size !== 1} disabled={$selection.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new_track')} {i18n._('menu.new_track')}
</Menubar.Item> </Menubar.Item>
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)} {:else if $selection
.getSelected()
.some((item) => item instanceof ListTrackItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
on:click={() => { onclick={() => {
let item = $selection.getSelected()[0]; let item = $selection.getSelected()[0];
dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex()); fileActions.addNewSegment(
item.getFileId(),
item.getTrackIndex()
);
}} }}
disabled={$selection.size !== 1} disabled={$selection.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new_segment')} {i18n._('menu.new_segment')}
</Menubar.Item> </Menubar.Item>
{/if} {/if}
{/if} {/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}> <Menubar.Item
<FileStack size="16" class="mr-1" /> onclick={() => selection.selectAll()}
{$_('menu.select_all')} disabled={fileStateCollection.size == 0}
>
<FileStack size="16" />
{i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
on:click={() => { onclick={() => {
if ($selection.size > 0) { if ($selection.size > 0) {
centerMapOnSelection(); boundsManager.centerMapOnSelection();
} }
}} }}
disabled={$selection.size == 0}
> >
<Maximize size="16" class="mr-1" /> <Maximize size="16" />
{$_('menu.center')} {i18n._('menu.center')}
<Shortcut key="⏎" ctrl={true} /> <Shortcut key="⏎" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{#if $verticalFileView} {#if $treeFileView}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}> <Menubar.Item
<ClipboardCopy size="16" class="mr-1" /> onclick={() => selection.copySelection()}
{$_('menu.copy')} disabled={$selection.size === 0}
>
<ClipboardCopy size="16" />
{i18n._('menu.copy')}
<Shortcut key="C" ctrl={true} /> <Shortcut key="C" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item on:click={cutSelection} disabled={$selection.size === 0}> <Menubar.Item
<Scissors size="16" class="mr-1" /> onclick={() => selection.cutSelection()}
{$_('menu.cut')} disabled={$selection.size === 0}
>
<Scissors size="16" />
{i18n._('menu.cut')}
<Shortcut key="X" ctrl={true} /> <Shortcut key="X" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
disabled={$copied === undefined || disabled={$copied === undefined ||
$copied.length === 0 || $copied.length === 0 ||
($selection.size > 0 && ($selection.size > 0 &&
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))} !allowedPastes[$copied[0].level].includes(
on:click={pasteSelection} $selection.getSelected().pop()!.level
))}
onclick={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" />
{$_('menu.paste')} {i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} /> <Shortcut key="V" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{/if} {/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}> <Menubar.Item
<Trash2 size="16" class="mr-1" /> onclick={() => tick().then(fileActions.deleteSelection)}
{$_('menu.delete')} disabled={$selection.size == 0}
>
<Trash2 size="16" />
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger aria-label={$_('menu.view')}> <Menubar.Trigger aria-label={i18n._('menu.view')}>
<View size="18" class="md:hidden" /> <View size="18" class="md:hidden" />
<span class="hidden md:block">{$_('menu.view')}</span> <span class="hidden md:block">{i18n._('menu.view')}</span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.CheckboxItem bind:checked={$elevationProfile}> <Menubar.CheckboxItem bind:checked={$elevationProfile}>
<ChartArea size="16" class="mr-1" /> <ChartArea size="16" />
{$_('menu.elevation_profile')} {i18n._('menu.elevation_profile')}
<Shortcut key="P" ctrl={true} /> <Shortcut key="P" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$verticalFileView}> <Menubar.CheckboxItem bind:checked={$treeFileView}>
<GalleryVertical size="16" class="mr-1" /> <ListTree size="16" />
{$_('menu.vertical_file_view')} {i18n._('menu.tree_file_view')}
<Shortcut key="L" ctrl={true} /> <Shortcut key="L" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={switchBasemaps}> <Menubar.Item inset onclick={switchBasemaps}>
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut key="F1" /> <Map size="16" />{i18n._('menu.switch_basemap')}<Shortcut key="F1" />
</Menubar.Item> </Menubar.Item>
<Menubar.Item inset on:click={toggleOverlays}> <Menubar.Item inset onclick={toggleOverlays}>
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut key="F2" /> <Layers2 size="16" />{i18n._('menu.toggle_overlays')}<Shortcut key="F2" />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.CheckboxItem bind:checked={$distanceMarkers}> <Menubar.CheckboxItem bind:checked={$distanceMarkers}>
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut key="F3" /> <Coins size="16" />{i18n._('menu.distance_markers')}<Shortcut key="F3" />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$directionMarkers}> <Menubar.CheckboxItem bind:checked={$directionMarkers}>
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut key="F4" /> <Milestone size="16" />{i18n._('menu.direction_markers')}<Shortcut
key="F4"
/>
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={toggle3D}> <Menubar.Item inset onclick={() => map.toggle3D()}>
<Box size="16" class="mr-1" /> <Box size="16" />
{$_('menu.toggle_3d')} {i18n._('menu.toggle_3d')}
<Shortcut key="{$_('menu.ctrl')}+{$_('menu.drag')}" /> <Shortcut key="{i18n._('menu.ctrl')} {i18n._('menu.drag')}" />
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger aria-label={$_('menu.settings')}> <Menubar.Trigger aria-label={i18n._('menu.settings')}>
<Settings size="18" class="md:hidden" /> <Settings size="18" class="md:hidden" />
<span class="hidden md:block"> <span class="hidden md:block">
{$_('menu.settings')} {i18n._('menu.settings')}
</span> </span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Ruler size="16" class="mr-1" />{$_('menu.distance_units')} <Ruler size="16" class="mr-2" />{i18n._('menu.distance_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}> <Menubar.RadioGroup bind:value={$distanceUnits}>
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem> <Menubar.RadioItem value="metric"
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem> >{i18n._('menu.metric')}</Menubar.RadioItem
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem> >
<Menubar.RadioItem value="imperial"
>{i18n._('menu.imperial')}</Menubar.RadioItem
>
<Menubar.RadioItem value="nautical"
>{i18n._('menu.nautical')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Zap size="16" class="mr-1" />{$_('menu.velocity_units')} <Zap size="16" class="mr-2" />{i18n._('menu.velocity_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}> <Menubar.RadioGroup bind:value={$velocityUnits}>
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem> <Menubar.RadioItem value="speed"
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem> >{i18n._('quantities.speed')}</Menubar.RadioItem
>
<Menubar.RadioItem value="pace"
>{i18n._('quantities.pace')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Thermometer size="16" class="mr-1" />{$_('menu.temperature_units')} <Thermometer size="16" class="mr-2" />{i18n._('menu.temperature_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}> <Menubar.RadioGroup bind:value={$temperatureUnits}>
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem> <Menubar.RadioItem value="celsius"
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem> >{i18n._('menu.celsius')}</Menubar.RadioItem
>
<Menubar.RadioItem value="fahrenheit"
>{i18n._('menu.fahrenheit')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Languages size="16" class="mr-1" /> <Languages size="16" class="mr-2" />
{$_('menu.language')} {i18n._('menu.language')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$locale}> <Menubar.RadioGroup value={i18n.lang}>
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang, '/app')}> <a href={getURLForLanguage(lang, '/app')}>
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem> <Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
@@ -414,41 +453,49 @@
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
{#if selectedMode === 'light'} {#if mode.current === 'light' || !mode.current}
<Sun size="16" class="mr-1" /> <Sun size="16" class="mr-2" />
{:else} {:else}
<Moon size="16" class="mr-1" /> <Moon size="16" class="mr-2" />
{/if} {/if}
{$_('menu.mode')} {i18n._('menu.mode')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup <Menubar.RadioGroup
bind:value={selectedMode} value={mode.current ?? 'light'}
onValueChange={(value) => { onValueChange={(value) => {
setMode(value); setMode(value as 'light' | 'dark');
}} }}
> >
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem> <Menubar.RadioItem value="light"
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem> >{i18n._('menu.light')}</Menubar.RadioItem
>
<Menubar.RadioItem value="dark"
>{i18n._('menu.dark')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<PersonStanding size="16" class="mr-1" /> <PersonStanding size="16" class="mr-2" />
{$_('menu.street_view_source')} {i18n._('menu.street_view_source')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$streetViewSource}> <Menubar.RadioGroup bind:value={$streetViewSource}>
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem> <Menubar.RadioItem value="mapillary"
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem> >{i18n._('menu.mapillary')}</Menubar.RadioItem
>
<Menubar.RadioItem value="google"
>{i18n._('menu.google')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Item on:click={() => (layerSettingsOpen = true)}> <Menubar.Item onclick={() => (layerSettingsOpen = true)}>
<Layers size="16" class="mr-1" /> <Layers size="16" />
{$_('menu.layers')} {i18n._('menu.layers')}
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
@@ -459,11 +506,11 @@
href="./help" href="./help"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-sm px-3 py-0.5" class="cursor-default h-fit rounded-sm px-3 py-0.5"
aria-label={$_('menu.help')} aria-label={i18n._('menu.help')}
> >
<BookOpenText size="18" class="md:hidden" /> <BookOpenText size="18" class="md:hidden" />
<span class="hidden md:block"> <span class="hidden md:block">
{$_('menu.help')} {i18n._('menu.help')}
</span> </span>
</Button> </Button>
<Button <Button
@@ -471,12 +518,12 @@
href="https://ko-fi.com/gpxstudio" href="https://ko-fi.com/gpxstudio"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5" class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
aria-label={$_('menu.donate')} aria-label={i18n._('menu.donate')}
> >
<HeartHandshake size="18" class="md:hidden" /> <HeartHandshake size="18" class="md:hidden" />
<span class="hidden md:flex flex-row items-center"> <span class="hidden md:flex flex-row items-center">
{$_('menu.donate')} {i18n._('menu.donate')}
<Heart size="16" class="ml-1" fill="rgb(var(--support))" /> <Heart size="16" class="ml-1" fill="var(--support)" />
</span> </span>
</Button> </Button>
</div> </div>
@@ -489,7 +536,9 @@
<svelte:window <svelte:window
on:keydown={(e) => { on:keydown={(e) => {
let targetInput = let targetInput =
e.target.tagName === 'INPUT' || e &&
e.target &&
(e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' || e.target.tagName === 'TEXTAREA' ||
e.target.tagName === 'SELECT' || e.target.tagName === 'SELECT' ||
e.target.role === 'combobox' || e.target.role === 'combobox' ||
@@ -497,7 +546,7 @@
e.target.role === 'menu' || e.target.role === 'menu' ||
e.target.role === 'menuitem' || e.target.role === 'menuitem' ||
e.target.role === 'menuitemradio' || e.target.role === 'menuitemradio' ||
e.target.role === 'menuitemcheckbox'; e.target.role === 'menuitemcheckbox');
if (e.key === '+' && (e.metaKey || e.ctrlKey)) { if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
createFile(); createFile();
@@ -506,16 +555,16 @@
triggerFileInput(); triggerFileInput();
e.preventDefault(); e.preventDefault();
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
dbUtils.duplicateSelection(); fileActions.duplicateSelection();
e.preventDefault(); e.preventDefault();
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
copySelection(); selection.copySelection();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
cutSelection(); selection.cutSelection();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
@@ -525,32 +574,32 @@
} }
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) { if (e.shiftKey) {
if ($fileObservers.size > 0) { if (fileStateCollection.size > 0) {
$exportState = ExportState.ALL; exportState.current = ExportState.ALL;
} }
} else if ($selection.size > 0) { } else if ($selection.size > 0) {
$exportState = ExportState.SELECTION; exportState.current = ExportState.SELECTION;
} }
e.preventDefault(); e.preventDefault();
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) { if (e.shiftKey) {
dbUtils.redo(); fileActionManager.redo();
} else { } else {
dbUtils.undo(); fileActionManager.undo();
} }
e.preventDefault(); e.preventDefault();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
if (e.shiftKey) { if (e.shiftKey) {
dbUtils.deleteAllFiles(); fileActions.deleteAllFiles();
} else { } else {
dbUtils.deleteSelection(); fileActions.deleteSelection();
} }
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
selectAll(); selection.selectAll();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
@@ -560,25 +609,25 @@
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem) .every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
) { ) {
$editMetadata = true; editMetadata.current = true;
} }
e.preventDefault(); e.preventDefault();
} else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
$elevationProfile = !$elevationProfile; $elevationProfile = !$elevationProfile;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
$verticalFileView = !$verticalFileView; $treeFileView = !$treeFileView;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
if ($allHidden) { if ($allHidden) {
dbUtils.setHiddenToSelection(false); fileActions.setHiddenToSelection(false);
} else { } else {
dbUtils.setHiddenToSelection(true); fileActions.setHiddenToSelection(true);
} }
e.preventDefault(); e.preventDefault();
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
if ($selection.size > 0) { if ($selection.size > 0) {
centerMapOnSelection(); boundsManager.centerMapOnSelection();
} }
} else if (e.key === 'F1') { } else if (e.key === 'F1') {
switchBasemaps(); switchBasemaps();
@@ -595,28 +644,20 @@
} else if (e.key === 'F5') { } else if (e.key === 'F5') {
$routing = !$routing; $routing = !$routing;
e.preventDefault(); e.preventDefault();
} else if (
e.key === 'ArrowRight' ||
e.key === 'ArrowDown' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowUp'
) {
if (!targetInput) {
updateSelectionFromKey(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey);
e.preventDefault();
}
} }
}} }}
on:dragover={(e) => e.preventDefault()} on:dragover={(e) => e.preventDefault()}
on:drop={(e) => { on:drop={(e) => {
e.preventDefault(); e.preventDefault();
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer && e.dataTransfer.files.length > 0) {
loadFiles(e.dataTransfer.files); loadFiles(e.dataTransfer.files);
} }
}} }}
/> />
<style lang="postcss"> <style lang="postcss">
@reference "../../app.css";
div :global(button) { div :global(button) {
@apply hover:bg-accent; @apply hover:bg-accent;
@apply px-3; @apply px-3;
+16 -13
View File
@@ -1,25 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Moon, Sun } from 'lucide-svelte'; import { Moon, Sun } from '@lucide/svelte';
import { mode, setMode, systemPrefersMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
export let size = '20'; let {
class: className = '',
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light'; }: {
class?: string;
} = $props();
</script> </script>
<Button <Button
variant="ghost" variant="ghost"
class="h-8 px-1.5 {$$props.class ?? ''}" size="icon"
on:click={() => { class={className}
setMode(selectedMode === 'light' ? 'dark' : 'light'); onclick={() => {
setMode(mode.current === 'light' ? 'dark' : 'light');
}} }}
aria-label={$_('menu.mode')} aria-label={i18n._('menu.mode')}
> >
{#if selectedMode === 'light'} {#if mode.current === 'light'}
<Sun {size} /> <Sun />
{:else} {:else}
<Moon {size} /> <Moon />
{/if} {/if}
</Button> </Button>
+25 -13
View File
@@ -3,30 +3,42 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte'; import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
import ModeSwitch from '$lib/components/ModeSwitch.svelte'; import ModeSwitch from '$lib/components/ModeSwitch.svelte';
import { BookOpenText, Home, Map } from 'lucide-svelte'; import { BookOpenText, House, Map } from '@lucide/svelte';
import { _, locale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
</script> </script>
<nav class="w-full sticky top-0 bg-background z-50"> <nav class="w-full sticky top-0 bg-background z-50">
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8"> <div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5"> <a href={getURLForLanguage(i18n.lang, '/')} class="shrink-0 translate-y-0.5">
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" /> <Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
<Logo class="h-8 hidden sm:block" width="153" /> <Logo class="h-8 hidden sm:block" width="153" />
</a> </a>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}> <Button
<Home size="18" class="mr-1.5" /> variant="link"
{$_('homepage.home')} class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/')}
>
<House size="18" />
{i18n._('homepage.home')}
</Button> </Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/app')}> <Button
<Map size="18" class="mr-1.5" /> variant="link"
{$_('homepage.app')} class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/app')}
>
<Map size="18" />
{i18n._('homepage.app')}
</Button> </Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/help')}> <Button
<BookOpenText size="18" class="mr-1.5" /> variant="link"
{$_('menu.help')} class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/help')}
>
<BookOpenText size="18" />
{i18n._('menu.help')}
</Button> </Button>
<AlgoliaDocSearch class="ml-auto" /> <AlgoliaDocSearch class="ml-auto" />
<ModeSwitch class="hidden xs:block" /> <ModeSwitch class="hidden xs:inline-flex" />
</div> </div>
</nav> </nav>
+16 -9
View File
@@ -1,9 +1,15 @@
<script lang="ts"> <script lang="ts">
export let orientation: 'col' | 'row' = 'col'; let {
orientation = 'col',
export let after: number; after = $bindable(),
export let minAfter: number = 0; minAfter = 0,
export let maxAfter: number = Number.MAX_SAFE_INTEGER; maxAfter = Number.MAX_SAFE_INTEGER,
}: {
orientation?: 'col' | 'row';
after: number;
minAfter?: number;
maxAfter?: number;
} = $props();
function handleMouseDown(event: PointerEvent) { function handleMouseDown(event: PointerEvent) {
const startX = event.clientX; const startX = event.clientX;
@@ -12,7 +18,8 @@
const handleMouseMove = (event: PointerEvent) => { const handleMouseMove = (event: PointerEvent) => {
const newAfter = const newAfter =
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY); startAfter +
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
if (newAfter >= minAfter && newAfter <= maxAfter) { if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter; after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) { } else if (newAfter < minAfter && after !== minAfter) {
@@ -32,10 +39,10 @@
} }
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="{orientation === 'col' class="{orientation === 'col'
? 'w-1 h-full cursor-col-resize border-l' ? 'w-1 h-full cursor-col-resize border-l'
: 'w-full h-1 cursor-row-resize border-t'} {orientation}" : 'w-full h-1 cursor-row-resize border-t'} {orientation}"
on:pointerdown={handleMouseDown} onpointerdown={handleMouseDown}
/> ></div>
+23 -16
View File
@@ -1,15 +1,25 @@
<script lang="ts"> <script lang="ts">
import { isMac, isSafari } from '$lib/utils'; import { isMac, isSafari } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import * as Kbd from '$lib/components/ui/kbd/index.js';
export let key: string | undefined = undefined; let {
export let shift: boolean = false; key = undefined,
export let ctrl: boolean = false; shift = false,
export let click: boolean = false; ctrl = false,
click = false,
class: className = '',
}: {
key?: string;
shift?: boolean;
ctrl?: boolean;
click?: boolean;
class?: string;
} = $props();
let mac = false; let mac = $state(false);
let safari = false; let safari = $state(false);
onMount(() => { onMount(() => {
mac = isMac(); mac = isMac();
@@ -17,20 +27,17 @@
}); });
</script> </script>
<div <Kbd.Root class="ml-auto {className}">
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
{...$$props}
>
{#if shift} {#if shift}
<span></span>
{/if} {/if}
{#if ctrl} {#if ctrl}
<span>{mac && !safari ? '⌘' : $_('menu.ctrl') + '+'}</span> {mac && !safari ? '⌘' : i18n._('menu.ctrl')}
{/if} {/if}
{#if key} {#if key}
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span> {key}
{/if} {/if}
{#if click} {#if click}
<span>{$_('menu.click')}</span> {i18n._('menu.click')}
{/if} {/if}
</div> </Kbd.Root>
+22 -8
View File
@@ -1,18 +1,32 @@
<script lang="ts"> <script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Snippet } from 'svelte';
export let label: string; let {
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top'; label,
side = 'top',
children,
extra,
class: className = '',
}: {
label: string;
side?: 'top' | 'right' | 'bottom' | 'left';
children: Snippet;
extra?: Snippet;
class?: string;
} = $props();
</script> </script>
<Tooltip.Root> <Tooltip.Provider>
<Tooltip.Trigger {...$$restProps} aria-label={label}> <Tooltip.Root>
<slot /> <Tooltip.Trigger class={className} aria-label={label}>
{@render children()}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content {side}> <Tooltip.Content {side}>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center gap-2">
<span>{label}</span> <span>{label}</span>
<slot name="extra" /> {@render extra?.()}
</div> </div>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider>
+19 -11
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { settings } from '$lib/db';
import { import {
celsiusToFahrenheit, celsiusToFahrenheit,
getConvertedDistance, getConvertedDistance,
@@ -8,20 +7,29 @@
getDistanceUnits, getDistanceUnits,
getElevationUnits, getElevationUnits,
getVelocityUnits, getVelocityUnits,
secondsToHHMMSS secondsToHHMMSS,
} from '$lib/units'; } from '$lib/units';
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import { _ } from 'svelte-i18n'; let {
value,
export let value: number; type,
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time'; showUnits = true,
export let showUnits: boolean = true; decimals = undefined,
export let decimals: number | undefined = undefined; class: className = '',
}: {
value: number;
type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
showUnits?: boolean;
decimals?: number;
class?: string;
} = $props();
const { distanceUnits, velocityUnits, temperatureUnits } = settings; const { distanceUnits, velocityUnits, temperatureUnits } = settings;
</script> </script>
<span class={$$props.class}> <span class={className}>
{#if type === 'distance'} {#if type === 'distance'}
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)} {getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
{showUnits ? getDistanceUnits($distanceUnits) : ''} {showUnits ? getDistanceUnits($distanceUnits) : ''}
@@ -38,9 +46,9 @@
{/if} {/if}
{:else if type === 'temperature'} {:else if type === 'temperature'}
{#if $temperatureUnits === 'celsius'} {#if $temperatureUnits === 'celsius'}
{value} {showUnits ? $_('units.celsius') : ''} {value} {showUnits ? i18n._('units.celsius') : ''}
{:else} {:else}
{celsiusToFahrenheit(value)} {showUnits ? $_('units.fahrenheit') : ''} {celsiusToFahrenheit(value)} {showUnits ? i18n._('units.fahrenheit') : ''}
{/if} {/if}
{:else if type === 'time'} {:else if type === 'time'}
{secondsToHHMMSS(value)} {secondsToHHMMSS(value)}
@@ -1,15 +1,23 @@
<script lang="ts"> <script lang="ts">
import { setContext } from 'svelte'; import { setContext, type Snippet } from 'svelte';
import { writable } from 'svelte/store'; import { CollapsibleTreeState } from './utils.svelte';
export let defaultState: 'open' | 'closed' = 'open'; const {
export let side: 'left' | 'right' = 'right'; defaultState = 'open',
export let nohover: boolean = false; side = 'right',
export let slotInsideTrigger: boolean = true; nohover = false,
slotInsideTrigger = true,
children,
}: {
defaultState?: 'open' | 'closed';
side?: 'left' | 'right';
nohover?: boolean;
slotInsideTrigger?: boolean;
children: Snippet;
} = $props();
let open = writable<Record<string, boolean>>({}); let open = $state(new CollapsibleTreeState(defaultState));
setContext('collapsible-tree-default-state', defaultState);
setContext('collapsible-tree-state', open); setContext('collapsible-tree-state', open);
setContext('collapsible-tree-side', side); setContext('collapsible-tree-side', side);
setContext('collapsible-tree-nohover', nohover); setContext('collapsible-tree-nohover', nohover);
@@ -17,4 +25,4 @@
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger); setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
</script> </script>
<slot /> {@render children()}
@@ -1,60 +1,56 @@
<script lang="ts"> <script lang="ts">
import * as Collapsible from '$lib/components/ui/collapsible'; import * as Collapsible from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-svelte'; import { ChevronDown, ChevronLeft, ChevronRight } from '@lucide/svelte';
import { getContext, onMount, setContext } from 'svelte'; import { getContext, setContext, type Snippet } from 'svelte';
import { get, type Writable } from 'svelte/store'; import type { ClassValue } from 'svelte/elements';
import type { CollapsibleTreeState } from './utils.svelte';
export let id: string | number; const props: {
id: string | number;
class?: ClassValue;
trigger: Snippet;
content: Snippet;
} = $props();
let defaultState = getContext<'open' | 'closed'>('collapsible-tree-default-state'); let state = getContext<CollapsibleTreeState>('collapsible-tree-state');
let open = getContext<Writable<Record<string, boolean>>>('collapsible-tree-state');
let side = getContext<'left' | 'right'>('collapsible-tree-side'); let side = getContext<'left' | 'right'>('collapsible-tree-side');
let nohover = getContext<boolean>('collapsible-tree-nohover'); let nohover = getContext<boolean>('collapsible-tree-nohover');
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger'); let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
let parentId = getContext<string>('collapsible-tree-parent-id'); let parentId = getContext<string>('collapsible-tree-parent-id');
let fullId = `${parentId}.${id}`; let fullId = `${parentId}.${props.id}`;
setContext('collapsible-tree-parent-id', fullId); setContext('collapsible-tree-parent-id', fullId);
onMount(() => { let open = state.get(fullId);
if (!get(open).hasOwnProperty(fullId)) {
open.update((value) => {
value[fullId] = defaultState === 'open';
return value;
});
}
});
export function openNode() { export function openNode() {
open.update((value) => { open.current = true;
value[fullId] = true;
return value;
});
} }
</script> </script>
<Collapsible.Root bind:open={$open[fullId]} class={$$props.class ?? ''}> <Collapsible.Root bind:open={open.current} class={props.class}>
{#if slotInsideTrigger} {#if slotInsideTrigger}
<Collapsible.Trigger class="w-full"> <Collapsible.Trigger class="w-full">
<Button <Button
variant="ghost" variant="ghost"
class="w-full flex flex-row {side === 'right' size="icon"
class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover : 'justify-start pl-1'} h-fit {nohover
? 'hover:bg-background' ? 'hover:bg-background'
: ''} pointer-events-none" : ''} pointer-events-none"
> >
{#if side === 'left'} {#if side === 'left'}
{#if $open[fullId]} {#if open.current}
<ChevronDown size="16" class="shrink-0" /> <ChevronDown size="16" class="shrink-0" />
{:else} {:else}
<ChevronRight size="16" class="shrink-0" /> <ChevronRight size="16" class="shrink-0" />
{/if} {/if}
{/if} {/if}
<slot name="trigger" /> {@render props.trigger()}
{#if side === 'right'} {#if side === 'right'}
{#if $open[fullId]} {#if open.current}
<ChevronDown size="16" class="shrink-0" /> <ChevronDown size="16" class="shrink-0" />
{:else} {:else}
<ChevronLeft size="16" class="shrink-0" /> <ChevronLeft size="16" class="shrink-0" />
@@ -65,23 +61,24 @@
{:else} {:else}
<Button <Button
variant="ghost" variant="ghost"
class="w-full flex flex-row {side === 'right' size="icon"
class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}" : 'justify-start pl-1'} h-fit {nohover ? 'hover:bg-background' : ''}"
> >
{#if side === 'left'} {#if side === 'left'}
<Collapsible.Trigger> <Collapsible.Trigger>
{#if $open[fullId]} {#if open.current}
<ChevronDown size="16" class="shrink-0" /> <ChevronDown size="16" class="shrink-0" />
{:else} {:else}
<ChevronRight size="16" class="shrink-0" /> <ChevronRight size="16" class="shrink-0" />
{/if} {/if}
</Collapsible.Trigger> </Collapsible.Trigger>
{/if} {/if}
<slot name="trigger" /> {@render props.trigger()}
{#if side === 'right'} {#if side === 'right'}
<Collapsible.Trigger> <Collapsible.Trigger>
{#if $open[fullId]} {#if open.current}
<ChevronDown size="16" class="shrink-0" /> <ChevronDown size="16" class="shrink-0" />
{:else} {:else}
<ChevronLeft size="16" class="shrink-0" /> <ChevronLeft size="16" class="shrink-0" />
@@ -90,8 +87,7 @@
{/if} {/if}
</Button> </Button>
{/if} {/if}
<Collapsible.Content>
<Collapsible.Content class="ml-2"> {@render props.content()}
<slot name="content" />
</Collapsible.Content> </Collapsible.Content>
</Collapsible.Root> </Collapsible.Root>
@@ -0,0 +1,31 @@
export class CollapsibleNodeState {
private _open: boolean;
constructor(defaultState: 'open' | 'closed') {
this._open = $state(defaultState === 'open');
}
get current(): boolean {
return this._open;
}
set current(value: boolean) {
this._open = value;
}
}
export class CollapsibleTreeState {
private _open: Record<string, CollapsibleNodeState> = {};
private _defaultState: 'open' | 'closed';
constructor(defaultState: 'open' | 'closed') {
this._defaultState = defaultState;
}
get(id: string): CollapsibleNodeState {
if (this._open[id] === undefined) {
this._open[id] = new CollapsibleNodeState(this._defaultState);
}
return this._open[id];
}
}
@@ -1,27 +0,0 @@
<script lang="ts">
import CustomControl from './CustomControl';
import { map } from '$lib/stores';
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
let container: HTMLDivElement;
let control: CustomControl | undefined = undefined;
$: if ($map && container) {
if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left');
container.classList.remove('hidden');
if (control === undefined) {
control = new CustomControl(container);
}
$map.addControl(control, position);
}
</script>
<div
bind:this={container}
class="{$$props.class ||
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
>
<slot />
</div>
@@ -1,14 +1,16 @@
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n'; import type { Component } from 'svelte';
export let module; let { module: Module }: { module: Component } = $props();
</script> </script>
<div class="markdown flex flex-col gap-3"> <div class="markdown flex flex-col gap-3">
<svelte:component this={module} /> <Module />
</div> </div>
<style lang="postcss"> <style lang="postcss">
@reference "../../../app.css";
:global(.markdown) { :global(.markdown) {
@apply text-muted-foreground; @apply text-muted-foreground;
} }
@@ -43,6 +45,7 @@
:global(.markdown > a) { :global(.markdown > a) {
@apply text-link; @apply text-link;
@apply hover:underline; @apply hover:underline;
@apply contents;
} }
:global(.markdown p > a) { :global(.markdown p > a) {
@@ -1,6 +1,11 @@
<script lang="ts"> <script lang="ts">
export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split'; let {
export let alt: string; src,
alt,
}: {
src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
alt: string;
} = $props();
</script> </script>
<div class="flex flex-col items-center py-6 w-full"> <div class="flex flex-col items-center py-6 w-full">
@@ -18,7 +23,11 @@
class="w-full max-w-3xl" class="w-full max-w-3xl"
/> />
{:else if src === 'tools/split'} {:else if src === 'tools/split'}
<enhanced:img src="/src/lib/assets/img/docs/tools/split.png" {alt} class="w-full max-w-3xl" /> <enhanced:img
src="/src/lib/assets/img/docs/tools/split.png"
{alt}
class="w-full max-w-3xl"
/>
{/if} {/if}
</div> </div>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p> <p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let type: 'note' | 'warning' = 'note'; import type { Snippet } from 'svelte';
let { type = 'note', children }: { type?: 'note' | 'warning'; children: Snippet } = $props();
</script> </script>
<div <div
@@ -7,10 +9,12 @@
? 'border-link' ? 'border-link'
: 'border-destructive'} p-2 text-sm rounded-md" : 'border-destructive'} p-2 text-sm rounded-md"
> >
<slot /> {@render children()}
</div> </div>
<style lang="postcss"> <style lang="postcss">
@reference "../../../app.css";
div :global(a) { div :global(a) {
@apply text-link; @apply text-link;
@apply hover:underline; @apply hover:underline;
+53 -28
View File
@@ -1,39 +1,64 @@
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte"; import {
import type { ComponentType } from "svelte"; File,
FilePen,
View,
Settings,
Pencil,
MapPin,
Scissors,
CalendarClock,
Group,
Ungroup,
Funnel,
SquareDashedMousePointer,
MountainSnow,
type IconProps,
} from '@lucide/svelte';
import type { Component } from 'svelte';
export const guides: Record<string, string[]> = { export const guides: Record<string, string[]> = {
'getting-started': [], 'getting-started': [],
menu: ['file', 'edit', 'view', 'settings'], menu: ['file', 'edit', 'view', 'settings'],
'files-and-stats': [], 'files-and-stats': [],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'], toolbar: [
'routing',
'poi',
'scissors',
'time',
'merge',
'extract',
'elevation',
'minify',
'clean',
],
'map-controls': [], 'map-controls': [],
'gpx': [], gpx: [],
'integration': [], integration: [],
'faq': [], faq: [],
}; };
export const guideIcons: Record<string, string | ComponentType<Icon>> = { export const guideIcons: Record<string, string | Component<IconProps>> = {
"getting-started": "🚀", 'getting-started': '🚀',
"menu": "📂 ⚙️", menu: '📂 ⚙️',
"file": File, file: File,
"edit": FilePen, edit: FilePen,
"view": View, view: View,
"settings": Settings, settings: Settings,
"files-and-stats": "🗂 📈", 'files-and-stats': '🗂 📈',
"toolbar": "🧰", toolbar: '🧰',
"routing": Pencil, routing: Pencil,
"poi": MapPin, poi: MapPin,
"scissors": Scissors, scissors: Scissors,
"time": CalendarClock, time: CalendarClock,
"merge": Group, merge: Group,
"extract": Ungroup, extract: Ungroup,
"elevation": MountainSnow, elevation: MountainSnow,
"minify": Filter, minify: Funnel,
"clean": SquareDashedMousePointer, clean: SquareDashedMousePointer,
"map-controls": "🗺", 'map-controls': '🗺',
"gpx": "💾", gpx: '💾',
"integration": "{ 👩‍💻 }", integration: '{ 👩‍💻 }',
"faq": "🔮", faq: '🔮',
}; };
export function getPreviousGuide(currentGuide: string): string | undefined { export function getPreviousGuide(currentGuide: string): string | undefined {
@@ -0,0 +1,203 @@
<script lang="ts">
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
import * as Popover from '$lib/components/ui/popover/index.js';
import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { onDestroy, onMount } from 'svelte';
import {
BrickWall,
TriangleRight,
HeartPulse,
Orbit,
SquareActivity,
Thermometer,
Zap,
Circle,
Check,
ChartNoAxesColumn,
Construction,
} from '@lucide/svelte';
import type { Readable, Writable } from 'svelte/store';
import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/logic/settings';
import { i18n } from '$lib/i18n.svelte';
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
const { velocityUnits } = settings;
let {
gpxStatistics,
slicedGPXStatistics,
additionalDatasets,
elevationFill,
showControls = true,
}: {
gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
additionalDatasets: Writable<string[]>;
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
showControls?: boolean;
} = $props();
let canvas: HTMLCanvasElement;
let overlay: HTMLCanvasElement;
let elevationProfile: ElevationProfile | null = null;
onMount(() => {
elevationProfile = new ElevationProfile(
gpxStatistics,
slicedGPXStatistics,
additionalDatasets,
elevationFill,
canvas,
overlay
);
});
onDestroy(() => {
if (elevationProfile) {
elevationProfile.destroy();
}
});
</script>
<div class="h-full grow min-w-0 relative py-2">
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
{#if showControls}
<div class="absolute bottom-10 right-1.5">
<Popover.Root>
<Popover.Trigger>
<ButtonWithTooltip
label={i18n._('chart.settings')}
variant="outline"
side="left"
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
>
<ChartNoAxesColumn size="18" />
</ButtonWithTooltip>
</Popover.Trigger>
<Popover.Content
class="w-fit p-0 flex flex-col"
side="top"
align="end"
sideOffset={-32}
>
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1 w-full border-none"
type="single"
bind:value={$elevationFill}
>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="slope"
>
<div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'slope'}
<Circle class="size-1.5 fill-current text-current" />
{/if}
</div>
<TriangleRight size="15" />
{i18n._('quantities.slope')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="surface"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'surface'}
<Circle class="size-1.5 fill-current text-current" />
{/if}
</div>
<BrickWall size="15" />
{i18n._('quantities.surface')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="highway"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'highway'}
<Circle class="size-1.5 fill-current text-current" />
{/if}
</div>
<Construction size="15" />
{i18n._('quantities.highway')}
</ToggleGroup.Item>
</ToggleGroup.Root>
<Separator />
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1"
type="multiple"
bind:value={$additionalDatasets}
>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="speed"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('speed')}
<Check size="14" />
{/if}
</div>
<Zap size="15" />
{$velocityUnits === 'speed'
? i18n._('quantities.speed')
: i18n._('quantities.pace')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="hr"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('hr')}
<Check size="14" />
{/if}
</div>
<HeartPulse size="15" />
{i18n._('quantities.heartrate')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="cad"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('cad')}
<Check size="14" />
{/if}
</div>
<Orbit size="15" />
{i18n._('quantities.cadence')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="atemp"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('atemp')}
<Check size="14" />
{/if}
</div>
<Thermometer size="15" />
{i18n._('quantities.temperature')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="power"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('power')}
<Check size="14" />
{/if}
</div>
<SquareActivity size="15" />
{i18n._('quantities.power')}
</ToggleGroup.Item>
</ToggleGroup.Root>
</Popover.Content>
</Popover.Root>
</div>
{/if}
</div>
@@ -0,0 +1,603 @@
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import {
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
getConvertedTemperature,
getConvertedVelocity,
getDistanceUnits,
getDistanceWithUnits,
getElevationWithUnits,
getHeartRateWithUnits,
getPowerWithUnits,
getTemperatureWithUnits,
getVelocityWithUnits,
} from '$lib/units';
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { get, type Readable, type Writable } from 'svelte/store';
import { map } from '$lib/components/map/map';
import type { GPXStatistics } from 'gpx';
import { mode } from 'mode-watcher';
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
Chart.defaults.font.family =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
export class ElevationProfile {
private _chart: Chart | null = null;
private _canvas: HTMLCanvasElement;
private _overlay: HTMLCanvasElement;
private _marker: mapboxgl.Marker | null = null;
private _dragging = false;
private _panning = false;
private _gpxStatistics: Readable<GPXStatistics>;
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
private _additionalDatasets: Readable<string[]>;
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
constructor(
gpxStatistics: Readable<GPXStatistics>,
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
additionalDatasets: Readable<string[]>,
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
canvas: HTMLCanvasElement,
overlay: HTMLCanvasElement
) {
this._gpxStatistics = gpxStatistics;
this._slicedGPXStatistics = slicedGPXStatistics;
this._additionalDatasets = additionalDatasets;
this._elevationFill = elevationFill;
this._canvas = canvas;
this._overlay = overlay;
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
this._marker = new mapboxgl.Marker({
element,
});
import('chartjs-plugin-zoom').then((module) => {
Chart.register(module.default);
this.initialize();
this._gpxStatistics.subscribe(() => {
this.updateData();
});
this._slicedGPXStatistics.subscribe(() => {
this.updateOverlay();
});
distanceUnits.subscribe(() => {
this.updateData();
});
velocityUnits.subscribe(() => {
this.updateData();
});
temperatureUnits.subscribe(() => {
this.updateData();
});
this._additionalDatasets.subscribe(() => {
this.updateDataVisibility();
});
this._elevationFill.subscribe(() => {
this.updateFill();
});
});
}
initialize() {
let options = {
animation: false,
parsing: false,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
ticks: {
callback: function (value: number) {
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
},
align: 'inner',
maxRotation: 0,
},
},
y: {
type: 'linear',
ticks: {
callback: function (value: number) {
return getElevationWithUnits(value, false);
},
},
},
},
datasets: {
line: {
pointRadius: 0,
tension: 0.4,
borderWidth: 2,
cubicInterpolationMode: 'monotone',
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
plugins: {
legend: {
display: false,
},
decimation: {
enabled: true,
},
tooltip: {
enabled: () => !this._dragging && !this._panning,
callbacks: {
title: () => {
return '';
},
label: (context: Chart.TooltipContext) => {
let point = context.raw;
if (context.datasetIndex === 0) {
const map_ = get(map);
if (map_ && this._marker) {
if (this._dragging) {
this._marker.remove();
} else {
this._marker.setLngLat(point.coordinates);
this._marker.addTo(map_);
}
}
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) {
return `${get(velocityUnits) === 'speed' ? i18n._('quantities.speed') : i18n._('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) {
return `${i18n._('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) {
return `${i18n._('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
} else if (context.datasetIndex === 4) {
return `${i18n._('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 5) {
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
}
},
afterBody: (contexts: Chart.TooltipContext[]) => {
let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return;
let point = context[0].raw;
let slope = {
at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length),
};
let surface = point.extensions.surface
? point.extensions.surface
: 'unknown';
let highway = point.extensions.highway
? point.extensions.highway
: 'unknown';
let sacScale = point.extensions.sac_scale;
let mtbScale = point.extensions.mtb_scale;
let labels = [
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${i18n._('quantities.slope')}: ${slope.at} %${get(this._elevationFill) === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
];
if (get(this._elevationFill) === 'surface') {
labels.push(
` ${i18n._('quantities.surface')}: ${i18n._(`toolbar.routing.surface.${surface}`)}`
);
}
if (get(this._elevationFill) === 'highway') {
labels.push(
` ${i18n._('quantities.highway')}: ${i18n._(`toolbar.routing.highway.${highway}`)}${
sacScale
? ` (${i18n._(`toolbar.routing.sac_scale.${sacScale}`)})`
: ''
}`
);
if (mtbScale) {
labels.push(
` ${i18n._('toolbar.routing.mtb_scale')}: ${mtbScale}`
);
}
}
if (point.time) {
labels.push(
` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
);
}
return labels;
},
},
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'shift',
onPanStart: () => {
this._panning = true;
this._slicedGPXStatistics.set(undefined);
},
onPanComplete: () => {
this._panning = false;
},
},
zoom: {
wheel: {
enabled: true,
},
mode: 'x',
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
if (
event.deltaY < 0 &&
Math.abs(
this._chart.getInitialScaleBounds().x.max /
this._chart.options.plugins.zoom.limits.x.minRange -
this._chart.getZoomLevel()
) < 0.01
) {
// Disable wheel pan if zoomed in to the max, and zooming in
return false;
}
this._slicedGPXStatistics.set(undefined);
},
},
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1,
},
},
},
},
stacked: false,
onResize: () => {
this.updateOverlay();
},
};
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
datasets.forEach((id) => {
options.scales[`y${id}`] = {
type: 'linear',
position: 'right',
grid: {
display: false,
},
reverse: () => id === 'speed' && get(velocityUnits) === 'pace',
display: false,
};
});
this._chart = new Chart(this._canvas, {
type: 'line',
data: {
datasets: [],
},
options,
plugins: [
{
id: 'toggleMarker',
events: ['mouseout'],
afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
if (args.event.type === 'mouseout') {
const map_ = get(map);
if (map_ && this._marker) {
this._marker.remove();
}
}
},
},
],
});
let startIndex = 0;
let endIndex = 0;
const getIndex = (evt) => {
if (!this._chart) {
return undefined;
}
const points = this._chart.getElementsAtEventForMode(
evt,
'x',
{
intersect: false,
},
true
);
if (points.length === 0) {
const rect = this._canvas.getBoundingClientRect();
if (evt.x - rect.left <= this._chart.chartArea.left) {
return 0;
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
return get(this._gpxStatistics).local.points.length - 1;
} else {
return undefined;
}
}
let point = points.find((point) => point.element.raw);
if (point) {
return point.element.raw.index;
} else {
return points[0].index;
}
};
let dragStarted = false;
const onMouseDown = (evt) => {
if (evt.shiftKey) {
// Panning interaction
return;
}
dragStarted = true;
this._canvas.style.cursor = 'col-resize';
startIndex = getIndex(evt);
};
const onMouseMove = (evt) => {
if (dragStarted) {
this._dragging = true;
endIndex = getIndex(evt);
if (endIndex !== undefined) {
if (startIndex === undefined) {
startIndex = endIndex;
} else if (startIndex !== endIndex) {
this._slicedGPXStatistics.set([
get(this._gpxStatistics).slice(
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex),
]);
}
}
}
};
const onMouseUp = (evt) => {
dragStarted = false;
this._dragging = false;
this._canvas.style.cursor = '';
endIndex = getIndex(evt);
if (startIndex === endIndex) {
this._slicedGPXStatistics.set(undefined);
}
};
this._canvas.addEventListener('pointerdown', onMouseDown);
this._canvas.addEventListener('pointermove', onMouseMove);
this._canvas.addEventListener('pointerup', onMouseUp);
}
updateData() {
if (!this._chart) {
return;
}
const data = get(this._gpxStatistics);
this._chart.data.datasets[0] = {
label: i18n._('quantities.elevation'),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index],
},
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index,
};
}),
normalized: true,
fill: 'start',
order: 1,
segment: {},
};
this._chart.data.datasets[1] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]),
index: index,
};
}),
normalized: true,
yAxisID: 'yspeed',
};
this._chart.data.datasets[2] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(),
index: index,
};
}),
normalized: true,
yAxisID: 'yhr',
};
this._chart.data.datasets[3] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(),
index: index,
};
}),
normalized: true,
yAxisID: 'ycad',
};
this._chart.data.datasets[4] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()),
index: index,
};
}),
normalized: true,
yAxisID: 'yatemp',
};
this._chart.data.datasets[5] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(),
index: index,
};
}),
normalized: true,
yAxisID: 'ypower',
};
this._chart.options.scales.x['min'] = 0;
this._chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
this.setVisibility();
this.setFill();
this._chart.update();
}
updateDataVisibility() {
if (!this._chart) {
return;
}
this.setVisibility();
this._chart.update();
}
setVisibility() {
if (!this._chart) {
return;
}
const additionalDatasets = get(this._additionalDatasets);
let includeSpeed = additionalDatasets.includes('speed');
let includeHeartRate = additionalDatasets.includes('hr');
let includeCadence = additionalDatasets.includes('cad');
let includeTemperature = additionalDatasets.includes('atemp');
let includePower = additionalDatasets.includes('power');
if (this._chart.data.datasets.length == 6) {
this._chart.data.datasets[1].hidden = !includeSpeed;
this._chart.data.datasets[2].hidden = !includeHeartRate;
this._chart.data.datasets[3].hidden = !includeCadence;
this._chart.data.datasets[4].hidden = !includeTemperature;
this._chart.data.datasets[5].hidden = !includePower;
}
}
updateFill() {
if (!this._chart) {
return;
}
this.setFill();
this._chart.update();
}
setFill() {
if (!this._chart) {
return;
}
const elevationFill = get(this._elevationFill);
if (elevationFill === 'slope') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.slopeFillCallback,
};
} else if (elevationFill === 'surface') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.surfaceFillCallback,
};
} else if (elevationFill === 'highway') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.highwayFillCallback,
};
} else {
this._chart.data.datasets[0]['segment'] = {};
}
}
updateOverlay() {
if (!this._chart) {
return;
}
this._overlay.width = this._canvas.width / window.devicePixelRatio;
this._overlay.height = this._canvas.height / window.devicePixelRatio;
this._overlay.style.width = `${this._overlay.width}px`;
this._overlay.style.height = `${this._overlay.height}px`;
const slicedGPXStatistics = get(this._slicedGPXStatistics);
if (slicedGPXStatistics) {
let startIndex = slicedGPXStatistics[1];
let endIndex = slicedGPXStatistics[2];
// Draw selection rectangle
let selectionContext = this._overlay.getContext('2d');
if (selectionContext) {
selectionContext.fillStyle = mode.current === 'dark' ? 'white' : 'black';
selectionContext.globalAlpha = mode.current === 'dark' ? 0.2 : 0.1;
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
const gpxStatistics = get(this._gpxStatistics);
let startPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[startIndex])
);
let endPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
);
selectionContext.fillRect(
startPixel,
this._chart.chartArea.top,
endPixel - startPixel,
this._chart.chartArea.height
);
}
} else if (this._overlay) {
let selectionContext = this._overlay.getContext('2d');
if (selectionContext) {
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
}
}
}
slopeFillCallback(context) {
return getSlopeColor(context.p0.raw.slope.segment);
}
surfaceFillCallback(context) {
return getSurfaceColor(context.p0.raw.extensions.surface);
}
highwayFillCallback(context) {
return getHighwayColor(
context.p0.raw.extensions.highway,
context.p0.raw.extensions.sac_scale,
context.p0.raw.extensions.mtb_scale
);
}
destroy() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
if (this._marker) {
this._marker.remove();
}
}
}
@@ -1,34 +1,34 @@
<script lang="ts"> <script lang="ts">
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte'; import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte'; import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte'; import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte'; import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/Map.svelte'; import Map from '$lib/components/map/Map.svelte';
import LayerControl from '$lib/components/layer-control/LayerControl.svelte'; import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
import OpenIn from '$lib/components/embedding/OpenIn.svelte'; import OpenIn from '$lib/components/embedding/OpenIn.svelte';
import { import { writable } from 'svelte/store';
gpxStatistics,
slicedGPXStatistics,
embedding,
loadFile,
map,
updateGPXData
} from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
import { readable } from 'svelte/store';
import type { GPXFile } from 'gpx'; import type { GPXFile } from 'gpx';
import { selection } from '$lib/components/file-list/Selection';
import { ListFileItem } from '$lib/components/file-list/FileList';
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions, getFilesFromEmbeddingOptions,
type EmbeddingOptions type EmbeddingOptions,
} from './Embedding'; } from './embedding';
import { mode, setMode } from 'mode-watcher'; import { setMode } from 'mode-watcher';
import { browser } from '$app/environment'; import { settings } from '$lib/logic/settings';
import { fileStateCollection } from '$lib/logic/file-state';
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
import { loadFile } from '$lib/logic/file-actions';
import { selection } from '$lib/logic/selection';
import { untrack } from 'svelte';
$embedding = true; let {
useHash = true,
options = $bindable(),
hash = $bindable(),
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
let additionalDatasets = writable<string[]>([]);
let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined);
const { const {
currentBasemap, currentBasemap,
@@ -37,202 +37,75 @@
temperatureUnits, temperatureUnits,
fileOrder, fileOrder,
distanceMarkers, distanceMarkers,
directionMarkers directionMarkers,
} = settings; } = settings;
export let useHash = true; settings.initialize();
export let options: EmbeddingOptions;
export let hash: string;
let prevSettings = {
distanceMarkers: false,
directionMarkers: false,
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system'
};
function applyOptions() { function applyOptions() {
fileObservers.update(($fileObservers) => { let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
$fileObservers.clear(); (url) => {
return $fileObservers; return fetch(url)
});
let downloads: Promise<GPXFile | null>[] = [];
getFilesFromEmbeddingOptions(options).forEach((url) => {
downloads.push(
fetch(url)
.then((response) => response.blob()) .then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? url)) .then((blob) => new File([blob], url.split('/').pop() ?? url))
.then(loadFile) .then(loadFile);
}
); );
}); Promise.all(downloads).then((answers) => {
const files = answers.filter((file) => file !== null) as GPXFile[];
Promise.all(downloads).then((files) => {
let ids: string[] = []; let ids: string[] = [];
let bounds = {
southWest: {
lat: 90,
lon: 180
},
northEast: {
lat: -90,
lon: -180
}
};
fileObservers.update(($fileObservers) => {
files.forEach((file, index) => { files.forEach((file, index) => {
if (file === null) {
return;
}
let id = `gpx-${index}-embed`; let id = `gpx-${index}-embed`;
file._data.id = id; file._data.id = id;
let statistics = new GPXStatisticsTree(file);
$fileObservers.set(
id,
readable({
file,
statistics
})
);
ids.push(id); ids.push(id);
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
}); });
fileStateCollection.setEmbeddedFiles(files);
return $fileObservers; $fileOrder = ids;
selection.selectAll();
}); });
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
selection.update(($selection) => {
$selection.clear();
ids.forEach((id) => {
$selection.toggle(new ListFileItem(id));
});
return $selection;
});
if (hash.length === 0) {
map.subscribe(($map) => {
if ($map) {
$map.fitBounds(
[
bounds.southWest.lon,
bounds.southWest.lat,
bounds.northEast.lon,
bounds.northEast.lat
],
{
padding: 80,
linear: true,
easing: () => 1
}
);
}
});
}
});
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
$currentBasemap = options.basemap; $currentBasemap = options.basemap;
} }
if (options.distanceMarkers !== $distanceMarkers) {
$distanceMarkers = options.distanceMarkers; $distanceMarkers = options.distanceMarkers;
}
if (options.directionMarkers !== $directionMarkers) {
$directionMarkers = options.directionMarkers; $directionMarkers = options.directionMarkers;
}
if (options.distanceUnits !== $distanceUnits) {
$distanceUnits = options.distanceUnits; $distanceUnits = options.distanceUnits;
}
if (options.velocityUnits !== $velocityUnits) {
$velocityUnits = options.velocityUnits; $velocityUnits = options.velocityUnits;
}
if (options.temperatureUnits !== $temperatureUnits) {
$temperatureUnits = options.temperatureUnits; $temperatureUnits = options.temperatureUnits;
} if (options.theme != 'system') {
if (options.theme !== $mode) {
setMode(options.theme); setMode(options.theme);
} }
additionalDatasets.set(
[
options.elevation.speed ? 'speed' : null,
options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null,
].filter((dataset) => dataset !== null)
);
elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
} }
onMount(() => { $effect(() => {
prevSettings.distanceMarkers = $distanceMarkers; options;
prevSettings.directionMarkers = $directionMarkers; untrack(applyOptions);
prevSettings.distanceUnits = $distanceUnits;
prevSettings.velocityUnits = $velocityUnits;
prevSettings.temperatureUnits = $temperatureUnits;
prevSettings.theme = $mode ?? 'system';
});
$: if (browser && options) {
applyOptions();
}
$: if ($fileOrder) {
updateGPXData();
}
onDestroy(() => {
if ($distanceMarkers !== prevSettings.distanceMarkers) {
$distanceMarkers = prevSettings.distanceMarkers;
}
if ($directionMarkers !== prevSettings.directionMarkers) {
$directionMarkers = prevSettings.directionMarkers;
}
if ($distanceUnits !== prevSettings.distanceUnits) {
$distanceUnits = prevSettings.distanceUnits;
}
if ($velocityUnits !== prevSettings.velocityUnits) {
$velocityUnits = prevSettings.velocityUnits;
}
if ($temperatureUnits !== prevSettings.temperatureUnits) {
$temperatureUnits = prevSettings.temperatureUnits;
}
if ($mode !== prevSettings.theme) {
setMode(prevSettings.theme);
}
$selection.clear();
$fileObservers.clear();
$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
}); });
</script> </script>
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip"> <div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
<div class="grow relative"> <div class="grow relative">
<Map <Map
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}" class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
accessToken={options.token} accessToken={options.token}
geocoder={false} geocoder={false}
geolocate={false} geolocate={true}
hash={useHash} hash={useHash}
/> />
<OpenIn bind:files={options.files} bind:ids={options.ids} /> <OpenIn files={options.files} ids={options.ids} />
<LayerControl /> <LayerControl />
<GPXLayers /> <GPXLayers />
{#if $fileObservers.size > 1} {#if $fileStateCollection.size > 1}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30"> <div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<FileList orientation="horizontal" /> <FileList orientation="horizontal" />
</div> </div>
@@ -252,17 +125,9 @@
<ElevationProfile <ElevationProfile
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
additionalDatasets={[ {additionalDatasets}
options.elevation.speed ? 'speed' : null, {elevationFill}
options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null
].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill}
panelSize={options.elevation.height}
showControls={options.elevation.controls} showControls={options.elevation.controls}
class="py-2"
/> />
{/if} {/if}
</div> </div>
@@ -13,68 +13,66 @@
SquareActivity, SquareActivity,
Coins, Coins,
Milestone, Milestone,
Video Video,
} from 'lucide-svelte'; } from '@lucide/svelte';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
defaultEmbeddingOptions,
getCleanedEmbeddingOptions, getCleanedEmbeddingOptions,
getDefaultEmbeddingOptions getMergedEmbeddingOptions,
} from './Embedding'; } from './embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte'; import Embedding from './Embedding.svelte';
import { map } from '$lib/stores'; import { onDestroy } from 'svelte';
import { tick } from 'svelte';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { map } from '$lib/components/map/map';
import { mode } from 'mode-watcher';
let options = getDefaultEmbeddingOptions(); let options = $state(
options.token = 'YOUR_MAPBOX_TOKEN'; getMergedEmbeddingOptions(
options.files = [ {
token: 'YOUR_MAPBOX_TOKEN',
theme: mode.current,
},
defaultEmbeddingOptions
)
);
let files = $state(
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx' 'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
]; );
let driveIds = $state('');
let files = options.files[0]; let iframeOptions = $derived(
$: { getMergedEmbeddingOptions(
let urls = files.split(','); {
urls = urls.filter((url) => url.length > 0); token:
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
options.files = urls;
}
}
let driveIds = '';
$: {
let ids = driveIds.split(',');
ids = ids.filter((id) => id.length > 0);
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
options.ids = ids;
}
}
let manualCamera = false;
let zoom = '0';
let lat = '0';
let lon = '0';
let bearing = '0';
let pitch = '0';
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
$: iframeOptions =
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN' options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN }) ? PUBLIC_MAPBOX_TOKEN
: options; : options.token,
files: files.split(',').filter((url) => url.length > 0),
ids: driveIds.split(',').filter((id) => id.length > 0),
elevation: {
fill: options.elevation.fill === 'none' ? undefined : options.elevation.fill,
},
},
options
)
);
async function resizeMap() { let manualCamera = $state(false);
if ($map) { let zoom = $state('0');
await tick(); let lat = $state('0');
$map.resize(); let lon = $state('0');
} let bearing = $state('0');
} let pitch = $state('0');
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
$: if (options.elevation.height || options.elevation.show) { $effect(() => {
resizeMap(); if (options.elevation.show || options.elevation.height) {
map.resize();
} }
});
function updateCamera() { function updateCamera() {
if ($map) { if ($map) {
@@ -87,113 +85,116 @@
} }
} }
$: if ($map) { map.onLoad((map_) => {
$map.on('moveend', updateCamera); map_.on('moveend', updateCamera);
});
onDestroy(() => {
if ($map) {
$map.off('moveend', updateCamera);
} }
});
</script> </script>
<Card.Root id="embedding-playground"> <Card.Root id="embedding-playground">
<Card.Header> <Card.Header>
<Card.Title>{$_('embedding.title')}</Card.Title> <Card.Title>{i18n._('embedding.title')}</Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<fieldset class="flex flex-col gap-3"> <fieldset class="flex flex-col gap-3">
<Label for="token">{$_('embedding.mapbox_token')}</Label> <Label for="token">{i18n._('embedding.mapbox_token')}</Label>
<Input id="token" type="text" class="h-8" bind:value={options.token} /> <Input id="token" type="text" class="h-8" bind:value={options.token} />
<Label for="file_urls">{$_('embedding.file_urls')}</Label> <Label for="file_urls">{i18n._('embedding.file_urls')}</Label>
<Input id="file_urls" type="text" class="h-8" bind:value={files} /> <Input id="file_urls" type="text" class="h-8" bind:value={files} />
<Label for="drive_ids">{$_('embedding.drive_ids')}</Label> <Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} /> <Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
<Label for="basemap">{$_('embedding.basemap')}</Label> <Label for="basemap">{i18n._('embedding.basemap')}</Label>
<Select.Root <Select.Root type="single" bind:value={options.basemap}>
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
onSelectedChange={(selected) => {
if (selected?.value) {
options.basemap = selected?.value;
}
}}
>
<Select.Trigger id="basemap" class="w-full h-8"> <Select.Trigger id="basemap" class="w-full h-8">
<Select.Value /> {i18n._(`layers.label.${options.basemap}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll"> <Select.Content class="max-h-60 overflow-y-scroll">
{#each allowedEmbeddingBasemaps as basemap} {#each allowedEmbeddingBasemaps as basemap}
<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item> <Select.Item value={basemap}
>{i18n._(`layers.label.${basemap}`)}</Select.Item
>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Label for="profile">{$_('menu.elevation_profile')}</Label> <Label for="profile">{i18n._('menu.elevation_profile')}</Label>
<Checkbox id="profile" bind:checked={options.elevation.show} /> <Checkbox id="profile" bind:checked={options.elevation.show} />
</div> </div>
{#if options.elevation.show} {#if options.elevation.show}
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1"> <div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
<Label class="flex flex-row items-center gap-2"> <Label class="flex flex-row items-center gap-2">
{$_('embedding.height')} {i18n._('embedding.height')}
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" /> <Input
type="number"
bind:value={options.elevation.height}
class="h-8 w-20"
/>
</Label> </Label>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<span class="shrink-0"> <span class="shrink-0">
{$_('embedding.fill_by')} {i18n._('embedding.fill_by')}
</span> </span>
<Select.Root <Select.Root type="single" bind:value={options.elevation.fill}>
selected={{ value: 'none', label: $_('embedding.none') }}
onSelectedChange={(selected) => {
let value = selected?.value;
if (value === 'none') {
options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface') {
options.elevation.fill = value;
}
}}
>
<Select.Trigger class="grow h-8"> <Select.Trigger class="grow h-8">
<Select.Value /> {options.elevation.fill !== 'none'
? i18n._(`quantities.${options.elevation.fill}`)
: i18n._('embedding.none')}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item> <Select.Item value="slope">{i18n._('quantities.slope')}</Select.Item
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item> >
<Select.Item value="none">{$_('embedding.none')}</Select.Item> <Select.Item value="surface"
>{i18n._('quantities.surface')}</Select.Item
>
<Select.Item value="highway"
>{i18n._('quantities.highway')}</Select.Item
>
<Select.Item value="none">{i18n._('embedding.none')}</Select.Item>
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="controls" bind:checked={options.elevation.controls} /> <Checkbox id="controls" bind:checked={options.elevation.controls} />
<Label for="controls">{$_('embedding.show_controls')}</Label> <Label for="controls">{i18n._('embedding.show_controls')}</Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-speed" bind:checked={options.elevation.speed} /> <Checkbox id="show-speed" bind:checked={options.elevation.speed} />
<Label for="show-speed" class="flex flex-row items-center gap-1"> <Label for="show-speed" class="flex flex-row items-center gap-1">
<Zap size="16" /> <Zap size="16" />
{$_('chart.show_speed')} {i18n._('quantities.speed')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-hr" bind:checked={options.elevation.hr} /> <Checkbox id="show-hr" bind:checked={options.elevation.hr} />
<Label for="show-hr" class="flex flex-row items-center gap-1"> <Label for="show-hr" class="flex flex-row items-center gap-1">
<HeartPulse size="16" /> <HeartPulse size="16" />
{$_('chart.show_heartrate')} {i18n._('quantities.heartrate')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-cad" bind:checked={options.elevation.cad} /> <Checkbox id="show-cad" bind:checked={options.elevation.cad} />
<Label for="show-cad" class="flex flex-row items-center gap-1"> <Label for="show-cad" class="flex flex-row items-center gap-1">
<Orbit size="16" /> <Orbit size="16" />
{$_('chart.show_cadence')} {i18n._('quantities.cadence')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-temp" bind:checked={options.elevation.temp} /> <Checkbox id="show-temp" bind:checked={options.elevation.temp} />
<Label for="show-temp" class="flex flex-row items-center gap-1"> <Label for="show-temp" class="flex flex-row items-center gap-1">
<Thermometer size="16" /> <Thermometer size="16" />
{$_('chart.show_temperature')} {i18n._('quantities.temperature')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-power" bind:checked={options.elevation.power} /> <Checkbox id="show-power" bind:checked={options.elevation.power} />
<Label for="show-power" class="flex flex-row items-center gap-1"> <Label for="show-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" /> <SquareActivity size="16" />
{$_('chart.show_power')} {i18n._('quantities.power')}
</Label> </Label>
</div> </div>
</div> </div>
@@ -202,75 +203,75 @@
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} /> <Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
<Label for="distance-markers" class="flex flex-row items-center gap-1"> <Label for="distance-markers" class="flex flex-row items-center gap-1">
<Coins size="16" /> <Coins size="16" />
{$_('menu.distance_markers')} {i18n._('menu.distance_markers')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} /> <Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
<Label for="direction-markers" class="flex flex-row items-center gap-1"> <Label for="direction-markers" class="flex flex-row items-center gap-1">
<Milestone size="16" /> <Milestone size="16" />
{$_('menu.direction_markers')} {i18n._('menu.direction_markers')}
</Label> </Label>
</div> </div>
<div class="flex flex-row flex-wrap justify-between gap-3"> <div class="flex flex-row flex-wrap justify-between gap-3">
<Label class="flex flex-col items-start gap-2"> <Label class="flex flex-col items-start gap-2">
{$_('menu.distance_units')} {i18n._('menu.distance_units')}
<RadioGroup.Root bind:value={options.distanceUnits}> <RadioGroup.Root bind:value={options.distanceUnits}>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="metric" id="metric" /> <RadioGroup.Item value="metric" id="metric" />
<Label for="metric">{$_('menu.metric')}</Label> <Label for="metric">{i18n._('menu.metric')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="imperial" id="imperial" /> <RadioGroup.Item value="imperial" id="imperial" />
<Label for="imperial">{$_('menu.imperial')}</Label> <Label for="imperial">{i18n._('menu.imperial')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="nautical" id="nautical" /> <RadioGroup.Item value="nautical" id="nautical" />
<Label for="nautical">{$_('menu.nautical')}</Label> <Label for="nautical">{i18n._('menu.nautical')}</Label>
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
</Label> </Label>
<Label class="flex flex-col items-start gap-2"> <Label class="flex flex-col items-start gap-2">
{$_('menu.velocity_units')} {i18n._('menu.velocity_units')}
<RadioGroup.Root bind:value={options.velocityUnits}> <RadioGroup.Root bind:value={options.velocityUnits}>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="speed" id="speed" /> <RadioGroup.Item value="speed" id="speed" />
<Label for="speed">{$_('quantities.speed')}</Label> <Label for="speed">{i18n._('quantities.speed')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="pace" id="pace" /> <RadioGroup.Item value="pace" id="pace" />
<Label for="pace">{$_('quantities.pace')}</Label> <Label for="pace">{i18n._('quantities.pace')}</Label>
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
</Label> </Label>
<Label class="flex flex-col items-start gap-2"> <Label class="flex flex-col items-start gap-2">
{$_('menu.temperature_units')} {i18n._('menu.temperature_units')}
<RadioGroup.Root bind:value={options.temperatureUnits}> <RadioGroup.Root bind:value={options.temperatureUnits}>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="celsius" id="celsius" /> <RadioGroup.Item value="celsius" id="celsius" />
<Label for="celsius">{$_('menu.celsius')}</Label> <Label for="celsius">{i18n._('menu.celsius')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="fahrenheit" id="fahrenheit" /> <RadioGroup.Item value="fahrenheit" id="fahrenheit" />
<Label for="fahrenheit">{$_('menu.fahrenheit')}</Label> <Label for="fahrenheit">{i18n._('menu.fahrenheit')}</Label>
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
</Label> </Label>
</div> </div>
<Label class="flex flex-col items-start gap-2"> <Label class="flex flex-col items-start gap-2">
{$_('menu.mode')} {i18n._('menu.mode')}
<RadioGroup.Root bind:value={options.theme} class="flex flex-row"> <RadioGroup.Root bind:value={options.theme} class="flex flex-row">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="system" id="system" /> <RadioGroup.Item value="system" id="system" />
<Label for="system">{$_('menu.system')}</Label> <Label for="system">{i18n._('menu.system')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light" /> <RadioGroup.Item value="light" id="light" />
<Label for="light">{$_('menu.light')}</Label> <Label for="light">{i18n._('menu.light')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark" /> <RadioGroup.Item value="dark" id="dark" />
<Label for="dark">{$_('menu.dark')}</Label> <Label for="dark">{i18n._('menu.dark')}</Label>
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
</Label> </Label>
@@ -279,47 +280,48 @@
<Checkbox id="manual-camera" bind:checked={manualCamera} /> <Checkbox id="manual-camera" bind:checked={manualCamera} />
<Label for="manual-camera" class="flex flex-row items-center gap-1"> <Label for="manual-camera" class="flex flex-row items-center gap-1">
<Video size="16" /> <Video size="16" />
{$_('embedding.manual_camera')} {i18n._('embedding.manual_camera')}
</Label> </Label>
</div> </div>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{$_('embedding.manual_camera_description')} {i18n._('embedding.manual_camera_description')}
</p> </p>
<div class="flex flex-row flex-wrap items-center gap-6"> <div class="flex flex-row flex-wrap items-center gap-6">
<Label class="flex flex-col gap-1"> <Label class="flex flex-col gap-1">
<span>{$_('embedding.latitude')}</span> <span>{i18n._('embedding.latitude')}</span>
<span>{lat}</span> <span>{lat}</span>
</Label> </Label>
<Label class="flex flex-col gap-1"> <Label class="flex flex-col gap-1">
<span>{$_('embedding.longitude')}</span> <span>{i18n._('embedding.longitude')}</span>
<span>{lon}</span> <span>{lon}</span>
</Label> </Label>
<Label class="flex flex-col gap-1"> <Label class="flex flex-col gap-1">
<span>{$_('embedding.zoom')}</span> <span>{i18n._('embedding.zoom')}</span>
<span>{zoom}</span> <span>{zoom}</span>
</Label> </Label>
<Label class="flex flex-col gap-1"> <Label class="flex flex-col gap-1">
<span>{$_('embedding.bearing')}</span> <span>{i18n._('embedding.bearing')}</span>
<span>{bearing}</span> <span>{bearing}</span>
</Label> </Label>
<Label class="flex flex-col gap-1"> <Label class="flex flex-col gap-1">
<span>{$_('embedding.pitch')}</span> <span>{i18n._('embedding.pitch')}</span>
<span>{pitch}</span> <span>{pitch}</span>
</Label> </Label>
</div> </div>
</div> </div>
<Label> <Label>
{$_('embedding.preview')} {i18n._('embedding.preview')}
</Label> </Label>
<div class="relative h-[600px]"> <div class="relative h-[600px]">
<Embedding bind:options={iframeOptions} bind:hash useHash={false} /> <Embedding options={iframeOptions} bind:hash useHash={false} />
</div> </div>
<Label> <Label>
{$_('embedding.code')} {i18n._('embedding.code')}
</Label> </Label>
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all"> <pre
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<code class="language-html"> <code class="language-html">
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`} {`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(iframeOptions)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
</code> </code>
</pre> </pre>
</fieldset> </fieldset>
@@ -2,22 +2,27 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { _, locale } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
export let files: string[]; let {
export let ids: string[]; files,
ids,
}: {
files: string[];
ids: string[];
} = $props();
</script> </script>
<Button <Button
variant="ghost" variant="ghost"
class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12" class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
href="{getURLForLanguage($locale, '/app')}?{files.length > 0 href="{getURLForLanguage(i18n.lang, '/app')}?{files.length > 0
? `files=${encodeURIComponent(JSON.stringify(files))}` ? `files=${encodeURIComponent(JSON.stringify(files))}`
: ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0 : ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0
? `ids=${encodeURIComponent(JSON.stringify(ids))}` ? `ids=${encodeURIComponent(JSON.stringify(ids))}`
: ''}" : ''}"
target="_blank" target="_blank"
> >
{$_('menu.open_in')} {i18n._('menu.open_in')}
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" /> <Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
</Button> </Button>
@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
show: boolean; show: boolean;
height: number; height: number;
controls: boolean; controls: boolean;
fill: 'slope' | 'surface' | undefined; fill: 'slope' | 'surface' | 'highway' | 'none';
speed: boolean; speed: boolean;
hr: boolean; hr: boolean;
cad: boolean; cad: boolean;
@@ -34,32 +34,32 @@ export const defaultEmbeddingOptions = {
show: true, show: true,
height: 170, height: 170,
controls: true, controls: true,
fill: undefined, fill: 'none',
speed: false, speed: false,
hr: false, hr: false,
cad: false, cad: false,
temp: false, temp: false,
power: false power: false,
}, },
distanceMarkers: false, distanceMarkers: false,
directionMarkers: false, directionMarkers: false,
distanceUnits: 'metric', distanceUnits: 'metric',
velocityUnits: 'speed', velocityUnits: 'speed',
temperatureUnits: 'celsius', temperatureUnits: 'celsius',
theme: 'system' theme: 'system',
}; };
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
}
export function getMergedEmbeddingOptions( export function getMergedEmbeddingOptions(
options: any, options: any,
defaultOptions: any = defaultEmbeddingOptions defaultOptions: any = defaultEmbeddingOptions
): EmbeddingOptions { ): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions)); const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) { for (const key in options) {
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) { if (
typeof options[key] === 'object' &&
options[key] !== null &&
!Array.isArray(options[key])
) {
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]); mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else { } else {
mergedOptions[key] = options[key]; mergedOptions[key] = options[key];
@@ -79,7 +79,10 @@ export function getCleanedEmbeddingOptions(
cleanedOptions[key] !== null && cleanedOptions[key] !== null &&
!Array.isArray(cleanedOptions[key]) !Array.isArray(cleanedOptions[key])
) { ) {
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]); cleanedOptions[key] = getCleanedEmbeddingOptions(
cleanedOptions[key],
defaultOptions[key]
);
if (Object.keys(cleanedOptions[key]).length === 0) { if (Object.keys(cleanedOptions[key]).length === 0) {
delete cleanedOptions[key]; delete cleanedOptions[key];
} }
@@ -141,7 +144,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
} }
if (options.has('slope')) { if (options.has('slope')) {
newOptions.elevation = { newOptions.elevation = {
fill: 'slope' fill: 'slope',
}; };
} }
return newOptions; return newOptions;
@@ -0,0 +1,195 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Separator } from '$lib/components/ui/separator';
import { Dialog } from 'bits-ui';
import {
exportAllFiles,
exportSelectedFiles,
ExportState,
exportState,
} from '$lib/components/export/utils.svelte';
import { currentTool } from '$lib/components/toolbar/tools';
import {
Download,
Zap,
Earth,
HeartPulse,
Orbit,
Thermometer,
SquareActivity,
} from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { GPXStatistics } from 'gpx';
import { ListRootItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store';
let open = $derived(exportState.current !== ExportState.NONE);
let exportOptions: Record<string, boolean> = $state({
time: true,
hr: true,
cad: true,
atemp: true,
power: true,
extensions: false,
});
let hide: Record<string, boolean> = $derived.by(() => {
if (exportState.current === ExportState.NONE) {
return {
time: false,
hr: false,
cad: false,
atemp: false,
power: false,
extensions: false,
};
} else {
let statistics = $gpxStatistics;
if (exportState.current === ExportState.ALL) {
statistics = Array.from(get(fileStateCollection).values())
.map((file) => file.statistics)
.reduce((acc, cur) => {
if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
}
return acc;
}, new GPXStatistics());
}
return {
time: statistics.global.time.total === 0,
hr: statistics.global.hr.count === 0,
cad: statistics.global.cad.count === 0,
atemp: statistics.global.atemp.count === 0,
power: statistics.global.power.count === 0,
extensions: Object.keys(statistics.global.extensions).length === 0,
};
}
});
let exclude = $derived(Object.keys(exportOptions).filter((key) => !exportOptions[key]));
$effect(() => {
if (open) {
currentTool.set(null);
}
});
</script>
<Dialog.Root
bind:open
onOpenChange={(isOpen) => {
if (!isOpen) {
exportState.current = ExportState.NONE;
}
}}
>
<Dialog.Trigger class="hidden" />
<Dialog.Portal>
<Dialog.Content
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
>
<div
class="w-full flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 border rounded-md p-2 bg-secondary"
>
<span class="w-12 shrink-0 text-center text-xl">⚠️</span>
<span class="text-sm">
{i18n._('menu.support_message')}
</span>
</div>
<div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{i18n._('menu.support_button')}
<span>🙏</span>
</Button>
<Button
variant="outline"
class="grow"
onclick={() => {
if (exportState.current === ExportState.SELECTION) {
exportSelectedFiles(exclude);
} else if (exportState.current === ExportState.ALL) {
exportAllFiles(exclude);
}
open = false;
exportState.current = ExportState.NONE;
}}
>
<Download size="16" />
{#if $fileStateCollection.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)}
{i18n._('menu.download_file')}
{:else}
{i18n._('menu.download_files')}
{/if}
</Button>
</div>
<div
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some(
(v) => !v
)
? ''
: 'hidden'}"
>
<div class="w-full flex flex-row items-center gap-3">
<div class="grow">
<Separator />
</div>
<Label class="shrink-0">
{i18n._('menu.export_options')}
</Label>
<div class="grow">
<Separator />
</div>
</div>
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
<Checkbox id="export-time" bind:checked={exportOptions.time} />
<Label for="export-time" class="flex flex-row items-center gap-1">
<Zap size="16" />
{i18n._('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{i18n._('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
<Label for="export-cadence" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{i18n._('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
<Label for="export-temperature" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{i18n._('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
<Checkbox id="export-power" bind:checked={exportOptions.power} />
<Label for="export-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{i18n._('quantities.power')}
</Label>
</div>
<div
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
>
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
<Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" />
{i18n._('quantities.osm_extensions')}
</Label>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
@@ -0,0 +1,66 @@
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { settings } from '$lib/logic/settings';
import { buildGPX, type GPXFile } from 'gpx';
import FileSaver from 'file-saver';
import JSZip from 'jszip';
import { get } from 'svelte/store';
export enum ExportState {
NONE,
SELECTION,
ALL,
}
export const exportState = $state({
current: ExportState.NONE,
});
async function exportFiles(fileIds: string[], exclude: string[]) {
if (fileIds.length > 1) {
await exportFilesAsZip(fileIds, exclude);
} else {
const firstFileId = fileIds.at(0);
if (firstFileId != null) {
const file = fileStateCollection.getFile(firstFileId);
if (file) {
exportFile(file, exclude);
}
}
}
}
export async function exportSelectedFiles(exclude: string[]) {
const fileIds: string[] = [];
selection.applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => {
fileIds.push(fileId);
});
await exportFiles(fileIds, exclude);
}
export async function exportAllFiles(exclude: string[]) {
await exportFiles(get(settings.fileOrder), exclude);
}
function exportFile(file: GPXFile, exclude: string[]) {
const blob = new Blob([buildGPX(file, exclude)], { type: 'application/gpx+xml' });
FileSaver.saveAs(blob, `${file.metadata.name}.gpx`);
}
async function exportFilesAsZip(fileIds: string[], exclude: string[]) {
const zip = new JSZip();
for (const fileId of fileIds) {
const file = fileStateCollection.getFile(fileId);
if (file) {
const gpx = buildGPX(file, exclude);
let filename = file.metadata.name;
for (let i = 1; zip.files[filename + '.gpx']; i++) {
filename = file.metadata.name + `-${i}`;
}
zip.file(filename + '.gpx', gpx);
}
}
if (Object.keys(zip.files).length > 0) {
const blob = await zip.generateAsync({ type: 'blob' });
FileSaver.saveAs(blob, 'gpx-files.zip');
}
}
@@ -2,34 +2,33 @@
import { ScrollArea } from '$lib/components/ui/scroll-area/index'; import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import * as ContextMenu from '$lib/components/ui/context-menu'; import * as ContextMenu from '$lib/components/ui/context-menu';
import FileListNode from './FileListNode.svelte'; import FileListNode from './FileListNode.svelte';
import { fileObservers, settings } from '$lib/db'; import { onMount, setContext } from 'svelte';
import { setContext } from 'svelte'; import { ListFileItem, ListLevel, ListRootItem } from './file-list';
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList'; import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
import { copied, pasteSelection, selectAll, selection } from './Selection';
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { createFile } from '$lib/stores'; import { fileStateCollection } from '$lib/logic/file-state';
import { createFile, pasteSelection } from '$lib/logic/file-actions';
import { selection, copied } from '$lib/logic/selection';
import { allowedPastes } from './sortable-file-list';
export let orientation: 'vertical' | 'horizontal'; let {
export let recursive = false; orientation,
recursive = false,
class: className = '',
style = '',
}: {
orientation: 'vertical' | 'horizontal';
recursive?: boolean;
class?: string;
style?: string;
} = $props();
setContext('orientation', orientation); setContext('orientation', orientation);
setContext('recursive', recursive); setContext('recursive', recursive);
const { verticalFileView } = settings; onMount(() => {
if (orientation === 'horizontal') {
verticalFileView.subscribe(($vertical) => {
if ($vertical) {
selection.update(($selection) => {
$selection.forEach((item) => {
if ($selection.hasAnyChildren(item, false)) {
$selection.toggle(item);
}
});
return $selection;
});
} else {
selection.update(($selection) => { selection.update(($selection) => {
$selection.forEach((item) => { $selection.forEach((item) => {
if (!(item instanceof ListFileItem)) { if (!(item instanceof ListFileItem)) {
@@ -46,29 +45,32 @@
<ScrollArea <ScrollArea
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}" class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
{orientation} {orientation}
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'} scrollbarXClasses={orientation === 'vertical' ? '' : 'hidden'}
scrollbarYClasses={orientation === 'vertical' ? '' : ''} scrollbarYClasses={orientation === 'vertical' ? '' : ''}
> >
<div <div
class="flex {orientation === 'vertical' class="flex {orientation === 'vertical'
? 'flex-col py-1 pl-1 min-h-screen' ? 'flex-col py-1 pl-1 min-h-screen'
: 'flex-row'} {$$props.class ?? ''}" : 'flex-row'} {className ?? ''}"
{...$$restProps} {style}
> >
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} /> <FileListNode node={$fileStateCollection} item={new ListRootItem()} />
{#if orientation === 'vertical'} {#if orientation === 'vertical'}
<ContextMenu.Root> <ContextMenu.Root>
<ContextMenu.Trigger class="grow" /> <ContextMenu.Trigger class="grow" />
<ContextMenu.Content> <ContextMenu.Content>
<ContextMenu.Item on:click={createFile}> <ContextMenu.Item onclick={createFile}>
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new_file')} {i18n._('menu.new_file')}
<Shortcut key="+" ctrl={true} /> <Shortcut key="+" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Separator />
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}> <ContextMenu.Item
<FileStack size="16" class="mr-1" /> onclick={() => selection.selectAll()}
{$_('menu.select_all')} disabled={$fileStateCollection.size === 0}
>
<FileStack size="16" />
{i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Separator />
@@ -76,10 +78,10 @@
disabled={$copied === undefined || disabled={$copied === undefined ||
$copied.length === 0 || $copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)} !allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
on:click={pasteSelection} onclick={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" />
{$_('menu.paste')} {i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} /> <Shortcut key="V" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
</ContextMenu.Content> </ContextMenu.Content>
@@ -5,62 +5,64 @@
TrackSegment, TrackSegment,
Waypoint, Waypoint,
type AnyGPXTreeElement, type AnyGPXTreeElement,
type GPXTreeElement type GPXTreeElement,
} from 'gpx'; } from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index'; import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { settings, type GPXFileWithStatistics } from '$lib/db'; import { type Readable } from 'svelte/store';
import { get, type Readable } from 'svelte/store';
import FileListNodeContent from './FileListNodeContent.svelte'; import FileListNodeContent from './FileListNodeContent.svelte';
import FileListNodeLabel from './FileListNodeLabel.svelte'; import FileListNodeLabel from './FileListNodeLabel.svelte';
import { afterUpdate, getContext } from 'svelte'; import { getContext } from 'svelte';
import { import {
ListFileItem, ListFileItem,
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
ListWaypointsItem, ListWaypointsItem,
type ListItem, type ListItem,
type ListTrackItem type ListTrackItem,
} from './FileList'; } from './file-list';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { selection } from './Selection'; import { settings } from '$lib/logic/settings';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import { selection } from '$lib/logic/selection';
export let node: let {
node,
item,
}: {
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>> | Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement> | GPXTreeElement<AnyGPXTreeElement>
| Waypoint[] | Waypoint[]
| Waypoint; | Waypoint;
export let item: ListItem; item: ListItem;
} = $props();
let recursive = getContext<boolean>('recursive'); let recursive = getContext<boolean>('recursive');
let collapsible: CollapsibleTreeNode; let collapsible: CollapsibleTreeNode | undefined = $state();
$: label = let label = $derived(
node instanceof GPXFile && item instanceof ListFileItem node instanceof GPXFile && item instanceof ListFileItem
? node.metadata.name ? node.metadata.name
: node instanceof Track : node instanceof Track
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}` ? (node.name ?? `${i18n._('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
: node instanceof TrackSegment : node instanceof TrackSegment
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}` ? `${i18n._('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint : node instanceof Waypoint
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}` ? (node.name ??
`${i18n._('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
: node instanceof GPXFile && item instanceof ListWaypointsItem : node instanceof GPXFile && item instanceof ListWaypointsItem
? $_('gpx.waypoints') ? i18n._('gpx.waypoints')
: ''; : ''
);
const { verticalFileView } = settings; const { treeFileView } = settings;
function openIfSelectedChild() { $effect(() => {
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) { if (collapsible && $treeFileView && $selection.hasAnyChildren(item, false)) {
collapsible.openNode(); collapsible.openNode();
} }
} });
if ($selection) {
openIfSelectedChild();
}
afterUpdate(openIfSelectedChild);
</script> </script>
{#if node instanceof Map} {#if node instanceof Map}
@@ -71,12 +73,16 @@
<FileListNodeLabel {node} {item} {label} /> <FileListNodeLabel {node} {item} {label} />
{:else if recursive} {:else if recursive}
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}> <CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
<FileListNodeLabel {node} {item} {label} slot="trigger" /> {#snippet trigger()}
<div slot="content" class="ml-2"> <FileListNodeLabel {node} {item} {label} />
{/snippet}
{#snippet content()}
<div class="ml-4">
{#key node} {#key node}
<FileListNodeContent {node} {item} /> <FileListNodeContent {node} {item} />
{/key} {/key}
</div> </div>
{/snippet}
</CollapsibleTreeNode> </CollapsibleTreeNode>
{:else} {:else}
<FileListNodeLabel {node} {item} {label} /> <FileListNodeLabel {node} {item} {label} />
@@ -1,39 +1,29 @@
<script lang="ts" context="module">
let dragging: Writable<ListLevel | null> = writable(null);
let updating = false;
</script>
<script lang="ts"> <script lang="ts">
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx'; import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte'; import { getContext, onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable'; import { type Readable } from 'svelte/store';
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
import { get, writable, type Readable, type Writable } from 'svelte/store';
import FileListNodeStore from './FileListNodeStore.svelte'; import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte'; import FileListNode from './FileListNode.svelte';
import { import FileListNodeContent from './FileListNodeContent.svelte';
ListFileItem, import { ListFileItem, ListLevel, ListWaypointsItem, type ListItem } from './file-list';
ListLevel, import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
ListRootItem, import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
ListWaypointsItem,
allowedMoves,
moveItems,
type ListItem
} from './FileList';
import { selection } from './Selection';
import { isMac } from '$lib/utils';
import { _ } from 'svelte-i18n';
export let node: let {
node,
item,
waypointRoot = false,
}: {
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>> | Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement> | GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint; | Waypoint;
export let item: ListItem; item: ListItem;
export let waypointRoot: boolean = false; waypointRoot?: boolean;
} = $props();
let container: HTMLElement; let container: HTMLElement;
let elements: { [id: string]: HTMLElement } = {};
let sortableLevel: ListLevel = let sortableLevel: ListLevel =
node instanceof Map node instanceof Map
? ListLevel.FILE ? ListLevel.FILE
@@ -46,255 +36,32 @@
: node instanceof Track : node instanceof Track
? ListLevel.SEGMENT ? ListLevel.SEGMENT
: ListLevel.WAYPOINT; : ListLevel.WAYPOINT;
let sortable: Sortable;
let orientation = getContext<'vertical' | 'horizontal'>('orientation'); let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let destroyed = false; let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
let lastUpdateStart = 0;
function updateToSelection(e) {
if (destroyed) {
return;
}
lastUpdateStart = Date.now(); let sortable: SortableFileList;
setTimeout(() => {
if (Date.now() - lastUpdateStart >= 40) {
if (updating) {
return;
}
updating = true; onMount(() => {
// Sortable updates selection sortable = new SortableFileList(
let changed = getChangedIds(); container,
if (changed.length > 0) { node,
selection.update(($selection) => { item,
$selection.clear(); waypointRoot,
Object.entries(elements).forEach(([id, element]) => { sortableLevel,
$selection.set( orientation
item.extend(getRealId(id)),
element.classList.contains('sortable-selected')
); );
}); });
if ( $effect(() => {
e.originalEvent && if (sortable && node) {
!( sortable.updateElements();
e.originalEvent.ctrlKey ||
e.originalEvent.metaKey ||
e.originalEvent.shiftKey
) &&
($selection.size > 1 ||
!$selection.has(item.extend(getRealId(changed[0]))))
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(item.extend(getRealId(changed[0])), true);
} }
return $selection;
});
}
updating = false;
}
}, 50);
}
function updateFromSelection() {
if (destroyed || updating) {
return;
}
updating = true;
// Selection updates sortable
let changed = getChangedIds();
for (let id of changed) {
let element = elements[id];
if (element) {
if ($selection.has(item.extend(id))) {
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} else {
Sortable.utils.deselect(element);
}
}
}
updating = false;
}
$: if ($selection) {
updateFromSelection();
}
const { fileOrder } = settings;
function syncFileOrder() {
if (!sortable || sortableLevel !== ListLevel.FILE) {
return;
}
const currentOrder = sortable.toArray();
if (currentOrder.length !== $fileOrder.length) {
sortable.sort($fileOrder);
} else {
for (let i = 0; i < currentOrder.length; i++) {
if (currentOrder[i] !== $fileOrder[i]) {
sortable.sort($fileOrder);
break;
}
}
}
}
$: if ($fileOrder) {
syncFileOrder();
}
function createSortable() {
sortable = Sortable.create(container, {
group: {
name: sortableLevel,
pull: allowedMoves[sortableLevel],
put: true
},
direction: orientation,
forceAutoScrollFallback: true,
multiDrag: true,
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
avoidImplicitDeselect: true,
onSelect: updateToSelection,
onDeselect: updateToSelection,
onStart: () => {
dragging.set(sortableLevel);
},
onEnd: () => {
dragging.set(null);
},
onSort: (e) => {
if (sortableLevel === ListLevel.FILE) {
let newFileOrder = sortable.toArray();
if (newFileOrder.length !== get(fileOrder).length) {
fileOrder.set(newFileOrder);
} else {
for (let i = 0; i < newFileOrder.length; i++) {
if (newFileOrder[i] !== get(fileOrder)[i]) {
fileOrder.set(newFileOrder);
break;
}
}
}
}
let fromItem = Sortable.get(e.from)._item;
let toItem = Sortable.get(e.to)._item;
if (item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (Sortable.get(e.from)._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0
? e.oldIndicies.map((i) => i.index)
: [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (Sortable.get(e.to)._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices: number[] =
e.newIndicies.length > 0
? e.newIndicies.map((i) => i.index)
: [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
$fileOrder.splice(i, 0, newFileIds[index]);
return item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
}
});
Object.defineProperty(sortable, '_item', {
value: item,
writable: true
});
Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot,
writable: true
});
}
onMount(() => {
createSortable();
destroyed = false;
});
afterUpdate(() => {
elements = {};
container.childNodes.forEach((element) => {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
if (attr) {
if (node instanceof Map && !node.has(attr)) {
element.remove();
} else {
elements[attr] = element;
}
}
}
});
syncFileOrder();
updateFromSelection();
}); });
onDestroy(() => { onDestroy(() => {
destroyed = true; sortable.destroy();
}); });
function getChangedIds() {
let changed: (string | number)[] = [];
Object.entries(elements).forEach(([id, element]) => {
let realId = getRealId(id);
let realItem = item.extend(realId);
let inSelection = get(selection).has(realItem);
let isSelected = element.classList.contains('sortable-selected');
if (inSelection !== isSelected) {
changed.push(realId);
}
});
return changed;
}
function getRealId(id: string | number) {
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
? id
: parseInt(id);
}
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
</script> </script>
<div <div
@@ -340,11 +107,13 @@
{#if node instanceof GPXFile && item instanceof ListFileItem} {#if node instanceof GPXFile && item instanceof ListFileItem}
{#if !waypointRoot} {#if !waypointRoot}
<svelte:self {node} {item} waypointRoot={true} /> <FileListNodeContent {node} {item} waypointRoot={true} />
{/if} {/if}
{/if} {/if}
<style lang="postcss"> <style lang="postcss">
@reference "../../../app.css";
.sortable > div { .sortable > div {
@apply rounded-md; @apply rounded-md;
@apply h-fit; @apply h-fit;
@@ -2,7 +2,6 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as ContextMenu from '$lib/components/ui/context-menu'; import * as ContextMenu from '$lib/components/ui/context-menu';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';
import { dbUtils, getFile } from '$lib/db';
import { import {
Copy, Copy,
Info, Info,
@@ -18,109 +17,107 @@
Maximize, Maximize,
Scissors, Scissors,
FileStack, FileStack,
FileX } from '@lucide/svelte';
} from 'lucide-svelte';
import { import {
ListFileItem, ListFileItem,
ListLevel, ListLevel,
ListTrackItem, ListTrackItem,
ListWaypointItem, ListWaypointItem,
allowedPastes, type ListItem,
type ListItem } from './file-list';
} from './FileList';
import {
copied,
copySelection,
cut,
cutSelection,
pasteSelection,
selectAll,
selectItem,
selection
} from './Selection';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { get } from 'svelte/store'; import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { import { i18n } from '$lib/i18n.svelte';
allHidden, import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
editMetadata, import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
editStyle, import StyleDialog from '$lib/components/file-list/style/StyleDialog.svelte';
embedding, import { editStyle } from '$lib/components/file-list/style/utils.svelte';
centerMapOnSelection, import { getSymbolKey, symbols } from '$lib/assets/symbols';
gpxLayers, import { selection, copied, cut } from '$lib/logic/selection';
map import { map } from '$lib/components/map/map';
} from '$lib/stores'; import { fileActions, pasteSelection } from '$lib/logic/file-actions';
import { import { allHidden } from '$lib/logic/hidden';
GPXTreeElement, import { boundsManager } from '$lib/logic/bounds';
Track, import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
TrackSegment, import { fileStateCollection } from '$lib/logic/file-state';
type AnyGPXTreeElement, import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
Waypoint, import { allowedPastes } from './sortable-file-list';
GPXFile
} from 'gpx';
import { _ } from 'svelte-i18n';
import MetadataDialog from './MetadataDialog.svelte';
import StyleDialog from './StyleDialog.svelte';
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint; let {
export let item: ListItem; node,
export let label: string | undefined; item,
label,
}: {
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
item: ListItem;
label: string | undefined;
} = $props();
let orientation = getContext<'vertical' | 'horizontal'>('orientation'); let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let embedding = getContext<boolean>('embedding');
$: singleSelection = $selection.size === 1; let singleSelection = $derived($selection.size === 1);
let nodeColors: string[] = []; let nodeColors: string[] = $state([]);
$: if (node && $map) {
nodeColors = [];
$effect.pre(() => {
let colors: string[] = [];
if (node && $map) {
if (node instanceof GPXFile) { if (node instanceof GPXFile) {
let style = node.getStyle(); let defaultColor = undefined;
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
if (layer) { if (layer) {
style.color.push(layer.layerColor); defaultColor = layer.layerColor;
} }
let style = node.getStyle(defaultColor);
style.color.forEach((c) => { style.color.forEach((c) => {
if (!nodeColors.includes(c)) { if (!colors.includes(c)) {
nodeColors.push(c); colors.push(c);
} }
}); });
} else if (node instanceof Track) { } else if (node instanceof Track) {
let style = node.getStyle(); let style = node.getStyle();
if (style) { if (style) {
if (style.color && !nodeColors.includes(style.color)) { if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
nodeColors.push(style.color); colors.push(style['gpx_style:color']);
} }
} }
if (nodeColors.length === 0) { if (colors.length === 0) {
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
if (layer) { if (layer) {
nodeColors.push(layer.layerColor); colors.push(layer.layerColor);
} }
} }
} }
} }
nodeColors = colors;
});
let openEditMetadata: boolean = false; let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
let openEditStyle: boolean = false;
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item); let openEditMetadata: boolean = $derived(
$: openEditStyle = editMetadata.current && singleSelection && $selection.has(item)
$editStyle && );
let openEditStyle: boolean = $derived(
editStyle.current &&
$selection.has(item) && $selection.has(item) &&
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0; $selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden; );
let hidden = $derived(
item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden
);
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<ContextMenu.Root <ContextMenu.Root
onOpenChange={(open) => { onOpenChange={(open) => {
if (open) { if (open) {
if (!get(selection).has(item)) { if (!$selection.has(item)) {
selectItem(item); selection.selectItem(item);
} }
} }
}} }}
@@ -128,7 +125,7 @@
<ContextMenu.Trigger class="grow truncate"> <ContextMenu.Trigger class="grow truncate">
<Button <Button
variant="ghost" variant="ghost"
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation === class="relative w-full p-0 overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
'vertical' 'vertical'
? 'h-fit' ? 'h-fit'
: 'h-9 px-1.5 shadow-md'} pointer-events-auto" : 'h-9 px-1.5 shadow-md'} pointer-events-auto"
@@ -150,7 +147,7 @@
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%` `${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
) )
.join(',')})" .join(',')})"
/> ></div>
{/if} {/if}
<span <span
class="w-full text-left truncate py-1 flex flex-row items-center {hidden class="w-full text-left truncate py-1 flex flex-row items-center {hidden
@@ -158,8 +155,8 @@
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId()) : ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
? 'text-muted-foreground' ? 'text-muted-foreground'
: ''}" : ''}"
on:contextmenu={(e) => { oncontextmenu={(e) => {
if ($embedding) { if (embedding) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
return; return;
@@ -172,42 +169,53 @@
$selection = $selection; $selection = $selection;
} }
}} }}
on:mouseenter={() => { onmouseenter={() => {
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
let file = getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (layer && file) { if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
layer.showWaypointPopup(waypoint); waypointPopup?.setItem({
item: waypoint,
fileId: item.getFileId(),
});
} }
} }
} }
}} }}
on:mouseleave={() => { onmouseleave={() => {
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
if (layer) { if (layer) {
layer.hideWaypointPopup(); waypointPopup?.setItem(null);
} }
} }
}} }}
> >
{#if item.level === ListLevel.SEGMENT} {#if item.level === ListLevel.SEGMENT}
<Waypoints size="16" class="mr-1 shrink-0" /> <Waypoints size="16" class="mx-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT} {:else if item.level === ListLevel.WAYPOINT}
<MapPin size="16" class="mr-1 shrink-0" /> {#if symbolKey && symbols[symbolKey].icon}
{@const SymbolIcon = symbols[symbolKey].icon}
<SymbolIcon size="16" class="mx-1 shrink-0" />
{:else}
<MapPin size="16" class="mx-1 shrink-0" />
{/if} {/if}
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}"> {/if}
<span
class="grow select-none truncate {orientation === 'vertical'
? 'last:mr-2'
: ''}"
>
{label} {label}
</span> </span>
{#if hidden} {#if hidden}
<EyeOff <EyeOff
size="12" size="10"
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level === class="shrink-0 size-3.5 ml-1 {orientation === 'vertical'
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
? 'mr-3' ? 'mr-3'
: ''}" : 'mt-0.5'}"
/> />
{/if} {/if}
</span> </span>
@@ -215,31 +223,34 @@
</ContextMenu.Trigger> </ContextMenu.Trigger>
<ContextMenu.Content> <ContextMenu.Content>
{#if item instanceof ListFileItem || item instanceof ListTrackItem} {#if item instanceof ListFileItem || item instanceof ListTrackItem}
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}> <ContextMenu.Item
<Info size="16" class="mr-1" /> disabled={!singleSelection}
{$_('menu.metadata.button')} onclick={() => (editMetadata.current = true)}
>
<Info size="16" />
{i18n._('menu.metadata.button')}
<Shortcut key="I" ctrl={true} /> <Shortcut key="I" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item on:click={() => ($editStyle = true)}> <ContextMenu.Item onclick={() => (editStyle.current = true)}>
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" />
{$_('menu.style.button')} {i18n._('menu.style.button')}
</ContextMenu.Item> </ContextMenu.Item>
{/if} {/if}
<ContextMenu.Item <ContextMenu.Item
on:click={() => { onclick={() => {
if ($allHidden) { if ($allHidden) {
dbUtils.setHiddenToSelection(false); fileActions.setHiddenToSelection(false);
} else { } else {
dbUtils.setHiddenToSelection(true); fileActions.setHiddenToSelection(true);
} }
}} }}
> >
{#if $allHidden} {#if $allHidden}
<Eye size="16" class="mr-1" /> <Eye size="16" />
{$_('menu.unhide')} {i18n._('menu.unhide')}
{:else} {:else}
<EyeOff size="16" class="mr-1" /> <EyeOff size="16" />
{$_('menu.hide')} {i18n._('menu.hide')}
{/if} {/if}
<Shortcut key="H" ctrl={true} /> <Shortcut key="H" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
@@ -248,72 +259,68 @@
{#if item instanceof ListFileItem} {#if item instanceof ListFileItem}
<ContextMenu.Item <ContextMenu.Item
disabled={!singleSelection} disabled={!singleSelection}
on:click={() => dbUtils.addNewTrack(item.getFileId())} onclick={() => fileActions.addNewTrack(item.getFileId())}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new_track')} {i18n._('menu.new_track')}
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Separator />
{:else if item instanceof ListTrackItem} {:else if item instanceof ListTrackItem}
<ContextMenu.Item <ContextMenu.Item
disabled={!singleSelection} disabled={!singleSelection}
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())} onclick={() =>
fileActions.addNewSegment(item.getFileId(), item.getTrackIndex())}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{$_('menu.new_segment')} {i18n._('menu.new_segment')}
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Separator />
{/if} {/if}
{/if} {/if}
{#if item.level !== ListLevel.WAYPOINTS} {#if item.level !== ListLevel.WAYPOINTS}
<ContextMenu.Item on:click={selectAll}> <ContextMenu.Item onclick={() => selection.selectAll()}>
<FileStack size="16" class="mr-1" /> <FileStack size="16" />
{$_('menu.select_all')} {i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
{/if} {/if}
<ContextMenu.Item on:click={centerMapOnSelection}> <ContextMenu.Item onclick={() => boundsManager.centerMapOnSelection()}>
<Maximize size="16" class="mr-1" /> <Maximize size="16" />
{$_('menu.center')} {i18n._('menu.center')}
<Shortcut key="⏎" ctrl={true} /> <Shortcut key="⏎" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Separator />
<ContextMenu.Item on:click={dbUtils.duplicateSelection}> <ContextMenu.Item onclick={fileActions.duplicateSelection}>
<Copy size="16" class="mr-1" /> <Copy size="16" />
{$_('menu.duplicate')} {i18n._('menu.duplicate')}
<Shortcut key="D" ctrl={true} /></ContextMenu.Item <Shortcut key="D" ctrl={true} />
> </ContextMenu.Item>
{#if orientation === 'vertical'} {#if orientation === 'vertical'}
<ContextMenu.Item on:click={copySelection}> <ContextMenu.Item onclick={() => selection.copySelection()}>
<ClipboardCopy size="16" class="mr-1" /> <ClipboardCopy size="16" />
{$_('menu.copy')} {i18n._('menu.copy')}
<Shortcut key="C" ctrl={true} /> <Shortcut key="C" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item on:click={cutSelection}> <ContextMenu.Item onclick={() => selection.cutSelection()}>
<Scissors size="16" class="mr-1" /> <Scissors size="16" />
{$_('menu.cut')} {i18n._('menu.cut')}
<Shortcut key="X" ctrl={true} /> <Shortcut key="X" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item <ContextMenu.Item
disabled={$copied === undefined || disabled={$copied === undefined ||
$copied.length === 0 || $copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(item.level)} !allowedPastes[$copied[0].level].includes(item.level)}
on:click={pasteSelection} onclick={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" />
{$_('menu.paste')} {i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} /> <Shortcut key="V" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
{/if} {/if}
<ContextMenu.Separator /> <ContextMenu.Separator />
<ContextMenu.Item on:click={dbUtils.deleteSelection}> <ContextMenu.Item onclick={fileActions.deleteSelection}>
{#if item instanceof ListFileItem} <Trash2 size="16" />
<FileX size="16" class="mr-1" /> {i18n._('menu.delete')}
{$_('menu.close')}
{:else}
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
{/if}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
</ContextMenu.Content> </ContextMenu.Content>
@@ -2,12 +2,16 @@
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte'; import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
import FileListNode from '$lib/components/file-list/FileListNode.svelte'; import FileListNode from '$lib/components/file-list/FileListNode.svelte';
import type { GPXFileWithStatistics } from '$lib/db';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { ListFileItem } from './FileList'; import { ListFileItem } from './file-list';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
export let file: Readable<GPXFileWithStatistics | undefined>; let {
file,
}: {
file: Readable<GPXFileWithStatistics | undefined>;
} = $props();
let recursive = getContext<boolean>('recursive'); let recursive = getContext<boolean>('recursive');
</script> </script>
@@ -1,315 +0,0 @@
import { get, writable } from "svelte/store";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
};
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
}
export function selectFile(fileId: string) {
selectItem(new ListFileItem(fileId));
}
export function addSelectItem(item: ListItem) {
selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
}
export function addSelectFile(fileId: string) {
addSelectItem(new ListFileItem(fileId));
}
export function selectAll() {
selection.update(($selection) => {
let item: ListItem = new ListRootItem();
$selection.forEach((i) => {
item = i;
});
if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear();
get(fileObservers).forEach((_file, fileId) => {
$selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
});
}
} else if (item instanceof ListTrackSegmentItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
});
}
} else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
}
}
return $selection;
});
}
export function getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
}, reverse);
return selected;
}
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
get(settings.fileOrder).forEach((fileId) => {
let level: ListLevel | undefined = undefined;
let items: ListItem[] = [];
selectedItems.forEach((item) => {
if (item.getFileId() === fileId) {
level = item.level;
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) {
items.push(item);
}
}
});
if (items.length > 0) {
sortItems(items, reverse);
callback(fileId, level, items);
}
});
}
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
}
export const copied = writable<ListItem[] | undefined>(undefined);
export const cut = writable(false);
export function copySelection(): boolean {
let selected = get(selection).getSelected();
if (selected.length > 0) {
copied.set(selected);
cut.set(false);
return true;
}
return false;
}
export function cutSelection() {
if (copySelection()) {
cut.set(true);
}
}
function resetCopied() {
copied.set(undefined);
cut.set(false);
}
export function pasteSelection() {
let fromItems = get(copied);
if (fromItems === undefined || fromItems.length === 0) {
return;
}
let selected = get(selection).getSelected();
if (selected.length === 0) {
selected = [new ListRootItem()];
}
let fromParent = fromItems[0].getParent();
let toParent = selected[selected.length - 1];
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
}
let toItems: ListItem[] = [];
if (toParent.level === ListLevel.ROOT) {
let fileIds = getFileIds(fromItems.length);
fileIds.forEach((fileId) => {
toItems.push(new ListFileItem(fileId));
});
} else {
let toFile = getFile(toParent.getFileId());
if (toFile) {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
}
}
});
}
}
if (fromItems.length === toItems.length) {
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
resetCopied();
}
}
@@ -1,167 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
import { Slider } from '$lib/components/ui/slider';
import * as Popover from '$lib/components/ui/popover';
import { dbUtils, getFile, settings } from '$lib/db';
import { Save } from 'lucide-svelte';
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
import { selection } from './Selection';
import { editStyle, gpxLayers } from '$lib/stores';
import { _ } from 'svelte-i18n';
export let item: ListItem;
export let open = false;
const { defaultOpacity, defaultWeight } = settings;
let colors: string[] = [];
let color: string | undefined = undefined;
let opacity: number[] = [];
let weight: number[] = [];
let colorChanged = false;
let opacityChanged = false;
let weightChanged = false;
function setStyleInputs() {
colors = [];
opacity = [];
weight = [];
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
let file = getFile(item.getFileId());
let layer = gpxLayers.get(item.getFileId());
if (file && layer) {
let style = file.getStyle();
style.color.push(layer.layerColor);
style.color.forEach((c) => {
if (!colors.includes(c)) {
colors.push(c);
}
});
style.opacity.forEach((o) => {
if (!opacity.includes(o)) {
opacity.push(o);
}
});
style.weight.forEach((w) => {
if (!weight.includes(w)) {
weight.push(w);
}
});
}
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
let layer = gpxLayers.get(item.getFileId());
if (file && layer) {
let track = file.trk[item.getTrackIndex()];
let style = track.getStyle();
if (style) {
if (style.color && !colors.includes(style.color)) {
colors.push(style.color);
}
if (style.opacity && !opacity.includes(style.opacity)) {
opacity.push(style.opacity);
}
if (style.weight && !weight.includes(style.weight)) {
weight.push(style.weight);
}
}
if (!colors.includes(layer.layerColor)) {
colors.push(layer.layerColor);
}
}
}
});
color = colors[0];
opacity = [opacity[0] ?? $defaultOpacity];
weight = [weight[0] ?? $defaultWeight];
colorChanged = false;
opacityChanged = false;
weightChanged = false;
}
$: if ($selection && open) {
setStyleInputs();
}
$: if (!open) {
$editStyle = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.color')}
<Input
bind:value={color}
type="color"
class="p-0 h-6 w-40"
on:change={() => (colorChanged = true)}
/>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.opacity')}
<div class="w-40 p-2">
<Slider
bind:value={opacity}
min={0.3}
max={1}
step={0.1}
onValueChange={() => (opacityChanged = true)}
/>
</div>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.width')}
<div class="w-40 p-2">
<Slider
bind:value={weight}
id="weight"
min={1}
max={10}
step={1}
onValueChange={() => (weightChanged = true)}
/>
</div>
</Label>
<Button
variant="outline"
disabled={!colorChanged && !opacityChanged && !weightChanged}
on:click={() => {
let style = {};
if (colorChanged) {
style.color = color;
}
if (opacityChanged) {
style.opacity = opacity[0];
}
if (weightChanged) {
style.weight = weight[0];
}
dbUtils.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
if (style.opacity) {
$defaultOpacity = style.opacity;
}
if (style.weight) {
$defaultWeight = style.weight;
}
}
open = false;
}}
>
<Save size="16" class="mr-1" />
{$_('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>
@@ -1,37 +1,14 @@
import { dbUtils, getFile } from "$lib/db";
import { freeze } from "immer";
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
import { selection } from "./Selection";
import { newGPXFile } from "$lib/stores";
export enum ListLevel { export enum ListLevel {
ROOT, ROOT,
FILE, FILE,
TRACK, TRACK,
SEGMENT, SEGMENT,
WAYPOINTS, WAYPOINTS,
WAYPOINT WAYPOINT,
} }
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
};
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
};
export abstract class ListItem { export abstract class ListItem {
[x: string]: any;
level: ListLevel; level: ListLevel;
constructor(level: ListLevel) { constructor(level: ListLevel) {
@@ -321,120 +298,3 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
items.reverse(); items.reverse();
} }
} }
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
if (fromItems.length === 0) {
return;
}
sortItems(fromItems, false);
sortItems(toItems, false);
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
fromItems.forEach((item) => {
let file = getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
context.push(file.clone());
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
context.push(file.trk[item.getTrackIndex()].clone());
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
} else if (item instanceof ListWaypointsItem) {
context.push(file.wpt.map((wpt) => wpt.clone()));
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
context.push(file.wpt[item.getWaypointIndex()].clone());
}
}
});
if (remove && !(fromParent instanceof ListRootItem)) {
sortItems(fromItems, true);
}
let files = [fromParent.getFileId(), toParent.getFileId()];
let callbacks = [
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
fromItems.forEach((item) => {
if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
} else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
}
});
},
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
} else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
trkseg: [context[i]]
})]);
}
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
} else if (item instanceof ListWaypointsItem) {
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
}
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
}
});
}
];
if (fromParent instanceof ListRootItem) {
files = [];
callbacks = [];
} else if (!remove) {
files.splice(0, 1);
callbacks.splice(0, 1);
}
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) {
let newFile = context[i];
if (remove) {
files.delete(newFile._data.id);
}
newFile._data.id = item.getFileId();
files.set(item.getFileId(), freeze(newFile));
} else if (context[i] instanceof Track) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
if (context[i].name) {
newFile.metadata.name = context[i].name;
}
newFile.replaceTracks(0, 0, [context[i]]);
files.set(item.getFileId(), freeze(newFile));
} else if (context[i] instanceof TrackSegment) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
newFile.replaceTracks(0, 0, [new Track({
trkseg: [context[i]]
})]);
files.set(item.getFileId(), freeze(newFile));
}
}
});
}, context);
selection.update(($selection) => {
$selection.clear();
toItems.forEach((item) => {
$selection.set(item, true);
});
return $selection;
});
}
@@ -4,46 +4,56 @@
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { dbUtils } from '$lib/db'; import { Save } from '@lucide/svelte';
import { Save } from 'lucide-svelte'; import { ListFileItem, ListTrackItem, type ListItem } from '../file-list';
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx'; import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { _ } from 'svelte-i18n'; import { i18n } from '$lib/i18n.svelte';
import { editMetadata } from '$lib/stores'; import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
import { fileActionManager } from '$lib/logic/file-action-manager';
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint; let {
export let item: ListItem; node,
export let open = false; item,
open = $bindable(),
}: {
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
item: ListItem;
open: boolean;
} = $props();
let name: string = let name: string = $derived(
node instanceof GPXFile node instanceof GPXFile
? node.metadata.name ?? '' ? (node.metadata.name ?? '')
: node instanceof Track : node instanceof Track
? node.name ?? '' ? (node.name ?? '')
: ''; : ''
let description: string = );
let description: string = $derived(
node instanceof GPXFile node instanceof GPXFile
? node.metadata.desc ?? '' ? (node.metadata.desc ?? '')
: node instanceof Track : node instanceof Track
? node.desc ?? '' ? (node.desc ?? '')
: ''; : ''
);
$: if (!open) { $effect(() => {
$editMetadata = false; if (!open) {
editMetadata.current = false;
} }
});
</script> </script>
<Popover.Root bind:open> <Popover.Root bind:open>
<Popover.Trigger /> <Popover.Trigger class="-mx-1" />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3"> <Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label for="name">{$_('menu.metadata.name')}</Label> <Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="font-semibold h-8" /> <Input bind:value={name} id="name" class="font-semibold h-8" />
<Label for="description">{$_('menu.metadata.description')}</Label> <Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea bind:value={description} id="description" /> <Textarea bind:value={description} id="description" />
<Button <Button
variant="outline" variant="outline"
on:click={() => { onclick={() => {
dbUtils.applyToFile(item.getFileId(), (file) => { fileActionManager.applyToFile(item.getFileId(), (file) => {
if (item instanceof ListFileItem && node instanceof GPXFile) { if (item instanceof ListFileItem && node instanceof GPXFile) {
file.metadata.name = name; file.metadata.name = name;
file.metadata.desc = description; file.metadata.desc = description;
@@ -58,8 +68,8 @@
open = false; open = false;
}} }}
> >
<Save size="16" class="mr-1" /> <Save size="16" />
{$_('menu.metadata.save')} {i18n._('menu.metadata.save')}
</Button> </Button>
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
@@ -0,0 +1,3 @@
export const editMetadata = $state({
current: false,
});
@@ -0,0 +1,284 @@
import { isMac } from '$lib/utils';
import Sortable, { type Direction } from 'sortablejs/Sortable';
import { ListItem, ListLevel, ListRootItem } from './file-list';
import { selection } from '$lib/logic/selection';
import { getFileIds, moveItems } from '$lib/logic/file-actions';
import { get, writable, type Readable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import type { AnyGPXTreeElement, GPXTreeElement, Waypoint } from 'gpx';
import { tick } from 'svelte';
const { fileOrder } = settings;
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export const dragging = writable<ListLevel | null>(null);
export class SortableFileList {
private _node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint;
private _item: ListItem;
private _sortableLevel: ListLevel;
private _container: HTMLElement;
private _sortable: Sortable | null = null;
private _elements: { [id: string]: HTMLElement } = {};
private _updatingSelection: boolean = false;
private _unsubscribes: (() => void)[] = [];
constructor(
container: HTMLElement,
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint,
item: ListItem,
waypointRoot: boolean,
sortableLevel: ListLevel,
orientation: Direction
) {
this._node = node;
this._item = item;
this._sortableLevel = sortableLevel;
this._container = container;
this._sortable = Sortable.create(container, {
group: {
name: sortableLevel,
pull: allowedMoves[sortableLevel],
put: true,
},
direction: orientation,
forceAutoScrollFallback: true,
multiDrag: true,
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
avoidImplicitDeselect: true,
onSelect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onDeselect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onStart: () => dragging.set(sortableLevel),
onEnd: () => dragging.set(null),
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
});
Object.defineProperty(this._sortable, '_item', {
value: item,
writable: true,
});
Object.defineProperty(this._sortable, '_waypointRoot', {
value: waypointRoot,
writable: true,
});
this._unsubscribes.push(
selection.subscribe(() => tick().then(() => this.updateFromSelection()))
);
this._unsubscribes.push(fileOrder.subscribe(() => this.updateFromFileOrder()));
}
onSort(e: Sortable.SortableEvent) {
this.updateToFileOrder();
const from = Sortable.get(e.from);
const to = Sortable.get(e.to);
if (!from || !to) {
return;
}
let fromItem = from._item;
let toItem = to._item;
if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (from._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (from._waypointRoot && to._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (to._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices: number[] =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
get(fileOrder).splice(i, 0, newFileIds[index]);
return this._item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
}
updateFromSelection() {
const changed = this.getChangedIds();
if (changed.length === 0) {
return;
}
const selection_ = get(selection);
for (let id of changed) {
let element = this._elements[id];
if (element) {
if (selection_.has(this._item.extend(id))) {
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
} else {
Sortable.utils.deselect(element);
}
}
}
}
updateToSelection(e: Sortable.SortableEvent) {
if (!this._sortable) return;
if (this._updatingSelection) return;
this._updatingSelection = true;
const changed = this.getChangedIds();
if (changed.length == 0) {
this._updatingSelection = false;
return;
}
selection.update(($selection) => {
$selection.clear();
Object.entries(this._elements).forEach(([id, element]) => {
$selection.set(
this._item.extend(this.getRealId(id)),
element.classList.contains('sortable-selected')
);
});
if (
e.originalEvent &&
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
($selection.size > 1 ||
!$selection.has(this._item.extend(this.getRealId(changed[0]))))
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(this._item.extend(this.getRealId(changed[0])), true);
}
return $selection;
});
this._updatingSelection = false;
}
updateFromFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
if (
fileOrder_.length !== sortableOrder.length ||
fileOrder_.some((value, index) => value !== sortableOrder[index])
) {
this._sortable.sort(fileOrder_);
}
}
updateToFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
if (
fileOrder_.length !== sortableOrder.length ||
fileOrder_.some((value, index) => value !== sortableOrder[index])
) {
fileOrder.set(sortableOrder);
}
}
updateElements() {
this._elements = {};
this._container.childNodes.forEach((element) => {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
if (attr) {
if (this._node instanceof Map && !this._node.has(attr)) {
element.remove();
} else {
this._elements[attr] = element;
}
}
}
});
}
destroy() {
this._sortable = null;
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = [];
}
getChangedIds() {
let changed: (string | number)[] = [];
const selection_ = get(selection);
Object.entries(this._elements).forEach(([id, element]) => {
let realId = this.getRealId(id);
let realItem = this._item.extend(realId);
let inSelection = selection_.has(realItem);
let isSelected = element.classList.contains('sortable-selected');
if (inSelection !== isSelected) {
changed.push(realId);
}
});
return changed;
}
getRealId(id: string | number) {
return this._sortableLevel === ListLevel.FILE || this._sortableLevel === ListLevel.WAYPOINTS
? id
: parseInt(id as string);
}
}
@@ -0,0 +1,171 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
import { Slider } from '$lib/components/ui/slider';
import * as Popover from '$lib/components/ui/popover';
import { Save } from '@lucide/svelte';
import {
ListFileItem,
ListTrackItem,
type ListItem,
} from '$lib/components/file-list/file-list';
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { i18n } from '$lib/i18n.svelte';
import type { LineStyleExtension } from 'gpx';
import { settings } from '$lib/logic/settings';
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { untrack } from 'svelte';
import { fileActions } from '$lib/logic/file-actions';
let {
item,
open = $bindable(),
}: {
item: ListItem;
open: boolean;
} = $props();
const { defaultOpacity, defaultWidth } = settings;
let color: string = $state('');
let opacity: number = $state(0);
let width: number = $state(0);
let colorChanged = $state(false);
let opacityChanged = $state(false);
let widthChanged = $state(false);
function setStyleInputs() {
opacity = $defaultOpacity;
width = $defaultWidth;
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
let file = fileStateCollection.getFile(item.getFileId());
let layer = gpxLayers.getLayer(item.getFileId());
if (file && layer) {
let style = file.getStyle();
color = layer.layerColor;
if (style.opacity.length > 0) {
opacity = style.opacity[0];
}
if (style.width.length > 0) {
width = style.width[0];
}
}
} else if (item instanceof ListTrackItem) {
let file = fileStateCollection.getFile(item.getFileId());
let layer = gpxLayers.getLayer(item.getFileId());
if (file && layer) {
color = layer.layerColor;
let track = file.trk[item.getTrackIndex()];
let style = track.getStyle();
if (style) {
if (style['gpx_style:color']) {
color = style['gpx_style:color'];
}
if (style['gpx_style:opacity']) {
opacity = style['gpx_style:opacity'];
}
if (style['gpx_style:width']) {
width = style['gpx_style:width'];
}
}
}
}
});
colorChanged = false;
opacityChanged = false;
widthChanged = false;
}
$effect(() => {
if ($selection && open) {
untrack(() => setStyleInputs());
}
});
$effect(() => {
if (!open) {
editStyle.current = false;
}
});
function applyStyle() {
let style: LineStyleExtension = {};
if (colorChanged) {
style['gpx_style:color'] = color;
}
if (opacityChanged) {
style['gpx_style:opacity'] = opacity;
}
if (widthChanged) {
style['gpx_style:width'] = width;
}
fileActions.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === fileStateCollection.size) {
if (style['gpx_style:opacity']) {
$defaultOpacity = style['gpx_style:opacity'];
}
if (style['gpx_style:width']) {
$defaultWidth = style['gpx_style:width'];
}
}
open = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger class="-mx-1" />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.color')}
<Input
bind:value={color}
type="color"
class="p-0 h-6 w-40"
onchange={() => (colorChanged = true)}
/>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.opacity')}
<div class="w-40 p-2">
<Slider
bind:value={opacity}
min={0.3}
max={1}
step={0.1}
onValueChange={() => (opacityChanged = true)}
type="single"
/>
</div>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.width')}
<div class="w-40 p-2">
<Slider
bind:value={width}
id="width"
min={1}
max={10}
step={1}
onValueChange={() => (widthChanged = true)}
type="single"
/>
</div>
</Label>
<Button
variant="outline"
disabled={!colorChanged && !opacityChanged && !widthChanged}
onclick={applyStyle}
>
<Save size="16" />
{i18n._('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>
@@ -0,0 +1,3 @@
export const editStyle = $state({
current: false,
});
@@ -1,95 +0,0 @@
import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store";
const { distanceMarkers, distanceUnits } = settings;
export class DistanceMarkers {
map: mapboxgl.Map;
updateBinded: () => void = this.update.bind(this);
unsubscribes: (() => void)[] = [];
constructor(map: mapboxgl.Map) {
this.map = map;
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
this.map.on('style.import.load', this.updateBinded);
}
update() {
try {
if (get(distanceMarkers)) {
let distanceSource = this.map.getSource('distance-markers');
if (distanceSource) {
distanceSource.setData(this.getDistanceMarkersGeoJSON());
} else {
this.map.addSource('distance-markers', {
type: 'geojson',
data: this.getDistanceMarkersGeoJSON()
});
}
if (!this.map.getLayer('distance-markers')) {
this.map.addLayer({
id: 'distance-markers',
type: 'symbol',
source: 'distance-markers',
layout: {
'text-field': ['get', 'distance'],
'text-size': 14,
'text-font': ['Open Sans Bold'],
'text-padding': 20,
},
paint: {
'text-color': 'black',
'text-halo-width': 2,
'text-halo-color': 'white',
}
});
} else {
this.map.moveLayer('distance-markers');
}
} else {
if (this.map.getLayer('distance-markers')) {
this.map.removeLayer('distance-markers');
}
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
}
remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe());
}
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
let statistics = get(gpxStatistics);
let features = [];
let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.total.length; i++) {
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
let distance = currentTargetDistance.toFixed(0);
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
},
properties: {
distance,
}
} as GeoJSON.Feature);
currentTargetDistance += 1;
}
}
return {
type: 'FeatureCollection',
features
};
}
}
@@ -1,471 +0,0 @@
import { currentTool, map, Tool } from "$lib/stores";
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
import { get, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
import type { Waypoint } from "gpx";
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
import { MapPin, Square } from "lucide-static";
import { getSymbolKey, symbols } from "$lib/assets/symbols";
const colors = [
'#ff0000',
'#0000ff',
'#46e646',
'#00ccff',
'#ff9900',
'#ff00ff',
'#ffff32',
'#288228',
'#9933ff',
'#50f0be',
'#8c645a'
];
const colorCount: { [key: string]: number } = {};
for (let color of colors) {
colorCount[color] = 0;
}
// Get the color with the least amount of uses
function getColor() {
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
colorCount[color]++;
return color;
}
function decrementColor(color: string) {
if (colorCount.hasOwnProperty(color)) {
colorCount[color]--;
}
}
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square
.replace('width="24"', 'width="12"')
.replace('height="24"', 'height="12"')
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)}
${MapPin
.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', '')
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
${symbolSvg?.replace('width="24"', 'width="10"')
.replace('height="24"', 'height="10"')
.replace('stroke="currentColor"', 'stroke="white"')
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
</svg>`;
}
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
export class GPXLayer {
map: mapboxgl.Map;
fileId: string;
file: Readable<GPXFileWithStatistics | undefined>;
layerColor: string;
markers: mapboxgl.Marker[] = [];
selected: boolean = false;
draggable: boolean;
unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
this.map = map;
this.fileId = fileId;
this.file = file;
this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded));
this.unsubscribe.push(selection.subscribe($selection => {
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
if (this.selected || newSelected) {
this.selected = newSelected;
this.update();
}
if (newSelected) {
this.moveToFront();
}
}));
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(currentTool.subscribe(tool => {
if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true;
this.markers.forEach(marker => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false;
this.markers.forEach(marker => marker.setDraggable(false));
}
}));
this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.import.load', this.updateBinded);
}
update() {
let file = get(this.file)?.file;
if (!file) {
return;
}
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`;
}
try {
let source = this.map.getSource(this.fileId);
if (source) {
source.setData(this.getGeoJSON());
} else {
this.map.addSource(this.fileId, {
type: 'geojson',
data: this.getGeoJSON()
});
}
if (!this.map.getLayer(this.fileId)) {
this.map.addLayer({
id: this.fileId,
type: 'line',
source: this.fileId,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': ['get', 'color'],
'line-width': ['get', 'weight'],
'line-opacity': ['get', 'opacity']
}
});
this.map.on('click', this.fileId, this.layerOnClickBinded);
this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
}
if (get(directionMarkers)) {
if (!this.map.getLayer(this.fileId + '-direction')) {
this.map.addLayer({
id: this.fileId + '-direction',
type: 'symbol',
source: this.fileId,
layout: {
'text-field': '»',
'text-offset': [0, -0.1],
'text-keep-upright': false,
'text-max-angle': 361,
'text-allow-overlap': true,
'text-font': ['Open Sans Bold'],
'symbol-placement': 'line',
'symbol-spacing': 20,
},
paint: {
'text-color': 'white',
'text-opacity': 0.7,
'text-halo-width': 0.2,
'text-halo-color': 'white'
}
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
}
} else {
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction');
}
}
let visibleItems: [number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) {
visibleItems.push([trackIndex, segmentIndex]);
}
});
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => { // Update markers
let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) {
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
let marker = new mapboxgl.Marker({
draggable: this.draggable,
element,
anchor: 'bottom'
}).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0;
marker.getElement().addEventListener('mouseover', (e) => {
if (marker._isDragging) {
return;
}
this.showWaypointPopup(marker._waypoint);
e.stopPropagation();
});
marker.getElement().addEventListener('click', (e) => {
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
return;
}
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
deleteWaypoint(this.fileId, marker._waypoint._data.index);
e.stopPropagation();
return;
}
if (get(verticalFileView)) {
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
} else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
}
} else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]);
} else {
this.showWaypointPopup(marker._waypoint);
}
e.stopPropagation();
});
marker.on('dragstart', () => {
setGrabbingCursor();
marker.getElement().style.cursor = 'grabbing';
this.hideWaypointPopup();
});
marker.on('dragend', (e) => {
resetCursor();
marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => {
dbUtils.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng
});
wpt.ele = ele[0];
});
});
dragEndTimestamp = Date.now()
});
this.markers.push(marker);
}
markerIndex++;
});
}
while (markerIndex < this.markers.length) { // Remove extra markers
this.markers.pop()?.remove();
}
this.markers.forEach((marker) => {
if (!marker._waypoint._data.hidden) {
marker.addTo(this.map);
} else {
marker.remove();
}
});
}
updateMap(map: mapboxgl.Map) {
this.map = map;
this.map.on('style.import.load', this.updateBinded);
this.update();
}
remove() {
if (get(map)) {
this.map.off('click', this.fileId, this.layerOnClickBinded);
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.off('style.import.load', this.updateBinded);
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction');
}
if (this.map.getLayer(this.fileId)) {
this.map.removeLayer(this.fileId);
}
if (this.map.getSource(this.fileId)) {
this.map.removeSource(this.fileId);
}
}
this.markers.forEach((marker) => {
marker.remove();
});
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
decrementColor(this.layerColor);
}
moveToFront() {
if (this.map.getLayer(this.fileId)) {
this.map.moveLayer(this.fileId);
}
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
}
}
layerOnMouseEnter(e: any) {
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
setScissorsCursor();
} else {
setPointerCursor();
}
}
layerOnMouseLeave() {
resetCursor();
}
layerOnClick(e: any) {
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
return;
}
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
return;
}
let file = get(this.file)?.file;
if (!file) {
return;
}
let item = undefined;
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
} else {
item = new ListFileItem(this.fileId);
}
if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
addSelectItem(item);
} else {
selectItem(item);
}
}
layerOnContextMenu(e: any) {
if (e.originalEvent.ctrlKey) {
this.layerOnClick(e);
}
}
showWaypointPopup(waypoint: Waypoint) {
if (get(currentPopupWaypoint) !== null) {
this.hideWaypointPopup();
}
let marker = this.markers[waypoint._data.index];
if (marker) {
currentPopupWaypoint.set([waypoint, this.fileId]);
marker.setPopup(waypointPopup);
marker.togglePopup();
this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
}
}
maybeHideWaypointPopup(e: any) {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
if (marker) {
if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 100) {
this.hideWaypointPopup();
}
} else {
this.hideWaypointPopup();
}
}
}
hideWaypointPopup() {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
marker?.getPopup()?.remove();
currentPopupWaypoint.set(null);
this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
}
}
getGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file;
if (!file) {
return {
type: 'FeatureCollection',
features: []
};
}
let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0;
for (let feature of data.features) {
if (!feature.properties) {
feature.properties = {};
}
if (!feature.properties.color) {
feature.properties.color = this.layerColor;
}
if (!feature.properties.weight) {
feature.properties.weight = get(defaultWeight);
}
if (!feature.properties.opacity) {
feature.properties.opacity = get(defaultOpacity);
}
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
feature.properties.weight = feature.properties.weight + 2;
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
}
feature.properties.trackIndex = trackIndex;
feature.properties.segmentIndex = segmentIndex;
segmentIndex++;
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
segmentIndex = 0;
trackIndex++;
}
}
return data;
}
}
@@ -1,58 +0,0 @@
<script lang="ts">
import { map, gpxLayers } from '$lib/stores';
import { GPXLayer } from './GPXLayer';
import WaypointPopup from './WaypointPopup.svelte';
import { fileObservers } from '$lib/db';
import { DistanceMarkers } from './DistanceMarkers';
import { StartEndMarkers } from './StartEndMarkers';
import { onDestroy } from 'svelte';
let distanceMarkers: DistanceMarkers | undefined = undefined;
let startEndMarkers: StartEndMarkers | undefined = undefined;
$: if ($map && $fileObservers) {
// remove layers for deleted files
gpxLayers.forEach((layer, fileId) => {
if (!$fileObservers.has(fileId)) {
layer.remove();
gpxLayers.delete(fileId);
} else if ($map !== layer.map) {
layer.updateMap($map);
}
});
// add layers for new files
$fileObservers.forEach((file, fileId) => {
if (!gpxLayers.has(fileId)) {
gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
}
});
}
$: if ($map) {
if (distanceMarkers) {
distanceMarkers.remove();
}
if (startEndMarkers) {
startEndMarkers.remove();
}
distanceMarkers = new DistanceMarkers($map);
startEndMarkers = new StartEndMarkers($map);
}
onDestroy(() => {
gpxLayers.forEach((layer) => layer.remove());
gpxLayers.clear();
if (distanceMarkers) {
distanceMarkers.remove();
distanceMarkers = undefined;
}
if (startEndMarkers) {
startEndMarkers.remove();
startEndMarkers = undefined;
}
});
</script>
<WaypointPopup />
@@ -1,110 +0,0 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import Shortcut from '$lib/components/Shortcut.svelte';
import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup';
import WithUnits from '$lib/components/WithUnits.svelte';
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
import { onMount } from 'svelte';
import { Tool, currentTool } from '$lib/stores';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { _ } from 'svelte-i18n';
import sanitizeHtml from 'sanitize-html';
let popupElement: HTMLDivElement;
onMount(() => {
waypointPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
function sanitize(text: string | undefined): string {
if (text === undefined) {
return '';
}
return sanitizeHtml(text, {
allowedTags: ['a', 'br', 'img'],
allowedAttributes: {
a: ['href', 'target'],
img: ['src']
}
}).trim();
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
<Card.Header class="p-0">
<Card.Title class="text-md">
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
<ExternalLink size="12" class="inline-block mb-1.5" />
</a>
{:else}
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
{/if}
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm">
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
{#if symbolKey}
<span>
{#if symbols[symbolKey].icon}
<svelte:component
this={symbols[symbolKey].icon}
size="12"
class="inline-block mb-0.5"
/>
{:else}
<span class="w-4 inline-block" />
{/if}
{$_(`gpx.symbol.${symbolKey}`)}
</span>
<Dot size="16" />
{/if}
{$currentPopupWaypoint[0].getLatitude().toFixed(6)}&deg; {$currentPopupWaypoint[0]
.getLongitude()
.toFixed(6)}&deg;
{#if $currentPopupWaypoint[0].ele !== undefined}
<Dot size="16" />
<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" />
{/if}
</div>
{#if $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
{/if}
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
{/if}
{#if $currentTool === Tool.WAYPOINT}
<Button
class="mt-2 w-full px-2 py-1 h-8 justify-start"
variant="outline"
on:click={() =>
deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut shift={true} click={true} />
</Button>
{/if}
</Card.Content>
</Card.Root>
{/if}
</div>
<style lang="postcss">
div :global(a) {
@apply text-link;
@apply hover:underline;
}
div :global(img) {
@apply my-0;
@apply rounded-md;
}
</style>
@@ -1,25 +0,0 @@
import { dbUtils } from "$lib/db";
import type { Waypoint } from "gpx";
import mapboxgl from "mapbox-gl";
import { writable } from "svelte/store";
export const currentPopupWaypoint = writable<[Waypoint, string] | null>(null);
export const waypointPopup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined,
offset: {
'top': [0, 0],
'top-left': [0, 0],
'top-right': [0, 0],
'bottom': [0, -30],
'bottom-left': [0, -30],
'bottom-right': [0, -30],
'left': [0, 0],
'right': [0, 0]
},
});
export function deleteWaypoint(fileId: string, waypointIndex: number) {
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
}
@@ -1,220 +0,0 @@
<script lang="ts">
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import LayerTree from './LayerTree.svelte';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Layers } from 'lucide-svelte';
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
import { settings } from '$lib/db';
import { map } from '$lib/stores';
import { get, writable } from 'svelte/store';
import { customBasemapUpdate, getLayers } from './utils';
import { OverpassLayer } from './OverpassLayer';
import OverpassPopup from './OverpassPopup.svelte';
let container: HTMLDivElement;
let overpassLayer: OverpassLayer;
const {
currentBasemap,
previousBasemap,
currentOverlays,
currentOverpassQueries,
selectedBasemapTree,
selectedOverlayTree,
selectedOverpassTree,
customLayers,
opacities
} = settings;
function setStyle() {
if ($map) {
let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap]
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
$map.removeImport('basemap');
if (typeof basemap === 'string') {
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
} else {
$map.addImport(
{
id: 'basemap',
data: basemap
},
'overlays'
);
}
}
}
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
setStyle();
}
function addOverlay(id: string) {
try {
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
if (typeof overlay === 'string') {
$map.addImport({ id, url: overlay });
} else {
if ($opacities.hasOwnProperty(id)) {
overlay = {
...overlay,
layers: overlay.layers.map((layer) => {
if (layer.type === 'raster') {
if (!layer.paint) {
layer.paint = {};
}
layer.paint['raster-opacity'] = $opacities[id];
}
return layer;
})
};
}
$map.addImport({
id,
data: overlay
});
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
function updateOverlays() {
if ($map && $currentOverlays) {
let overlayLayers = getLayers($currentOverlays);
try {
let activeOverlays = $map
.getStyle()
.imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
toRemove.forEach((i) => {
$map.removeImport(i.id);
});
let toAdd = Object.entries(overlayLayers)
.filter(([id, selected]) => selected && !activeOverlays.some((j) => j.id === id))
.map(([id]) => id);
toAdd.forEach((id) => {
addOverlay(id);
});
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
}
$: if ($map && $currentOverlays) {
updateOverlays();
}
$: if ($map) {
if (overpassLayer) {
overpassLayer.remove();
}
overpassLayer = new OverpassLayer($map);
overpassLayer.add();
$map.on('style.import.load', updateOverlays);
}
let selectedBasemap = writable(get(currentBasemap));
selectedBasemap.subscribe((value) => {
// Updates coming from radio buttons
if (value !== get(currentBasemap)) {
previousBasemap.set(get(currentBasemap));
currentBasemap.set(value);
}
});
currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps
selectedBasemap.set(value);
});
let open = false;
function openLayerControl() {
open = true;
}
function closeLayerControl() {
open = false;
}
let cancelEvents = false;
</script>
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={container}
class="h-full w-full"
on:mouseenter={openLayerControl}
on:mouseleave={closeLayerControl}
on:pointerenter={() => {
if (!open) {
cancelEvents = true;
openLayerControl();
setTimeout(() => {
cancelEvents = false;
}, 500);
}
}}
>
<div
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
? 'opacity-0 w-0 h-0 delay-0'
: 'w-[29px] h-[29px]'}"
>
<Layers size="20" />
</div>
<div
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
? 'grid-rows-[1fr] grid-cols-[1fr]'
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
>
<ScrollArea>
<div class="h-fit">
<div class="p-2">
<LayerTree
layerTree={$selectedBasemapTree}
name="basemaps"
bind:selected={$selectedBasemap}
/>
</div>
<Separator class="w-full" />
<div class="p-2">
{#if $currentOverlays}
<LayerTree
layerTree={$selectedOverlayTree}
name="overlays"
multiple={true}
bind:checked={$currentOverlays}
/>
{/if}
</div>
<Separator class="w-full" />
<div class="p-2">
{#if $currentOverpassQueries}
<LayerTree
layerTree={$selectedOverpassTree}
name="overpass"
multiple={true}
bind:checked={$currentOverpassQueries}
/>
{/if}
</div>
</div>
</ScrollArea>
</div>
</div>
</CustomControl>
<OverpassPopup />
<svelte:window
on:click={(e) => {
if (open && !cancelEvents && !container.contains(e.target)) {
closeLayerControl();
}
}}
/>
@@ -1,188 +0,0 @@
<script lang="ts">
import LayerTree from './LayerTree.svelte';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import * as Sheet from '$lib/components/ui/sheet';
import * as Accordion from '$lib/components/ui/accordion';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { Slider } from '$lib/components/ui/slider';
import {
basemapTree,
defaultBasemap,
overlays,
overlayTree,
overpassTree
} from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store';
import { map } from '$lib/stores';
import CustomLayers from './CustomLayers.svelte';
const {
selectedBasemapTree,
selectedOverlayTree,
selectedOverpassTree,
currentBasemap,
currentOverlays,
customLayers,
opacities
} = settings;
export let open: boolean;
let accordionValue: string | string[] | undefined = undefined;
let selectedOverlay = writable(undefined);
let overlayOpacity = writable([1]);
function setOpacityFromSelection() {
if ($selectedOverlay) {
let overlayId = $selectedOverlay.value;
if ($opacities.hasOwnProperty(overlayId)) {
$overlayOpacity = [$opacities[overlayId]];
} else {
$overlayOpacity = [1];
}
} else {
$overlayOpacity = [1];
}
}
$: if ($selectedBasemapTree && $currentBasemap) {
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
}
$currentBasemap = defaultBasemap;
}
}
$: if ($selectedOverlayTree && $currentOverlays) {
let overlayLayers = getLayers($currentOverlays);
let toRemove = Object.entries(overlayLayers).filter(
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
);
if (toRemove.length > 0) {
currentOverlays.update((tree) => {
toRemove.forEach(([id]) => {
toggle(tree, id);
});
return tree;
});
}
}
$: if ($selectedOverlay) {
setOpacityFromSelection();
}
</script>
<Sheet.Root bind:open>
<Sheet.Trigger class="hidden" />
<Sheet.Content>
<Sheet.Header class="h-full">
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
<ScrollArea class="w-[105%] min-h-full pr-4">
<Sheet.Description>
{$_('layers.settings_help')}
</Sheet.Description>
<Accordion.Root class="flex flex-col" bind:value={accordionValue}>
<Accordion.Item value="layer-selection" class="flex flex-col">
<Accordion.Trigger>{$_('layers.selection')}</Accordion.Trigger>
<Accordion.Content class="grow flex flex-col border rounded">
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={basemapTree}
name="basemapSettings"
multiple={true}
bind:checked={$selectedBasemapTree}
/>
</div>
<Separator />
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={overlayTree}
name="overlaySettings"
multiple={true}
bind:checked={$selectedOverlayTree}
/>
</div>
<Separator />
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={overpassTree}
name="overpassSettings"
multiple={true}
bind:checked={$selectedOverpassTree}
/>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="overlay-opacity">
<Accordion.Trigger>{$_('layers.opacity')}</Accordion.Trigger>
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
<div class="flex flex-row gap-6 items-center">
<Label>
{$_('layers.custom_layers.overlay')}
</Label>
<Select.Root bind:selected={$selectedOverlay}>
<Select.Trigger class="h-8 mr-1">
<Select.Value />
</Select.Trigger>
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(overlays) as id}
{#if isSelected($selectedOverlayTree, id)}
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item>
{/if}
{/each}
{#each Object.entries($customLayers) as [id, layer]}
{#if layer.layerType === 'overlay'}
<Select.Item value={id}>{layer.name}</Select.Item>
{/if}
{/each}
</Select.Content>
</Select.Root>
</div>
<Label class="flex flex-row gap-6 items-center">
{$_('menu.style.opacity')}
<div class="p-2 pr-3 grow">
<Slider
bind:value={$overlayOpacity}
min={0.1}
max={1}
step={0.1}
disabled={$selectedOverlay === undefined}
onValueChange={() => {
if ($selectedOverlay) {
$opacities[$selectedOverlay.value] = $overlayOpacity[0];
if ($map) {
if ($map.getLayer($selectedOverlay.value)) {
$map.removeLayer($selectedOverlay.value);
$currentOverlays = $currentOverlays;
}
}
}
}}
/>
</div>
</Label>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="custom-layers">
<Accordion.Trigger>{$_('layers.custom_layers.title')}</Accordion.Trigger>
<Accordion.Content>
<ScrollArea>
<CustomLayers />
</ScrollArea>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</ScrollArea>
</Sheet.Header>
</Sheet.Content>
</Sheet.Root>
@@ -1,20 +0,0 @@
<script lang="ts">
import LayerTreeNode from './LayerTreeNode.svelte';
import { type LayerTreeType } from '$lib/assets/layers';
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
export let layerTree: LayerTreeType;
export let name: string;
export let selected: string | undefined = undefined;
export let multiple: boolean = false;
export let checked: LayerTreeType = {};
</script>
<form>
<fieldset class="min-w-64 mb-1">
<CollapsibleTree nohover={true}>
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
</CollapsibleTree>
</fieldset>
</form>
@@ -1,86 +0,0 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import CollapsibleTreeNode from '../collapsible-tree/CollapsibleTreeNode.svelte';
import { type LayerTreeType } from '$lib/assets/layers';
import { anySelectedLayer } from './utils';
import { _ } from 'svelte-i18n';
import { settings } from '$lib/db';
import { beforeUpdate } from 'svelte';
export let name: string;
export let node: LayerTreeType;
export let selected: string | undefined = undefined;
export let multiple: boolean = false;
export let checked: LayerTreeType;
const { customLayers } = settings;
beforeUpdate(() => {
if (checked !== undefined) {
Object.keys(node).forEach((id) => {
if (!checked.hasOwnProperty(id)) {
if (typeof node[id] == 'boolean') {
checked[id] = false;
} else {
checked[id] = {};
}
}
});
}
});
</script>
<div class="flex flex-col gap-[3px]">
{#each Object.keys(node) as id}
{#if typeof node[id] == 'boolean'}
{#if node[id]}
<div class="flex flex-row items-center gap-2 first:mt-0.5 h-4">
{#if multiple}
<Checkbox
id="{name}-{id}"
{name}
value={id}
bind:checked={checked[id]}
class="scale-90"
aria-label={$_(`layers.label.${id}`)}
/>
{:else}
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
{/if}
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
{#if $customLayers.hasOwnProperty(id)}
{$customLayers[id].name}
{:else}
{$_(`layers.label.${id}`)}
{/if}
</Label>
</div>
{/if}
{:else if anySelectedLayer(node[id])}
<CollapsibleTreeNode {id}>
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
<div slot="content">
<svelte:self node={node[id]} {name} bind:selected {multiple} bind:checked={checked[id]} />
</div>
</CollapsibleTreeNode>
{/if}
{/each}
</div>
<style lang="postcss">
div :global(input[type='radio']) {
@apply appearance-none;
@apply w-4 h-4;
@apply border-[1.5px] border-primary;
@apply rounded-full;
@apply ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
@apply cursor-pointer;
@apply checked:bg-primary;
@apply checked:bg-clip-content;
@apply checked:p-0.5;
}
</style>
@@ -1,102 +0,0 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
import { selection } from '$lib/components/file-list/Selection';
import { PencilLine, MapPin } from 'lucide-svelte';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { dbUtils } from '$lib/db';
let popupElement: HTMLDivElement;
onMount(() => {
overpassPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
let tags = {};
let name = '';
$: if ($overpassPopupPOI) {
tags = JSON.parse($overpassPopupPOI.tags);
if (tags.name !== undefined && tags.name !== '') {
name = tags.name;
} else {
name = $_(`layers.label.${$overpassPopupPOI.query}`);
}
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $overpassPopupPOI}
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
<Card.Header class="p-0">
<Card.Title class="text-md">
<div class="flex flex-row gap-3">
<div class="flex flex-col">
{name}
<div class="text-muted-foreground text-sm font-normal">
{$overpassPopupPOI.lat.toFixed(6)}&deg; {$overpassPopupPOI.lon.toFixed(6)}&deg;
</div>
</div>
<Button
class="ml-auto p-1.5 h-8"
variant="outline"
href="https://www.openstreetmap.org/edit?editor=id&node={$overpassPopupPOI.id}"
target="_blank"
>
<PencilLine size="16" />
</Button>
</div>
</Card.Title>
</Card.Header>
{#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
<!-- svelte-ignore a11y-missing-attribute -->
<img src={tags.image ?? tags['image:0']} />
</div>
{/if}
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<div class="grid grid-cols-[auto_auto] gap-x-3">
{#each Object.entries(tags) as [key, value]}
{#if key !== 'name' && !key.includes('image')}
<span class="font-mono">{key}</span>
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
<a href={value} target="_blank" class="text-link underline">{value}</a>
{:else if key === 'phone' || key === 'contact:phone'}
<a href={'tel:' + value} class="text-link underline">{value}</a>
{:else if key === 'email' || key === 'contact:email'}
<a href={'mailto:' + value} class="text-link underline">{value}</a>
{:else}
<span>{value}</span>
{/if}
{/if}
{/each}
</div>
<Button
class="mt-2"
variant="outline"
disabled={$selection.size === 0}
on:click={() => {
let desc = Object.entries(tags)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
dbUtils.addOrUpdateWaypoint({
attributes: {
lat: $overpassPopupPOI.lat,
lon: $overpassPopupPOI.lon
},
name: name,
desc: desc,
cmt: desc,
sym: $overpassPopupPOI.sym
});
}}
>
<MapPin size="16" class="mr-1" />
{$_('toolbar.waypoint.add')}
</Button>
</Card.Content>
</Card.Root>
{/if}
</div>
@@ -1,53 +0,0 @@
import type { LayerTreeType } from "$lib/assets/layers";
import { writable } from "svelte/store";
export function anySelectedLayer(node: LayerTreeType) {
return Object.keys(node).find((id) => {
if (typeof node[id] == "boolean") {
if (node[id]) {
return true;
}
} else {
if (anySelectedLayer(node[id])) {
return true;
}
}
return false;
}) !== undefined;
}
export function getLayers(node: LayerTreeType, layers: { [key: string]: boolean } = {}): { [key: string]: boolean } {
Object.keys(node).forEach((id) => {
if (typeof node[id] == "boolean") {
layers[id] = node[id];
} else {
getLayers(node[id], layers);
}
});
return layers;
}
export function isSelected(node: LayerTreeType, id: string) {
return Object.keys(node).some((key) => {
if (key === id) {
return node[key];
}
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) {
return true;
}
return false;
});
}
export function toggle(node: LayerTreeType, id: string) {
Object.keys(node).forEach((key) => {
if (key === id) {
node[key] = !node[key];
} else if (typeof node[key] !== "boolean") {
toggle(node[key], id);
}
});
return node;
}
export const customBasemapUpdate = writable(0);
@@ -0,0 +1,18 @@
<script lang="ts">
import { map } from '$lib/components/map/map';
import { trackpointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
import { TrackPoint } from 'gpx';
map.onLoad((map_) => {
map_.on('contextmenu', (e) => {
trackpointPopup?.setItem({
item: new TrackPoint({
attributes: {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
},
}),
});
});
});
</script>
+239
View File
@@ -0,0 +1,239 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { Button } from '$lib/components/ui/button';
import { i18n } from '$lib/i18n.svelte';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { page } from '$app/state';
import { map } from '$lib/components/map/map';
let {
accessToken = PUBLIC_MAPBOX_TOKEN,
geolocate = true,
geocoder = true,
hash = true,
class: className = '',
}: {
accessToken?: string;
geolocate?: boolean;
geocoder?: boolean;
hash?: boolean;
class?: string;
} = $props();
mapboxgl.accessToken = accessToken;
let webgl2Supported = $state(true);
let embeddedApp = $state(false);
onMount(() => {
let gl = document.createElement('canvas').getContext('webgl2');
if (!gl) {
webgl2Supported = false;
return;
}
if (window.top !== window.self && !page.route.id?.includes('embed')) {
embeddedApp = true;
return;
}
let language = page.params.language;
if (language === 'zh') {
language = 'zh-Hans';
} else if (language?.includes('-')) {
language = language.split('-')[0];
} else if (language === '' || language === undefined) {
language = 'en';
}
map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
});
onDestroy(() => {
map.destroy();
});
</script>
<div class={className}>
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
<div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported &&
!embeddedApp
? 'hidden'
: ''} {embeddedApp ? 'z-30' : ''}"
>
{#if !webgl2Supported}
<p>{i18n._('webgl2_required')}</p>
<Button href="https://get.webgl.org/webgl2/" target="_blank">
{i18n._('enable_webgl2')}
</Button>
{:else if embeddedApp}
<p>The app cannot be embedded in an iframe.</p>
<Button href="https://gpx.studio/help/integration" target="_blank">
Learn how to create a map for your website
</Button>
{/if}
</div>
</div>
<style lang="postcss">
@reference "../../../app.css";
div :global(.mapboxgl-map) {
@apply font-sans;
}
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-icon) {
@apply dark:brightness-[4.7];
}
div :global(.mapboxgl-ctrl-geocoder) {
@apply flex;
@apply flex-row;
@apply w-fit;
@apply min-w-fit;
@apply items-center;
@apply shadow-md;
}
div :global(.suggestions) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
@apply text-foreground;
@apply hover:text-accent-foreground;
@apply hover:bg-accent;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
@apply bg-background;
}
div :global(.mapboxgl-ctrl-geocoder--button) {
@apply bg-transparent;
@apply hover:bg-transparent;
}
div :global(.mapboxgl-ctrl-geocoder--icon) {
@apply fill-foreground;
@apply hover:fill-accent-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
@apply relative;
@apply top-0;
@apply left-0;
@apply my-2;
@apply w-[29px];
}
div :global(.mapboxgl-ctrl-geocoder--input) {
@apply relative;
@apply w-64;
@apply py-0;
@apply pl-2;
@apply focus:outline-none;
@apply transition-[width];
@apply duration-200;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
@apply w-0;
@apply p-0;
}
div :global(.mapboxgl-ctrl-top-right) {
@apply z-40;
@apply flex;
@apply flex-col;
@apply items-end;
@apply h-full;
@apply overflow-hidden;
}
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px];
}
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px];
}
div :global(.mapboxgl-ctrl-attrib) {
@apply dark:bg-transparent;
}
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
@apply dark:bg-background;
}
div :global(.mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-ctrl-attrib a) {
@apply text-foreground;
}
div :global(.mapboxgl-popup) {
@apply w-fit;
@apply z-50;
}
div :global(.mapboxgl-popup-content) {
@apply p-0;
@apply bg-transparent;
@apply shadow-none;
}
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
@apply border-r-background;
}
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
@apply border-l-background;
}
</style>
@@ -0,0 +1,34 @@
<script lang="ts">
import { TrackPoint, Waypoint } from 'gpx';
import WaypointPopup from '$lib/components/map/gpx-layer/WaypointPopup.svelte';
import TrackpointPopup from '$lib/components/map/gpx-layer/TrackpointPopup.svelte';
import OverpassPopup from '$lib/components/map/layer-control/OverpassPopup.svelte';
import type { PopupItem } from '$lib/components/map/map-popup';
import type { Writable } from 'svelte/store';
let {
item,
onContainerReady,
}: { item: Writable<PopupItem | null>; onContainerReady: (div: HTMLDivElement) => void } =
$props();
let container: HTMLDivElement | null = $state(null);
$effect(() => {
if (container) {
onContainerReady(container);
}
});
</script>
<div bind:this={container}>
{#if $item}
{#if $item.item instanceof Waypoint}
<WaypointPopup waypoint={$item} />
{:else if $item.item instanceof TrackPoint}
<TrackpointPopup trackpoint={$item} />
{:else}
<OverpassPopup poi={$item} />
{/if}
{/if}
</div>
@@ -0,0 +1,38 @@
<script lang="ts">
import CustomControl from './custom-control';
import { map } from '$lib/components/map/map';
import { onMount, type Snippet } from 'svelte';
let {
position = 'top-right',
class: className = '',
children,
}: {
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
class: string;
children: Snippet;
} = $props();
let container: HTMLDivElement;
let control: CustomControl | null = null;
onMount(() => {
map.onLoad((map: mapboxgl.Map) => {
if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left');
container.classList.remove('hidden');
if (control === null) {
control = new CustomControl(container);
}
map.addControl(control, position);
});
});
</script>
<div
bind:this={container}
class="{className ||
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
>
{@render children()}
</div>
@@ -0,0 +1,30 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { ClipboardCopy } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import type { Coordinates } from 'gpx';
let {
coordinates,
onCopy = () => {},
class: className = '',
}: {
coordinates: Coordinates;
onCopy?: () => void;
class?: string;
} = $props();
</script>
<Button
class="p-1 has-[>svg]:px-2 h-8 justify-start {className}"
variant="outline"
onclick={() => {
navigator.clipboard.writeText(
`${coordinates.lat.toFixed(6)}, ${coordinates.lon.toFixed(6)}`
);
onCopy();
}}
>
<ClipboardCopy size="16" />
{i18n._('menu.copy_coordinates')}
</Button>
@@ -0,0 +1,31 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
import { createPopups, removePopups } from '$lib/components/map/gpx-layer/gpx-layer-popup';
import { map } from '$lib/components/map/map';
let distanceMarkers: DistanceMarkers;
let startEndMarkers: StartEndMarkers;
onMount(() => {
gpxLayers.init();
startEndMarkers = new StartEndMarkers();
distanceMarkers = new DistanceMarkers();
});
map.onLoad((map_) => {
createPopups(map_);
});
onDestroy(() => {
if (startEndMarkers) {
startEndMarkers.remove();
}
if (distanceMarkers) {
distanceMarkers.remove();
}
removePopups();
});
</script>

Some files were not shown because too many files have changed in this diff Show More