Compare commits
35 Commits
6c7d217463
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 13f4f79829 | |||
| a76b09af26 | |||
| 4fd09204ec | |||
| f554c57d8d | |||
| baa26a6e79 | |||
| b6d06dd3b4 | |||
| 6e12da08c0 | |||
| 5ebf5bccdb | |||
| f966cf302b | |||
| 582432dbb9 | |||
| ece0669f7d | |||
| 01b4b44010 | |||
| 93b9929f8f | |||
| 2858f910c9 | |||
| f185b3273a | |||
| 831ea56869 | |||
| 215911cf29 | |||
| e5873f73b9 | |||
| 582eff8750 | |||
| 2cb8321d07 | |||
| 194d8d1857 | |||
| 22e9c43f78 | |||
| 4c04d00323 | |||
| 0fd42439e2 | |||
| d970cfbe0c | |||
| 91ab109ec5 | |||
| d9fcd9f624 | |||
| 092f9b622b | |||
| 843686358f | |||
| cfcb901adb | |||
| ba5d9e0e4e | |||
| e8707b36f1 | |||
| d0ba7c0fd6 | |||
| a09817a975 | |||
| 37dbb7b374 |
@@ -4,4 +4,10 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Allow cleartext HTTP in debug builds so developers can point the
|
||||
Marianum-Connect custom endpoint at a local backend (e.g.
|
||||
http://10.0.2.2:8080 from the Android emulator). Release builds
|
||||
keep the default cleartext block. -->
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:label="Marianum Fulda"
|
||||
android:name="${applicationName}"
|
||||
@@ -106,4 +107,10 @@
|
||||
<!-- Workmanager periodic widget refresh needs to reschedule after device
|
||||
reboot, otherwise the widget freezes until the user opens the app. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<!-- Aus open_filex gemergt: die App öffnet nur selbst heruntergeladene Dateien
|
||||
über dessen FileProvider (content://-URIs) und braucht keinen Zugriff auf
|
||||
geteilte Medien. Entfernt wegen Google-Play "Photo and Video Permissions". -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove"/>
|
||||
</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"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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>
|
||||
|
||||
|
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 |
@@ -4,3 +4,7 @@ android.enableJetifier=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -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 |
@@ -46,5 +46,8 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,311 +1,64 @@
|
||||
PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- emoji_picker_flutter (0.0.1):
|
||||
- Flutter
|
||||
- eraser (0.0.1):
|
||||
- Flutter
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- Firebase/Messaging (12.12.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.12.0)
|
||||
- firebase_core (4.7.0):
|
||||
- Firebase/CoreOnly (= 12.12.0)
|
||||
- Flutter
|
||||
- firebase_messaging (16.2.0):
|
||||
- Firebase/Messaging (= 12.12.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseCore (12.12.1):
|
||||
- FirebaseCoreInternal (~> 12.12.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.12.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- FirebaseInstallations (~> 12.12.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Reachability (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_badge (2.0.0):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_secure_storage_darwin (10.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Network (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/Reachability (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- open_filex (0.0.2):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- PhoneNumberKit (3.7.11):
|
||||
- PhoneNumberKit/PhoneNumberKitCore (= 3.7.11)
|
||||
- PhoneNumberKit/UIKit (= 3.7.11)
|
||||
- PhoneNumberKit/PhoneNumberKitCore (3.7.11)
|
||||
- PhoneNumberKit/UIKit (3.7.11):
|
||||
- PhoneNumberKit/PhoneNumberKitCore
|
||||
- PromisesObjC (2.4.0)
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- screen_brightness_ios (2.1.3):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.7):
|
||||
- SDWebImage/Core (= 5.21.7)
|
||||
- SDWebImage/Core (5.21.7)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- workmanager_apple (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
||||
- eraser (from `.symlinks/plugins/eraser/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_badge (from `.symlinks/plugins/flutter_app_badge/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- PhoneNumberKit (~> 3.7.6)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Firebase
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- PhoneNumberKit
|
||||
- PromisesObjC
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
emoji_picker_flutter:
|
||||
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
|
||||
eraser:
|
||||
:path: ".symlinks/plugins/eraser/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
firebase_core:
|
||||
:path: ".symlinks/plugins/firebase_core/ios"
|
||||
firebase_messaging:
|
||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_app_badge:
|
||||
:path: ".symlinks/plugins/flutter_app_badge/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage_darwin:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
open_filex:
|
||||
:path: ".symlinks/plugins/open_filex/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
workmanager_apple:
|
||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
||||
eraser: 83a4b06985f3702aa3d8dec816f9693266012937
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33
|
||||
firebase_core: 9156a152117c843440b0b990c785aa0259bc5447
|
||||
firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269
|
||||
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
|
||||
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
|
||||
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
|
||||
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_app_badge: ca742dd659a157c1090ef7cd881cb78f48f3bcdf
|
||||
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
PhoneNumberKit: ced55861269312a5e3bc2ef82a58d6255b1c976a
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PhoneNumberKit: 9ff0c5ae9fe4770193b68a3d3e6c938fe976788c
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
screen_brightness_ios: 212d950bb99c915eee971c884f4a6c87c92cd13d
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
PODFILE CHECKSUM: 424a9b4c0fe81d8ebeaa9cb0dfedb60a68b19a0d
|
||||
PODFILE CHECKSUM: 087d168982f24fb137e2d46f893b771b4b9955c6
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
AA0101070000000011111111 /* TimetableWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0101020000000011111111 /* TimetableWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
AA0102010000000022222222 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0102020000000022222222 /* SceneDelegate.swift */; };
|
||||
B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90960A132A5F91779B3FBE28 /* Pods_Runner.framework */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -96,6 +97,7 @@
|
||||
C7E1879BE78835C7E3256316 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
DD904D7C0FC0AD11449CEB80 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
EF5279D9BF8FCBB117AF998E /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@@ -155,6 +157,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -194,6 +197,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -274,6 +278,9 @@
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
@@ -286,7 +293,6 @@
|
||||
3321F8102FB1C00C0011C712 /* Embed Foundation Extensions */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
174B54D80220E5F588BD9737 /* [CP] Embed Pods Frameworks */,
|
||||
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -323,6 +329,9 @@
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
@@ -425,23 +434,6 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -647,7 +639,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */;
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
@@ -691,7 +683,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */;
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
@@ -732,7 +724,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */;
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
@@ -941,7 +933,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@@ -983,7 +975,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@@ -1023,7 +1015,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@@ -1103,6 +1095,18 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||
"version" : "1.2024072200.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
||||
"version" : "11.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "d10045cace0b4c335c4efa8f7df7e9a9fc5a7c60",
|
||||
"version" : "12.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf",
|
||||
"version" : "3.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "c2c76bebcfbb90d90ea10599f934f9af160e1604",
|
||||
"version" : "12.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
||||
"version" : "8.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||
"version" : "1.69.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
|
||||
"version" : "1.22.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version" : "2.30910.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -5,8 +5,44 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c",
|
||||
"version" : "1.2022062300.0"
|
||||
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||
"version" : "1.2024072200.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1",
|
||||
"version" : "11.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14,8 +50,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "df2171b0c6afb9e9d4f7e07669d558c510b9f6be",
|
||||
"version" : "10.13.0"
|
||||
"revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
||||
"version" : "3.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23,8 +68,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "03b9beee1a61f62d32c521e172e192a1663a5e8b",
|
||||
"version" : "10.13.0"
|
||||
"revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,8 +77,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "aae45a320fd0d11811820335b1eabc8753902a40",
|
||||
"version" : "9.2.5"
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -41,8 +86,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "c38ce365d77b04a9a300c31061c5227589e5597b",
|
||||
"version" : "7.11.5"
|
||||
"revision" : "c46e5f8b7c23265f17c24ca7f9fa1b13ded7a822",
|
||||
"version" : "8.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -50,8 +95,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "f1b366129d1125be7db83247e003fc333104b569",
|
||||
"version" : "1.50.2"
|
||||
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||
"version" : "1.69.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -59,8 +104,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd",
|
||||
"version" : "3.1.1"
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68,8 +122,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
|
||||
"version" : "1.22.2"
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,8 +131,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
|
||||
"version" : "2.30909.0"
|
||||
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||
"version" : "2.30910.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,17 +140,35 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e",
|
||||
"version" : "2.3.1"
|
||||
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-protobuf",
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "ce20dc083ee485524b802669890291c0d8090170",
|
||||
"version" : "1.22.1"
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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" : "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"
|
||||
}
|
||||
}
|
||||
{"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"}}
|
||||
|
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>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="800" height="1131"/>
|
||||
<image name="LaunchImage" width="512" height="512"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -6,13 +6,11 @@ import 'package:http/http.dart' as http;
|
||||
|
||||
import '../api_error.dart';
|
||||
import '../marianumcloud/talk/talk_error.dart';
|
||||
import '../webuntis/webuntis_error.dart';
|
||||
import 'app_exception.dart';
|
||||
import 'network_exception.dart';
|
||||
import 'parse_exception.dart';
|
||||
import 'server_exception.dart';
|
||||
import 'talk_exception.dart';
|
||||
import 'webuntis_exception.dart';
|
||||
|
||||
const String _defaultFallback =
|
||||
'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
|
||||
@@ -57,7 +55,6 @@ String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
|
||||
if (error is AppException) return error.userMessage;
|
||||
|
||||
if (error is TalkError) return TalkException(error).userMessage;
|
||||
if (error is WebuntisError) return WebuntisException(error).userMessage;
|
||||
|
||||
if (error is DioException) {
|
||||
final mapped = _dioToAppException(error);
|
||||
@@ -90,7 +87,6 @@ String? errorToTechnicalDetails(Object? error) {
|
||||
if (error == null) return null;
|
||||
if (error is AppException) return error.technicalDetails ?? error.toString();
|
||||
if (error is TalkError) return TalkException(error).technicalDetails;
|
||||
if (error is WebuntisError) return WebuntisException(error).technicalDetails;
|
||||
if (error is DioException) {
|
||||
final mapped = _dioToAppException(error);
|
||||
if (mapped != null) return mapped.technicalDetails ?? mapped.toString();
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import '../webuntis/webuntis_error.dart';
|
||||
import 'app_exception.dart';
|
||||
|
||||
class WebuntisException extends AppException {
|
||||
final WebuntisError source;
|
||||
|
||||
WebuntisException(this.source)
|
||||
: super(
|
||||
userMessage: _mapMessage(source),
|
||||
technicalDetails: 'WebUntis (${source.code}): ${source.message}',
|
||||
allowRetry: true,
|
||||
);
|
||||
|
||||
static String _mapMessage(WebuntisError e) {
|
||||
switch (e.code) {
|
||||
case -8504:
|
||||
case -8502:
|
||||
return 'WebUntis-Anmeldung abgelaufen. Bitte erneut anmelden.';
|
||||
case -8520:
|
||||
return 'Bitte melde dich erneut an.';
|
||||
case -7004:
|
||||
return 'Für diesen Zeitraum sind keine Stundenplandaten verfügbar.';
|
||||
case -32601:
|
||||
return 'WebUntis kennt diese Anfrage nicht. Bitte App aktualisieren.';
|
||||
default:
|
||||
return e.message.isNotEmpty
|
||||
? 'WebUntis: ${e.message}'
|
||||
: 'WebUntis konnte die Anfrage nicht bearbeiten (Code ${e.code}).';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,39 @@ import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'autocomplete_response.dart';
|
||||
|
||||
class AutocompleteApi {
|
||||
Future<AutocompleteResponse> find(String query) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'core/autocomplete/get',
|
||||
queryParameters: {
|
||||
/// Searches sharees (users by default). Pass [shareTypes] to widen the search
|
||||
/// — e.g. `[0, 1]` for both users and groups (0 = user, 1 = group).
|
||||
Future<AutocompleteResponse> find(
|
||||
String query, {
|
||||
List<int> shareTypes = const [0],
|
||||
}) async {
|
||||
// NextcloudOcs.uri serialises every query value via `toString()`, which
|
||||
// would turn the `shareTypes[]` list into `"[0, 1]"`. Build the Uri here so
|
||||
// Dart encodes the list as repeated `shareTypes[]=0&shareTypes[]=1` params.
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
final uri = Uri.https(
|
||||
endpoint.domain,
|
||||
'${endpoint.path}/ocs/v2.php/core/autocomplete/get',
|
||||
{
|
||||
'format': 'json',
|
||||
'search': query,
|
||||
'itemType': ' ',
|
||||
'itemId': ' ',
|
||||
'shareTypes[]': ['0'],
|
||||
'shareTypes[]': shareTypes.map((t) => t.toString()).toList(),
|
||||
'limit': '10',
|
||||
},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
final response = await http.get(uri, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception(
|
||||
'Api call failed with ${response.statusCode}: ${response.body}',
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'core/autocomplete/get: ${response.body}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../files_sharing/queries/share/share.dart';
|
||||
|
||||
part 'autocomplete_response.g.dart';
|
||||
|
||||
/// Maps an autocomplete result's `source` to the matching Nextcloud share type.
|
||||
/// Groups become [kShareTypeGroup]; everything else (users, and any unknown
|
||||
/// source) defaults to [kShareTypeUser].
|
||||
int shareTypeFromSource(String? source) {
|
||||
if (source != null && source.startsWith('groups')) return kShareTypeGroup;
|
||||
return kShareTypeUser;
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class AutocompleteResponse {
|
||||
List<AutocompleteResponseObject> data;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'nextcloud_sharing_capabilities.dart';
|
||||
|
||||
/// Fetches the current user's Nextcloud capabilities via OCS
|
||||
/// `GET cloud/capabilities` and extracts the `files_sharing` block. This is the
|
||||
/// per-user, group-aware source of truth the sharing UI gates on — no custom
|
||||
/// backend involved.
|
||||
class GetNextcloudCapabilities {
|
||||
Future<NextcloudSharingCapabilities> run() async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'cloud/capabilities',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'cloud/capabilities: ${response.body}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(decoded['ocs'] as Map<String, dynamic>?)?['data']
|
||||
as Map<String, dynamic>?;
|
||||
final capabilities = data?['capabilities'] as Map<String, dynamic>?;
|
||||
final filesSharing = capabilities?['files_sharing'];
|
||||
if (filesSharing is! Map<String, dynamic>) {
|
||||
// Server doesn't advertise files_sharing (app disabled) — treat as no
|
||||
// sharing capability rather than failing the whole load.
|
||||
return const NextcloudSharingCapabilities();
|
||||
}
|
||||
final passwordPolicy = capabilities?['password_policy'];
|
||||
return NextcloudSharingCapabilities.fromFilesSharing(
|
||||
filesSharing,
|
||||
passwordPolicy: passwordPolicy is Map<String, dynamic>
|
||||
? passwordPolicy
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/// Subset of Nextcloud's `files_sharing` capabilities block that the mobile
|
||||
/// sharing UI gates on. Nextcloud reports these per authenticated user, so a
|
||||
/// group that an admin excluded from creating public links sees
|
||||
/// `public.enabled == false` here — exactly how the web UI hides those buttons.
|
||||
///
|
||||
/// The block is deeply nested and varies between server versions, so this is
|
||||
/// parsed by hand from the raw OCS map with safe fallbacks rather than via
|
||||
/// code generation. Missing fields default to the most restrictive value so a
|
||||
/// newer/older server never accidentally unlocks a capability.
|
||||
class NextcloudSharingCapabilities {
|
||||
/// `files_sharing.api_enabled` — master switch. When false the user may not
|
||||
/// create any share (user, group or link).
|
||||
final bool apiEnabled;
|
||||
|
||||
/// `files_sharing.public.enabled` — public link shares allowed.
|
||||
final bool publicEnabled;
|
||||
|
||||
/// `files_sharing.public.multiple_links` — more than one link per file.
|
||||
final bool publicMultipleLinks;
|
||||
|
||||
/// `files_sharing.public.upload` — public upload / file-drop folders.
|
||||
final bool publicUploadEnabled;
|
||||
|
||||
/// `files_sharing.public.password.enforced` — a password is mandatory on
|
||||
/// public links, so the create flow must collect one upfront.
|
||||
final bool publicPasswordEnforced;
|
||||
|
||||
/// `files_sharing.public.expire_date.enabled`.
|
||||
final bool publicExpireEnabled;
|
||||
|
||||
/// `files_sharing.public.expire_date.days` — default/maximum lifetime.
|
||||
final int? publicExpireDays;
|
||||
|
||||
/// `files_sharing.public.expire_date.enforced` — expiry cannot be removed.
|
||||
final bool publicExpireEnforced;
|
||||
|
||||
/// `files_sharing.group.enabled` (falls back to the older `group_sharing`).
|
||||
final bool groupEnabled;
|
||||
|
||||
/// `files_sharing.resharing` — recipients may reshare.
|
||||
final bool resharing;
|
||||
|
||||
// --- password_policy (a sibling capability of files_sharing) ---
|
||||
// These let the link-password UI state the rules up front instead of only
|
||||
// surfacing them after the server rejects a weak password. The
|
||||
// "non-common password" (breach) check can only be enforced server-side.
|
||||
|
||||
/// `password_policy.minLength`.
|
||||
final int? passwordMinLength;
|
||||
|
||||
/// `password_policy.enforceUpperLowerCase`.
|
||||
final bool passwordEnforceUpperLower;
|
||||
|
||||
/// `password_policy.enforceNumericCharacters`.
|
||||
final bool passwordEnforceNumeric;
|
||||
|
||||
/// `password_policy.enforceSpecialCharacters`.
|
||||
final bool passwordEnforceSpecial;
|
||||
|
||||
const NextcloudSharingCapabilities({
|
||||
this.apiEnabled = false,
|
||||
this.publicEnabled = false,
|
||||
this.publicMultipleLinks = false,
|
||||
this.publicUploadEnabled = false,
|
||||
this.publicPasswordEnforced = false,
|
||||
this.publicExpireEnabled = false,
|
||||
this.publicExpireDays,
|
||||
this.publicExpireEnforced = false,
|
||||
this.groupEnabled = false,
|
||||
this.resharing = false,
|
||||
this.passwordMinLength,
|
||||
this.passwordEnforceUpperLower = false,
|
||||
this.passwordEnforceNumeric = false,
|
||||
this.passwordEnforceSpecial = false,
|
||||
});
|
||||
|
||||
/// Parses the `files_sharing` sub-map of an OCS `cloud/capabilities`
|
||||
/// response, plus the optional sibling `password_policy` map. Tolerates
|
||||
/// missing intermediate maps and type drift.
|
||||
factory NextcloudSharingCapabilities.fromFilesSharing(
|
||||
Map<String, dynamic> filesSharing, {
|
||||
Map<String, dynamic>? passwordPolicy,
|
||||
}) {
|
||||
Map<String, dynamic>? sub(Map<String, dynamic>? m, String key) {
|
||||
final value = m?[key];
|
||||
return value is Map<String, dynamic> ? value : null;
|
||||
}
|
||||
|
||||
bool boolAt(Map<String, dynamic>? m, String key) => m?[key] == true;
|
||||
int? intAt(Map<String, dynamic>? m, String key) {
|
||||
final v = m?[key];
|
||||
if (v is int) return v;
|
||||
if (v is String) return int.tryParse(v);
|
||||
return null;
|
||||
}
|
||||
|
||||
final public = sub(filesSharing, 'public');
|
||||
final password = sub(public, 'password');
|
||||
final expire = sub(public, 'expire_date');
|
||||
final group = sub(filesSharing, 'group');
|
||||
|
||||
return NextcloudSharingCapabilities(
|
||||
apiEnabled: boolAt(filesSharing, 'api_enabled'),
|
||||
publicEnabled: boolAt(public, 'enabled'),
|
||||
publicMultipleLinks: boolAt(public, 'multiple_links'),
|
||||
publicUploadEnabled: boolAt(public, 'upload'),
|
||||
publicPasswordEnforced: boolAt(password, 'enforced'),
|
||||
publicExpireEnabled: boolAt(expire, 'enabled'),
|
||||
publicExpireDays: intAt(expire, 'days'),
|
||||
publicExpireEnforced: boolAt(expire, 'enforced'),
|
||||
// Newer servers nest it under `group.enabled`; older ones expose a flat
|
||||
// `group_sharing` boolean.
|
||||
groupEnabled:
|
||||
boolAt(group, 'enabled') || boolAt(filesSharing, 'group_sharing'),
|
||||
resharing: boolAt(filesSharing, 'resharing'),
|
||||
passwordMinLength: intAt(passwordPolicy, 'minLength'),
|
||||
passwordEnforceUpperLower: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceUpperLowerCase',
|
||||
),
|
||||
passwordEnforceNumeric: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceNumericCharacters',
|
||||
),
|
||||
passwordEnforceSpecial: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceSpecialCharacters',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
import '../../errors/not_found_exception.dart';
|
||||
import '../../errors/parse_exception.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
/// Mix of two Nextcloud surfaces:
|
||||
/// - User info comes from the OCS provisioning API
|
||||
/// (`/ocs/v2.php/cloud/users/{userId}`).
|
||||
/// - The own-avatar upload/delete uses the *core* AvatarController at
|
||||
/// `/avatar/` — the OCS provisioning route has no POST (it answers 405).
|
||||
/// This is the same controller the read path (`/avatar/{id}/{size}` in
|
||||
/// [UserAvatar]) already talks to. CSRF is bypassed because we use Basic
|
||||
/// Auth without a session cookie.
|
||||
|
||||
/// Core AvatarController endpoint for the logged-in user (POST sets, DELETE
|
||||
/// removes). Built against the bare Nextcloud base (domain + optional path),
|
||||
/// not the OCS wrapper.
|
||||
Uri _coreAvatarUri() {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
return Uri.https(endpoint.domain, '${endpoint.path}/avatar/');
|
||||
}
|
||||
|
||||
Uri _userInfoUri() =>
|
||||
NextcloudOcs.uri('cloud/users/${AccountData().getUsername()}');
|
||||
|
||||
Future<http.Response> _send(
|
||||
Future<http.Response> Function(Uri uri, Map<String, String> headers)
|
||||
perform,
|
||||
Uri uri,
|
||||
) async {
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await perform(uri, headers);
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'Cloud $uri: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
}
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status >= 200 && status < 300) return response;
|
||||
|
||||
final body = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Cloud $uri -> HTTP $status'
|
||||
: 'Cloud $uri -> HTTP $status body=$preview';
|
||||
log(detail);
|
||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
}
|
||||
|
||||
class SetUserAvatar {
|
||||
final Uint8List bytes;
|
||||
final String filename;
|
||||
SetUserAvatar(this.bytes, {this.filename = 'avatar.jpg'});
|
||||
|
||||
Future<void> run() async {
|
||||
await _send((uri, headers) async {
|
||||
// Core AvatarController reads $_FILES['files']['error'][0] — the field
|
||||
// must be `files[]` so PHP exposes it as an array, matching the web UI.
|
||||
final req = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll(headers)
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes('files[]', bytes, filename: filename),
|
||||
);
|
||||
final streamed = await req.send();
|
||||
return http.Response.fromStream(streamed);
|
||||
}, _coreAvatarUri());
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteUserAvatar {
|
||||
Future<void> run() async {
|
||||
await _send(
|
||||
(uri, headers) => http.delete(uri, headers: headers),
|
||||
_coreAvatarUri(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CloudUserInfo {
|
||||
final String userId;
|
||||
final String displayName;
|
||||
const CloudUserInfo({required this.userId, required this.displayName});
|
||||
}
|
||||
|
||||
/// Reads the current user's provisioning record. The OCS wrapper looks like:
|
||||
/// `{ "ocs": { "meta": {...}, "data": { "id": "...", "displayname": "...", ... } } }`.
|
||||
/// We only need displayname; everything else is discarded.
|
||||
class GetUserInfo {
|
||||
Future<CloudUserInfo> run() async {
|
||||
final uri = _userInfoUri();
|
||||
final response = await _send(
|
||||
(u, headers) => http.get(u, headers: headers),
|
||||
uri,
|
||||
);
|
||||
try {
|
||||
final root = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(root['ocs'] as Map<String, dynamic>)['data']
|
||||
as Map<String, dynamic>;
|
||||
return CloudUserInfo(
|
||||
userId: data['id'] as String,
|
||||
displayName: (data['displayname'] as String?) ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
throw ParseException(
|
||||
technicalDetails: 'Cloud $uri user info parse: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,144 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'file_sharing_api_params.dart';
|
||||
import 'queries/share/share.dart';
|
||||
import 'queries/share/share_update_params.dart';
|
||||
|
||||
/// OCS `files_sharing` API (Nextcloud Sharing API v1). Per the official docs:
|
||||
/// list/delete pass parameters via the URL (query / path), create and update
|
||||
/// pass them in the request **body** (form-urlencoded), and every call adds
|
||||
/// `format=json` so the server replies with JSON instead of XML.
|
||||
///
|
||||
/// All calls surface failures as [ServerException] so they map to friendly
|
||||
/// messages via `errorToUserMessage`.
|
||||
class FileSharingApi {
|
||||
Future<void> share(FileSharingApiParams query) async {
|
||||
static const String _base = 'apps/files_sharing/api/v1/shares';
|
||||
|
||||
/// Creates a share. Returns the created [Share] (callers that don't need it —
|
||||
/// e.g. Talk file sharing — can ignore the result).
|
||||
Future<Share> share(FileSharingApiParams query) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'apps/files_sharing/api/v1/shares',
|
||||
queryParameters: query.toJson(),
|
||||
_base,
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.post(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception(
|
||||
'Api call failed with ${response.statusCode}: ${response.body}',
|
||||
final response = await http.post(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
body: _stringForm(query.toJson()),
|
||||
);
|
||||
return _decodeShare(response, action: 'Freigabe erstellen');
|
||||
}
|
||||
|
||||
/// Lists shares for the given OCS [path] (see `ocs_path.dart`). [reshares]
|
||||
/// includes shares the current user received and re-shared.
|
||||
Future<List<Share>> listForPath(String path, {bool reshares = false}) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
_base,
|
||||
queryParameters: {
|
||||
'format': 'json',
|
||||
'path': path,
|
||||
'reshares': reshares.toString(),
|
||||
'subfiles': 'false',
|
||||
},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
final data = _decodeData(response, action: 'Freigaben laden');
|
||||
if (data is! List) return const [];
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Share.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Updates an existing share. Returns the updated [Share].
|
||||
Future<Share> update(int shareId, ShareUpdateParams params) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'$_base/$shareId',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.put(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
body: params.toQuery(),
|
||||
);
|
||||
return _decodeShare(response, action: 'Freigabe ändern');
|
||||
}
|
||||
|
||||
/// Deletes (revokes) a share.
|
||||
Future<void> remove(int shareId) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'$_base/$shareId',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.delete(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
);
|
||||
_decodeData(response, action: 'Freigabe löschen');
|
||||
}
|
||||
|
||||
/// Stringifies a json map into form fields (the OCS body is
|
||||
/// `application/x-www-form-urlencoded`). Null values are already dropped by
|
||||
/// the params' `includeIfNull: false`.
|
||||
Map<String, String> _stringForm(Map<String, dynamic> json) =>
|
||||
json.map((key, value) => MapEntry(key, value.toString()));
|
||||
|
||||
/// Decodes a single-share response (create/update). Throws if the payload is
|
||||
/// not a share object.
|
||||
Share _decodeShare(http.Response response, {required String action}) {
|
||||
final data = _decodeData(response, action: action);
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'Unerwartete Antwort für "$action": ${response.body}',
|
||||
);
|
||||
}
|
||||
return Share.fromJson(data);
|
||||
}
|
||||
|
||||
/// Validates the HTTP/OCS envelope and returns the `ocs.data` payload, or
|
||||
/// throws a [ServerException] carrying the server's OCS message when present.
|
||||
///
|
||||
/// Every access is type-checked rather than cast: PHP's `json_encode` turns
|
||||
/// an empty associative array into a JSON array (`[]`), and `statuscode` can
|
||||
/// arrive as a string — a hard `as` cast on either would throw a raw
|
||||
/// TypeError that surfaces as the generic "something went wrong" message.
|
||||
Object? _decodeData(http.Response response, {required String action}) {
|
||||
dynamic decoded;
|
||||
try {
|
||||
decoded = jsonDecode(response.body);
|
||||
} catch (_) {
|
||||
decoded = null;
|
||||
}
|
||||
final ocs = decoded is Map<String, dynamic> ? decoded['ocs'] : null;
|
||||
final ocsMap = ocs is Map<String, dynamic> ? ocs : null;
|
||||
final meta = ocsMap?['meta'];
|
||||
final metaMap = meta is Map<String, dynamic> ? meta : null;
|
||||
|
||||
final rawStatus = metaMap?['statuscode'];
|
||||
final ocsStatus = rawStatus is int
|
||||
? rawStatus
|
||||
: (rawStatus is String ? int.tryParse(rawStatus) : null);
|
||||
final rawMessage = metaMap?['message'];
|
||||
final ocsMessage = rawMessage is String ? rawMessage : null;
|
||||
|
||||
// OCS v2 mirrors the HTTP status; accept any 2xx. Success OCS statuscodes
|
||||
// are 100 (v1 carry-over) or 200.
|
||||
final httpOk = response.statusCode >= 200 && response.statusCode < 300;
|
||||
final ocsOk = ocsStatus == null || ocsStatus == 100 || ocsStatus == 200;
|
||||
if (!httpOk || !ocsOk) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
userMessage: ocsMessage != null && ocsMessage.isNotEmpty
|
||||
? '$action fehlgeschlagen: $ocsMessage'
|
||||
: null,
|
||||
technicalDetails: '$action: ${response.statusCode} ${response.body}',
|
||||
);
|
||||
}
|
||||
return ocsMap?['data'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'file_sharing_api_params.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
// includeIfNull:false so the optional sharing fields below are only sent when
|
||||
// set — otherwise the OCS query would carry `permissions=null` etc. and be
|
||||
// rejected.
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class FileSharingApiParams {
|
||||
int shareType;
|
||||
String shareWith;
|
||||
@@ -10,12 +13,24 @@ class FileSharingApiParams {
|
||||
String? referenceId;
|
||||
String? talkMetaData;
|
||||
|
||||
/// Permission bitmask (see `share_permissions.dart`).
|
||||
int? permissions;
|
||||
|
||||
/// Public link password.
|
||||
String? password;
|
||||
|
||||
/// Expiry as `YYYY-MM-DD`.
|
||||
String? expireDate;
|
||||
|
||||
FileSharingApiParams({
|
||||
required this.shareType,
|
||||
required this.shareWith,
|
||||
required this.path,
|
||||
this.referenceId,
|
||||
this.talkMetaData,
|
||||
this.permissions,
|
||||
this.password,
|
||||
this.expireDate,
|
||||
});
|
||||
|
||||
factory FileSharingApiParams.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -14,6 +14,9 @@ FileSharingApiParams _$FileSharingApiParamsFromJson(
|
||||
path: json['path'] as String,
|
||||
referenceId: json['referenceId'] as String?,
|
||||
talkMetaData: json['talkMetaData'] as String?,
|
||||
permissions: (json['permissions'] as num?)?.toInt(),
|
||||
password: json['password'] as String?,
|
||||
expireDate: json['expireDate'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileSharingApiParamsToJson(
|
||||
@@ -22,6 +25,9 @@ Map<String, dynamic> _$FileSharingApiParamsToJson(
|
||||
'shareType': instance.shareType,
|
||||
'shareWith': instance.shareWith,
|
||||
'path': instance.path,
|
||||
'referenceId': instance.referenceId,
|
||||
'talkMetaData': instance.talkMetaData,
|
||||
'referenceId': ?instance.referenceId,
|
||||
'talkMetaData': ?instance.talkMetaData,
|
||||
'permissions': ?instance.permissions,
|
||||
'password': ?instance.password,
|
||||
'expireDate': ?instance.expireDate,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import '../webdav/queries/list_files/cacheable_file.dart';
|
||||
|
||||
/// Converts a [CacheableFile.path] (relative to the WebDAV files root, folders
|
||||
/// ending in `/`) into the path the OCS `files_sharing` API expects: rooted
|
||||
/// with a single leading slash and without a trailing slash on folders.
|
||||
///
|
||||
/// Examples:
|
||||
/// '' -> '/' (files root)
|
||||
/// 'Documents/x.pdf' -> '/Documents/x.pdf'
|
||||
/// 'Documents/' -> '/Documents'
|
||||
/// '/Shared/a/' -> '/Shared/a'
|
||||
String ocsPathFor(String webdavPath) {
|
||||
final trimmed = webdavPath.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
return trimmed.isEmpty ? '/' : '/$trimmed';
|
||||
}
|
||||
|
||||
/// Convenience wrapper for a [CacheableFile].
|
||||
String ocsPathOf(CacheableFile file) => ocsPathFor(file.path);
|
||||
@@ -0,0 +1,108 @@
|
||||
/// Nextcloud share types (subset the app uses).
|
||||
const int kShareTypeUser = 0;
|
||||
const int kShareTypeGroup = 1;
|
||||
const int kShareTypePublicLink = 3;
|
||||
const int kShareTypeEmail = 4;
|
||||
|
||||
/// A Talk conversation ("room") the file is linked into.
|
||||
const int kShareTypeRoom = 10;
|
||||
|
||||
/// A single share as returned by the OCS `files_sharing` API.
|
||||
///
|
||||
/// Parsed by hand rather than via code generation: OCS is inconsistent about
|
||||
/// types across versions (e.g. `id`/`share_type` may arrive as either strings
|
||||
/// or numbers) and omits optional fields entirely, so defensive parsing is
|
||||
/// safer than generated `as int` casts.
|
||||
class Share {
|
||||
final int id;
|
||||
final int shareType;
|
||||
final int permissions;
|
||||
|
||||
/// Server path of the shared item (e.g. `/Documents/x.pdf`).
|
||||
final String? path;
|
||||
|
||||
/// `'file'` or `'folder'`.
|
||||
final String? itemType;
|
||||
|
||||
/// Recipient id (user/group id); empty for public links.
|
||||
final String? shareWith;
|
||||
final String? shareWithDisplayname;
|
||||
|
||||
/// Public link URL (only set for [kShareTypePublicLink]).
|
||||
final String? url;
|
||||
|
||||
/// Raw expiration as `"YYYY-MM-DD HH:MM:SS"` (or null when none).
|
||||
final String? expiration;
|
||||
final String? label;
|
||||
|
||||
/// Redacted password marker: the server returns `null` when no password is
|
||||
/// set and a placeholder (`"redacted"`) when one is — never the real value.
|
||||
final String? password;
|
||||
|
||||
const Share({
|
||||
required this.id,
|
||||
required this.shareType,
|
||||
required this.permissions,
|
||||
this.path,
|
||||
this.itemType,
|
||||
this.shareWith,
|
||||
this.shareWithDisplayname,
|
||||
this.url,
|
||||
this.expiration,
|
||||
this.label,
|
||||
this.password,
|
||||
});
|
||||
|
||||
bool get isPublicLink => shareType == kShareTypePublicLink;
|
||||
bool get isGroup => shareType == kShareTypeGroup;
|
||||
bool get isEmail => shareType == kShareTypeEmail;
|
||||
bool get isRoom => shareType == kShareTypeRoom;
|
||||
bool get isFolder => itemType == 'folder';
|
||||
|
||||
/// Whether a (link) password is currently set. See [password].
|
||||
bool get hasPassword => password != null && password!.isNotEmpty;
|
||||
|
||||
/// Best display title for the share row.
|
||||
String get displayTitle {
|
||||
if (isPublicLink) return label?.isNotEmpty == true ? label! : 'Link';
|
||||
final name = shareWithDisplayname;
|
||||
if (name != null && name.isNotEmpty) return name;
|
||||
return shareWith ?? 'Unbekannt';
|
||||
}
|
||||
|
||||
/// Human label for the kind of share (for subtitles/headers).
|
||||
String get kindLabel {
|
||||
if (isPublicLink) return 'Öffentlicher Link';
|
||||
if (isGroup) return 'Gruppe';
|
||||
if (isRoom) return 'Talk-Chat';
|
||||
if (isEmail) return 'E-Mail';
|
||||
if (shareType == kShareTypeUser) return 'Person';
|
||||
return 'Freigabe';
|
||||
}
|
||||
|
||||
static int _asInt(Object? value, {int fallback = 0}) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static String? _asString(Object? value) {
|
||||
if (value == null) return null;
|
||||
final s = value.toString();
|
||||
return s.isEmpty ? null : s;
|
||||
}
|
||||
|
||||
factory Share.fromJson(Map<String, dynamic> json) => Share(
|
||||
id: _asInt(json['id']),
|
||||
shareType: _asInt(json['share_type']),
|
||||
permissions: _asInt(json['permissions']),
|
||||
path: _asString(json['path']),
|
||||
itemType: _asString(json['item_type']),
|
||||
shareWith: _asString(json['share_with']),
|
||||
shareWithDisplayname: _asString(json['share_with_displayname']),
|
||||
url: _asString(json['url']),
|
||||
expiration: _asString(json['expiration']),
|
||||
label: _asString(json['label']),
|
||||
password: _asString(json['password']),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Parameters for updating an existing share via OCS `PUT shares/{id}`.
|
||||
/// Only the fields that are explicitly set are sent — every field is optional
|
||||
/// and a null is omitted from the request (sending `permissions=null` would be
|
||||
/// rejected by the server).
|
||||
///
|
||||
/// Use an empty string for [expireDate]/[password] to explicitly clear the
|
||||
/// value server-side (Nextcloud treats `expireDate=` as "remove expiry").
|
||||
class ShareUpdateParams {
|
||||
final int? permissions;
|
||||
final String? password;
|
||||
final String? expireDate;
|
||||
|
||||
const ShareUpdateParams({this.permissions, this.password, this.expireDate});
|
||||
|
||||
Map<String, String> toQuery() => {
|
||||
if (permissions != null) 'permissions': permissions.toString(),
|
||||
'password': ?password,
|
||||
'expireDate': ?expireDate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/// Nextcloud share permission bitmask helpers. These mirror the constants the
|
||||
/// OCS `files_sharing` API expects in the `permissions` field. Kept as pure
|
||||
/// functions (no Flutter/IO) so they are unit-testable.
|
||||
library;
|
||||
|
||||
/// Individual permission bits (Nextcloud `OCS\Constants`).
|
||||
const int kPermissionRead = 1;
|
||||
const int kPermissionUpdate = 2;
|
||||
const int kPermissionCreate = 4;
|
||||
const int kPermissionDelete = 8;
|
||||
const int kPermissionShare = 16;
|
||||
|
||||
/// User-facing presets that map onto a bitmask.
|
||||
enum SharePreset {
|
||||
/// Recipient can only view/download.
|
||||
readOnly,
|
||||
|
||||
/// Recipient can view, edit, add and remove (full editing).
|
||||
edit,
|
||||
|
||||
/// Upload-only "file request" — recipient can add files to a folder but not
|
||||
/// see existing contents. Only meaningful for folders.
|
||||
fileDrop,
|
||||
}
|
||||
|
||||
extension SharePresetLabel on SharePreset {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case SharePreset.readOnly:
|
||||
return 'Nur Lesen';
|
||||
case SharePreset.edit:
|
||||
return 'Bearbeiten';
|
||||
case SharePreset.fileDrop:
|
||||
return 'Datei-Anfrage';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if [mask] contains the given [flag].
|
||||
bool hasPermission(int mask, int flag) => mask & flag == flag;
|
||||
|
||||
/// Builds the permission bitmask for a [preset].
|
||||
///
|
||||
/// [isFolder] matters for the `edit` preset: a file can only carry
|
||||
/// read+update, while a folder additionally supports create+delete. Nextcloud
|
||||
/// rejects create/delete on a file ("Failed to update share"), so they must be
|
||||
/// omitted there. When [allowReshare] is true the reshare bit is added to the
|
||||
/// editing presets — mirroring how the Nextcloud clients respect the
|
||||
/// `resharing` capability.
|
||||
int permissionsFor(
|
||||
SharePreset preset, {
|
||||
bool allowReshare = false,
|
||||
bool isFolder = false,
|
||||
}) {
|
||||
switch (preset) {
|
||||
case SharePreset.readOnly:
|
||||
return kPermissionRead;
|
||||
case SharePreset.edit:
|
||||
var base = kPermissionRead | kPermissionUpdate;
|
||||
if (isFolder) base |= kPermissionCreate | kPermissionDelete;
|
||||
return allowReshare ? base | kPermissionShare : base;
|
||||
case SharePreset.fileDrop:
|
||||
return kPermissionCreate;
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies an arbitrary permission bitmask into the closest preset, or null
|
||||
/// if it doesn't match any (e.g. a custom combination). The reshare bit is
|
||||
/// ignored for matching so an "edit" share stays "edit" regardless of reshare.
|
||||
SharePreset? presetFromBitmask(int mask) {
|
||||
final normalized = mask & ~kPermissionShare;
|
||||
if (normalized == kPermissionCreate) return SharePreset.fileDrop;
|
||||
if (normalized == kPermissionRead) return SharePreset.readOnly;
|
||||
// Any read share that also carries a write bit (update/create/delete) is
|
||||
// surfaced as "edit".
|
||||
const writeBits = kPermissionUpdate | kPermissionCreate | kPermissionDelete;
|
||||
if (hasPermission(normalized, kPermissionRead) &&
|
||||
normalized & writeBits != 0) {
|
||||
return SharePreset.edit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -83,9 +83,49 @@ class SearchFilesEntry {
|
||||
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() {
|
||||
final path = webdavPath;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../api_params.dart';
|
||||
@@ -62,3 +64,44 @@ class DeleteMessage extends TalkApi {
|
||||
Map<String, String>? headers,
|
||||
) => http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
class SetRoomAvatar extends TalkApi {
|
||||
final String chatToken;
|
||||
final Uint8List bytes;
|
||||
final String filename;
|
||||
|
||||
SetRoomAvatar(this.chatToken, this.bytes, {this.filename = 'avatar.jpg'})
|
||||
: super('v1/room/$chatToken/avatar', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
) async {
|
||||
final req = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll(headers ?? const {})
|
||||
..files.add(http.MultipartFile.fromBytes('file', bytes, filename: filename));
|
||||
final streamed = await req.send();
|
||||
return http.Response.fromStream(streamed);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteRoomAvatar extends TalkApi {
|
||||
final String chatToken;
|
||||
|
||||
DeleteRoomAvatar(this.chatToken) : super('v1/room/$chatToken/avatar', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
@@ -16,26 +16,20 @@ class GetRoomResponse extends ApiResponse {
|
||||
Map<String, dynamic> toJson() => _$GetRoomResponseToJson(this);
|
||||
|
||||
List<GetRoomResponseObject> sortBy({
|
||||
bool lastActivity = true,
|
||||
required bool favoritesToTop,
|
||||
required bool unreadToTop,
|
||||
}) {
|
||||
for (var chat in data) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
if (favoritesToTop) {
|
||||
buffer.write(chat.isFavorite ? 'b' : 'a');
|
||||
return data.toList()..sort((a, b) {
|
||||
if (favoritesToTop && a.isFavorite != b.isFavorite) {
|
||||
return a.isFavorite ? -1 : 1;
|
||||
}
|
||||
if (unreadToTop) {
|
||||
buffer.write(chat.unreadMessages > 0 ? 'b' : 'a');
|
||||
final aUnread = a.unreadMessages > 0;
|
||||
final bUnread = b.unreadMessages > 0;
|
||||
if (aUnread != bUnread) return aUnread ? -1 : 1;
|
||||
}
|
||||
|
||||
buffer.write(chat.lastActivity);
|
||||
|
||||
chat.sort = buffer.toString();
|
||||
}
|
||||
|
||||
return data.toList()..sort((a, b) => b.sort!.compareTo(a.sort!));
|
||||
return b.lastActivity.compareTo(a.lastActivity);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +65,6 @@ class GetRoomResponseObject {
|
||||
String? status;
|
||||
String? statusIcon;
|
||||
String? statusMessage;
|
||||
String? sort;
|
||||
|
||||
GetRoomResponseObject(
|
||||
this.id,
|
||||
|
||||
@@ -60,7 +60,7 @@ GetRoomResponseObject _$GetRoomResponseObjectFromJson(
|
||||
json['status'] as String?,
|
||||
json['statusIcon'] as String?,
|
||||
json['statusMessage'] as String?,
|
||||
)..sort = json['sort'] as String?;
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GetRoomResponseObjectToJson(
|
||||
GetRoomResponseObject instance,
|
||||
@@ -97,7 +97,6 @@ Map<String, dynamic> _$GetRoomResponseObjectToJson(
|
||||
'status': instance.status,
|
||||
'statusIcon': instance.statusIcon,
|
||||
'statusMessage': instance.statusMessage,
|
||||
'sort': instance.sort,
|
||||
};
|
||||
|
||||
const _$GetRoomResponseObjectConversationTypeEnumMap = {
|
||||
|
||||
@@ -60,7 +60,15 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
|
||||
final status = data.statusCode;
|
||||
if (status < 200 || status >= 300) {
|
||||
final detail = 'Talk $endpoint -> HTTP $status';
|
||||
// Talk's OCS errors put the real reason in the response body (e.g.
|
||||
// expired session, removed participant, malformed reply target).
|
||||
// Include a trimmed preview so the in-app error dialog and logs
|
||||
// surface the cause instead of just the bare status code.
|
||||
final body = data.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Talk $endpoint -> HTTP $status'
|
||||
: 'Talk $endpoint -> HTTP $status body=$preview';
|
||||
log(detail);
|
||||
if (status == 401) {
|
||||
throw AuthException.unauthorized(technicalDetails: detail);
|
||||
|
||||
@@ -13,7 +13,21 @@ class CacheableFile {
|
||||
String? eTag;
|
||||
DateTime? createdAt;
|
||||
DateTime? modifiedAt;
|
||||
String? sort;
|
||||
|
||||
/// Nextcloud's instance-local file id (`oc:fileid`). Used to address the
|
||||
/// preview API by id, which is more reliable than the path-based variant
|
||||
/// on some server configurations.
|
||||
int? fileId;
|
||||
|
||||
/// Server's answer to "can I render a thumbnail for this file?"
|
||||
/// (`nc:has-preview`). Lets the file viewer skip the placeholder text
|
||||
/// when a preview is going to load anyway.
|
||||
bool? hasPreview;
|
||||
|
||||
/// True when this entry is an incoming share — i.e. shared with the current
|
||||
/// user by someone else (`nc:mount-type == 'shared'`). Used to badge the
|
||||
/// file/folder in the list. Nullable so older cached entries decode fine.
|
||||
bool? isSharedWithMe;
|
||||
|
||||
CacheableFile({
|
||||
required this.path,
|
||||
@@ -24,6 +38,9 @@ class CacheableFile {
|
||||
this.eTag,
|
||||
this.createdAt,
|
||||
this.modifiedAt,
|
||||
this.fileId,
|
||||
this.hasPreview,
|
||||
this.isSharedWithMe,
|
||||
});
|
||||
|
||||
CacheableFile.fromDavFile(WebDavFile file) {
|
||||
@@ -35,6 +52,13 @@ class CacheableFile {
|
||||
eTag = file.etag;
|
||||
createdAt = file.createdDate;
|
||||
modifiedAt = file.lastModified;
|
||||
fileId = int.tryParse(file.fileId ?? '');
|
||||
hasPreview = file.hasPreview;
|
||||
// Incoming share: the item is mounted into the user's files by someone
|
||||
// else. Outgoing shares ([isSharedByMe]) can't be derived from WebDAV with
|
||||
// the pinned package, so they are filled in by ListFiles via one OCS call
|
||||
// per folder.
|
||||
isSharedWithMe = file.props.ncmounttype == 'shared';
|
||||
}
|
||||
|
||||
factory CacheableFile.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -20,7 +20,10 @@ CacheableFile _$CacheableFileFromJson(Map<String, dynamic> json) =>
|
||||
modifiedAt: json['modifiedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['modifiedAt'] as String),
|
||||
)..sort = json['sort'] as String?;
|
||||
fileId: (json['fileId'] as num?)?.toInt(),
|
||||
hasPreview: json['hasPreview'] as bool?,
|
||||
isSharedWithMe: json['isSharedWithMe'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -32,5 +35,7 @@ Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
|
||||
'eTag': instance.eTag,
|
||||
'createdAt': instance.createdAt?.toIso8601String(),
|
||||
'modifiedAt': instance.modifiedAt?.toIso8601String(),
|
||||
'sort': instance.sort,
|
||||
'fileId': instance.fileId,
|
||||
'hasPreview': instance.hasPreview,
|
||||
'isSharedWithMe': instance.isSharedWithMe,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
|
||||
import '../../webdav_api.dart';
|
||||
@@ -25,16 +27,58 @@ class ListFiles extends WebdavApi<ListFilesParams> {
|
||||
Future<ListFilesResponse> run() async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
final timeout = _isRoot ? _rootTimeout : _subfolderTimeout;
|
||||
// Explicit prop list — without it the server omits OC-namespaced
|
||||
// properties like oc:fileid, which the preview endpoint relies on.
|
||||
final prop = WebDavPropWithoutValues.fromBools(
|
||||
davgetlastmodified: true,
|
||||
davgetetag: true,
|
||||
davgetcontenttype: true,
|
||||
davgetcontentlength: true,
|
||||
davresourcetype: true,
|
||||
ocfileid: true,
|
||||
ocsize: true,
|
||||
nccreationtime: true,
|
||||
nchaspreview: true,
|
||||
// 'shared' here means an incoming share (mounted into the user's files
|
||||
// by someone else); used to badge those entries in the list.
|
||||
ncmounttype: true,
|
||||
);
|
||||
|
||||
var files = await _fetch(webdav, prop, timeout);
|
||||
// A freshly-entered incoming share sometimes answers its first PROPFIND
|
||||
// without the OC/NC props (no fileid / has-preview / mount-type) while the
|
||||
// share mount warms up server-side — which drops thumbnails AND share
|
||||
// badges together. Retry a couple of times so the folder self-heals
|
||||
// instead of needing manual re-entry.
|
||||
for (var attempt = 0; attempt < 2 && _looksIncomplete(files); attempt++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||
files = await _fetch(webdav, prop, timeout);
|
||||
}
|
||||
|
||||
return ListFilesResponse(files);
|
||||
}
|
||||
|
||||
Future<Set<CacheableFile>> _fetch(
|
||||
WebDavClient webdav,
|
||||
WebDavPropWithoutValues prop,
|
||||
Duration timeout,
|
||||
) async {
|
||||
final davFiles =
|
||||
(await webdav.propfind(PathUri.parse(params.path)).timeout(timeout))
|
||||
(await webdav
|
||||
.propfind(PathUri.parse(params.path), prop: prop)
|
||||
.timeout(timeout))
|
||||
.toWebDavFiles();
|
||||
final files = davFiles.map(CacheableFile.fromDavFile).toSet();
|
||||
|
||||
// somehow the current working folder is also listed, it is filtered here.
|
||||
files.removeWhere(
|
||||
(element) => element.path == '/${params.path}/' || element.path == '/',
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
return ListFilesResponse(files);
|
||||
}
|
||||
/// True when the server returned entries but none carry a `fileId` — a sign
|
||||
/// the OC/NC properties were omitted (cold share mount), so thumbnails and
|
||||
/// share badges would be missing for the whole folder.
|
||||
bool _looksIncomplete(Set<CacheableFile> files) =>
|
||||
files.isNotEmpty && files.every((file) => file.fileId == null);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
|
||||
/// [invalidate].
|
||||
static int _cacheTimeFor(String path) {
|
||||
final stripped = path.replaceAll('/', '').trim();
|
||||
return stripped.isEmpty
|
||||
? RequestCache.cacheDay
|
||||
: RequestCache.cacheNothing;
|
||||
return stripped.isEmpty ? RequestCache.cacheDay : RequestCache.cacheNothing;
|
||||
}
|
||||
|
||||
/// Triggers a root-listing fetch in the background if no cached payload
|
||||
|
||||