Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46d6b3410e | |||
| 067012cc84 | |||
| f185b3273a | |||
| 831ea56869 | |||
| 215911cf29 | |||
| e5873f73b9 | |||
| 582eff8750 | |||
| 2cb8321d07 | |||
| 194d8d1857 | |||
| 22e9c43f78 | |||
| 4c04d00323 | |||
| 0fd42439e2 | |||
| d970cfbe0c | |||
| 91ab109ec5 | |||
| d9fcd9f624 | |||
| 092f9b622b | |||
| 843686358f | |||
| cfcb901adb | |||
| ba5d9e0e4e | |||
| e8707b36f1 | |||
| d0ba7c0fd6 | |||
| a09817a975 | |||
| 37dbb7b374 |
@@ -4,7 +4,8 @@
|
|||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules">
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -106,4 +107,7 @@
|
|||||||
<!-- Workmanager periodic widget refresh needs to reschedule after device
|
<!-- Workmanager periodic widget refresh needs to reschedule after device
|
||||||
reboot, otherwise the widget freezes until the user opens the app. -->
|
reboot, otherwise the widget freezes until the user opens the app. -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<!-- RMV "in meiner Nähe"-Suche. Coarse reicht (RMV-Suchradius >= 500 m). -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 454 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 1021 B |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 69 B |
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
|
android:inset="16%" />
|
||||||
|
</foreground>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<!-- Allow cleartext for the MarianumConnect test instance only. Once the
|
||||||
|
production URL with HTTPS is live, drop this domain-config entry. -->
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">muelleel.ddns.net</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="69.999985mm"
|
||||||
|
height="82.227501mm"
|
||||||
|
viewBox="0 0 69.999985 82.227501"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="g1"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458334,120.1078,-90.601937)"><g
|
||||||
|
id="group-R5"
|
||||||
|
transform="translate(-749.41293,290.52252)"><path
|
||||||
|
id="path2"
|
||||||
|
d="m 418.75901,57.817748 c -2.19013,14.15999 -3.96053,26.66133 -5.83906,39.13333 11.5776,2.532 18.4688,3.680002 29.7848,5.841332 8.50666,1.62133 23.36533,2.71733 23.952,11.096 0.86133,12.35866 -16.62534,7.388 -25.11734,5.84133 -11.244,-2.04667 -21.21693,-3.844 -30.9596,-5.84133 -1.82346,8.09866 -3.57813,15.82533 -5.25733,23.948 -4.90253,23.77866 -9.824,49.08933 -14.01666,73.59866 2.4136,-3.42133 4.11413,-5.69733 5.84226,-8.76267 8.89267,-15.76799 18.71867,-32.67066 30.37027,-47.31066 4.46106,-5.60667 10.24506,-12.64133 15.77306,-15.188 13.12267,-6.048 22.224,2.96133 22.78,15.77067 0.49334,11.372 -3.07733,24.15733 -4.676,36.80133 -1.65333,13.08933 -3.05066,25.98933 -4.66933,36.796 9.65733,-13.69067 19.72267,-35.95334 32.12533,-50.81733 3.668,-4.4 9.80667,-10.748 15.76934,-11.68134 29.16799,-4.57866 16.87733,41.62267 16.94133,63.084 0.0547,19.32001 5.86667,34.04934 23.36133,36.21068 5.73333,0.70933 14.10933,-1.48667 15.02533,3.51466 1.15467,6.31467 -10.42133,8.068 -16.77866,8.168 -38.84667,0.60267 -42.94133,-34.97734 -41.46933,-76.51601 -2.64534,2.476 -4.76134,6.336 -7.00934,9.93067 -13.00933,20.78267 -27.192,43.62801 -39.72133,67.17068 -3.45467,6.504 -7.15867,15.29333 -18.10667,11.09733 -2.30133,-0.88267 -6.43466,-5.92 -7.008,-7.59467 -2.60786,-7.62666 0.20667,-18.32533 1.168,-27.45333 2.87334,-27.16134 7.70934,-51.87868 10.516,-78.26534 -3.50533,-0.0707 -5.85733,4.008 -7.59333,6.42667 -6.2548,8.70266 -11.47147,18.76666 -16.94133,28.61866 -7.04214,12.684 -14.42814,25.71067 -21.02867,39.13201 -10.03173,20.41333 -25.14373,66.92133 -32.28013,90.50933 -1.15573,3.82133 -2.35933,7.744 -3.5052,11.68667 -0.998,3.428 -1.38693,7.69066 -4.67347,9.928 -13.33853,-2.52534 -11.962,-13.98534 -9.3464,-27.45467 6.31414,-32.45733 22.78547,-94.19334 28.19587,-126.13867 -9.72813,9.464 -20.53067,27.65466 -38.552,31.54133 -18.1928,3.92667 -29.05788,-6.35733 -36.2136,-16.35333 -2.28489,-3.19467 -5.52188,-6.60534 -3.50417,-10.51467 11.19375,-4.73333 15.11457,8.69467 26.28537,9.93067 3.0204,0.33199 7.2756,-0.32001 9.93027,-1.17067 17.65253,-5.64667 31.67346,-29.168 39.71506,-46.14133 9.88747,-20.86267 16.70987,-44.06667 19.86094,-65.42133 -8.76254,-1.51867 -19.064,-2.95467 -29.2344,-4.86933 -14.64054,-2.76134 -21.17334,-1.432 -26.83947,-7.980002 1.2228,-5.592 5.5228,-8.108 9.34627,-11.09867 16.83653,2.24533 33.6584,4.504 49.64946,7.59333 2.72454,-11.9 6.92973,-29.13199 9.92813,-41.47199 5.40267,-3.468 11.84947,1.12533 14.0204,4.676"
|
||||||
|
style="fill:#941a1f;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.133333" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
@@ -647,7 +647,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */;
|
baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -691,7 +691,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */;
|
baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -732,7 +732,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */;
|
baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -941,7 +941,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */;
|
baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -983,7 +983,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */;
|
baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -1023,7 +1023,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */;
|
baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
|||||||
@@ -1,122 +1 @@
|
|||||||
{
|
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 695 B After Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
@@ -38,7 +38,7 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="LaunchImage" width="800" height="1131"/>
|
<image name="LaunchImage" width="512" height="512"/>
|
||||||
<image name="LaunchBackground" width="1" height="1"/>
|
<image name="LaunchBackground" width="1" height="1"/>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -1,90 +1,105 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
|
||||||
<key>AppGroupId</key>
|
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Marianum Fulda</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>client</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Editor</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
|
|
||||||
<key>UIApplicationSceneManifest</key>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>AppGroupId</key>
|
||||||
<false/>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>UISceneConfigurations</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Marianum Fulda</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>client</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>Um Haltestellen in deiner Nähe im RMV-Fahrplan zu finden, wird dein aktueller Standort benötigt.</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
<key>NSExceptionDomains</key>
|
||||||
<array>
|
<dict>
|
||||||
|
<key>muelleel.ddns.net</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UISceneClassName</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<string>UIWindowScene</string>
|
<true/>
|
||||||
<key>UISceneConfigurationName</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<string>flutter</string>
|
<true/>
|
||||||
<key>UISceneDelegateClassName</key>
|
|
||||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
|
||||||
<key>UISceneStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>fetch</string>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UIStatusBarHidden</key>
|
|
||||||
<false/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import 'login_request.dart';
|
||||||
|
import 'login_response.dart';
|
||||||
|
|
||||||
|
class Login extends ConnectApi<LoginResponse> {
|
||||||
|
final LoginRequest payload;
|
||||||
|
|
||||||
|
Login(this.payload) : super('auth/login');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get requiresAuth => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConnectHttpMethod get method => ConnectHttpMethod.post;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? get body => payload.toJson();
|
||||||
|
|
||||||
|
@override
|
||||||
|
LoginResponse assemble(String raw) =>
|
||||||
|
LoginResponse.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'login_request.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginRequest {
|
||||||
|
final String username;
|
||||||
|
final String password;
|
||||||
|
final String tokenName;
|
||||||
|
|
||||||
|
LoginRequest({
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
required this.tokenName,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'login_request.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) => LoginRequest(
|
||||||
|
username: json['username'] as String,
|
||||||
|
password: json['password'] as String,
|
||||||
|
tokenName: json['tokenName'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LoginRequestToJson(LoginRequest instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'username': instance.username,
|
||||||
|
'password': instance.password,
|
||||||
|
'tokenName': instance.tokenName,
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/// Hand-rolled to be tolerant of the actual server payload: only [token] is
|
||||||
|
/// load-bearing. `expiresAt` may be `null` (server-issued tokens without an
|
||||||
|
/// explicit expiry); every other field shape is also tolerated so a stray
|
||||||
|
/// rename on the backend does not break login for everyone.
|
||||||
|
class LoginResponse {
|
||||||
|
final String token;
|
||||||
|
final String? tokenId;
|
||||||
|
|
||||||
|
/// `null` when the backend did not provide an expiry. In that case the
|
||||||
|
/// token is treated as long-lived; callers should refresh on 401.
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
final ConnectUserDto? user;
|
||||||
|
|
||||||
|
LoginResponse({
|
||||||
|
required this.token,
|
||||||
|
required this.tokenId,
|
||||||
|
required this.expiresAt,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
final token = json['token'];
|
||||||
|
if (token is! String || token.isEmpty) {
|
||||||
|
throw const FormatException('login response missing "token" string');
|
||||||
|
}
|
||||||
|
final expiresRaw = json['expiresAt'];
|
||||||
|
final expires = expiresRaw is String ? DateTime.tryParse(expiresRaw) : null;
|
||||||
|
final userJson = json['user'];
|
||||||
|
return LoginResponse(
|
||||||
|
token: token,
|
||||||
|
tokenId: json['tokenId']?.toString(),
|
||||||
|
expiresAt: expires,
|
||||||
|
user: userJson is Map<String, dynamic>
|
||||||
|
? ConnectUserDto.fromJson(userJson)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectUserDto {
|
||||||
|
final String? id;
|
||||||
|
final String? username;
|
||||||
|
final String? firstName;
|
||||||
|
final String? lastName;
|
||||||
|
final String? userType;
|
||||||
|
final String? className;
|
||||||
|
|
||||||
|
ConnectUserDto({
|
||||||
|
this.id,
|
||||||
|
this.username,
|
||||||
|
this.firstName,
|
||||||
|
this.lastName,
|
||||||
|
this.userType,
|
||||||
|
this.className,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ConnectUserDto.fromJson(Map<String, dynamic> json) => ConnectUserDto(
|
||||||
|
id: json['id']?.toString(),
|
||||||
|
username: json['username']?.toString(),
|
||||||
|
firstName: json['firstName']?.toString(),
|
||||||
|
lastName: json['lastName']?.toString(),
|
||||||
|
userType: json['userType']?.toString(),
|
||||||
|
className: json['className']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../api_request.dart';
|
||||||
|
import '../errors/network_exception.dart';
|
||||||
|
import '../errors/parse_exception.dart';
|
||||||
|
import '../errors/server_exception.dart';
|
||||||
|
import 'connect_auth_store.dart';
|
||||||
|
import 'connect_endpoint.dart';
|
||||||
|
import 'errors/connect_exception.dart';
|
||||||
|
import 'errors/rmv_rate_limited_exception.dart';
|
||||||
|
import 'errors/rmv_upstream_exception.dart';
|
||||||
|
|
||||||
|
enum ConnectHttpMethod { get, post }
|
||||||
|
|
||||||
|
/// Mirrors the [MhslApi] pattern: each endpoint subclasses this, declares the
|
||||||
|
/// subpath/query/body, and implements [assemble]. Handles bearer-token
|
||||||
|
/// injection (via [ConnectAuthStore]), one transparent 401-retry after a
|
||||||
|
/// fresh login, and turns the structured `RmvController.wrap` error strings
|
||||||
|
/// into typed exceptions.
|
||||||
|
abstract class ConnectApi<T> extends ApiRequest {
|
||||||
|
final String subpath;
|
||||||
|
|
||||||
|
ConnectApi(this.subpath);
|
||||||
|
|
||||||
|
/// Override to `false` for endpoints that must NOT receive a bearer token
|
||||||
|
/// (currently only login itself, to avoid an infinite refresh loop).
|
||||||
|
bool get requiresAuth => true;
|
||||||
|
|
||||||
|
ConnectHttpMethod get method => ConnectHttpMethod.get;
|
||||||
|
|
||||||
|
Map<String, String>? get queryParameters => null;
|
||||||
|
|
||||||
|
/// Returns the body to send for POST requests. Should be JSON-encodable.
|
||||||
|
Object? get body => null;
|
||||||
|
|
||||||
|
T assemble(String raw);
|
||||||
|
|
||||||
|
Future<T> run() async {
|
||||||
|
final response = await _runOnce(forceTokenRefresh: false);
|
||||||
|
if (response.statusCode == 401 && requiresAuth) {
|
||||||
|
// Single transparent retry after a forced refresh, then bail.
|
||||||
|
await ConnectAuthStore.instance.invalidate();
|
||||||
|
final retry = await _runOnce(forceTokenRefresh: true);
|
||||||
|
if (retry.statusCode == 401) {
|
||||||
|
throw ConnectException.authFailed(
|
||||||
|
technicalDetails:
|
||||||
|
'connect $subpath HTTP 401 after token refresh: ${_safeBody(retry)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _handleResponse(retry);
|
||||||
|
}
|
||||||
|
return _handleResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _runOnce({required bool forceTokenRefresh}) async {
|
||||||
|
final uri = ConnectEndpoint.resolve(subpath).replace(
|
||||||
|
queryParameters: _normaliseQuery(queryParameters),
|
||||||
|
);
|
||||||
|
|
||||||
|
final headers = <String, String>{
|
||||||
|
if (method == ConnectHttpMethod.post) 'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
if (requiresAuth) {
|
||||||
|
final token = await ConnectAuthStore.instance.getToken(
|
||||||
|
forceRefresh: forceTokenRefresh,
|
||||||
|
);
|
||||||
|
headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case ConnectHttpMethod.get:
|
||||||
|
return await http.get(uri, headers: headers);
|
||||||
|
case ConnectHttpMethod.post:
|
||||||
|
final payload = body;
|
||||||
|
return await http.post(
|
||||||
|
uri,
|
||||||
|
headers: headers,
|
||||||
|
body: payload == null ? null : jsonEncode(payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
throw NetworkException(
|
||||||
|
technicalDetails: 'connect $subpath: ${e.message}',
|
||||||
|
);
|
||||||
|
} on TimeoutException catch (e) {
|
||||||
|
throw NetworkException.timeout(
|
||||||
|
technicalDetails: 'connect $subpath: $e',
|
||||||
|
);
|
||||||
|
} on http.ClientException catch (e) {
|
||||||
|
throw NetworkException(
|
||||||
|
technicalDetails: 'connect $subpath: ${e.message}',
|
||||||
|
);
|
||||||
|
} on HandshakeException catch (e) {
|
||||||
|
throw NetworkException(
|
||||||
|
technicalDetails: 'connect $subpath TLS: ${e.message}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T _handleResponse(http.Response response) {
|
||||||
|
final status = response.statusCode;
|
||||||
|
final bodyText = _safeBody(response);
|
||||||
|
|
||||||
|
if (status == 503) {
|
||||||
|
final retryAfter = _parseRetryAfter(bodyText);
|
||||||
|
throw RmvRateLimitedException(
|
||||||
|
retryAfter: retryAfter,
|
||||||
|
technicalDetails: 'connect $subpath HTTP 503: $bodyText',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status == 502) {
|
||||||
|
final code = _parseUpstreamErrorCode(bodyText);
|
||||||
|
throw RmvUpstreamException(
|
||||||
|
errorCode: code,
|
||||||
|
technicalDetails: 'connect $subpath HTTP 502: $bodyText',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status > 299) {
|
||||||
|
throw ServerException(
|
||||||
|
statusCode: status,
|
||||||
|
technicalDetails: 'connect $subpath HTTP $status: $bodyText',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return assemble(bodyText);
|
||||||
|
} catch (e, st) {
|
||||||
|
final preview = bodyText.length > 1024
|
||||||
|
? '${bodyText.substring(0, 1024)}…'
|
||||||
|
: bodyText;
|
||||||
|
log(
|
||||||
|
'connect $subpath assemble failed: $e\nbody: $preview',
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
|
throw ParseException(
|
||||||
|
technicalDetails: 'connect $subpath assemble: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _safeBody(http.Response response) {
|
||||||
|
try {
|
||||||
|
return utf8.decode(response.bodyBytes);
|
||||||
|
} catch (_) {
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body format from `RmvController.wrap`: `upstream_rate_limited|retryAfter=60`.
|
||||||
|
Duration _parseRetryAfter(String body) {
|
||||||
|
final match = RegExp(r'retryAfter=(\d+)').firstMatch(body);
|
||||||
|
final seconds = match == null ? 60 : int.tryParse(match.group(1)!) ?? 60;
|
||||||
|
return Duration(seconds: seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body format: `upstream_error|H390` — the segment after the pipe is the
|
||||||
|
/// RMV/HaFAS error code.
|
||||||
|
String? _parseUpstreamErrorCode(String body) {
|
||||||
|
final idx = body.indexOf('|');
|
||||||
|
if (idx < 0 || idx >= body.length - 1) return null;
|
||||||
|
return body.substring(idx + 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String>? _normaliseQuery(Map<String, String>? raw) {
|
||||||
|
if (raw == null) return null;
|
||||||
|
final cleaned = <String, String>{};
|
||||||
|
raw.forEach((key, value) {
|
||||||
|
if (value.isNotEmpty) cleaned[key] = value;
|
||||||
|
});
|
||||||
|
return cleaned.isEmpty ? null : cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
import '../../model/account_data.dart';
|
||||||
|
import 'auth/login/login.dart';
|
||||||
|
import 'auth/login/login_request.dart';
|
||||||
|
import 'auth/login/login_response.dart';
|
||||||
|
import 'errors/connect_exception.dart';
|
||||||
|
|
||||||
|
/// Holds the Bearer token issued by `POST /auth/login` so that subsequent
|
||||||
|
/// RMV calls can attach it without prompting the user. Uses the LDAP
|
||||||
|
/// credentials already kept in [AccountData], so this is transparent to the
|
||||||
|
/// user — no extra login UI.
|
||||||
|
class ConnectAuthStore {
|
||||||
|
static const _tokenKey = 'connect_bearer_token';
|
||||||
|
static const _expiresAtKey = 'connect_token_expires_at';
|
||||||
|
static const _tokenName = 'MarianumMobile App';
|
||||||
|
static const _expiryGuard = Duration(minutes: 1);
|
||||||
|
|
||||||
|
static final ConnectAuthStore _instance = ConnectAuthStore._();
|
||||||
|
factory ConnectAuthStore() => _instance;
|
||||||
|
static ConnectAuthStore get instance => _instance;
|
||||||
|
ConnectAuthStore._();
|
||||||
|
|
||||||
|
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
String? _token;
|
||||||
|
DateTime? _expiresAt;
|
||||||
|
bool _hydrated = false;
|
||||||
|
Future<String>? _inflightLogin;
|
||||||
|
|
||||||
|
Future<void> _hydrate() async {
|
||||||
|
if (_hydrated) return;
|
||||||
|
_token = await _storage.read(key: _tokenKey);
|
||||||
|
final rawExp = await _storage.read(key: _expiresAtKey);
|
||||||
|
_expiresAt = rawExp == null ? null : DateTime.tryParse(rawExp);
|
||||||
|
_hydrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isUsable() {
|
||||||
|
if (_token == null || _token!.isEmpty) return false;
|
||||||
|
final exp = _expiresAt;
|
||||||
|
if (exp == null) return true;
|
||||||
|
return DateTime.now().add(_expiryGuard).isBefore(exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a usable bearer token, logging in if necessary. Concurrent
|
||||||
|
/// callers share the same in-flight login future so a single 401 doesn't
|
||||||
|
/// trigger N parallel logins.
|
||||||
|
Future<String> getToken({bool forceRefresh = false}) async {
|
||||||
|
await _hydrate();
|
||||||
|
if (!forceRefresh && _isUsable()) return _token!;
|
||||||
|
return _inflightLogin ??= _login().whenComplete(() {
|
||||||
|
_inflightLogin = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _login() async {
|
||||||
|
if (!AccountData().isPopulated()) {
|
||||||
|
throw ConnectException.notAuthenticated();
|
||||||
|
}
|
||||||
|
final username = AccountData().getUsername();
|
||||||
|
final password = AccountData().getPassword();
|
||||||
|
final LoginResponse response;
|
||||||
|
try {
|
||||||
|
response = await Login(
|
||||||
|
LoginRequest(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
tokenName: _tokenName,
|
||||||
|
),
|
||||||
|
).run();
|
||||||
|
} on ConnectException {
|
||||||
|
rethrow;
|
||||||
|
} catch (e, st) {
|
||||||
|
log('connect login threw: $e', stackTrace: st);
|
||||||
|
throw ConnectException(
|
||||||
|
userMessage:
|
||||||
|
'Anmeldung am Connect-Server fehlgeschlagen. Bitte später erneut versuchen.',
|
||||||
|
technicalDetails: 'connect login failed: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_token = response.token;
|
||||||
|
_expiresAt = response.expiresAt;
|
||||||
|
await _storage.write(key: _tokenKey, value: response.token);
|
||||||
|
if (response.expiresAt != null) {
|
||||||
|
await _storage.write(
|
||||||
|
key: _expiresAtKey,
|
||||||
|
value: response.expiresAt!.toIso8601String(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _storage.delete(key: _expiresAtKey);
|
||||||
|
}
|
||||||
|
return response.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> invalidate() async {
|
||||||
|
_token = null;
|
||||||
|
_expiresAt = null;
|
||||||
|
await _storage.delete(key: _tokenKey);
|
||||||
|
await _storage.delete(key: _expiresAtKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [invalidate] — separate method to make logout call-sites read
|
||||||
|
/// clearly.
|
||||||
|
Future<void> clear() => invalidate();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/// Base URL for the MarianumConnect backend. Hardcoded against the test
|
||||||
|
/// instance for now; once the production URL is finalised this should move
|
||||||
|
/// into `EndpointData` alongside webuntis/nextcloud.
|
||||||
|
class ConnectEndpoint {
|
||||||
|
ConnectEndpoint._();
|
||||||
|
|
||||||
|
static const String _baseUrl = 'http://muelleel.ddns.net:8080';
|
||||||
|
static const String _apiPrefix = '/api/mobile/v1';
|
||||||
|
|
||||||
|
static Uri resolve(String subpath) =>
|
||||||
|
Uri.parse('$_baseUrl$_apiPrefix/${subpath.startsWith('/') ? subpath.substring(1) : subpath}');
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import '../../errors/app_exception.dart';
|
||||||
|
|
||||||
|
class ConnectException extends AppException {
|
||||||
|
const ConnectException({
|
||||||
|
super.userMessage =
|
||||||
|
'Verbindung zum Marianum-Connect-Server fehlgeschlagen.',
|
||||||
|
super.technicalDetails,
|
||||||
|
super.allowRetry,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ConnectException.authFailed({String? technicalDetails}) =>
|
||||||
|
ConnectException(
|
||||||
|
userMessage:
|
||||||
|
'Anmeldung am Connect-Server fehlgeschlagen. Bitte prüfe deine Anmeldedaten.',
|
||||||
|
technicalDetails: technicalDetails,
|
||||||
|
allowRetry: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ConnectException.notAuthenticated() => const ConnectException(
|
||||||
|
userMessage:
|
||||||
|
'Für diese Funktion ist eine Anmeldung am Connect-Server nötig.',
|
||||||
|
technicalDetails: 'AccountData missing while trying to log in to connect',
|
||||||
|
allowRetry: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import '../../errors/app_exception.dart';
|
||||||
|
|
||||||
|
class RmvRateLimitedException extends AppException {
|
||||||
|
final Duration retryAfter;
|
||||||
|
|
||||||
|
RmvRateLimitedException({required this.retryAfter, super.technicalDetails})
|
||||||
|
: super(
|
||||||
|
userMessage:
|
||||||
|
'Die Fahrplanauskunft ist gerade überlastet. Bitte in ${retryAfter.inSeconds} Sekunden erneut versuchen.',
|
||||||
|
allowRetry: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import '../../errors/app_exception.dart';
|
||||||
|
|
||||||
|
class RmvUpstreamException extends AppException {
|
||||||
|
final String? errorCode;
|
||||||
|
|
||||||
|
RmvUpstreamException({required this.errorCode, super.technicalDetails})
|
||||||
|
: super(userMessage: _mapMessage(errorCode), allowRetry: true);
|
||||||
|
|
||||||
|
static String _mapMessage(String? code) {
|
||||||
|
switch (code) {
|
||||||
|
case 'H390':
|
||||||
|
return 'Keine Verbindung gefunden.';
|
||||||
|
case 'H891':
|
||||||
|
return 'Eine der angegebenen Stationen ist ungültig.';
|
||||||
|
case 'H895':
|
||||||
|
return 'Start- und Zielhaltestelle sind identisch.';
|
||||||
|
case 'H900':
|
||||||
|
case 'H892':
|
||||||
|
return 'Die Fahrplanauskunft ist gerade nicht verfügbar.';
|
||||||
|
case 'H910':
|
||||||
|
return 'Die angegebene Zeit liegt außerhalb des Fahrplans.';
|
||||||
|
case null:
|
||||||
|
return 'Die Fahrplanauskunft konnte keine Antwort liefern.';
|
||||||
|
default:
|
||||||
|
return 'Die Fahrplanauskunft hat einen Fehler gemeldet ($code).';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/// ISO-8601 duration (`PT1H30M5S`) ↔ Dart `Duration`. Backend serialises
|
||||||
|
/// `java.time.Duration` in this format; Dart has no builtin parser.
|
||||||
|
class IsoDuration {
|
||||||
|
IsoDuration._();
|
||||||
|
|
||||||
|
static final RegExp _pattern = RegExp(
|
||||||
|
r'^P(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$',
|
||||||
|
);
|
||||||
|
|
||||||
|
static Duration? fromJson(String? iso) {
|
||||||
|
if (iso == null || iso.isEmpty) return null;
|
||||||
|
final match = _pattern.firstMatch(iso);
|
||||||
|
if (match == null) return null;
|
||||||
|
final hours = int.parse(match.group(1) ?? '0');
|
||||||
|
final minutes = int.parse(match.group(2) ?? '0');
|
||||||
|
final secondsRaw = match.group(3) ?? '0';
|
||||||
|
final secondsValue = double.parse(secondsRaw);
|
||||||
|
return Duration(
|
||||||
|
hours: hours,
|
||||||
|
minutes: minutes,
|
||||||
|
milliseconds: (secondsValue * 1000).round(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? toJson(Duration? d) {
|
||||||
|
if (d == null) return null;
|
||||||
|
final hours = d.inHours;
|
||||||
|
final minutes = d.inMinutes.remainder(60);
|
||||||
|
final seconds = d.inSeconds.remainder(60);
|
||||||
|
final buf = StringBuffer('PT');
|
||||||
|
if (hours > 0) buf.write('${hours}H');
|
||||||
|
if (minutes > 0) buf.write('${minutes}M');
|
||||||
|
if (seconds > 0 || (hours == 0 && minutes == 0)) buf.write('${seconds}S');
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/// Formats a [DateTime] as `2026-05-19T14:30:00` for Java's
|
||||||
|
/// `LocalDateTime` parser (no timezone, no millis).
|
||||||
|
String formatLocalDateTime(DateTime dt) {
|
||||||
|
String two(int v) => v.toString().padLeft(2, '0');
|
||||||
|
return '${dt.year}-${two(dt.month)}-${two(dt.day)}T'
|
||||||
|
'${two(dt.hour)}:${two(dt.minute)}:${two(dt.second)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a [DateTime] as `2026-05-19` for Java's `LocalDate` parser.
|
||||||
|
String formatLocalDate(DateTime dt) {
|
||||||
|
String two(int v) => v.toString().padLeft(2, '0');
|
||||||
|
return '${dt.year}-${two(dt.month)}-${two(dt.day)}';
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
import '_query_format.dart';
|
||||||
|
|
||||||
|
class GetArrivals extends ConnectApi<List<Arrival>> {
|
||||||
|
final String stopId;
|
||||||
|
final DateTime? when;
|
||||||
|
final int durationMinutes;
|
||||||
|
final int maxJourneys;
|
||||||
|
|
||||||
|
GetArrivals({
|
||||||
|
required this.stopId,
|
||||||
|
this.when,
|
||||||
|
this.durationMinutes = 60,
|
||||||
|
this.maxJourneys = -1,
|
||||||
|
}) : super('rmv/arrivals');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'stopId': stopId,
|
||||||
|
if (when != null) 'when': formatLocalDateTime(when!),
|
||||||
|
'duration': durationMinutes.toString(),
|
||||||
|
'max': maxJourneys.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Arrival> assemble(String raw) => (jsonDecode(raw) as List)
|
||||||
|
.map((e) => Arrival.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
import '_query_format.dart';
|
||||||
|
|
||||||
|
class GetDepartures extends ConnectApi<List<Departure>> {
|
||||||
|
final String stopId;
|
||||||
|
final DateTime? when;
|
||||||
|
final int durationMinutes;
|
||||||
|
final int maxJourneys;
|
||||||
|
|
||||||
|
GetDepartures({
|
||||||
|
required this.stopId,
|
||||||
|
this.when,
|
||||||
|
this.durationMinutes = 60,
|
||||||
|
this.maxJourneys = -1,
|
||||||
|
}) : super('rmv/departures');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'stopId': stopId,
|
||||||
|
if (when != null) 'when': formatLocalDateTime(when!),
|
||||||
|
'duration': durationMinutes.toString(),
|
||||||
|
'max': maxJourneys.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Departure> assemble(String raw) => (jsonDecode(raw) as List)
|
||||||
|
.map((e) => Departure.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
import '_query_format.dart';
|
||||||
|
|
||||||
|
class GetDisruptions extends ConnectApi<List<HimMessage>> {
|
||||||
|
final DateTime? when;
|
||||||
|
|
||||||
|
GetDisruptions({this.when}) : super('rmv/disruptions');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters =>
|
||||||
|
when == null ? null : {'when': formatLocalDateTime(when!)};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<HimMessage> assemble(String raw) => (jsonDecode(raw) as List)
|
||||||
|
.map((e) => HimMessage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
import '_query_format.dart';
|
||||||
|
|
||||||
|
class GetJourneyDetail extends ConnectApi<JourneyDetail> {
|
||||||
|
final String journeyRef;
|
||||||
|
final DateTime? date;
|
||||||
|
|
||||||
|
GetJourneyDetail({required this.journeyRef, this.date})
|
||||||
|
: super('rmv/journey');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'ref': journeyRef,
|
||||||
|
if (date != null) 'date': formatLocalDate(date!),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
JourneyDetail assemble(String raw) =>
|
||||||
|
JourneyDetail.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
|
||||||
|
class MoreTrips extends ConnectApi<TripSearchResult> {
|
||||||
|
final String ctx;
|
||||||
|
|
||||||
|
MoreTrips({required this.ctx}) : super('rmv/trips/more');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {'ctx': ctx};
|
||||||
|
|
||||||
|
@override
|
||||||
|
TripSearchResult assemble(String raw) =>
|
||||||
|
TripSearchResult.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
|
||||||
|
class NearbyStops extends ConnectApi<List<StopLocation>> {
|
||||||
|
final double lat;
|
||||||
|
final double lon;
|
||||||
|
final int radiusMeters;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
NearbyStops({
|
||||||
|
required this.lat,
|
||||||
|
required this.lon,
|
||||||
|
this.radiusMeters = 1000,
|
||||||
|
this.max = 20,
|
||||||
|
}) : super('rmv/stops/nearby');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'lat': lat.toString(),
|
||||||
|
'lon': lon.toString(),
|
||||||
|
'radius': radiusMeters.toString(),
|
||||||
|
'max': max.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<StopLocation> assemble(String raw) => (jsonDecode(raw) as List)
|
||||||
|
.map((e) => StopLocation.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
|
||||||
|
class SearchStops extends ConnectApi<List<StopLocation>> {
|
||||||
|
final String query;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
SearchStops({required this.query, this.max = 10}) : super('rmv/stops');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'q': query,
|
||||||
|
'max': max.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<StopLocation> assemble(String raw) => (jsonDecode(raw) as List)
|
||||||
|
.map((e) => StopLocation.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../connect_api.dart';
|
||||||
|
import '../rmv_models.dart';
|
||||||
|
import '_query_format.dart';
|
||||||
|
|
||||||
|
class SearchTrips extends ConnectApi<TripSearchResult> {
|
||||||
|
final String fromStopId;
|
||||||
|
final String toStopId;
|
||||||
|
final DateTime? when;
|
||||||
|
final bool searchByArrival;
|
||||||
|
|
||||||
|
SearchTrips({
|
||||||
|
required this.fromStopId,
|
||||||
|
required this.toStopId,
|
||||||
|
this.when,
|
||||||
|
this.searchByArrival = false,
|
||||||
|
}) : super('rmv/trips');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get queryParameters => {
|
||||||
|
'from': fromStopId,
|
||||||
|
'to': toStopId,
|
||||||
|
if (when != null) 'when': formatLocalDateTime(when!),
|
||||||
|
'searchByArrival': searchByArrival.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
TripSearchResult assemble(String raw) =>
|
||||||
|
TripSearchResult.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'iso_duration.dart';
|
||||||
|
|
||||||
|
part 'rmv_models.freezed.dart';
|
||||||
|
part 'rmv_models.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Product with _$Product {
|
||||||
|
const factory Product({
|
||||||
|
String? name,
|
||||||
|
String? line,
|
||||||
|
String? displayNumber,
|
||||||
|
String? category,
|
||||||
|
String? categoryCode,
|
||||||
|
String? operator,
|
||||||
|
}) = _Product;
|
||||||
|
|
||||||
|
factory Product.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ProductFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class StopLocation with _$StopLocation {
|
||||||
|
const factory StopLocation({
|
||||||
|
required String id,
|
||||||
|
String? extId,
|
||||||
|
required String name,
|
||||||
|
String? description,
|
||||||
|
double? lat,
|
||||||
|
double? lon,
|
||||||
|
int? products,
|
||||||
|
int? distanceMeters,
|
||||||
|
}) = _StopLocation;
|
||||||
|
|
||||||
|
factory StopLocation.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$StopLocationFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Departure with _$Departure {
|
||||||
|
const factory Departure({
|
||||||
|
required String stopId,
|
||||||
|
String? stopExtId,
|
||||||
|
required String stopName,
|
||||||
|
required String name,
|
||||||
|
required String direction,
|
||||||
|
String? directionFlag,
|
||||||
|
required DateTime scheduledTime,
|
||||||
|
DateTime? realTime,
|
||||||
|
int? delayMinutes,
|
||||||
|
String? track,
|
||||||
|
String? realTrack,
|
||||||
|
@Default(false) bool cancelled,
|
||||||
|
@Default(true) bool reachable,
|
||||||
|
Product? product,
|
||||||
|
String? journeyRef,
|
||||||
|
}) = _Departure;
|
||||||
|
|
||||||
|
factory Departure.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$DepartureFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Arrival with _$Arrival {
|
||||||
|
const factory Arrival({
|
||||||
|
required String stopId,
|
||||||
|
String? stopExtId,
|
||||||
|
required String stopName,
|
||||||
|
required String name,
|
||||||
|
required String origin,
|
||||||
|
required DateTime scheduledTime,
|
||||||
|
DateTime? realTime,
|
||||||
|
int? delayMinutes,
|
||||||
|
String? track,
|
||||||
|
String? realTrack,
|
||||||
|
@Default(false) bool cancelled,
|
||||||
|
Product? product,
|
||||||
|
String? journeyRef,
|
||||||
|
}) = _Arrival;
|
||||||
|
|
||||||
|
factory Arrival.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ArrivalFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class TripEndpoint with _$TripEndpoint {
|
||||||
|
const factory TripEndpoint({
|
||||||
|
required String stopId,
|
||||||
|
String? stopExtId,
|
||||||
|
required String name,
|
||||||
|
double? lat,
|
||||||
|
double? lon,
|
||||||
|
required DateTime scheduledTime,
|
||||||
|
DateTime? realTime,
|
||||||
|
int? delayMinutes,
|
||||||
|
String? track,
|
||||||
|
String? realTrack,
|
||||||
|
String? type,
|
||||||
|
}) = _TripEndpoint;
|
||||||
|
|
||||||
|
factory TripEndpoint.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$TripEndpointFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class JourneyStop with _$JourneyStop {
|
||||||
|
const factory JourneyStop({
|
||||||
|
required String id,
|
||||||
|
String? extId,
|
||||||
|
required String name,
|
||||||
|
double? lat,
|
||||||
|
double? lon,
|
||||||
|
int? routeIdx,
|
||||||
|
DateTime? scheduledArrival,
|
||||||
|
DateTime? scheduledDeparture,
|
||||||
|
DateTime? realArrival,
|
||||||
|
DateTime? realDeparture,
|
||||||
|
String? arrTrack,
|
||||||
|
String? depTrack,
|
||||||
|
String? realArrTrack,
|
||||||
|
String? realDepTrack,
|
||||||
|
@Default(false) bool cancelled,
|
||||||
|
@Default(false) bool cancelledArrival,
|
||||||
|
@Default(false) bool cancelledDeparture,
|
||||||
|
}) = _JourneyStop;
|
||||||
|
|
||||||
|
factory JourneyStop.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$JourneyStopFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LegType {
|
||||||
|
@JsonValue('JOURNEY')
|
||||||
|
journey,
|
||||||
|
@JsonValue('WALK')
|
||||||
|
walk,
|
||||||
|
@JsonValue('TRANSFER')
|
||||||
|
transfer,
|
||||||
|
@JsonValue('BIKE')
|
||||||
|
bike,
|
||||||
|
@JsonValue('CAR')
|
||||||
|
car,
|
||||||
|
@JsonValue('PARK_RIDE')
|
||||||
|
parkRide,
|
||||||
|
@JsonValue('TAXI')
|
||||||
|
taxi,
|
||||||
|
@JsonValue('CHECK_IN')
|
||||||
|
checkIn,
|
||||||
|
@JsonValue('CHECK_OUT')
|
||||||
|
checkOut,
|
||||||
|
@JsonValue('DUMMY')
|
||||||
|
dummy,
|
||||||
|
@JsonValue('UNKNOWN')
|
||||||
|
unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Leg with _$Leg {
|
||||||
|
const factory Leg({
|
||||||
|
required String id,
|
||||||
|
required int idx,
|
||||||
|
@Default(LegType.unknown) LegType type,
|
||||||
|
String? name,
|
||||||
|
String? category,
|
||||||
|
String? number,
|
||||||
|
String? direction,
|
||||||
|
required TripEndpoint origin,
|
||||||
|
required TripEndpoint destination,
|
||||||
|
@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson)
|
||||||
|
Duration? duration,
|
||||||
|
@Default(false) bool cancelled,
|
||||||
|
@Default(false) bool partCancelled,
|
||||||
|
@Default(true) bool reachable,
|
||||||
|
Product? product,
|
||||||
|
String? journeyRef,
|
||||||
|
@Default(<JourneyStop>[]) List<JourneyStop> stops,
|
||||||
|
}) = _Leg;
|
||||||
|
|
||||||
|
factory Leg.fromJson(Map<String, Object?> json) => _$LegFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Trip with _$Trip {
|
||||||
|
const factory Trip({
|
||||||
|
String? tripId,
|
||||||
|
String? ctxRecon,
|
||||||
|
String? checksum,
|
||||||
|
@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson)
|
||||||
|
Duration? duration,
|
||||||
|
@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson)
|
||||||
|
Duration? realDuration,
|
||||||
|
int? transferCount,
|
||||||
|
@Default(<Leg>[]) List<Leg> legs,
|
||||||
|
}) = _Trip;
|
||||||
|
|
||||||
|
factory Trip.fromJson(Map<String, Object?> json) => _$TripFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class TripSearchResult with _$TripSearchResult {
|
||||||
|
const factory TripSearchResult({
|
||||||
|
@Default(<Trip>[]) List<Trip> trips,
|
||||||
|
String? scrollContextLater,
|
||||||
|
String? scrollContextEarlier,
|
||||||
|
}) = _TripSearchResult;
|
||||||
|
|
||||||
|
factory TripSearchResult.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$TripSearchResultFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class JourneyDetail with _$JourneyDetail {
|
||||||
|
const factory JourneyDetail({
|
||||||
|
String? journeyId,
|
||||||
|
Product? product,
|
||||||
|
String? direction,
|
||||||
|
@Default(<JourneyStop>[]) List<JourneyStop> stops,
|
||||||
|
}) = _JourneyDetail;
|
||||||
|
|
||||||
|
factory JourneyDetail.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$JourneyDetailFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class HimMessage with _$HimMessage {
|
||||||
|
const factory HimMessage({
|
||||||
|
required String id,
|
||||||
|
String? externalId,
|
||||||
|
String? head,
|
||||||
|
String? lead,
|
||||||
|
String? text,
|
||||||
|
String? category,
|
||||||
|
String? company,
|
||||||
|
int? priority,
|
||||||
|
int? products,
|
||||||
|
DateTime? startValidity,
|
||||||
|
DateTime? endValidity,
|
||||||
|
DateTime? modified,
|
||||||
|
}) = _HimMessage;
|
||||||
|
|
||||||
|
factory HimMessage.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$HimMessageFromJson(json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'rmv_models.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_Product _$ProductFromJson(Map<String, dynamic> json) => _Product(
|
||||||
|
name: json['name'] as String?,
|
||||||
|
line: json['line'] as String?,
|
||||||
|
displayNumber: json['displayNumber'] as String?,
|
||||||
|
category: json['category'] as String?,
|
||||||
|
categoryCode: json['categoryCode'] as String?,
|
||||||
|
operator: json['operator'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ProductToJson(_Product instance) => <String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'line': instance.line,
|
||||||
|
'displayNumber': instance.displayNumber,
|
||||||
|
'category': instance.category,
|
||||||
|
'categoryCode': instance.categoryCode,
|
||||||
|
'operator': instance.operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
_StopLocation _$StopLocationFromJson(Map<String, dynamic> json) =>
|
||||||
|
_StopLocation(
|
||||||
|
id: json['id'] as String,
|
||||||
|
extId: json['extId'] as String?,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
lat: (json['lat'] as num?)?.toDouble(),
|
||||||
|
lon: (json['lon'] as num?)?.toDouble(),
|
||||||
|
products: (json['products'] as num?)?.toInt(),
|
||||||
|
distanceMeters: (json['distanceMeters'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$StopLocationToJson(_StopLocation instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'extId': instance.extId,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'lat': instance.lat,
|
||||||
|
'lon': instance.lon,
|
||||||
|
'products': instance.products,
|
||||||
|
'distanceMeters': instance.distanceMeters,
|
||||||
|
};
|
||||||
|
|
||||||
|
_Departure _$DepartureFromJson(Map<String, dynamic> json) => _Departure(
|
||||||
|
stopId: json['stopId'] as String,
|
||||||
|
stopExtId: json['stopExtId'] as String?,
|
||||||
|
stopName: json['stopName'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
direction: json['direction'] as String,
|
||||||
|
directionFlag: json['directionFlag'] as String?,
|
||||||
|
scheduledTime: DateTime.parse(json['scheduledTime'] as String),
|
||||||
|
realTime: json['realTime'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['realTime'] as String),
|
||||||
|
delayMinutes: (json['delayMinutes'] as num?)?.toInt(),
|
||||||
|
track: json['track'] as String?,
|
||||||
|
realTrack: json['realTrack'] as String?,
|
||||||
|
cancelled: json['cancelled'] as bool? ?? false,
|
||||||
|
reachable: json['reachable'] as bool? ?? true,
|
||||||
|
product: json['product'] == null
|
||||||
|
? null
|
||||||
|
: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
|
journeyRef: json['journeyRef'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$DepartureToJson(_Departure instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'stopId': instance.stopId,
|
||||||
|
'stopExtId': instance.stopExtId,
|
||||||
|
'stopName': instance.stopName,
|
||||||
|
'name': instance.name,
|
||||||
|
'direction': instance.direction,
|
||||||
|
'directionFlag': instance.directionFlag,
|
||||||
|
'scheduledTime': instance.scheduledTime.toIso8601String(),
|
||||||
|
'realTime': instance.realTime?.toIso8601String(),
|
||||||
|
'delayMinutes': instance.delayMinutes,
|
||||||
|
'track': instance.track,
|
||||||
|
'realTrack': instance.realTrack,
|
||||||
|
'cancelled': instance.cancelled,
|
||||||
|
'reachable': instance.reachable,
|
||||||
|
'product': instance.product,
|
||||||
|
'journeyRef': instance.journeyRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
_Arrival _$ArrivalFromJson(Map<String, dynamic> json) => _Arrival(
|
||||||
|
stopId: json['stopId'] as String,
|
||||||
|
stopExtId: json['stopExtId'] as String?,
|
||||||
|
stopName: json['stopName'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
origin: json['origin'] as String,
|
||||||
|
scheduledTime: DateTime.parse(json['scheduledTime'] as String),
|
||||||
|
realTime: json['realTime'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['realTime'] as String),
|
||||||
|
delayMinutes: (json['delayMinutes'] as num?)?.toInt(),
|
||||||
|
track: json['track'] as String?,
|
||||||
|
realTrack: json['realTrack'] as String?,
|
||||||
|
cancelled: json['cancelled'] as bool? ?? false,
|
||||||
|
product: json['product'] == null
|
||||||
|
? null
|
||||||
|
: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
|
journeyRef: json['journeyRef'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ArrivalToJson(_Arrival instance) => <String, dynamic>{
|
||||||
|
'stopId': instance.stopId,
|
||||||
|
'stopExtId': instance.stopExtId,
|
||||||
|
'stopName': instance.stopName,
|
||||||
|
'name': instance.name,
|
||||||
|
'origin': instance.origin,
|
||||||
|
'scheduledTime': instance.scheduledTime.toIso8601String(),
|
||||||
|
'realTime': instance.realTime?.toIso8601String(),
|
||||||
|
'delayMinutes': instance.delayMinutes,
|
||||||
|
'track': instance.track,
|
||||||
|
'realTrack': instance.realTrack,
|
||||||
|
'cancelled': instance.cancelled,
|
||||||
|
'product': instance.product,
|
||||||
|
'journeyRef': instance.journeyRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
_TripEndpoint _$TripEndpointFromJson(Map<String, dynamic> json) =>
|
||||||
|
_TripEndpoint(
|
||||||
|
stopId: json['stopId'] as String,
|
||||||
|
stopExtId: json['stopExtId'] as String?,
|
||||||
|
name: json['name'] as String,
|
||||||
|
lat: (json['lat'] as num?)?.toDouble(),
|
||||||
|
lon: (json['lon'] as num?)?.toDouble(),
|
||||||
|
scheduledTime: DateTime.parse(json['scheduledTime'] as String),
|
||||||
|
realTime: json['realTime'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['realTime'] as String),
|
||||||
|
delayMinutes: (json['delayMinutes'] as num?)?.toInt(),
|
||||||
|
track: json['track'] as String?,
|
||||||
|
realTrack: json['realTrack'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TripEndpointToJson(_TripEndpoint instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'stopId': instance.stopId,
|
||||||
|
'stopExtId': instance.stopExtId,
|
||||||
|
'name': instance.name,
|
||||||
|
'lat': instance.lat,
|
||||||
|
'lon': instance.lon,
|
||||||
|
'scheduledTime': instance.scheduledTime.toIso8601String(),
|
||||||
|
'realTime': instance.realTime?.toIso8601String(),
|
||||||
|
'delayMinutes': instance.delayMinutes,
|
||||||
|
'track': instance.track,
|
||||||
|
'realTrack': instance.realTrack,
|
||||||
|
'type': instance.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_JourneyStop _$JourneyStopFromJson(Map<String, dynamic> json) => _JourneyStop(
|
||||||
|
id: json['id'] as String,
|
||||||
|
extId: json['extId'] as String?,
|
||||||
|
name: json['name'] as String,
|
||||||
|
lat: (json['lat'] as num?)?.toDouble(),
|
||||||
|
lon: (json['lon'] as num?)?.toDouble(),
|
||||||
|
routeIdx: (json['routeIdx'] as num?)?.toInt(),
|
||||||
|
scheduledArrival: json['scheduledArrival'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['scheduledArrival'] as String),
|
||||||
|
scheduledDeparture: json['scheduledDeparture'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['scheduledDeparture'] as String),
|
||||||
|
realArrival: json['realArrival'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['realArrival'] as String),
|
||||||
|
realDeparture: json['realDeparture'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['realDeparture'] as String),
|
||||||
|
arrTrack: json['arrTrack'] as String?,
|
||||||
|
depTrack: json['depTrack'] as String?,
|
||||||
|
realArrTrack: json['realArrTrack'] as String?,
|
||||||
|
realDepTrack: json['realDepTrack'] as String?,
|
||||||
|
cancelled: json['cancelled'] as bool? ?? false,
|
||||||
|
cancelledArrival: json['cancelledArrival'] as bool? ?? false,
|
||||||
|
cancelledDeparture: json['cancelledDeparture'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$JourneyStopToJson(_JourneyStop instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'extId': instance.extId,
|
||||||
|
'name': instance.name,
|
||||||
|
'lat': instance.lat,
|
||||||
|
'lon': instance.lon,
|
||||||
|
'routeIdx': instance.routeIdx,
|
||||||
|
'scheduledArrival': instance.scheduledArrival?.toIso8601String(),
|
||||||
|
'scheduledDeparture': instance.scheduledDeparture?.toIso8601String(),
|
||||||
|
'realArrival': instance.realArrival?.toIso8601String(),
|
||||||
|
'realDeparture': instance.realDeparture?.toIso8601String(),
|
||||||
|
'arrTrack': instance.arrTrack,
|
||||||
|
'depTrack': instance.depTrack,
|
||||||
|
'realArrTrack': instance.realArrTrack,
|
||||||
|
'realDepTrack': instance.realDepTrack,
|
||||||
|
'cancelled': instance.cancelled,
|
||||||
|
'cancelledArrival': instance.cancelledArrival,
|
||||||
|
'cancelledDeparture': instance.cancelledDeparture,
|
||||||
|
};
|
||||||
|
|
||||||
|
_Leg _$LegFromJson(Map<String, dynamic> json) => _Leg(
|
||||||
|
id: json['id'] as String,
|
||||||
|
idx: (json['idx'] as num).toInt(),
|
||||||
|
type: $enumDecodeNullable(_$LegTypeEnumMap, json['type']) ?? LegType.unknown,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
category: json['category'] as String?,
|
||||||
|
number: json['number'] as String?,
|
||||||
|
direction: json['direction'] as String?,
|
||||||
|
origin: TripEndpoint.fromJson(json['origin'] as Map<String, dynamic>),
|
||||||
|
destination: TripEndpoint.fromJson(
|
||||||
|
json['destination'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
duration: IsoDuration.fromJson(json['duration'] as String?),
|
||||||
|
cancelled: json['cancelled'] as bool? ?? false,
|
||||||
|
partCancelled: json['partCancelled'] as bool? ?? false,
|
||||||
|
reachable: json['reachable'] as bool? ?? true,
|
||||||
|
product: json['product'] == null
|
||||||
|
? null
|
||||||
|
: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
|
journeyRef: json['journeyRef'] as String?,
|
||||||
|
stops:
|
||||||
|
(json['stops'] as List<dynamic>?)
|
||||||
|
?.map((e) => JourneyStop.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const <JourneyStop>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LegToJson(_Leg instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'idx': instance.idx,
|
||||||
|
'type': _$LegTypeEnumMap[instance.type]!,
|
||||||
|
'name': instance.name,
|
||||||
|
'category': instance.category,
|
||||||
|
'number': instance.number,
|
||||||
|
'direction': instance.direction,
|
||||||
|
'origin': instance.origin,
|
||||||
|
'destination': instance.destination,
|
||||||
|
'duration': IsoDuration.toJson(instance.duration),
|
||||||
|
'cancelled': instance.cancelled,
|
||||||
|
'partCancelled': instance.partCancelled,
|
||||||
|
'reachable': instance.reachable,
|
||||||
|
'product': instance.product,
|
||||||
|
'journeyRef': instance.journeyRef,
|
||||||
|
'stops': instance.stops,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$LegTypeEnumMap = {
|
||||||
|
LegType.journey: 'JOURNEY',
|
||||||
|
LegType.walk: 'WALK',
|
||||||
|
LegType.transfer: 'TRANSFER',
|
||||||
|
LegType.bike: 'BIKE',
|
||||||
|
LegType.car: 'CAR',
|
||||||
|
LegType.parkRide: 'PARK_RIDE',
|
||||||
|
LegType.taxi: 'TAXI',
|
||||||
|
LegType.checkIn: 'CHECK_IN',
|
||||||
|
LegType.checkOut: 'CHECK_OUT',
|
||||||
|
LegType.dummy: 'DUMMY',
|
||||||
|
LegType.unknown: 'UNKNOWN',
|
||||||
|
};
|
||||||
|
|
||||||
|
_Trip _$TripFromJson(Map<String, dynamic> json) => _Trip(
|
||||||
|
tripId: json['tripId'] as String?,
|
||||||
|
ctxRecon: json['ctxRecon'] as String?,
|
||||||
|
checksum: json['checksum'] as String?,
|
||||||
|
duration: IsoDuration.fromJson(json['duration'] as String?),
|
||||||
|
realDuration: IsoDuration.fromJson(json['realDuration'] as String?),
|
||||||
|
transferCount: (json['transferCount'] as num?)?.toInt(),
|
||||||
|
legs:
|
||||||
|
(json['legs'] as List<dynamic>?)
|
||||||
|
?.map((e) => Leg.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const <Leg>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TripToJson(_Trip instance) => <String, dynamic>{
|
||||||
|
'tripId': instance.tripId,
|
||||||
|
'ctxRecon': instance.ctxRecon,
|
||||||
|
'checksum': instance.checksum,
|
||||||
|
'duration': IsoDuration.toJson(instance.duration),
|
||||||
|
'realDuration': IsoDuration.toJson(instance.realDuration),
|
||||||
|
'transferCount': instance.transferCount,
|
||||||
|
'legs': instance.legs,
|
||||||
|
};
|
||||||
|
|
||||||
|
_TripSearchResult _$TripSearchResultFromJson(Map<String, dynamic> json) =>
|
||||||
|
_TripSearchResult(
|
||||||
|
trips:
|
||||||
|
(json['trips'] as List<dynamic>?)
|
||||||
|
?.map((e) => Trip.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const <Trip>[],
|
||||||
|
scrollContextLater: json['scrollContextLater'] as String?,
|
||||||
|
scrollContextEarlier: json['scrollContextEarlier'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TripSearchResultToJson(_TripSearchResult instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'trips': instance.trips,
|
||||||
|
'scrollContextLater': instance.scrollContextLater,
|
||||||
|
'scrollContextEarlier': instance.scrollContextEarlier,
|
||||||
|
};
|
||||||
|
|
||||||
|
_JourneyDetail _$JourneyDetailFromJson(Map<String, dynamic> json) =>
|
||||||
|
_JourneyDetail(
|
||||||
|
journeyId: json['journeyId'] as String?,
|
||||||
|
product: json['product'] == null
|
||||||
|
? null
|
||||||
|
: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
|
direction: json['direction'] as String?,
|
||||||
|
stops:
|
||||||
|
(json['stops'] as List<dynamic>?)
|
||||||
|
?.map((e) => JourneyStop.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const <JourneyStop>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$JourneyDetailToJson(_JourneyDetail instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'journeyId': instance.journeyId,
|
||||||
|
'product': instance.product,
|
||||||
|
'direction': instance.direction,
|
||||||
|
'stops': instance.stops,
|
||||||
|
};
|
||||||
|
|
||||||
|
_HimMessage _$HimMessageFromJson(Map<String, dynamic> json) => _HimMessage(
|
||||||
|
id: json['id'] as String,
|
||||||
|
externalId: json['externalId'] as String?,
|
||||||
|
head: json['head'] as String?,
|
||||||
|
lead: json['lead'] as String?,
|
||||||
|
text: json['text'] as String?,
|
||||||
|
category: json['category'] as String?,
|
||||||
|
company: json['company'] as String?,
|
||||||
|
priority: (json['priority'] as num?)?.toInt(),
|
||||||
|
products: (json['products'] as num?)?.toInt(),
|
||||||
|
startValidity: json['startValidity'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['startValidity'] as String),
|
||||||
|
endValidity: json['endValidity'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['endValidity'] as String),
|
||||||
|
modified: json['modified'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['modified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$HimMessageToJson(_HimMessage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'externalId': instance.externalId,
|
||||||
|
'head': instance.head,
|
||||||
|
'lead': instance.lead,
|
||||||
|
'text': instance.text,
|
||||||
|
'category': instance.category,
|
||||||
|
'company': instance.company,
|
||||||
|
'priority': instance.priority,
|
||||||
|
'products': instance.products,
|
||||||
|
'startValidity': instance.startValidity?.toIso8601String(),
|
||||||
|
'endValidity': instance.endValidity?.toIso8601String(),
|
||||||
|
'modified': instance.modified?.toIso8601String(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'nominatim_result.freezed.dart';
|
||||||
|
part 'nominatim_result.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class NominatimResult with _$NominatimResult {
|
||||||
|
const factory NominatimResult({
|
||||||
|
required String displayName,
|
||||||
|
required double lat,
|
||||||
|
required double lon,
|
||||||
|
}) = _NominatimResult;
|
||||||
|
|
||||||
|
factory NominatimResult.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$NominatimResultFromJson(json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'nominatim_result.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$NominatimResult {
|
||||||
|
|
||||||
|
String get displayName; double get lat; double get lon;
|
||||||
|
/// Create a copy of NominatimResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$NominatimResultCopyWith<NominatimResult> get copyWith => _$NominatimResultCopyWithImpl<NominatimResult>(this as NominatimResult, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this NominatimResult to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is NominatimResult&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,displayName,lat,lon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NominatimResult(displayName: $displayName, lat: $lat, lon: $lon)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $NominatimResultCopyWith<$Res> {
|
||||||
|
factory $NominatimResultCopyWith(NominatimResult value, $Res Function(NominatimResult) _then) = _$NominatimResultCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String displayName, double lat, double lon
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$NominatimResultCopyWithImpl<$Res>
|
||||||
|
implements $NominatimResultCopyWith<$Res> {
|
||||||
|
_$NominatimResultCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final NominatimResult _self;
|
||||||
|
final $Res Function(NominatimResult) _then;
|
||||||
|
|
||||||
|
/// Create a copy of NominatimResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? displayName = null,Object? lat = null,Object? lon = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,lat: null == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,lon: null == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [NominatimResult].
|
||||||
|
extension NominatimResultPatterns on NominatimResult {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _NominatimResult value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _NominatimResult value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult():
|
||||||
|
return $default(_that);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _NominatimResult value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String displayName, double lat, double lon)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult() when $default != null:
|
||||||
|
return $default(_that.displayName,_that.lat,_that.lon);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String displayName, double lat, double lon) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult():
|
||||||
|
return $default(_that.displayName,_that.lat,_that.lon);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String displayName, double lat, double lon)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NominatimResult() when $default != null:
|
||||||
|
return $default(_that.displayName,_that.lat,_that.lon);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _NominatimResult implements NominatimResult {
|
||||||
|
const _NominatimResult({required this.displayName, required this.lat, required this.lon});
|
||||||
|
factory _NominatimResult.fromJson(Map<String, dynamic> json) => _$NominatimResultFromJson(json);
|
||||||
|
|
||||||
|
@override final String displayName;
|
||||||
|
@override final double lat;
|
||||||
|
@override final double lon;
|
||||||
|
|
||||||
|
/// Create a copy of NominatimResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$NominatimResultCopyWith<_NominatimResult> get copyWith => __$NominatimResultCopyWithImpl<_NominatimResult>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$NominatimResultToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NominatimResult&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,displayName,lat,lon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NominatimResult(displayName: $displayName, lat: $lat, lon: $lon)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$NominatimResultCopyWith<$Res> implements $NominatimResultCopyWith<$Res> {
|
||||||
|
factory _$NominatimResultCopyWith(_NominatimResult value, $Res Function(_NominatimResult) _then) = __$NominatimResultCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String displayName, double lat, double lon
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$NominatimResultCopyWithImpl<$Res>
|
||||||
|
implements _$NominatimResultCopyWith<$Res> {
|
||||||
|
__$NominatimResultCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _NominatimResult _self;
|
||||||
|
final $Res Function(_NominatimResult) _then;
|
||||||
|
|
||||||
|
/// Create a copy of NominatimResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? displayName = null,Object? lat = null,Object? lon = null,}) {
|
||||||
|
return _then(_NominatimResult(
|
||||||
|
displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,lat: null == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,lon: null == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'nominatim_result.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_NominatimResult _$NominatimResultFromJson(Map<String, dynamic> json) =>
|
||||||
|
_NominatimResult(
|
||||||
|
displayName: json['displayName'] as String,
|
||||||
|
lat: (json['lat'] as num).toDouble(),
|
||||||
|
lon: (json['lon'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$NominatimResultToJson(_NominatimResult instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'displayName': instance.displayName,
|
||||||
|
'lat': instance.lat,
|
||||||
|
'lon': instance.lon,
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../errors/network_exception.dart';
|
||||||
|
import '../errors/parse_exception.dart';
|
||||||
|
import '../errors/server_exception.dart';
|
||||||
|
import 'nominatim_result.dart';
|
||||||
|
|
||||||
|
/// Tiny wrapper around the public Nominatim geocoder. Only used in the
|
||||||
|
/// commute-settings flow to look up a home address; not called from any
|
||||||
|
/// hot path. The User-Agent header is **required** by the Nominatim usage
|
||||||
|
/// policy — without it the service throttles/blocks the client.
|
||||||
|
class NominatimSearch {
|
||||||
|
static const _userAgent = 'MarianumMobile/1.0 (contact@elias-mueller.com)';
|
||||||
|
static final Uri _base = Uri.parse('https://nominatim.openstreetmap.org/search');
|
||||||
|
|
||||||
|
/// Returns up to [limit] geocoded matches for the user-typed [query].
|
||||||
|
Future<List<NominatimResult>> run(String query, {int limit = 5}) async {
|
||||||
|
final uri = _base.replace(
|
||||||
|
queryParameters: {
|
||||||
|
'q': query,
|
||||||
|
'format': 'json',
|
||||||
|
'limit': limit.toString(),
|
||||||
|
'addressdetails': '0',
|
||||||
|
'accept-language': 'de',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final http.Response response;
|
||||||
|
try {
|
||||||
|
response = await http
|
||||||
|
.get(uri, headers: {'User-Agent': _userAgent, 'Accept': 'application/json'})
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
throw NetworkException(technicalDetails: 'nominatim: ${e.message}');
|
||||||
|
} on TimeoutException catch (e) {
|
||||||
|
throw NetworkException.timeout(technicalDetails: 'nominatim: $e');
|
||||||
|
} on http.ClientException catch (e) {
|
||||||
|
throw NetworkException(technicalDetails: 'nominatim: ${e.message}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode > 299) {
|
||||||
|
throw ServerException(
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
technicalDetails: 'nominatim HTTP ${response.statusCode}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final raw = jsonDecode(utf8.decode(response.bodyBytes)) as List;
|
||||||
|
return raw
|
||||||
|
.map((e) => _resultFromRaw(e as Map<String, dynamic>))
|
||||||
|
.toList(growable: false);
|
||||||
|
} catch (e) {
|
||||||
|
throw ParseException(technicalDetails: 'nominatim assemble: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static NominatimResult _resultFromRaw(Map<String, dynamic> json) {
|
||||||
|
// Nominatim returns lat/lon as strings, not numbers. Normalise here.
|
||||||
|
final lat = double.parse(json['lat'].toString());
|
||||||
|
final lon = double.parse(json['lon'].toString());
|
||||||
|
return NominatimResult(
|
||||||
|
displayName: json['display_name'] as String? ?? '?',
|
||||||
|
lat: lat,
|
||||||
|
lon: lon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,9 +83,49 @@ class SearchFilesEntry {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives a filename with its extension from the WebDAV-relative path.
|
||||||
|
/// The provider's `title` is often the display name *without* the
|
||||||
|
/// extension, which would defeat extension-based icon mapping.
|
||||||
|
String _nameFromPath(String path) {
|
||||||
|
final stripped = path.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||||
|
if (stripped.isEmpty) return title;
|
||||||
|
final last = stripped.split('/').last;
|
||||||
|
return last.isEmpty ? title : last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The files search provider exposes the numeric Nextcloud file id either
|
||||||
|
/// as an attribute, or implicitly in the `?openfile=<id>` query parameter
|
||||||
|
/// of [resourceUrl]. Parses both into an [int] when available — used to
|
||||||
|
/// hit the preview endpoint directly.
|
||||||
|
int? _extractFileId() {
|
||||||
|
final attr = attributes?['fileId'];
|
||||||
|
if (attr is int) return attr;
|
||||||
|
if (attr is String) {
|
||||||
|
final parsed = int.tryParse(attr);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
final url = resourceUrl;
|
||||||
|
if (url != null) {
|
||||||
|
final raw = Uri.tryParse(url)?.queryParameters['openfile'];
|
||||||
|
if (raw != null) return int.tryParse(raw);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
CacheableFile? toCacheable() {
|
CacheableFile? toCacheable() {
|
||||||
final path = webdavPath;
|
final path = webdavPath;
|
||||||
if (path == null) return null;
|
if (path == null) return null;
|
||||||
return CacheableFile(path: path, isDirectory: isDirectory, name: title);
|
final fileId = _extractFileId();
|
||||||
|
return CacheableFile(
|
||||||
|
path: path,
|
||||||
|
isDirectory: isDirectory,
|
||||||
|
name: _nameFromPath(path),
|
||||||
|
fileId: fileId,
|
||||||
|
// Search provider responses don't carry `nc:has-preview`. Probe
|
||||||
|
// optimistically when a fileId is available — the preview endpoint
|
||||||
|
// simply returns 404 for unsupported formats, which the file-leading
|
||||||
|
// widget already falls back from to the typed icon.
|
||||||
|
hasPreview: (!isDirectory && fileId != null) ? true : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||