├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .metadata ├── .tx └── config ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── nextcloud_cookbook_flutter │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-night-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-night │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ ├── icon.png │ │ │ ├── launch_background.xml │ │ │ └── notification_icon.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── launcher_icon.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── values-night-v31 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── key_backup_rules.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── i18n │ ├── bg_BG.json │ ├── cs_CZ.json │ ├── de.json │ ├── de_DE.json │ ├── en.json │ ├── en_GB.json │ ├── es.json │ ├── eu.json │ ├── fi_FI.json │ ├── fr.json │ ├── gl.json │ ├── he.json │ ├── hr.json │ ├── hu_HU.json │ ├── is.json │ ├── it.json │ ├── nl.json │ ├── pl.json │ ├── pt_BR.json │ ├── ru.json │ ├── sc.json │ ├── sk_SK.json │ ├── sl.json │ ├── tr.json │ ├── vi.json │ ├── zh_CN.json │ └── zh_HK.json ├── icon.svg └── launcher │ ├── adaptive_background.png │ ├── adaptive_foreground.png │ └── icon.png ├── build.yaml ├── docker-compose.yaml ├── docker ├── 80-setup ├── data │ └── Recipes │ │ ├── Bauernbrot mit Sauerteig │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Blaubeer Cluster │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Chantal's New York Cheesecake │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Chef John's Gazpacho │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Chili sin Carne mit Jackfruit │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Easy Heart-Shaped Cake │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Frühlingsfladen mit Rindfleisch │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Gelber Smoothie │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Grandma's Sour Cream Pound Cake │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Lachs auf Frühlingssalat │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Recipe Without an image │ │ └── recipe.json │ │ ├── Reines Roggenbrot aus Sauerteig │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Restaurant-Style Zuppa Toscana │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Sellerie-Rucola-Suppe mit Zitronenöl │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Sommerlicher Himbeerkuchen │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Sweet and Spicy Baked Keto Chicken WingsNEW │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── The Best Baked Ziti │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ ├── Vegi-Tortillas │ │ ├── full.jpg │ │ ├── recipe.json │ │ ├── thumb.jpg │ │ └── thumb16.jpg │ │ └── problem │ │ ├── Readme.md │ │ └── recipe.json └── setup_library ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 14.txt │ ├── 15.txt │ ├── 16.txt │ ├── 17.txt │ ├── 18.txt │ ├── 19.txt │ ├── 20.txt │ ├── 21.txt │ ├── 22.txt │ ├── 23.txt │ └── 24.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── short_description.txt │ └── title.txt ├── flutter_launcher_icons.yaml ├── flutter_native_splash.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-50x50@1x.png │ │ ├── Icon-App-50x50@2x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── main.dart └── src │ ├── blocs │ ├── authentication │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── categories │ │ ├── categories_bloc.dart │ │ ├── categories_event.dart │ │ └── categories_state.dart │ ├── login │ │ ├── login_bloc.dart │ │ ├── login_event.dart │ │ └── login_state.dart │ ├── recipe │ │ ├── recipe_bloc.dart │ │ ├── recipe_event.dart │ │ └── recipe_state.dart │ ├── recipes_short │ │ ├── recipes_short_bloc.dart │ │ ├── recipes_short_event.dart │ │ └── recipes_short_state.dart │ └── simple_bloc_delegatae.dart │ ├── models │ ├── animated_list.dart │ ├── app_authentication.dart │ ├── app_authentication.g.dart │ ├── image_response.dart │ ├── recipe.dart │ ├── timer.dart │ └── timer.g.dart │ ├── screens │ ├── category_screen.dart │ ├── form │ │ └── login_form.dart │ ├── loading_screen.dart │ ├── login_screen.dart │ ├── my_settings_screen.dart │ ├── recipe_edit_screen.dart │ ├── recipe_import_screen.dart │ ├── recipe_screen.dart │ ├── recipes_list_screen.dart │ ├── splash_screen.dart │ └── timer_screen.dart │ ├── services │ ├── api_provider.dart │ ├── authentication_provider.dart │ ├── data_repository.dart │ ├── intent_repository.dart │ ├── net │ │ └── nextcloud_metadata_api.dart │ ├── notification_provider.dart │ ├── services.dart │ ├── timer_repository.dart │ └── user_repository.dart │ ├── util │ ├── category_grid_delegate.dart │ ├── custom_cache_manager.dart │ ├── duration_utils.dart │ ├── lifecycle_event_handler.dart │ ├── setting_keys.dart │ ├── supported_locales.dart │ ├── theme_data.dart │ ├── theme_mode_manager.dart │ ├── translate_preferences.dart │ ├── url_validator.dart │ └── wakelock.dart │ └── widget │ ├── alerts │ ├── recipe_delete_alert.dart │ └── recipe_edit_alert.dart │ ├── animated_time_progress_bar.dart │ ├── category_card.dart │ ├── checkbox_form_field.dart │ ├── drawer.dart │ ├── drawer_item.dart │ ├── input │ ├── duration_form_field.dart │ ├── integer_text_form_field.dart │ └── reorderable_list_form_field.dart │ ├── recipe │ ├── recipe_screen.dart │ └── widget │ │ ├── duration_list.dart │ │ ├── ingredient_list.dart │ │ ├── instruction_list.dart │ │ ├── nutrition_list.dart │ │ ├── recipe_yield.dart │ │ ├── rounded_box_item.dart │ │ └── tool_list.dart │ ├── recipe_image.dart │ ├── recipe_list_item.dart │ ├── timer_list_item.dart │ └── user_image.dart ├── privacy-policy.md ├── pubspec.yaml └── test ├── models ├── app_authentication_test.dart └── timer_test.dart └── util └── url_validator_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "develop" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ "develop", "main" ] 6 | pull_request: 7 | types: [ "review_requested", "ready_for_review" ] 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: ⬇️ Checkout repository 13 | uses: actions/checkout@v3 14 | - name: ⚙️ Setup Java 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '12.x' 18 | distribution: 'adopt' 19 | cache: 'gradle' 20 | - name: ⚙️ Setup Flutter 21 | uses: subosito/flutter-action@v2 22 | with: 23 | channel: 'stable' 24 | cache: true 25 | cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' 26 | cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:' 27 | architecture: x64 28 | # This is seems to be only needed when running github actions locally with act 29 | # - run: git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.13.0-x64 30 | - name: 📥 Downloading Dependencies 31 | run: flutter pub get 32 | # There are many issues currently that first need to be fixed to allow this rule 33 | # - run: flutter analyze --fatal-infos ./ 34 | - name: 📝 Format 35 | run: dart format -o none --set-exit-if-changed ./ 36 | - name: 🧪 Testing 37 | run: flutter test 38 | - name: 🛠️ Build APK 39 | run: flutter build apk --debug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/mingit/ 28 | /dev/benchmarks/mega_gallery/ 29 | /dev/bots/.recipe_deps 30 | /dev/bots/android_tools/ 31 | /dev/docs/doc/ 32 | /dev/docs/flutter.docs.zip 33 | /dev/docs/lib/ 34 | /dev/docs/pubspec.yaml 35 | /dev/integration_tests/**/xcuserdata 36 | /dev/integration_tests/**/Pods 37 | /packages/flutter/coverage/ 38 | version 39 | 40 | # packages file containing multi-root paths 41 | .packages.generated 42 | 43 | # Flutter/Dart/Pub related 44 | **/doc/api/ 45 | .dart_tool/ 46 | .flutter-plugins 47 | .flutter-plugins-dependencies 48 | .packages 49 | .pub-cache/ 50 | .pub/ 51 | build/ 52 | flutter_*.png 53 | linked_*.ds 54 | unlinked.ds 55 | unlinked_spec.ds 56 | 57 | # Android related 58 | **/android/**/gradle-wrapper.jar 59 | **/android/.gradle 60 | **/android/captures/ 61 | **/android/gradlew 62 | **/android/gradlew.bat 63 | **/android/local.properties 64 | **/android/**/GeneratedPluginRegistrant.java 65 | **/android/key.properties 66 | *.jks 67 | 68 | # iOS/XCode related 69 | **/ios/**/*.mode1v3 70 | **/ios/**/*.mode2v3 71 | **/ios/**/*.moved-aside 72 | **/ios/**/*.pbxuser 73 | **/ios/**/*.perspectivev3 74 | **/ios/**/*sync/ 75 | **/ios/**/.sconsign.dblite 76 | **/ios/**/.tags* 77 | **/ios/**/.vagrant/ 78 | **/ios/**/DerivedData/ 79 | **/ios/**/Icon? 80 | **/ios/**/Pods/ 81 | **/ios/**/.symlinks/ 82 | **/ios/**/profile 83 | **/ios/**/xcuserdata 84 | **/ios/.generated/ 85 | **/ios/Flutter/App.framework 86 | **/ios/Flutter/Flutter.framework 87 | **/ios/Flutter/Flutter.podspec 88 | **/ios/Flutter/Generated.xcconfig 89 | **/ios/Flutter/app.flx 90 | **/ios/Flutter/app.zip 91 | **/ios/Flutter/flutter_assets/ 92 | **/ios/Flutter/flutter_export_environment.sh 93 | **/ios/ServiceDefinitions.json 94 | **/ios/Runner/GeneratedPluginRegistrant.* 95 | 96 | # macOS 97 | **/macos/Flutter/GeneratedPluginRegistrant.swift 98 | **/macos/Flutter/Flutter-Debug.xcconfig 99 | **/macos/Flutter/Flutter-Release.xcconfig 100 | **/macos/Flutter/Flutter-Profile.xcconfig 101 | 102 | # Coverage 103 | coverage/ 104 | 105 | # Symbols 106 | app.*.symbols 107 | 108 | # Exceptions to above rules. 109 | !**/ios/**/default.mode1v3 110 | !**/ios/**/default.mode2v3 111 | !**/ios/**/default.pbxuser 112 | !**/ios/**/default.perspectivev3 113 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 114 | !/dev/ci/**/Gemfile.lock 115 | © 2020 GitHub, Inc. 116 | Terms 117 | Privacy 118 | Security 119 | Status 120 | Help 121 | Contact GitHub 122 | Pricing 123 | API 124 | Training 125 | Blog 126 | About 127 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:nextcloud:p:nextcloud:r:cookbook_flutter] 5 | file_filter = assets/i18n/.json 6 | source_file = assets/i18n/en.json 7 | source_lang = en 8 | type = KEYVALUEJSON 9 | 10 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer to use the lint rule set from `package:lint` 2 | 3 | include: package:lint/strict.yaml 4 | 5 | linter: 6 | rules: 7 | sort_pub_dependencies: false 8 | prefer_final_parameters: false 9 | prefer_asserts_with_message: false 10 | only_throw_errors: false 11 | 12 | prefer_mixin: true 13 | discarded_futures: true 14 | unawaited_futures: true 15 | prefer_expression_function_bodies: true 16 | always_put_control_body_on_new_line: true 17 | use_key_in_widget_constructors: true 18 | always_put_required_named_parameters_first: true 19 | prefer_single_quotes: true 20 | sort_constructors_first: true 21 | omit_local_variable_types: true 22 | prefer_int_literals: true 23 | cascade_invocations: true 24 | avoid_equals_and_hash_code_on_mutable_classes: true 25 | avoid_types_on_closure_parameters: true 26 | use_decorated_box: true 27 | unnecessary_lambdas: true 28 | prefer_foreach: true 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def keystoreProperties = new Properties() 10 | def keystorePropertiesFile = rootProject.file('key.properties') 11 | if (keystorePropertiesFile.exists()) { 12 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 13 | } 14 | 15 | def flutterRoot = localProperties.getProperty('flutter.sdk') 16 | if (flutterRoot == null) { 17 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 18 | } 19 | 20 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 21 | if (flutterVersionCode == null) { 22 | flutterVersionCode = '1' 23 | } 24 | 25 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 26 | if (flutterVersionName == null) { 27 | flutterVersionName = '1.0' 28 | } 29 | 30 | apply plugin: 'com.android.application' 31 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 32 | 33 | android { 34 | compileSdkVersion 33 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId "com.nextcloud_cookbook_flutter" 39 | minSdkVersion 18 40 | targetSdkVersion flutter.targetSdkVersion 41 | versionCode flutterVersionCode.toInteger() 42 | versionName flutterVersionName 43 | } 44 | 45 | // signingConfigs { 46 | // release { 47 | // keyAlias keystoreProperties['keyAlias'] 48 | // keyPassword keystoreProperties['keyPassword'] 49 | // storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 50 | // storePassword keystoreProperties['storePassword'] 51 | // } 52 | // } 53 | // buildTypes { 54 | // release { 55 | // signingConfig signingConfigs.release 56 | // } 57 | // } 58 | } 59 | 60 | flutter { 61 | source '../..' 62 | } 63 | 64 | dependencies { 65 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 66 | } 67 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/nextcloud_cookbook_flutter/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nextcloud_cookbook_flutter; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import io.flutter.plugin.common.MethodChannel; 10 | import io.flutter.embedding.android.FlutterActivity; 11 | import io.flutter.embedding.engine.FlutterEngine; 12 | import io.flutter.plugins.GeneratedPluginRegistrant; 13 | 14 | public class MainActivity extends FlutterActivity { 15 | private String importUrl; 16 | private static final String CHANNEL = "app.channel.shared.data"; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | Intent intent = getIntent(); 22 | String action = intent.getAction(); 23 | String type = intent.getType(); 24 | 25 | if (Intent.ACTION_SEND.equals(action) && type != null) { 26 | if ("text/plain".equals(type)) { 27 | handleSendText(intent); // Handle text being sent 28 | } 29 | } 30 | } 31 | 32 | @Override 33 | public void configureFlutterEngine(FlutterEngine flutterEngine) { 34 | GeneratedPluginRegistrant.registerWith(flutterEngine); 35 | 36 | new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) 37 | .setMethodCallHandler( 38 | (call, result) -> { 39 | if (call.method.contentEquals("getImportUrl")) { 40 | result.success(importUrl); 41 | importUrl = null; 42 | } 43 | } 44 | ); 45 | } 46 | 47 | void handleSendText(Intent intent) { 48 | importUrl = intent.getStringExtra(Intent.EXTRA_TEXT); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-night-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-night/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/drawable/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #0082c9 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/key_backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.0.4' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/i18n/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_bar": { 3 | "search": "חיפוש", 4 | "refresh": "רענון", 5 | "logout": "יציאה" 6 | }, 7 | "login": { 8 | "title": "כניסה", 9 | "server_url": { 10 | "field": "כתובת השרת", 11 | "validator": { 12 | "empty": "נא למלא את כתובת עותק ה־Nextcloud שלך.", 13 | "pattern": "נא למלא כתובת תקנית" 14 | } 15 | }, 16 | "username": { 17 | "field": "שם משתמש" 18 | }, 19 | "password": { 20 | "field": "ססמה" 21 | }, 22 | "settings": { 23 | "title": "הגדרות מתקדמות", 24 | "app_password": "להשתמש ב־AppPassword (ססמת יישומון) שיצרת בעצמך.\nנחוץ לטובת חשבונות עם אימות דו־שלבי", 25 | "self_signed_certificate": "התעלמות מאישור שגוי\nנחוץ למקרה של אישורים שנחתמו עצמית\n(על אחריותך בלבד!)" 26 | }, 27 | "button": "כניסה", 28 | "errors": { 29 | "not_reachable": "לא ניתן לגשת אל: {server_url} \n {error_msg}", 30 | "certificate_failed": "לא ניתן לאמת את אישור השרת: {server_url} \n {error_msg}", 31 | "request_failed": "בקשת AppPassword (ססמת יישומון) נכשלה:\n {error_msg}", 32 | "parse_failed": "לא ניתן לפענח את תגובת ה־AppPassword (ססמת יישומון)!\n {error_msg}", 33 | "parse_missing": "לא ניתן לאתר את תגובת ה־AppPassword (ססמת יישומון)!\n{error_msg}", 34 | "auth_failed": "שם המשתמש ו/או הססמה שגויים!", 35 | "authentication_not_found": "לא נמצא אימות באחסון", 36 | "failed_remove_remote": "הסרת ה־AppPassword (ססמת יישומון) המרוחקת נכשלה!", 37 | "failure": "תהליך הכניסה לא הסתיים כראוי.\n {status_code}\n {status_message}", 38 | "credentials_invalid": "פרטי הגישה המאוחסנים אינם תקפים עוד ויצאת מהחשבון!" 39 | } 40 | }, 41 | "categories": { 42 | "title": "ספר בישול", 43 | "all_categories": "הכול", 44 | "errors": { 45 | "unknown": "הקטגוריות נמצאות במצב לא ידוע", 46 | "load_failed": "טעינת הקטגוריות נכשלה: {error_msg}", 47 | "load_no_response": "לא ניתן לקבל את הקטגוריות מהשרת.", 48 | "api_version_check_failed": "בדיקת גרסת ה־API של השרת נכשלה:\n {error_msg}", 49 | "api_version_above_confirmed": "גרסת ה־API של השרת עודכנה. חלק מהתכונות עשויות לעבוד אחרת מהמצופה. נא להמתין לעדכון!\n {version}" 50 | } 51 | }, 52 | "recipe_list": { 53 | "title_category": "קטגוריה: {category}", 54 | "errors": { 55 | "load_failed": "טעינת תקצירי המתכונים נכשלה!" 56 | } 57 | }, 58 | "recipe": { 59 | "title": "מתכון", 60 | "fields": { 61 | "servings": "מנות:", 62 | "source": "מקור", 63 | "time": { 64 | "prep": "זמן הכנה", 65 | "cook": "זמן בישול", 66 | "total": "זמן כולל" 67 | }, 68 | "tools": "כלים", 69 | "ingredients": "רכיבים", 70 | "instructions": "הוראות" 71 | }, 72 | "errors": { 73 | "load_failed": "טעינת המתכון נכשלה!" 74 | } 75 | }, 76 | "recipe_edit": { 77 | "title": "עריכת מתכון", 78 | "errors": { 79 | "update_failed": "העדכון נכשל {error_msg}" 80 | } 81 | }, 82 | "search": { 83 | "title": "חיפוש", 84 | "nothing_found": "לא נמצאו מתכונים!" 85 | } 86 | } -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/launcher/adaptive_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/assets/launcher/adaptive_background.png -------------------------------------------------------------------------------- /assets/launcher/adaptive_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/assets/launcher/adaptive_foreground.png -------------------------------------------------------------------------------- /assets/launcher/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/assets/launcher/icon.png -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | copy_with_extension_gen: 5 | enabled: true 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | nextcloud: 4 | image: lscr.io/linuxserver/nextcloud:latest 5 | container_name: plugin_development 6 | environment: 7 | - PUID=1000 8 | - PGID=1000 9 | - TZ=Europe/London 10 | - COOKBOOK=0.10.2 11 | volumes: 12 | - /config 13 | - /data 14 | - ./docker/data:/config/www/nextcloud/data/preset 15 | - ./docker/80-setup:/custom-cont-init.d/80-setup:ro 16 | - ./docker/setup_library:/etc/setup_library:ro 17 | ports: 18 | - 443:443 19 | - 80:80 -------------------------------------------------------------------------------- /docker/80-setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # Default Setup 4 | occ maintenance:install --database \ 5 | "sqlite" --database-name "nextcloud" --database-user "root" --database-pass \ 6 | "password" --admin-user "admin" --admin-pass "password" 7 | sed -i "s/'localhost'/'*'/g" /config/www/nextcloud/config/config.php 8 | 9 | # Install Cookbook Plugin 10 | wget https://github.com/nextcloud/cookbook/releases/download/v"${COOKBOOK}"/Cookbook-"${COOKBOOK}".tar.gz -P /config/www/nextcloud/apps 11 | tar -zxf /config/www/nextcloud/apps/Cookbook-"${COOKBOOK}".tar.gz -C /config/www/nextcloud/apps 12 | rm /config/www/nextcloud/apps/Cookbook-"${COOKBOOK}".tar.gz 13 | occ app:enable cookbook 14 | 15 | bash /etc/setup_library -------------------------------------------------------------------------------- /docker/data/Recipes/Bauernbrot mit Sauerteig/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Bauernbrot mit Sauerteig/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Bauernbrot mit Sauerteig/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Bauernbrot mit Sauerteig/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Bauernbrot mit Sauerteig/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Bauernbrot mit Sauerteig/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Blaubeer Cluster/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Blaubeer Cluster/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Blaubeer Cluster/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Blaubeer Cluster","image":"https:\/\/recipecontent.fooby.ch\/16683_3-2_480-320.jpg","recipeYield":20,"keywords":"Desserts,Sommer,Herbst","recipeIngredient":[" 120 vegane, helle Schokolade (70 % Kakao), fein gehackt"," 1 EL Kokos\u00f6l"," 1 Prise Meersalz"," 250 g Heidelbeeren (oder Blaubeeren)"," 2 TL Kokosraspel"],"recipeInstructions":["\n \n \n \n Schokolade in einer d\u00fcnnwandigen Sch\u00fcssel \u00fcber dem nur leicht siedenden Wasserbad schmelzen, glatt r\u00fchren, Kokos\u00f6l und Salz darunterr\u00fchren. Mit einem Teel\u00f6ffel wenig Schokolade auf die Beeren geben, je 3-5 Beeren zusammenkleben, auf ein Blech legen, ca. 10 Min. gefrieren. Die einzelnen Cluster mit einer Gabel in die restliche Schokolade tauchen, nebeneinander auf ein Backpapier legen, Kokosraspel dar\u00fcber streuen, bis zum Naschen ca. 15 Min. k\u00fchl stellen.\n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/16683\/blaubeer-cluster","nutrition":[],"dateModified":"2021-04-17T12:51:51+0000","dateCreated":"2021-04-17T12:51:51+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Blaubeer Cluster/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Blaubeer Cluster/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Blaubeer Cluster/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Blaubeer Cluster/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chantal's New York Cheesecake/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chantal's New York Cheesecake/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chantal's New York Cheesecake/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chantal's New York Cheesecake/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chantal's New York Cheesecake/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chantal's New York Cheesecake/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chef John's Gazpacho/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chef John's Gazpacho/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chef John's Gazpacho/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chef John's Gazpacho/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chef John's Gazpacho/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chef John's Gazpacho/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chili sin Carne mit Jackfruit/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chili sin Carne mit Jackfruit/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chili sin Carne mit Jackfruit/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Chili sin Carne mit Jackfruit","image":"https:\/\/recipecontent.fooby.ch\/20246_3-2_480-320.jpg","recipeYield":4,"keywords":"Hauptgericht,Suppen und Eintopf,Fr\u00fchling","recipeIngredient":[" 1 EL Raps\u00f6l"," 1 TL getrockneter Oregano"," 1 TL Kreuzk\u00fcmmel"," 2 TL Paprika"," \u00bc TL Chiliflocken"," 2 Knoblauchzehen, fein gehackt"," 1 rote Zwiebel, fein gehackt"," \u00bd rote Peperoni, in W\u00fcrfeli"," 1 Dose Jackfruit (ca. 400 g), abgetropft"," 1 Dose gehackte Tomaten (ca. 400 g)"," 1 Dose Red Kidney Bohnen (ca. 250 g), abgesp\u00fclt, abgetropft"," 1 Dose Maisk\u00f6rner (ca. 140 g), abgetropft"," 1 EL Kakaopulver"," 2 \u00bc dl Wasser"," 1 TL Salz"," 1 Limette, nur Saft"," 100 g Kokosnussmilch-Jogurt"," \u00bd Bund Koriander, zerzupft"],"recipeInstructions":["\n \n Gew\u00fcrze r\u00f6sten\n \n \u00d6l in einer Bratpfanne erhitzen. Oregano, Kreuzk\u00fcmmel, Paprika und Chiliflocken beigeben, unter R\u00fchren ca. 1 Min. r\u00f6sten, Hitze reduzieren.\n \n \n Chili\n \n Knoblauch und Zwiebel zu den Gew\u00fcrzen geben, kurz and\u00e4mpfen. Peperoni und Jackfruit beigeben, ca. 5 Min. mitd\u00e4mpfen. Tomaten, Bohnen, Mais und Kakao beigeben, Wasser dazugiessen, aufkochen, salzen. Hitze reduzieren, zugedeckt ca. 30 Min. k\u00f6cheln.\n \n \n Anrichten\n \n Limettensaft daruntermischen. Chili anrichten, mit Jogurt und Koriander garnieren.\n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/20246\/chili-sin-carne-mit-jackfruit?startAuto1=0","nutrition":[],"dateModified":"2021-04-17T12:55:55+0000","dateCreated":"2021-04-17T12:55:55+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Chili sin Carne mit Jackfruit/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chili sin Carne mit Jackfruit/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Chili sin Carne mit Jackfruit/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Chili sin Carne mit Jackfruit/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Easy Heart-Shaped Cake/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Easy Heart-Shaped Cake/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Easy Heart-Shaped Cake/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Easy Heart-Shaped Cake/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Easy Heart-Shaped Cake/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Easy Heart-Shaped Cake/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Frühlingsfladen mit Rindfleisch/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Frühlingsfladen mit Rindfleisch/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Frühlingsfladen mit Rindfleisch/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Fr\u00c3\u00bchlingsfladen mit Rindfleisch","image":"https:\/\/recipecontent.fooby.ch\/17431_3-2_480-320.jpg","recipeYield":4,"keywords":"Hauptgericht,Sommer,Fr\u00c3\u00bchling","recipeIngredient":[" 350 g Halbweissmehl"," \u00c2\u00be TL Salz"," \u00c2\u00bc W\u00c3\u00bcrfel Hefe (ca. 10 g), zerbr\u00c3\u00b6ckelt"," \u00c2\u00bc TL Zucker"," 1 Bund Koriander"," 3 EL \u00c3\u0096l"," 2 dl Wasser"," \u00c2\u00bd Bund Koriander, grob geschnitten"," 200 g saurer Halbrahm"," 250 g d\u00c3\u00bcnne gr\u00c3\u00bcne Spargeln"," 2 Bundzwiebeln mit dem Gr\u00c3\u00bcn, halbiert, l\u00c3\u00a4ngs in feinen Streifen"," 1 EL Oliven\u00c3\u00b6l zum Braten"," 250 g Hohr\u00c3\u00bcckensteaks"," \u00c2\u00bc TL orientalische Gew\u00c3\u00bcrzmischung (z.B. Smokey Beef)"," 100 g Edamame, ausgel\u00c3\u00b6st"," 30 g Micro Greens "," \u00c2\u00bd Bund Koriander, Bl\u00c3\u00a4tter abgezupft"," 2 Prisen Salz"," 150 g Burratas, in St\u00c3\u00bccken"," 2 EL Oliven\u00c3\u00b6l"," 2 EL Zitronensaft"," \u00c2\u00bc TL Salz"],"recipeInstructions":["\n \n Gr\u00c3\u00bcner Pizzateig\n \n Mehl, Salz, Zucker und Hefe in einer Sch\u00c3\u00bcssel mischen Koriander mit \u00c3\u0096l und Wasser p\u00c3\u00bcrieren, dazugiessen, mischen, zu einem glatten Teig kneten. Zugedeckt bei Raumtemperatur ca. 1\u00c2\u00bd Std. aufs Doppelte aufgehen lassen.\n \n \n Belag\n \n Teig etwas flach dr\u00c3\u00bccken, auf wenig Mehl auswallen (ca. 30 cm \u00c3\u0098), auf ein Backpapier legen. Koriander mit dem sauren Halbrahm p\u00c3\u00bcrieren, auf dem Teigboden verteilen, dabei rundum einen ca. 2 cm breiten Rand frei lassen. Spargeln und Bundzwiebeln darauf verteilen. \n \n \n Backen\n \n Blech im auf 240 \u00c2\u00b0C vorgeheizten Ofen vorheizen. Herausnehmen, Fladen darauf ziehen, ca. 25 Min. auf der untersten Rille des auf 240 \u00c2\u00b0C vorgeheizten Ofens backen. \n \n \n Fr\u00c3\u00bchlings-Belag\n \n \u00c3\u0096l in einer Bratpfanne erhitzen. Hohr\u00c3\u00bcckensteak w\u00c3\u00bcrzen, beidseitig je ca. 2 Min. braten, herausnehmen, ca. 5 Min. ziehen lassen, in Tranchen schneiden. Edamame, Micro Greens und Koriander mischen, salzen, mit dem Fleisch auf dem Fladen verteilen. Burrata auf dem Fladen verteilen. \u00c3\u0096l und Zitronensaft verr\u00c3\u00bchren, salzen, dar\u00c3\u00bcbertr\u00c3\u00a4ufeln.\n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/17431\/fruehlingsfladen-mit-rindfleisch?startAuto1=0","nutrition":[],"dateModified":"2021-04-17T13:04:16+0000","dateCreated":"2021-04-17T13:04:16+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Frühlingsfladen mit Rindfleisch/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Frühlingsfladen mit Rindfleisch/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Frühlingsfladen mit Rindfleisch/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Frühlingsfladen mit Rindfleisch/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Gelber Smoothie/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Gelber Smoothie/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Gelber Smoothie/recipe.json: -------------------------------------------------------------------------------- 1 | {"@context":"http:\/\/schema.org","@type":"Recipe","nutrition":[],"dateCreated":"2021-04-11T14:23:36+0000","printImage":false,"id":922169,"name":"Gelber Smoothie ","imageUrl":"\/apps\/cookbook\/recipes\/922169\/image?size=full","recipeCategory":"","description":"","recipeIngredient":[" 1 Mango, in St\u00fccken"," \u00bd Ananas, in St\u00fccken"," \u00bd TL Kurkuma"," \u00bd EL Limettensaft"," 1 dl Wasser"," 1 dl Kokosmilch"],"recipeInstructions":["\n \n Smoothie\n \n Mango mit allen Zutaten bis und mit Kokosmilch p\u00fcrieren, in die Gl\u00e4ser verteilen. \n \n \n "],"tool":[],"recipeYield":5,"prepTime":"","cookTime":"","totalTime":"","keywords":"Vegan,Vegetarisch,Schnelle K\u00fcche","image":"https:\/\/recipecontent.fooby.ch\/12744_3-2_480-320.jpg","url":"https:\/\/fooby.ch\/de\/rezepte\/12744\/gelber-smoothie-?startAuto1=0","dateModified":"2021-04-17T12:45:04+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Gelber Smoothie/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Gelber Smoothie/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Gelber Smoothie/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Gelber Smoothie/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Grandma's Sour Cream Pound Cake/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Grandma's Sour Cream Pound Cake/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Grandma's Sour Cream Pound Cake/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Grandma's Sour Cream Pound Cake/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Grandma's Sour Cream Pound Cake/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Grandma's Sour Cream Pound Cake/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Lachs auf Frühlingssalat/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Lachs auf Frühlingssalat/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Lachs auf Frühlingssalat/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Lachs auf Fr\u00c3\u00bchlingssalat ","image":"https:\/\/recipecontent.fooby.ch\/19134_3-2_480-320.jpg","recipeYield":4,"keywords":"Hauptgericht,Salate,Sommer","recipeIngredient":[" 100 g schwarze Linsen (Beluga)"," Wasser, siedend"," \u00c2\u00bc TL Salz"," 1 Bio-Zitrone, abgeriebene Schale und 2 EL Saft"," 3 EL Haselnuss\u00c3\u00b6l"," \u00c2\u00bd TL Salz"," wenig Pfeffer"," 1 Bund Kerbel, fein geschnitten"," 1 Bund Radiesli mit dem Gr\u00c3\u00bcn, Radiesli geviertelt, 10 g Gr\u00c3\u00bcn fein geschnitten"," 300 g R\u00c3\u00bcebli, mit dem Sparsch\u00c3\u00a4ler in d\u00c3\u00bcnne Streifen geschnitten"," \u00c2\u00bd EL Oliven\u00c3\u00b6l"," 250 g Rhabarber, in ca. 6 cm langen St\u00c3\u00bccken, halbiert"," 2 EL Yaconsirup"," \u00c2\u00bd EL Oliven\u00c3\u00b6l"," 400 g Lachsr\u00c3\u00bcckenfilet"],"recipeInstructions":["\n \n Linsen \n \n Linsen im siedenden Wasser bei mittlerer Hitze ca. 20 Min. k\u00c3\u00b6cheln, abtropfen, salzen, beiseite stellen.\n \n \n Salat \n \n Zitronenschale und -saft mit dem \u00c3\u0096l in einer Sch\u00c3\u00bcssel verr\u00c3\u00bchren, w\u00c3\u00bcrzen, Kerbel und Radiesligr\u00c3\u00bcn daruntermischen. Radiesli, R\u00c3\u00bcebli und beiseite gestellte Linsen beigeben, mischen. \n \n \n Lachs\n \n \u00c3\u0096l in einer beschichteten Bratpfanne erhitzen. Rhabarber beigeben, ca. 2 Min. r\u00c3\u00bchrbraten. Yaconsirup beigeben, ca. 2 Min. k\u00c3\u00b6cheln, mit der entstandenen Fl\u00c3\u00bcssigkeit unter den Salat mischen. \u00c3\u0096l in derselben Pfanne erhitzen. Lachs beidseitig je ca. 3 Min. braten, auf dem Salat anrichten. \n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/19134\/lachs-auf-fruehlingssalat-","nutrition":[],"dateModified":"2021-04-17T12:59:22+0000","dateCreated":"2021-04-17T12:59:22+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Lachs auf Frühlingssalat/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Lachs auf Frühlingssalat/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Lachs auf Frühlingssalat/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Lachs auf Frühlingssalat/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Recipe Without an image/recipe.json: -------------------------------------------------------------------------------- 1 | {"id":0,"name":"Recipe Without an image","description":"","url":"","image":"","prepTime":"","cookTime":"","totalTime":"","recipeCategory":"","keywords":"","recipeYield":1,"tool":[],"recipeIngredient":[],"recipeInstructions":[],"nutrition":[],"@context":"http:\/\/schema.org","@type":"Recipe","dateModified":"2021-12-18T16:30:25+0000","dateCreated":"2021-12-18T16:30:25+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Reines Roggenbrot aus Sauerteig/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Reines Roggenbrot aus Sauerteig/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Reines Roggenbrot aus Sauerteig/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Reines Roggenbrot aus Sauerteig/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Reines Roggenbrot aus Sauerteig/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Reines Roggenbrot aus Sauerteig/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Restaurant-Style Zuppa Toscana/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Restaurant-Style Zuppa Toscana/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Restaurant-Style Zuppa Toscana/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Restaurant-Style Zuppa Toscana/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Restaurant-Style Zuppa Toscana/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Restaurant-Style Zuppa Toscana/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/recipe.json: -------------------------------------------------------------------------------- 1 | {"@context":"http:\/\/schema.org","@type":"Recipe","nutrition":[],"dateCreated":"2021-04-17T12:41:40+0000","printImage":false,"id":922659,"name":"Sellerie-Rucola-Suppe mit Zitronen\u00c3\u00b6l","imageUrl":"\/apps\/cookbook\/recipes\/922659\/image?size=full","recipeCategory":"","description":"","recipeIngredient":[" 1 dl Oliven\u00c3\u00b6l"," 2 Bio-Zitronen, nur abgeriebene Schale"," 1 EL Oliven\u00c3\u00b6l"," 1 Zwiebel, grob gehackt"," 1 Knoblauchzehe, grob gehackt"," 800 g Knollensellerie, in St\u00c3\u00bccken"," 1 Liter Wasser"," 2 TL Salz"," 100 g Rucola, grob geschnitten"," 1 EL Zitronensaft"," Salz, Pfeffer, nach Bedarf"," 50 g Rucola"],"recipeInstructions":["\n \n Schnelles Zitronen\u00c3\u00b6l\n \n \u00c3\u0096l in einer kleinen Pfanne erw\u00c3\u00a4rmen. Zitronenschale beigeben, in eine kleine, saubere Flasche umf\u00c3\u00bcllen.\n \n \n Suppe\n \n \u00c3\u0096l in einer Pfanne erw\u00c3\u00a4rmen. Zwiebel und Knoblauch and\u00c3\u00a4mpfen. Sellerie beigeben, ca. 3 Min. mitd\u00c3\u00a4mpfen. Wasser dazugiessen, aufkochen, salzen. Suppe ca. 25 Min. k\u00c3\u00b6cheln. Rucola beigeben, ca. 5 Min. mitk\u00c3\u00b6cheln, fein p\u00c3\u00bcrieren. \n \n \n Anrichten\n \n Zitronensaft unter die Suppe r\u00c3\u00bchren, w\u00c3\u00bcrzen. Suppe anrichten, Rucola mit wenig Zitronen\u00c3\u00b6l auf der Suppe verteilen. \n \n \n "],"tool":[],"recipeYield":4,"prepTime":"","cookTime":"PT0H2M","totalTime":"","keywords":"Winter,Suppen und Eintopf","image":"https:\/\/recipecontent.fooby.ch\/18757_3-2_480-320.jpg","url":"https:\/\/fooby.ch\/de\/rezepte\/18757\/sellerie-rucola-suppe-mit-zitronenoel","dateModified":"2021-07-05T14:57:07+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sellerie-Rucola-Suppe mit Zitronenöl/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sommerlicher Himbeerkuchen/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sommerlicher Himbeerkuchen/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sommerlicher Himbeerkuchen/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Sommerlicher Himbeerkuchen","image":"https:\/\/recipecontent.fooby.ch\/18323_3-2_480-320.jpg","recipeYield":8,"keywords":"Desserts,Sommer,Herbst","recipeIngredient":[" 200 g Kokosraspel"," 180 g Datteln, entsteint"," 1 EL Ahornsirup"," 5 EL Kokos\u00f6l"," 1 Prise Salz"," 1 Dose Kokosmilch (ca. 4 dl), \u00fcber Nacht in den K\u00fchlschrank gestellt"," 100 g vegane, weisse Schokolade, in St\u00fccken"," 2 EL Kokos\u00f6l"," 1 dl Mandeldrink"," 80 g Kokosbl\u00fctenzucker"," 10 g Agar-Agar (Morga)"," 1 TL Vanillepaste"," 250 g Himbeeren"],"recipeInstructions":["\n \n Boden\n \n Alle Zutaten im Cutter oder Mixglas p\u00fcrieren, bis eine klebrige Masse entsteht. Die Masse in die vorbereitete Springform geben und mit den H\u00e4nden flachdr\u00fccken, dabei einen ca. 2 cm hohen Rand formen.\n \n \n F\u00fcllung\n \n Den hart gewordenen Teil der Kokosmilch (Kokoscreme) absch\u00f6pfen. Schokolade mit dem Kokos\u00f6l im Wasserbad schmelzen, gut verr\u00fchren. Mandeldrink mit Zucker und Agar-Agar unter R\u00fchren aufkochen, mit der Kokoscreme, der Schokolade und der Vanillepaste gut verr\u00fchren. F\u00fcllung auf den Kuchenboden giessen. Kuchen zugedeckt ca. 4 Std. k\u00fchl stellen.\n \n \n Beeren\n \n Den Kuchen vor dem Servieren mit den Himbeeren verzieren.\n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/18323\/sommerlicher-himbeerkuchen","nutrition":[],"dateModified":"2021-04-17T13:02:08+0000","dateCreated":"2021-04-17T13:02:08+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Sommerlicher Himbeerkuchen/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sommerlicher Himbeerkuchen/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sommerlicher Himbeerkuchen/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sommerlicher Himbeerkuchen/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/recipe.json: -------------------------------------------------------------------------------- 1 | {"@context":"http:\/\/schema.org","@type":"Recipe","mainEntityOfPage":"https:\/\/www.allrecipes.com\/recipe\/276110\/sweet-and-spicy-baked-keto-chicken-wings\/","datePublished":"2019-10-16T23:43:05.000Z","recipeCuisine":[],"author":[{"@type":"Person","name":"SunnyDaysNora"}],"aggregateRating":{"@type":"AggregateRating","ratingValue":5,"ratingCount":3,"itemReviewed":"Sweet and Spicy Baked Keto Chicken Wings","bestRating":"5","worstRating":"1"},"nutrition":{"@type":"NutritionInformation","calories":"670.1 calories","carbohydrateContent":"15.1 g","cholesterolContent":"137.6 mg","fatContent":"55.8 g","fiberContent":"0.1 g","proteinContent":"24.8 g","saturatedFatContent":"22.3 g","sodiumContent":"1822.2 mg","sugarContent":"13.4 g"},"review":[{"@type":"Review","datePublished":"2020-09-26T13:05:32.147Z","reviewBody":"Delicious! I used Erythritol Sweetener, Frank's Hot Sauce and unsalted butter.","reviewRating":{"@type":"Rating","worstRating":"1","bestRating":"5","ratingValue":5},"author":{"@type":"Person","name":"jalbanese","image":null,"sameAs":"https:\/\/www.allrecipes.com\/cook\/10729683\/"}},{"@type":"Review","datePublished":"2020-02-02T03:48:54.84Z","reviewBody":"This was so easy and I loved it. Unfortunately I am not Keto so I did not have Splenda; I used sugar. I will need to try Splenda the next. I used Frank Original Red Hot Sauce.","reviewRating":{"@type":"Rating","worstRating":"1","bestRating":"5","ratingValue":5},"author":{"@type":"Person","name":"Sydney","image":null,"sameAs":"https:\/\/www.allrecipes.com\/cook\/8837556\/"}}],"dateCreated":"2021-01-30T14:58:58+0000","printImage":false,"id":908736,"name":"Sweet and Spicy Baked Keto Chicken WingsNEW","imageUrl":"\/apps\/cookbook\/recipes\/908736\/image?size=full","recipeCategory":"Appetizer","description":"My family loves these low-carb and keto-friendly baked chicken wings! They're a staple around our house during football season. I love to serve them with blue cheese dressing for dipping. Updated! AGAINN!! And again!","recipeIngredient":["aluminum foilll","cooking spray","\u00bd cup butter","\u00be cup hot pepper sauce (such as Valentina\u00ae)","\u00bc cup sucralose sugar substitute (such as Splenda\u00ae)","\u00bc teaspoon salt","\u00bc teaspoon garlic powder","3 pounds chicken wing pieces, drumettes and flats","\u00bd cup blue cheese salad dressing"],"recipeInstructions":["Melt TO MUCH butter in a small saucepan. Mix in hot sauce, sucralose, salt, and garlic powder. Remove from heat and set sauce aside.\n","Bake in the preheated oven for 15 minutes. Remove from the oven, pour off any juices accumulated in the bottom of the pan, and turn chicken pieces over. Bake for an additional 15 minutes.\n","Preheat the oven to 425 degrees F (220 degrees C). \n\nLine a rimmed baking pan with foil. Spray a wire rack with cooking spray and set inside the baking pan.","Remove chicken from the oven and transfer to a large bowl. Cover with sauce and toss to coat. Return chicken to the rack on the baking pan and bake until no longer pink at the bone and the juices run clear, 15 to 30 minutes. An instant-read thermometer inserted near the bone should read 165 degrees F (74 degrees C).\n","Don't Worry be Happy :D","Don't forget to fart! Much"],"tool":[],"recipeYield":4,"prepTime":"PT0H5M","cookTime":"PT0H45M","totalTime":"PT1H50M","keywords":"","image":"https:\/\/imagesvc.meredithcorp.io\/v3\/mm\/image?url=https%3A%2F%2Fimages.media-allrecipes.com%2Fuserphotos%2F7067746.jpg","url":"https:\/\/www.allrecipes.com\/recipe\/276110\/sweet-and-spicy-baked-keto-chicken-wings\/","dateModified":"2021-05-24T15:12:32+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Sweet and Spicy Baked Keto Chicken WingsNEW/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/The Best Baked Ziti/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/The Best Baked Ziti/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/The Best Baked Ziti/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/The Best Baked Ziti/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/The Best Baked Ziti/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/The Best Baked Ziti/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Vegi-Tortillas/full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Vegi-Tortillas/full.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Vegi-Tortillas/recipe.json: -------------------------------------------------------------------------------- 1 | {"name":"Vegi-Tortillas","image":"https:\/\/recipecontent.fooby.ch\/14222_3-2_480-320.jpg","recipeYield":8,"keywords":"Vorspeise","recipeIngredient":[" 400 g Rotkabis, fein gehobelt"," 1 Limette, nur Saft"," 1 EL Rohzucker"," 2 TL Sambal Oelek"," \u00bd TL Salz"," 16 Mini-Tortillas"," 8 EL Guacamole "," 1 Dose rote Indianerbohnen (ca. 290 g), abgesp\u00fclt, abgetropft"," 60 g eingelegte Jalape\u00f1os in Scheiben"," 200 g Manchego, grob gerieben"," 200 g Cr\u00e8me fra\u00eeche"," 1 Limette, heiss abgesp\u00fclt, trocken getupft, in Schnitzen"," wenig Pfeffer"," wenig Fleur de Sel"],"recipeInstructions":["\n \n Rotkabis\n \n Kabis mit Limettensaft, Rohzucker, Sambal Oelek und Salz mischen., zugedeckt ca. 30 Min. ziehen lassen.\n \n \n Tortillas\n \n Tortillas mit je \u00bd EL Guacamole bestreichen, auf zwei mit Backpapier belegte Bleche legen. Bohnen, Jalape\u00f1os und K\u00e4se darauf verteilen.\n \n \n Backen\n \n Pro Blech ca. 3 Min. in der oberen H\u00e4lfte des auf 220 \u00b0C vorgeheizten Ofens. Rotkabis und Cr\u00e8me fra\u00eeche darauf verteilen, mit Limettensaft betr\u00e4ufeln, w\u00fcrzen und umschlagen. \n \n \n "],"@context":"http:\/\/schema.org","@type":"Recipe","recipeCategory":"","tool":[],"description":"","url":"https:\/\/fooby.ch\/de\/rezepte\/14222\/vegi-tortillas?startAuto1=0","nutrition":[],"dateModified":"2021-04-17T12:25:01+0000","dateCreated":"2021-04-17T12:25:01+0000"} -------------------------------------------------------------------------------- /docker/data/Recipes/Vegi-Tortillas/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Vegi-Tortillas/thumb.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/Vegi-Tortillas/thumb16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/docker/data/Recipes/Vegi-Tortillas/thumb16.jpg -------------------------------------------------------------------------------- /docker/data/Recipes/problem/Readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/data/Recipes/problem/recipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 128051, 3 | "name": "Test-Recipe", 4 | "description": "", 5 | "url": "", 6 | "image": "", 7 | "prepTime": "", 8 | "cookTime": "", 9 | "totalTime": "", 10 | "recipeCategory": "", 11 | "keywords": "", 12 | "recipeYield": 1, 13 | "tool": [], 14 | "recipeIngredient": [ 15 | "Ingredient 1", 16 | "", 17 | "Ingredient 3" 18 | ], 19 | "recipeInstructions": [ 20 | "Step 1", 21 | "", 22 | "Step 3" 23 | ], 24 | "nutrition": [], 25 | "@context": "http:\/\/schema.org", 26 | "@type": "Recipe", 27 | "dateModified": "2021-10-30T16:02:47+0000", 28 | "dateCreated": "2021-10-30T16:02:22+0000", 29 | "printImage": false, 30 | "imageUrl": "\/index.php\/apps\/cookbook\/recipes\/128051\/image?size=full" 31 | } 32 | -------------------------------------------------------------------------------- /docker/setup_library: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # Setup Cookbook Library 4 | rm -R /config/www/nextcloud/data/admin/files 5 | cp -R /config/www/nextcloud/data/preset /config/www/nextcloud/data/admin/files 6 | chown abc:users -R /config/www/nextcloud/data/admin/files/ 7 | occ files:scan admin -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | - Further Fixes for new cookbook plugin version 2 | - Added a lot initial translations 3 | 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | - Fixing wrong Cooking Duration in case of duration > 24 hours 2 | - Added two new languages -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | - Editing Recipes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/17.txt: -------------------------------------------------------------------------------- 1 | - Importing Recipes from URL now possible 2 | - Fixed a bug that caused Recipe Edit to be broken for Recipes with no cooking times -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/18.txt: -------------------------------------------------------------------------------- 1 | - Recipe Creation was added 2 | - Recipe Search overhauled 3 | - Dark mode (Thanks to SeineEloquenz) -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/19.txt: -------------------------------------------------------------------------------- 1 | - Timers added 2 | - Settings (Darkmode, StayAwake, Language) 3 | - Better support for tablets -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | - Improved Caching for better performance 2 | - Added nutrition to recipe view 3 | - URL Import 4 | - Small Improvements -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | - Bugfixes 2 | - Markdown Support in Description field 3 | - Category Autocomplete -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/22.txt: -------------------------------------------------------------------------------- 1 | - Bugfixes 2 | - Support for API 1.0 3 | - Added Languages -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/23.txt: -------------------------------------------------------------------------------- 1 | - Bugfixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/24.txt: -------------------------------------------------------------------------------- 1 | - API Update 2 | - Bugfixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Nextcloud Cookbook Mobile Client written in Flutter 2 | This project aims to provide a mobile client for both Android and IOs for the Nextcloud Cookbook App (https://github.com/nextcloud/cookbook) 3 | 4 | It works best with an Nextcloud installation >= 19 and a Cookbook plugin version 0.7.9 5 | 6 | Roadmap 7 | 8 | Current Features: 9 | 10 | View all recipes by Category 11 | Search Recipes by Name 12 | Recipe Creating 13 | Recipe Editing 14 | Recipe Import 15 | Darkmode (Thanks to SeineEloquenz) 16 | Timer (Thanks to fab920) 17 | Stay awake on recipe screen 18 | Settings Tab 19 | 20 | 21 | Planned Features: 22 | 23 | Offline Usage (Caching) 24 | Image Upload 25 | Integrate new values of nextcloud plugin * 26 | 27 | * Currently worked on! 28 | 29 | Translation 30 | To help the translation of the app visit: https://www.transifex.com/nextcloud/nextcloud/cookbook_flutter/ -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/11.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/12.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Nextcloud Cookbook Mobile Client written in Flutter -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Nextcloud Cookbook -------------------------------------------------------------------------------- /flutter_launcher_icons.yaml: -------------------------------------------------------------------------------- 1 | flutter_icons: 2 | android: "launcher_icon" 3 | ios: true 4 | remove_alpha_ios: true 5 | image_path: "assets/launcher/icon.png" 6 | adaptive_icon_background: "assets/launcher/adaptive_background.png" 7 | adaptive_icon_foreground: "assets/launcher/adaptive_foreground.png" 8 | -------------------------------------------------------------------------------- /flutter_native_splash.yaml: -------------------------------------------------------------------------------- 1 | flutter_native_splash: 2 | color: "#fcfcff" 3 | color_dark: "#1a1c1e" 4 | #image: assets/splash_icon.png 5 | android_12: 6 | #image: assets/splash_icon_android_12.png 7 | icon_background_color: "#fcfcff" 8 | icon_background_color_dark: "#1a1c1e" 9 | ios: false 10 | web: false 11 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | if (@available(iOS 10.0, *)) { 11 | [UNUserNotificationCenter currentNotificationCenter].delegate = (id) self; 12 | } 13 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teifun2/nextcloud-cookbook-flutter/0e290f4a93184e9090672e6af495851148264938/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Nextcloud Cookbook 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | nextcloud_cookbook_flutter 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/blocs/authentication/authentication_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 5 | 6 | part 'authentication_event.dart'; 7 | part 'authentication_state.dart'; 8 | 9 | class AuthenticationBloc 10 | extends Bloc { 11 | AuthenticationBloc() : super(AuthenticationState()) { 12 | on(_mapAppStartedEventToState); 13 | on(_mapLoggedInEventToState); 14 | on(_mapLoggedOutEventToState); 15 | } 16 | final UserRepository userRepository = UserRepository(); 17 | 18 | Future _mapAppStartedEventToState( 19 | AppStarted event, 20 | Emitter emit, 21 | ) async { 22 | final hasToken = await userRepository.hasAppAuthentication(); 23 | 24 | if (hasToken) { 25 | await userRepository.loadAppAuthentication(); 26 | try { 27 | final validCredentials = await userRepository.checkAppAuthentication(); 28 | 29 | if (validCredentials) { 30 | emit(AuthenticationState(status: AuthenticationStatus.authenticated)); 31 | } else { 32 | await userRepository.deleteAppAuthentication(); 33 | emit(AuthenticationState(status: AuthenticationStatus.invalid)); 34 | } 35 | } catch (e) { 36 | emit( 37 | AuthenticationState( 38 | status: AuthenticationStatus.error, 39 | error: e.toString(), 40 | ), 41 | ); 42 | } 43 | } else { 44 | emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); 45 | } 46 | } 47 | 48 | Future _mapLoggedInEventToState( 49 | LoggedIn event, 50 | Emitter emit, 51 | ) async { 52 | emit(AuthenticationState()); 53 | await userRepository.persistAppAuthentication(event.appAuthentication); 54 | emit(AuthenticationState(status: AuthenticationStatus.authenticated)); 55 | } 56 | 57 | Future _mapLoggedOutEventToState( 58 | LoggedOut event, 59 | Emitter emit, 60 | ) async { 61 | emit(AuthenticationState()); 62 | await userRepository.deleteAppAuthentication(); 63 | emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/blocs/authentication/authentication_event.dart: -------------------------------------------------------------------------------- 1 | part of 'authentication_bloc.dart'; 2 | 3 | abstract class AuthenticationEvent extends Equatable { 4 | const AuthenticationEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class AppStarted extends AuthenticationEvent { 11 | const AppStarted(); 12 | } 13 | 14 | class LoggedIn extends AuthenticationEvent { 15 | const LoggedIn({required this.appAuthentication}); 16 | final AppAuthentication appAuthentication; 17 | 18 | @override 19 | List get props => [appAuthentication]; 20 | 21 | @override 22 | String toString() => appAuthentication.toString(); 23 | } 24 | 25 | class LoggedOut extends AuthenticationEvent { 26 | const LoggedOut(); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/blocs/authentication/authentication_state.dart: -------------------------------------------------------------------------------- 1 | part of 'authentication_bloc.dart'; 2 | 3 | enum AuthenticationStatus { 4 | /// The user has not been authenticated 5 | unauthenticated, 6 | 7 | /// The user has been authenticated 8 | authenticated, 9 | 10 | /// The provided authentication is invalid 11 | invalid, 12 | 13 | /// Loading 14 | /// 15 | /// Can either: 16 | /// - retrive saved authentication 17 | /// - log in witht he provided authentication 18 | loading, 19 | 20 | /// An error accured while authenticating 21 | error; 22 | } 23 | 24 | class AuthenticationState extends Equatable { 25 | const AuthenticationState({ 26 | this.status = AuthenticationStatus.loading, 27 | this.error, 28 | }) : assert( 29 | (status != AuthenticationStatus.error && error == null) || 30 | (status == AuthenticationStatus.error && error != null), 31 | ); 32 | final AuthenticationStatus status; 33 | final String? error; 34 | 35 | @override 36 | List get props => [status, error]; 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/blocs/categories/categories_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 5 | 6 | part 'categories_event.dart'; 7 | part 'categories_state.dart'; 8 | 9 | class CategoriesBloc extends Bloc { 10 | CategoriesBloc() : super(CategoriesState()) { 11 | on(_mapCategoriesLoadedEventToState); 12 | } 13 | final DataRepository dataRepository = DataRepository(); 14 | 15 | Future _mapCategoriesLoadedEventToState( 16 | CategoriesLoaded event, 17 | Emitter emit, 18 | ) async { 19 | try { 20 | final categories = await dataRepository.fetchCategories(); 21 | emit( 22 | CategoriesState( 23 | status: CategoriesStatus.loadSuccess, 24 | categories: categories, 25 | ), 26 | ); 27 | final recipes = await dataRepository.fetchCategoryMainRecipes(categories); 28 | emit( 29 | CategoriesState( 30 | status: CategoriesStatus.imageLoadSuccess, 31 | categories: categories, 32 | recipes: recipes, 33 | ), 34 | ); 35 | } on Exception catch (e) { 36 | emit( 37 | CategoriesState( 38 | status: CategoriesStatus.loadFailure, 39 | error: e.toString(), 40 | ), 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/blocs/categories/categories_event.dart: -------------------------------------------------------------------------------- 1 | part of 'categories_bloc.dart'; 2 | 3 | abstract class CategoriesEvent extends Equatable { 4 | const CategoriesEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class CategoriesLoaded extends CategoriesEvent { 11 | const CategoriesLoaded(); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/blocs/categories/categories_state.dart: -------------------------------------------------------------------------------- 1 | part of 'categories_bloc.dart'; 2 | 3 | enum CategoriesStatus { 4 | loadInProgress, 5 | loadFailure, 6 | loadSuccess, 7 | imageLoadSuccess; 8 | } 9 | 10 | class CategoriesState extends Equatable { 11 | CategoriesState({ 12 | this.status = CategoriesStatus.loadInProgress, 13 | this.error, 14 | this.categories, 15 | this.recipes, 16 | }) { 17 | switch (status) { 18 | case CategoriesStatus.loadInProgress: 19 | assert(error == null && categories == null && recipes == null); 20 | break; 21 | case CategoriesStatus.loadSuccess: 22 | assert(error == null && categories != null && recipes == null); 23 | break; 24 | case CategoriesStatus.imageLoadSuccess: 25 | assert(error == null && categories != null && recipes != null); 26 | break; 27 | case CategoriesStatus.loadFailure: 28 | assert(error != null && categories == null && recipes == null); 29 | } 30 | } 31 | final CategoriesStatus status; 32 | final String? error; 33 | final Iterable? categories; 34 | final Iterable? recipes; 35 | 36 | @override 37 | List get props => [status, error, categories]; 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/blocs/login/login_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; 7 | 8 | part 'login_event.dart'; 9 | part 'login_state.dart'; 10 | 11 | class LoginBloc extends Bloc { 12 | LoginBloc({ 13 | required this.authenticationBloc, 14 | }) : super(LoginState()) { 15 | on(_mapLoginButtonPressedEventToState); 16 | } 17 | final UserRepository userRepository = UserRepository(); 18 | final AuthenticationBloc authenticationBloc; 19 | 20 | Future _mapLoginButtonPressedEventToState( 21 | LoginButtonPressed event, 22 | Emitter emit, 23 | ) async { 24 | emit(LoginState(status: LoginStatus.loading)); 25 | 26 | try { 27 | AppAuthentication appAuthentication; 28 | assert(URLUtils.isSanitized(event.serverURL)); 29 | 30 | if (!event.isAppPassword) { 31 | appAuthentication = await userRepository.authenticate( 32 | event.serverURL, 33 | event.username, 34 | event.originalBasicAuth, 35 | isSelfSignedCertificate: event.isSelfSignedCertificate, 36 | ); 37 | } else { 38 | appAuthentication = await userRepository.authenticateAppPassword( 39 | event.serverURL, 40 | event.username, 41 | event.originalBasicAuth, 42 | isSelfSignedCertificate: event.isSelfSignedCertificate, 43 | ); 44 | } 45 | 46 | authenticationBloc.add(LoggedIn(appAuthentication: appAuthentication)); 47 | emit(LoginState()); 48 | } catch (error) { 49 | emit( 50 | LoginState( 51 | status: LoginStatus.failure, 52 | error: error.toString(), 53 | ), 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/blocs/login/login_event.dart: -------------------------------------------------------------------------------- 1 | part of 'login_bloc.dart'; 2 | 3 | abstract class LoginEvent extends Equatable { 4 | const LoginEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class LoginButtonPressed extends LoginEvent { 11 | const LoginButtonPressed({ 12 | required this.serverURL, 13 | required this.username, 14 | required this.originalBasicAuth, 15 | required this.isAppPassword, 16 | required this.isSelfSignedCertificate, 17 | }); 18 | final String serverURL; 19 | final String username; 20 | final String originalBasicAuth; 21 | final bool isAppPassword; 22 | final bool isSelfSignedCertificate; 23 | 24 | @override 25 | List get props => 26 | [serverURL, username, isAppPassword, isSelfSignedCertificate]; 27 | 28 | @override 29 | String toString() => 30 | 'LoginButtonPressed {serverURL: $serverURL, username: $username, isAppPassword: $isAppPassword}, isSelfSignedCertificate: $isSelfSignedCertificate'; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/blocs/login/login_state.dart: -------------------------------------------------------------------------------- 1 | part of 'login_bloc.dart'; 2 | 3 | enum LoginStatus { 4 | initial, 5 | loading, 6 | failure; 7 | } 8 | 9 | class LoginState extends Equatable { 10 | const LoginState({ 11 | this.status = LoginStatus.initial, 12 | this.error, 13 | }) : assert( 14 | (status != LoginStatus.failure && error == null) || 15 | (status == LoginStatus.failure && error != null), 16 | ); 17 | final LoginStatus status; 18 | final String? error; 19 | 20 | @override 21 | List get props => [status, error]; 22 | 23 | @override 24 | String toString() => status == LoginStatus.failure 25 | ? 'LoginFailure { error: $error }' 26 | : 'Instance of LoginState'; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/blocs/recipe/recipe_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 5 | 6 | part 'recipe_event.dart'; 7 | part 'recipe_state.dart'; 8 | 9 | class RecipeBloc extends Bloc { 10 | RecipeBloc() : super(RecipeState()) { 11 | on(_mapRecipeLoadedToState); 12 | on(_mapRecipeUpdatedToState); 13 | on(_mapRecipeImportedToState); 14 | on(_mapRecipeCreatedToState); 15 | on(_mapRecipeDeletedToState); 16 | } 17 | final DataRepository dataRepository = DataRepository(); 18 | 19 | Future _mapRecipeLoadedToState( 20 | RecipeLoaded recipeLoaded, 21 | Emitter emit, 22 | ) async { 23 | try { 24 | final recipe = await dataRepository.fetchRecipe(recipeLoaded.recipeId); 25 | emit(RecipeState(status: RecipeStatus.loadSuccess, recipe: recipe)); 26 | } catch (e) { 27 | emit(RecipeState(status: RecipeStatus.loadFailure, error: e.toString())); 28 | } 29 | } 30 | 31 | Future _mapRecipeUpdatedToState( 32 | RecipeUpdated recipeUpdated, 33 | Emitter emit, 34 | ) async { 35 | try { 36 | emit(RecipeState(status: RecipeStatus.updateInProgress)); 37 | final recipeId = await dataRepository.updateRecipe(recipeUpdated.recipe); 38 | emit(RecipeState(status: RecipeStatus.updateSuccess, recipeId: recipeId)); 39 | } catch (e) { 40 | emit( 41 | RecipeState( 42 | status: RecipeStatus.updateFailure, 43 | error: e.toString(), 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | Future _mapRecipeCreatedToState( 50 | RecipeCreated recipeCreated, 51 | Emitter emit, 52 | ) async { 53 | try { 54 | emit(RecipeState(status: RecipeStatus.createInProgress)); 55 | final recipeId = await dataRepository.createRecipe(recipeCreated.recipe); 56 | emit(RecipeState(status: RecipeStatus.createSuccess, recipeId: recipeId)); 57 | } catch (e) { 58 | emit( 59 | RecipeState( 60 | status: RecipeStatus.createFailure, 61 | error: e.toString(), 62 | ), 63 | ); 64 | } 65 | } 66 | 67 | Future _mapRecipeImportedToState( 68 | RecipeImported recipeImported, 69 | Emitter emit, 70 | ) async { 71 | try { 72 | emit(RecipeState(status: RecipeStatus.importInProgress)); 73 | final recipe = await dataRepository.importRecipe(recipeImported.url); 74 | emit(RecipeState(status: RecipeStatus.importSuccess, recipe: recipe)); 75 | } catch (e) { 76 | emit( 77 | RecipeState( 78 | status: RecipeStatus.importFailure, 79 | error: e.toString(), 80 | ), 81 | ); 82 | } 83 | } 84 | 85 | Future _mapRecipeDeletedToState( 86 | RecipeDeleted recipeDeleted, 87 | Emitter emit, 88 | ) async { 89 | try { 90 | emit(RecipeState(status: RecipeStatus.deleteInProgress)); 91 | await dataRepository.deleteRecipe(recipeDeleted.recipe); 92 | emit(RecipeState(status: RecipeStatus.delteSuccess)); 93 | } catch (e) { 94 | emit( 95 | RecipeState( 96 | status: RecipeStatus.deleteFailure, 97 | error: e.toString(), 98 | ), 99 | ); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/blocs/recipe/recipe_event.dart: -------------------------------------------------------------------------------- 1 | part of 'recipe_bloc.dart'; 2 | 3 | abstract class RecipeEvent extends Equatable { 4 | const RecipeEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class RecipeLoaded extends RecipeEvent { 11 | const RecipeLoaded(this.recipeId); 12 | final String recipeId; 13 | 14 | @override 15 | List get props => [recipeId]; 16 | } 17 | 18 | class RecipeUpdated extends RecipeEvent { 19 | const RecipeUpdated(this.recipe); 20 | final Recipe recipe; 21 | 22 | @override 23 | List get props => [recipe]; 24 | } 25 | 26 | class RecipeCreated extends RecipeEvent { 27 | const RecipeCreated(this.recipe); 28 | final Recipe recipe; 29 | 30 | @override 31 | List get props => [recipe]; 32 | } 33 | 34 | class RecipeImported extends RecipeEvent { 35 | const RecipeImported(this.url); 36 | final String url; 37 | 38 | @override 39 | List get props => [url]; 40 | } 41 | 42 | class RecipeDeleted extends RecipeEvent { 43 | const RecipeDeleted(this.recipe); 44 | final Recipe recipe; 45 | 46 | @override 47 | List get props => [recipe]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/blocs/recipe/recipe_state.dart: -------------------------------------------------------------------------------- 1 | part of 'recipe_bloc.dart'; 2 | 3 | enum RecipeStatus { 4 | loadSuccess, 5 | loadFailure, 6 | loadInProgress, 7 | updateFailure, 8 | updateInProgress, 9 | updateSuccess, 10 | createFailure, 11 | createSuccess, 12 | createInProgress, 13 | deleteInProgress, 14 | deleteFailure, 15 | delteSuccess, 16 | importSuccess, 17 | importFailure, 18 | importInProgress; 19 | } 20 | 21 | class RecipeState extends Equatable { 22 | RecipeState({ 23 | this.status = RecipeStatus.loadInProgress, 24 | this.error, 25 | this.recipe, 26 | this.recipeId, 27 | }) { 28 | switch (status) { 29 | case RecipeStatus.loadInProgress: 30 | case RecipeStatus.updateInProgress: 31 | case RecipeStatus.createInProgress: 32 | case RecipeStatus.importInProgress: 33 | case RecipeStatus.deleteInProgress: 34 | case RecipeStatus.delteSuccess: 35 | assert(error == null && recipe == null && recipeId == null); 36 | break; 37 | case RecipeStatus.createSuccess: 38 | case RecipeStatus.updateSuccess: 39 | assert(error == null && recipe == null && recipeId != null); 40 | break; 41 | case RecipeStatus.loadSuccess: 42 | case RecipeStatus.importSuccess: 43 | assert(error == null && recipe != null && recipeId == null); 44 | break; 45 | case RecipeStatus.deleteFailure: 46 | case RecipeStatus.loadFailure: 47 | case RecipeStatus.updateFailure: 48 | case RecipeStatus.createFailure: 49 | case RecipeStatus.importFailure: 50 | assert(error != null && recipe == null && recipeId == null); 51 | } 52 | } 53 | final RecipeStatus status; 54 | final String? error; 55 | final Recipe? recipe; 56 | final String? recipeId; 57 | 58 | @override 59 | List get props => [status, error, recipe, recipeId]; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/blocs/recipes_short/recipes_short_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 5 | 6 | part 'recipes_short_event.dart'; 7 | part 'recipes_short_state.dart'; 8 | 9 | class RecipesShortBloc extends Bloc { 10 | RecipesShortBloc() : super(RecipesShortState()) { 11 | on(_mapRecipesShortLoadedToState); 12 | on(_mapRecipesShortLoadedAllToState); 13 | } 14 | final DataRepository dataRepository = DataRepository(); 15 | 16 | Future _mapRecipesShortLoadedToState( 17 | RecipesShortLoaded recipesShortLoaded, 18 | Emitter emit, 19 | ) async { 20 | try { 21 | final recipesShort = await dataRepository.fetchRecipesShort( 22 | category: recipesShortLoaded.category, 23 | ); 24 | emit( 25 | RecipesShortState( 26 | status: RecipesShortStatus.loadSuccess, 27 | recipesShort: recipesShort, 28 | ), 29 | ); 30 | } catch (_) { 31 | emit( 32 | RecipesShortState( 33 | status: RecipesShortStatus.loadFailure, 34 | error: '', 35 | ), 36 | ); 37 | } 38 | } 39 | 40 | Future _mapRecipesShortLoadedAllToState( 41 | RecipesShortLoadedAll recipesShortLoadedAll, 42 | Emitter emit, 43 | ) async { 44 | try { 45 | emit( 46 | RecipesShortState( 47 | status: RecipesShortStatus.loadAllInProgress, 48 | ), 49 | ); 50 | final recipesShort = await dataRepository.fetchAllRecipes(); 51 | emit( 52 | RecipesShortState( 53 | status: RecipesShortStatus.loadAllSuccess, 54 | recipesShort: recipesShort, 55 | ), 56 | ); 57 | } catch (e) { 58 | emit( 59 | RecipesShortState( 60 | status: RecipesShortStatus.loadAllFailure, 61 | error: '', 62 | ), 63 | ); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/blocs/recipes_short/recipes_short_event.dart: -------------------------------------------------------------------------------- 1 | part of 'recipes_short_bloc.dart'; 2 | 3 | abstract class RecipesShortEvent extends Equatable { 4 | const RecipesShortEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class RecipesShortLoaded extends RecipesShortEvent { 11 | const RecipesShortLoaded({required this.category}); 12 | final String category; 13 | 14 | @override 15 | List get props => [category]; 16 | } 17 | 18 | class RecipesShortLoadedAll extends RecipesShortEvent {} 19 | -------------------------------------------------------------------------------- /lib/src/blocs/recipes_short/recipes_short_state.dart: -------------------------------------------------------------------------------- 1 | part of 'recipes_short_bloc.dart'; 2 | 3 | enum RecipesShortStatus { 4 | loadInProgress, 5 | loadFailure, 6 | loadSuccess, 7 | loadAllSuccess, 8 | loadAllFailure, 9 | loadAllInProgress; 10 | } 11 | 12 | class RecipesShortState extends Equatable { 13 | RecipesShortState({ 14 | this.status = RecipesShortStatus.loadInProgress, 15 | this.error, 16 | this.recipesShort, 17 | }) { 18 | switch (status) { 19 | case RecipesShortStatus.loadInProgress: 20 | case RecipesShortStatus.loadAllInProgress: 21 | assert(error == null, recipesShort == null); 22 | break; 23 | case RecipesShortStatus.loadAllSuccess: 24 | case RecipesShortStatus.loadSuccess: 25 | assert(error == null, recipesShort != null); 26 | break; 27 | case RecipesShortStatus.loadAllFailure: 28 | case RecipesShortStatus.loadFailure: 29 | assert(error != null, recipesShort == null); 30 | break; 31 | } 32 | } 33 | final RecipesShortStatus status; 34 | final String? error; 35 | final Iterable? recipesShort; 36 | 37 | @override 38 | List get props => [status, error, recipesShort]; 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/blocs/simple_bloc_delegatae.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | class SimpleBlocDelegate extends BlocObserver { 5 | @override 6 | void onEvent(Bloc bloc, Object? event) { 7 | debugPrint(event?.toString()); 8 | super.onEvent(bloc, event); 9 | } 10 | 11 | @override 12 | void onTransition(Bloc bloc, Transition transition) { 13 | debugPrint(transition.toString()); 14 | super.onTransition(bloc, transition); 15 | } 16 | 17 | @override 18 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 19 | debugPrint(error.toString()); 20 | super.onError(bloc, error, stackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/models/animated_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 4 | 5 | typedef RemovedItemBuilder = Widget Function( 6 | T item, 7 | BuildContext context, 8 | Animation animation, 9 | ); 10 | 11 | abstract class AnimatedListModel { 12 | AnimatedListModel({ 13 | required this.listKey, 14 | required this.removedItemBuilder, 15 | Iterable? initialItems, 16 | }) : _items = List.from(initialItems ?? []); 17 | 18 | final GlobalKey listKey; 19 | final RemovedItemBuilder removedItemBuilder; 20 | final List _items; 21 | 22 | SliverAnimatedListState get _animatedList => listKey.currentState!; 23 | 24 | void insert(int index, E item) { 25 | _items.insert(index, item); 26 | _animatedList.insertItem(index); 27 | } 28 | 29 | E removeAt(int index) { 30 | final removedItem = _items.removeAt(index); 31 | if (removedItem != null) { 32 | _animatedList.removeItem( 33 | index, 34 | (context, animation) => 35 | removedItemBuilder(removedItem, context, animation), 36 | ); 37 | } 38 | return removedItem; 39 | } 40 | 41 | Future removeAll() async { 42 | while (_items.isNotEmpty) { 43 | removeAt(0); 44 | } 45 | } 46 | 47 | void add(E item) { 48 | insert(_items.length, item); 49 | } 50 | 51 | void insertAll(int index, Iterable items) { 52 | for (var i = 0; i < items.length; i++) { 53 | insert(index + i, items.elementAt(i)); 54 | } 55 | } 56 | 57 | bool get isNotEmpty => _items.isNotEmpty; 58 | 59 | int get length => _items.length; 60 | 61 | E operator [](int index) => _items[index]; 62 | 63 | int indexOf(E item) => _items.indexOf(item); 64 | } 65 | 66 | class AnimatedTimerList extends AnimatedListModel { 67 | AnimatedTimerList({ 68 | required super.listKey, 69 | required super.removedItemBuilder, 70 | }) : super(initialItems: TimerList().timers); 71 | 72 | AnimatedTimerList.forId({ 73 | required String recipeId, 74 | required super.listKey, 75 | required super.removedItemBuilder, 76 | }) : super( 77 | initialItems: TimerList() 78 | .timers 79 | .where((element) => element.recipeId == recipeId), 80 | ); 81 | 82 | @override 83 | Timer removeAt(int index) { 84 | TimerList().remove(_items[index]); 85 | return super.removeAt(index); 86 | } 87 | 88 | @override 89 | void insert(int index, Timer item) { 90 | TimerList().add(item); 91 | super.insert(index, item); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/models/app_authentication.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio/io.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:json_annotation/json_annotation.dart'; 7 | 8 | part 'app_authentication.g.dart'; 9 | 10 | @JsonSerializable() 11 | class AppAuthentication extends Equatable { 12 | AppAuthentication({ 13 | required this.server, 14 | required this.loginName, 15 | required this.basicAuth, 16 | required this.isSelfSignedCertificate, 17 | }) { 18 | authenticatedClient.options 19 | ..headers['authorization'] = basicAuth 20 | ..headers['User-Agent'] = 'Cookbook App' 21 | ..responseType = ResponseType.plain; 22 | 23 | if (isSelfSignedCertificate) { 24 | authenticatedClient.httpClientAdapter = IOHttpClientAdapter( 25 | onHttpClientCreate: (client) { 26 | client.badCertificateCallback = (cert, host, port) => true; 27 | return client; 28 | }, 29 | ); 30 | } 31 | } 32 | 33 | factory AppAuthentication.fromJsonString(String jsonString) => 34 | AppAuthentication.fromJson( 35 | json.decode(jsonString) as Map, 36 | ); 37 | 38 | factory AppAuthentication.fromJson(Map jsonData) { 39 | try { 40 | return _$AppAuthenticationFromJson(jsonData); 41 | // ignore: avoid_catching_errors 42 | } on TypeError { 43 | final basicAuth = parseBasicAuth( 44 | jsonData['loginName'] as String, 45 | jsonData['appPassword'] as String, 46 | ); 47 | 48 | final selfSignedCertificate = 49 | jsonData['isSelfSignedCertificate'] as bool? ?? false; 50 | 51 | return AppAuthentication( 52 | server: jsonData['server'] as String, 53 | loginName: jsonData['loginName'] as String, 54 | basicAuth: basicAuth, 55 | isSelfSignedCertificate: selfSignedCertificate, 56 | ); 57 | } 58 | } 59 | final String server; 60 | final String loginName; 61 | final String basicAuth; 62 | final bool isSelfSignedCertificate; 63 | 64 | final Dio authenticatedClient = Dio(); 65 | 66 | String toJsonString() => json.encode(toJson()); 67 | Map toJson() => _$AppAuthenticationToJson(this); 68 | 69 | String get password { 70 | final base64 = basicAuth.substring(6); 71 | final string = utf8.decode(base64Decode(base64)); 72 | final auth = string.split(':'); 73 | return auth[1]; 74 | } 75 | 76 | static String parseBasicAuth(String loginName, String appPassword) => 77 | 'Basic ${base64Encode(utf8.encode('$loginName:$appPassword'))}'; 78 | 79 | @override 80 | String toString() => 81 | 'LoggedIn { token: $server, $loginName, isSelfSignedCertificate $isSelfSignedCertificate}'; 82 | 83 | @override 84 | List get props => [ 85 | server, 86 | loginName, 87 | basicAuth, 88 | isSelfSignedCertificate, 89 | ]; 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/models/app_authentication.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_authentication.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AppAuthentication _$AppAuthenticationFromJson(Map json) => 10 | AppAuthentication( 11 | server: json['server'] as String, 12 | loginName: json['loginName'] as String, 13 | basicAuth: json['basicAuth'] as String, 14 | isSelfSignedCertificate: json['isSelfSignedCertificate'] as bool, 15 | ); 16 | 17 | Map _$AppAuthenticationToJson(AppAuthentication instance) => 18 | { 19 | 'server': instance.server, 20 | 'loginName': instance.loginName, 21 | 'basicAuth': instance.basicAuth, 22 | 'isSelfSignedCertificate': instance.isSelfSignedCertificate, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/src/models/image_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class ImageResponse { 4 | const ImageResponse({ 5 | required this.data, 6 | required this.isSvg, 7 | }); 8 | final Uint8List data; 9 | final bool isSvg; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/models/recipe.dart: -------------------------------------------------------------------------------- 1 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 2 | 3 | extension RecipeExtension on Recipe { 4 | Map get nutritionList { 5 | final items = {}; 6 | if (nutrition.calories != null) { 7 | items['calories'] = nutrition.calories!; 8 | } 9 | if (nutrition.carbohydrateContent != null) { 10 | items['carbohydrateContent'] = nutrition.carbohydrateContent!; 11 | } 12 | if (nutrition.cholesterolContent != null) { 13 | items['cholesterolContent'] = nutrition.cholesterolContent!; 14 | } 15 | if (nutrition.fatContent != null) { 16 | items['fatContent'] = nutrition.fatContent!; 17 | } 18 | if (nutrition.fiberContent != null) { 19 | items['fiberContent'] = nutrition.fiberContent!; 20 | } 21 | if (nutrition.proteinContent != null) { 22 | items['proteinContent'] = nutrition.proteinContent!; 23 | } 24 | if (nutrition.saturatedFatContent != null) { 25 | items['saturatedFatContent'] = nutrition.saturatedFatContent!; 26 | } 27 | if (nutrition.servingSize != null) { 28 | items['servingSize'] = nutrition.servingSize!; 29 | } 30 | if (nutrition.sodiumContent != null) { 31 | items['sodiumContent'] = nutrition.sodiumContent!; 32 | } 33 | if (nutrition.sugarContent != null) { 34 | items['sugarContent'] = nutrition.sugarContent!; 35 | } 36 | if (nutrition.transFatContent != null) { 37 | items['transFatContent'] = nutrition.transFatContent!; 38 | } 39 | if (nutrition.unsaturatedFatContent != null) { 40 | items['unsaturatedFatContent'] = nutrition.unsaturatedFatContent!; 41 | } 42 | 43 | return items; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/models/timer.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'timer.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Timer _$TimerFromJson(Map json) => Timer.restore( 10 | _recipeFromJson(json['recipe'] as String), 11 | DateTime.parse(json['done'] as String), 12 | json['id'] as int?, 13 | ); 14 | 15 | Map _$TimerToJson(Timer instance) => { 16 | 'recipe': _recipeToJson(instance.recipe), 17 | 'done': instance.done.toIso8601String(), 18 | 'id': instance.id, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/screens/loading_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; 5 | 6 | class LoadingErrorScreen extends StatelessWidget { 7 | const LoadingErrorScreen({required this.message, super.key}); 8 | 9 | final String message; 10 | 11 | @override 12 | Widget build(BuildContext context) => Scaffold( 13 | body: Center( 14 | child: Padding( 15 | padding: const EdgeInsets.all(8), 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Text(message), 20 | const SizedBox(height: 10), 21 | ElevatedButton( 22 | onPressed: () { 23 | BlocProvider.of(context) 24 | .add(const AppStarted()); 25 | }, 26 | child: Text(translate('login.retry')), 27 | ), 28 | const SizedBox(height: 10), 29 | ElevatedButton( 30 | onPressed: () { 31 | BlocProvider.of(context) 32 | .add(const LoggedOut()); 33 | }, 34 | child: Text(translate('login.reset')), 35 | ), 36 | ], 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/screens/login_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | 5 | import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; 7 | import 'package:nextcloud_cookbook_flutter/src/screens/form/login_form.dart'; 8 | import 'package:nextcloud_cookbook_flutter/src/util/theme_data.dart'; 9 | 10 | class LoginScreen extends StatelessWidget { 11 | const LoginScreen({ 12 | super.key, 13 | this.invalidCredentials = false, 14 | }); 15 | final bool invalidCredentials; 16 | 17 | @override 18 | Widget build(BuildContext context) => Scaffold( 19 | appBar: AppBar( 20 | title: Text(translate('login.title')), 21 | ), 22 | body: BlocProvider( 23 | create: (context) { 24 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 25 | notifyIfInvalidCredentials(context); 26 | }); 27 | return LoginBloc( 28 | authenticationBloc: BlocProvider.of(context), 29 | ); 30 | }, 31 | child: const LoginForm(), 32 | ), 33 | ); 34 | 35 | void notifyIfInvalidCredentials(BuildContext context) { 36 | if (invalidCredentials) { 37 | final theme = 38 | Theme.of(context).extension()!.errorSnackBar; 39 | ScaffoldMessenger.of(context).showSnackBar( 40 | SnackBar( 41 | content: Text( 42 | translate('login.errors.credentials_invalid'), 43 | style: theme.contentTextStyle, 44 | ), 45 | backgroundColor: theme.backgroundColor, 46 | ), 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/screens/my_settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 5 | import 'package:flutter_translate/flutter_translate.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; 7 | import 'package:nextcloud_cookbook_flutter/src/util/supported_locales.dart'; 8 | import 'package:theme_mode_handler/theme_mode_handler.dart'; 9 | 10 | class MySettingsScreen extends StatelessWidget { 11 | const MySettingsScreen({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) => SettingsScreen( 15 | title: translate('settings.title'), 16 | children: [ 17 | SwitchSettingsTile( 18 | title: translate('settings.stay_awake.title'), 19 | settingKey: SettingKeys.stay_awake.name, 20 | subtitle: translate('settings.stay_awake.subtitle'), 21 | ), 22 | DropDownSettingsTile( 23 | title: translate('settings.dark_mode.title'), 24 | settingKey: SettingKeys.dark_mode.name, 25 | values: { 26 | ThemeMode.system.toString(): 27 | translate('settings.dark_mode.system'), 28 | ThemeMode.dark.toString(): translate('settings.dark_mode.dark'), 29 | ThemeMode.light.toString(): translate('settings.dark_mode.light'), 30 | }, 31 | selected: ThemeModeHandler.of(context)!.themeMode.toString(), 32 | onChange: (value) async { 33 | final theme = ThemeMode.values.firstWhere( 34 | (v) => v.toString() == value, 35 | orElse: () => ThemeMode.system, 36 | ); 37 | await ThemeModeHandler.of(context)?.saveThemeMode(theme); 38 | }, 39 | ), 40 | DropDownSettingsTile( 41 | title: translate('settings.language.title'), 42 | settingKey: SettingKeys.language.name, 43 | selected: Settings.getValue( 44 | SettingKeys.language.name, 45 | defaultValue: 'default', 46 | )!, 47 | values: Map.from( 48 | { 49 | 'default': translate('settings.dark_mode.system'), 50 | }, 51 | )..addAll(SupportedLocales.locales), 52 | onChange: (value) async { 53 | if (value == 'default') { 54 | await changeLocale(context, Platform.localeName); 55 | } else { 56 | await changeLocale(context, value); 57 | } 58 | }, 59 | ) 60 | ], 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/screens/recipes_list_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 4 | import 'package:flutter_translate/flutter_translate.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/widget/recipe_list_item.dart'; 7 | 8 | class RecipesListScreen extends StatefulWidget { 9 | const RecipesListScreen({ 10 | required this.category, 11 | super.key, 12 | }); 13 | final String category; 14 | 15 | @override 16 | State createState() => _RecipesListScreenState(); 17 | } 18 | 19 | class _RecipesListScreenState extends State { 20 | Future refresh() async { 21 | await DefaultCacheManager().emptyCache(); 22 | // ignore: use_build_context_synchronously 23 | BlocProvider.of(context) 24 | .add(RecipesShortLoaded(category: widget.category)); 25 | } 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | BlocProvider.of(context) 32 | .add(RecipesShortLoaded(category: widget.category)); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) => Scaffold( 37 | appBar: AppBar( 38 | title: Text( 39 | translate( 40 | 'recipe_list.title_category', 41 | args: {'category': widget.category}, 42 | ), 43 | ), 44 | actions: [ 45 | // action button 46 | IconButton( 47 | icon: const Icon(Icons.refresh_outlined), 48 | tooltip: MaterialLocalizations.of(context) 49 | .refreshIndicatorSemanticLabel, 50 | onPressed: refresh, 51 | ), 52 | ], 53 | ), 54 | body: RefreshIndicator( 55 | onRefresh: refresh, 56 | child: BlocBuilder( 57 | builder: (context, recipesShortState) { 58 | if (recipesShortState.status == RecipesShortStatus.loadSuccess) { 59 | return Padding( 60 | padding: const EdgeInsets.all(8), 61 | child: ListView.separated( 62 | itemCount: recipesShortState.recipesShort!.length, 63 | itemBuilder: (context, index) => RecipeListItem( 64 | recipe: recipesShortState.recipesShort!.elementAt(index), 65 | ), 66 | separatorBuilder: (context, index) => const Divider(), 67 | ), 68 | ); 69 | } else { 70 | return const Center(child: CircularProgressIndicator()); 71 | } 72 | }, 73 | ), 74 | ), 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/screens/splash_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 3 | 4 | class SplashPage extends StatelessWidget { 5 | const SplashPage({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) => Scaffold( 9 | body: Center( 10 | child: SpinKitWave( 11 | color: Theme.of(context).colorScheme.primary, 12 | ), 13 | ), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/screens/timer_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/models/animated_list.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/widget/timer_list_item.dart'; 6 | 7 | class TimerScreen extends StatefulWidget { 8 | const TimerScreen({super.key}); 9 | 10 | @override 11 | State createState() => _TimerScreenState(); 12 | } 13 | 14 | class _TimerScreenState extends State { 15 | final GlobalKey _listKey = 16 | GlobalKey(); 17 | late AnimatedTimerList _list; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _list = AnimatedTimerList( 23 | listKey: _listKey, 24 | removedItemBuilder: _buildRemovedItem, 25 | ); 26 | } 27 | 28 | Widget _buildItem( 29 | BuildContext context, 30 | int index, 31 | Animation animation, 32 | ) => 33 | TimerListItem( 34 | animation: animation, 35 | item: _list[index], 36 | onDismissed: () { 37 | _list.removeAt(index); 38 | }, 39 | ); 40 | 41 | Widget _buildRemovedItem( 42 | Timer item, 43 | BuildContext context, 44 | Animation animation, 45 | ) => 46 | TimerListItem( 47 | animation: animation, 48 | item: item, 49 | enabled: false, 50 | ); 51 | 52 | @override 53 | Widget build(BuildContext context) => Scaffold( 54 | appBar: AppBar( 55 | title: Text(translate('timer.title')), 56 | actions: [ 57 | if (_list.isNotEmpty) 58 | IconButton( 59 | icon: const Icon(Icons.clear_all_outlined), 60 | tooltip: translate('app_bar.clear_all'), 61 | onPressed: _list.removeAll, 62 | ), 63 | ], 64 | ), 65 | body: _list.isNotEmpty 66 | ? CustomScrollView( 67 | slivers: [ 68 | SliverPadding( 69 | padding: const EdgeInsets.only(bottom: 16), 70 | sliver: SliverAnimatedList( 71 | key: _listKey, 72 | initialItemCount: _list.length, 73 | itemBuilder: _buildItem, 74 | ), 75 | ), 76 | ], 77 | ) 78 | : Center( 79 | child: Text(translate('timer.empty_list')), 80 | ), 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/services/api_provider.dart: -------------------------------------------------------------------------------- 1 | part of 'services.dart'; 2 | 3 | class ApiProvider { 4 | factory ApiProvider() => _apiProvider; 5 | ApiProvider._() { 6 | final auth = UserRepository().currentAppAuthentication; 7 | 8 | final client = Dio( 9 | BaseOptions( 10 | baseUrl: '${auth.server}/apps/cookbook', 11 | connectTimeout: const Duration(milliseconds: 30000), 12 | receiveTimeout: const Duration(milliseconds: 30000), 13 | ), 14 | )..httpClientAdapter = IOHttpClientAdapter( 15 | onHttpClientCreate: (client) => 16 | client..badCertificateCallback = (cert, host, port) => true, 17 | ); 18 | 19 | if (auth.isSelfSignedCertificate) { 20 | client.httpClientAdapter = IOHttpClientAdapter( 21 | onHttpClientCreate: (client) => 22 | client..badCertificateCallback = (cert, host, port) => true, 23 | ); 24 | } 25 | 26 | ncCookbookApi = NcCookbookApi( 27 | dio: client, 28 | ); 29 | 30 | ncCookbookApi.setBasicAuth( 31 | 'app_password', 32 | auth.loginName, 33 | auth.password, 34 | ); 35 | recipeApi = ncCookbookApi.getRecipesApi(); 36 | categoryApi = ncCookbookApi.getCategoriesApi(); 37 | miscApi = ncCookbookApi.getMiscApi(); 38 | tagsApi = ncCookbookApi.getTagsApi(); 39 | } 40 | static final ApiProvider _apiProvider = ApiProvider._(); 41 | 42 | late NcCookbookApi ncCookbookApi; 43 | late RecipesApi recipeApi; 44 | late CategoriesApi categoryApi; 45 | late MiscApi miscApi; 46 | late TagsApi tagsApi; 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/services/intent_repository.dart: -------------------------------------------------------------------------------- 1 | part of 'services.dart'; 2 | 3 | class IntentRepository { 4 | factory IntentRepository() => _intentRepository; 5 | 6 | IntentRepository._(); 7 | // Singleton Pattern 8 | static final IntentRepository _intentRepository = IntentRepository._(); 9 | 10 | static final _navigationKey = GlobalKey(); 11 | static const platform = MethodChannel('app.channel.shared.data'); 12 | 13 | Future handleIntent() async { 14 | final importUrl = await platform.invokeMethod('getImportUrl') as String?; 15 | if (importUrl != null) { 16 | await _navigationKey.currentState?.pushAndRemoveUntil( 17 | MaterialPageRoute( 18 | builder: (context) => RecipeImportScreen(importUrl: importUrl), 19 | ), 20 | ModalRoute.withName('/'), 21 | ); 22 | } 23 | } 24 | 25 | GlobalKey getNavigationKey() => _navigationKey; 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/services/net/nextcloud_metadata_api.dart: -------------------------------------------------------------------------------- 1 | part of '../services.dart'; 2 | 3 | class NextcloudMetadataApi { 4 | factory NextcloudMetadataApi() => NextcloudMetadataApi._( 5 | UserRepository().currentAppAuthentication, 6 | ); 7 | 8 | NextcloudMetadataApi._(this._appAuthentication); 9 | final AppAuthentication _appAuthentication; 10 | 11 | String getUserAvatarUrl() => 12 | '${_appAuthentication.server}/avatar/${_appAuthentication.loginName}/80'; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/services/services.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:developer'; 4 | 5 | import 'package:dio/dio.dart' as dio; 6 | import 'package:dio/dio.dart'; 7 | import 'package:dio/io.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 11 | import 'package:flutter_native_timezone/flutter_native_timezone.dart'; 12 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 13 | import 'package:flutter_translate/flutter_translate.dart'; 14 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 15 | import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; 16 | import 'package:nextcloud_cookbook_flutter/src/models/image_response.dart'; 17 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 18 | import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; 19 | import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; 20 | import 'package:timezone/data/latest_10y.dart' as tz; 21 | import 'package:timezone/timezone.dart' as tz; 22 | import 'package:xml/xml.dart'; 23 | 24 | part 'api_provider.dart'; 25 | part 'authentication_provider.dart'; 26 | part 'data_repository.dart'; 27 | part 'intent_repository.dart'; 28 | part 'net/nextcloud_metadata_api.dart'; 29 | part 'notification_provider.dart'; 30 | part 'user_repository.dart'; 31 | part 'timer_repository.dart'; 32 | -------------------------------------------------------------------------------- /lib/src/services/timer_repository.dart: -------------------------------------------------------------------------------- 1 | part of 'services.dart'; 2 | 3 | class TimerList { 4 | factory TimerList() => _instance; 5 | 6 | TimerList._() : _timers = []; 7 | static final TimerList _instance = TimerList._(); 8 | final List _timers; 9 | 10 | List get timers => _timers; 11 | 12 | void add(Timer timer) { 13 | unawaited(NotificationService().start(timer)); 14 | _timers.add(timer); 15 | } 16 | 17 | void remove(Timer timer) { 18 | unawaited(NotificationService().cancel(timer)); 19 | _timers.remove(timer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/services/user_repository.dart: -------------------------------------------------------------------------------- 1 | part of 'services.dart'; 2 | 3 | class UserRepository { 4 | factory UserRepository() => _userRepository; 5 | 6 | UserRepository._(); 7 | // Singleton 8 | static final UserRepository _userRepository = UserRepository._(); 9 | 10 | AuthenticationProvider authenticationProvider = AuthenticationProvider(); 11 | 12 | Future authenticate( 13 | String serverUrl, 14 | String username, 15 | String originalBasicAuth, { 16 | required bool isSelfSignedCertificate, 17 | }) async => 18 | authenticationProvider.authenticate( 19 | serverUrl: serverUrl, 20 | username: username, 21 | originalBasicAuth: originalBasicAuth, 22 | isSelfSignedCertificate: isSelfSignedCertificate, 23 | ); 24 | 25 | Future authenticateAppPassword( 26 | String serverUrl, 27 | String username, 28 | String basicAuth, { 29 | required bool isSelfSignedCertificate, 30 | }) async => 31 | authenticationProvider.authenticateAppPassword( 32 | serverUrl: serverUrl, 33 | username: username, 34 | basicAuth: basicAuth, 35 | isSelfSignedCertificate: isSelfSignedCertificate, 36 | ); 37 | 38 | void stopAuthenticate() { 39 | authenticationProvider.stopAuthenticate(); 40 | } 41 | 42 | AppAuthentication get currentAppAuthentication => 43 | authenticationProvider.currentAppAuthentication!; 44 | 45 | Dio get authenticatedClient => currentAppAuthentication.authenticatedClient; 46 | 47 | Future hasAppAuthentication() async => 48 | authenticationProvider.hasAppAuthentication(); 49 | 50 | Future loadAppAuthentication() async => 51 | authenticationProvider.loadAppAuthentication(); 52 | 53 | Future checkAppAuthentication() async => 54 | authenticationProvider.checkAppAuthentication( 55 | currentAppAuthentication.server, 56 | currentAppAuthentication.basicAuth, 57 | isSelfSignedCertificate: 58 | currentAppAuthentication.isSelfSignedCertificate, 59 | ); 60 | 61 | Future persistAppAuthentication( 62 | AppAuthentication appAuthentication, 63 | ) async => 64 | authenticationProvider.persistAppAuthentication(appAuthentication); 65 | 66 | Future deleteAppAuthentication() async => 67 | authenticationProvider.deleteAppAuthentication(); 68 | 69 | bool isVersionSupported(APIVersion version) => 70 | ApiProvider().ncCookbookApi.isSupportedSync(version); 71 | 72 | Future fetchApiVersion() async { 73 | final response = await ApiProvider().miscApi.version(); 74 | 75 | return response.data!.apiVersion; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/util/category_grid_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/rendering.dart'; 4 | 5 | class CategoryGridDelegate extends SliverGridDelegate { 6 | const CategoryGridDelegate({ 7 | this.extent = 0.0, 8 | }); 9 | 10 | final double extent; 11 | 12 | static const double maxCrossAxisExtent = 250; 13 | static const double mainAxisSpacing = 8; 14 | static const double crossAxisSpacing = 8; 15 | 16 | @override 17 | SliverGridLayout getLayout(SliverConstraints constraints) { 18 | var crossAxisCount = 19 | (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) 20 | .ceil(); 21 | // Ensure a minimum count of 1, can be zero and result in an infinite extent 22 | // below when the window size is 0. 23 | crossAxisCount = math.max(1, crossAxisCount); 24 | final double usableCrossAxisExtent = math.max( 25 | 0, 26 | constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), 27 | ); 28 | final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; 29 | final childMainAxisExtent = childCrossAxisExtent + extent; 30 | 31 | return SliverGridRegularTileLayout( 32 | crossAxisCount: crossAxisCount, 33 | mainAxisStride: childMainAxisExtent + mainAxisSpacing, 34 | crossAxisStride: childCrossAxisExtent + crossAxisSpacing, 35 | childMainAxisExtent: childMainAxisExtent, 36 | childCrossAxisExtent: childCrossAxisExtent, 37 | reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), 38 | ); 39 | } 40 | 41 | @override 42 | bool shouldRelayout(CategoryGridDelegate oldDelegate) => 43 | oldDelegate.extent != extent; 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/util/custom_cache_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 4 | import 'package:http/io_client.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 6 | 7 | class CustomCacheManager { 8 | factory CustomCacheManager() => _cacheManager; 9 | 10 | CustomCacheManager._(); 11 | static final CustomCacheManager _cacheManager = CustomCacheManager._(); 12 | 13 | static const key = 'customCacheKey'; 14 | 15 | CacheManager selfSignedCacheManager = CacheManager( 16 | Config( 17 | key, 18 | fileService: HttpFileService( 19 | httpClient: IOClient( 20 | HttpClient()..badCertificateCallback = (cert, host, port) => true, 21 | ), 22 | ), 23 | ), 24 | ); 25 | 26 | CacheManager get instance { 27 | if (UserRepository().currentAppAuthentication.isSelfSignedCertificate) { 28 | return selfSignedCacheManager; 29 | } else { 30 | return DefaultCacheManager(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/util/duration_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_translate/flutter_translate.dart'; 2 | 3 | extension DurationExtension on Duration { 4 | String get translatedString => 5 | "$inHours ${translate('recipe.fields.time.hours')} : ${inMinutes.remainder(60)} ${translate('recipe.fields.time.minutes')}"; 6 | 7 | String formatMinutes() => 8 | "$inHours:${inMinutes.remainder(60).toString().padLeft(2, '0')}"; 9 | 10 | String formatSeconds() => 11 | "${inHours.toString().padLeft(2, '0')}:${inMinutes.remainder(60).toString().padLeft(2, '0')}:${(inSeconds.remainder(60)).toString().padLeft(2, '0')}"; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/util/lifecycle_event_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class LifecycleEventHandler extends WidgetsBindingObserver { 5 | LifecycleEventHandler({ 6 | required this.resumeCallBack, 7 | this.suspendingCallBack, 8 | }); 9 | final AsyncCallback resumeCallBack; 10 | final AsyncCallback? suspendingCallBack; 11 | 12 | @override 13 | Future didChangeAppLifecycleState(AppLifecycleState state) async { 14 | switch (state) { 15 | case AppLifecycleState.resumed: 16 | await resumeCallBack(); 17 | break; 18 | case AppLifecycleState.inactive: 19 | case AppLifecycleState.paused: 20 | case AppLifecycleState.detached: 21 | await suspendingCallBack?.call(); 22 | break; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/util/setting_keys.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | enum SettingKeys { 4 | dark_mode, 5 | language, 6 | stay_awake, 7 | @Deprecated('The font size will try to scale propperly') 8 | recipe_font_size, 9 | @Deprecated('The font size will try to scale propperly') 10 | category_font_size, 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/util/supported_locales.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_classes_with_only_static_members 2 | 3 | class SupportedLocales { 4 | static final locales = { 5 | 'bg_BG': 'Български език (Bulgaria)', 6 | 'cs_CZ': 'čeština (Czechia)', 7 | 'de_DE': 'Deutsch (Deutschland)', 8 | 'de': 'Deutsch', 9 | 'en': 'English', 10 | 'en_GB': 'English (United Kingdom)', 11 | 'es': 'Español', 12 | 'eu': 'euskara', 13 | 'fi_FI': 'suomi (Finland)', 14 | 'fr': 'français', 15 | 'gl': 'Galego', 16 | 'he': 'עברית', 17 | 'hr': 'hrvatski jezik', 18 | 'hu_HU': 'magyar (Hungary)', 19 | 'is': 'Íslenska', 20 | 'it': 'Italiano', 21 | 'nl': 'Nederlands', 22 | 'pl': 'język polski', 23 | 'pt_BR': 'Português (Brazil)', 24 | 'ru': 'Runa Simi', 25 | 'sc': 'sardu', 26 | 'sk_SK': 'Slovenčina (Slovakia)', 27 | 'sl': 'Slovenski jezik', 28 | 'tr': 'Türkçe', 29 | 'vi': 'Tiếng Việt', 30 | 'zh_CN': '中文 (China)', 31 | 'zh_HK': '中文 (Hong Kong)', 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/util/theme_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTheme { 4 | const AppTheme._(); 5 | 6 | /// Light theme 7 | static final lightThemeData = ThemeData( 8 | hintColor: Colors.grey, 9 | colorScheme: lightColorSheme, 10 | inputDecorationTheme: inputdecoration, 11 | useMaterial3: true, 12 | extensions: [ 13 | SnackBarThemes( 14 | colorScheme: lightColorSheme, 15 | ), 16 | ], 17 | ); 18 | 19 | /// Dark theme 20 | static final darkThemeData = ThemeData( 21 | hintColor: Colors.grey, 22 | colorScheme: darkColorSheme, 23 | inputDecorationTheme: inputdecoration, 24 | useMaterial3: true, 25 | extensions: [ 26 | SnackBarThemes( 27 | colorScheme: darkColorSheme, 28 | ), 29 | ], 30 | ); 31 | 32 | static const nextCloudBlue = Color.fromRGBO(00, 130, 201, 1); 33 | 34 | static final lightColorSheme = ColorScheme.fromSeed( 35 | seedColor: nextCloudBlue, 36 | ); 37 | 38 | static final darkColorSheme = ColorScheme.fromSeed( 39 | seedColor: nextCloudBlue, 40 | brightness: Brightness.dark, 41 | ); 42 | 43 | static const inputdecoration = InputDecorationTheme( 44 | border: OutlineInputBorder(), 45 | floatingLabelBehavior: FloatingLabelBehavior.always, 46 | ); 47 | 48 | static final snackbarTheme = SnackBarThemeData( 49 | backgroundColor: lightColorSheme.error, 50 | contentTextStyle: TextStyle( 51 | color: lightColorSheme.onError, 52 | ), 53 | ); 54 | } 55 | 56 | @immutable 57 | class SnackBarThemes extends ThemeExtension { 58 | const SnackBarThemes({ 59 | required this.colorScheme, 60 | }); 61 | 62 | final ColorScheme colorScheme; 63 | 64 | SnackBarThemeData get errorSnackBar => SnackBarThemeData( 65 | backgroundColor: colorScheme.errorContainer, 66 | contentTextStyle: TextStyle( 67 | color: colorScheme.onErrorContainer, 68 | ), 69 | ); 70 | 71 | SnackBarThemeData get warningSnackBar => const SnackBarThemeData( 72 | backgroundColor: Colors.orange, 73 | ); 74 | 75 | @override 76 | SnackBarThemes copyWith({ColorScheme? colorScheme}) => SnackBarThemes( 77 | colorScheme: colorScheme ?? this.colorScheme, 78 | ); 79 | 80 | @override 81 | SnackBarThemes lerp(SnackBarThemes? other, double t) { 82 | if (other is! SnackBarThemes) { 83 | return this; 84 | } 85 | return SnackBarThemes( 86 | colorScheme: ColorScheme.lerp(colorScheme, other.colorScheme, t), 87 | ); 88 | } 89 | 90 | @override 91 | String toString() => 'SnackBarThemes(colorScheme: $colorScheme)'; 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/util/theme_mode_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; 4 | import 'package:theme_mode_handler/theme_mode_manager_interface.dart'; 5 | 6 | class ThemeModeManager implements IThemeModeManager { 7 | @override 8 | Future loadThemeMode() => Future.value( 9 | Settings.getValue( 10 | SettingKeys.dark_mode.name, 11 | defaultValue: ThemeMode.system.toString(), 12 | ), 13 | ); 14 | 15 | @override 16 | Future saveThemeMode(String value) async => Future.value(true); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/util/translate_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 5 | import 'package:flutter_translate/flutter_translate.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; 7 | 8 | class TranslatePreferences implements ITranslatePreferences { 9 | @override 10 | Future getPreferredLocale() { 11 | final locale = Settings.getValue( 12 | SettingKeys.language.name, 13 | defaultValue: Platform.localeName, 14 | ); 15 | if (locale == 'default') { 16 | return Future.value(Locale(Platform.localeName)); 17 | } 18 | return Future.value(Locale(locale!)); 19 | } 20 | 21 | @override 22 | Future savePreferredLocale(Locale locale) => Future.value(true); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/util/url_validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:punycode/punycode.dart'; 2 | 3 | /// Utilities for validating and eycaping urls 4 | class URLUtils { 5 | const URLUtils._(); // coverage:ignore-line 6 | 7 | /// Validates a given [url]. 8 | /// 9 | /// Punycode urls are encoded before checking. 10 | /// Example: 11 | /// ```dart 12 | /// print(URLUtils.isValid('http://foo.bar'); // true 13 | /// print(URLUtils.isValid('foo.bar'); // true 14 | /// print(URLUtils.isValid('https://öüäööß.foo.bar/'); // true 15 | /// print(URLUtils.isValid('https://foo/bar'); // false 16 | /// print(URLUtils.isValid(''); // false 17 | /// ``` 18 | static bool isValid(String url) { 19 | const urlPattern = 20 | r"^(?:http(s)?:\/\/)?[\w.-]+(?:(?:\.[\w\.-]+)|(?:\:\d+))+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*$"; 21 | return RegExp(urlPattern, caseSensitive: false) 22 | .hasMatch(_punyEncodeUrl(url)); 23 | } 24 | 25 | /// Punycode encodes an entire [url]. 26 | static String _punyEncodeUrl(String url) { 27 | var prefix = ''; 28 | var punycodeUrl = url; 29 | if (url.startsWith('https://')) { 30 | punycodeUrl = url.replaceFirst('https://', ''); 31 | prefix = 'https://'; 32 | } else if (url.startsWith('http://')) { 33 | punycodeUrl = url.replaceFirst('http://', ''); 34 | prefix = 'http://'; 35 | } 36 | 37 | const pattern = r'(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)'; 38 | final expression = RegExp(pattern, caseSensitive: false); 39 | 40 | final matches = expression.allMatches(punycodeUrl); 41 | for (final exp in matches) { 42 | final match = exp.group(1)!; 43 | 44 | punycodeUrl = 45 | punycodeUrl.replaceFirst(match, 'xn--${punycodeEncode(match)}'); 46 | } 47 | 48 | return prefix + punycodeUrl; 49 | } 50 | 51 | /// Checks if the url has been sanitized 52 | static bool isSanitized(String url) => url == sanitizeUrl(url); 53 | 54 | /// Sanitizes a given [url]. 55 | /// 56 | /// Strips trailing `/` and guesses the protocol to be `https` when not specified. 57 | /// Throws a `FormatException` when the url cannot be validated with [URLUtils.isValid]. 58 | /// 59 | /// Example: 60 | /// ```dart 61 | /// print(URLUtils.sanitizeUrl('http://foo.bar'); // http://foo.bar 62 | /// print(URLUtils.sanitizeUrl('http://foo.bar/'); // http://foo.bar 63 | /// print(URLUtils.sanitizeUrl('https://foo.bar'); // https://foo.bar 64 | /// print(URLUtils.sanitizeUrl('foo.bar'); // https://foo.bar 65 | /// print(URLUtils.sanitizeUrl('foo.bar/cloud/'); // https://foo.bar/cloud 66 | /// print(URLUtils.sanitizeUrl(''); // FormatException 67 | /// ``` 68 | static String sanitizeUrl(String url) { 69 | if (!isValid(url)) { 70 | throw const FormatException( 71 | 'given url is not valid. Please validate first with URLUtils.isValid(url)', 72 | ); 73 | } 74 | 75 | var encodedUrl = _punyEncodeUrl(url); 76 | if (encodedUrl.substring(0, 4) != 'http') { 77 | encodedUrl = 'https://$encodedUrl'; 78 | } 79 | if (encodedUrl.endsWith('/')) { 80 | encodedUrl = encodedUrl.substring(0, encodedUrl.length - 1); 81 | } 82 | 83 | return encodedUrl; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/util/wakelock.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 2 | import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; 3 | import 'package:wakelock/wakelock.dart'; 4 | 5 | Future disableWakelock() async { 6 | await Wakelock.disable(); 7 | } 8 | 9 | Future enableWakelock() async { 10 | final enable = Settings.getValue( 11 | SettingKeys.stay_awake.name, 12 | defaultValue: false, 13 | )!; 14 | 15 | if (enable) { 16 | await Wakelock.enable(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/widget/alerts/recipe_delete_alert.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | 5 | class DeleteRecipeAlert extends StatelessWidget { 6 | const DeleteRecipeAlert({ 7 | required this.recipe, 8 | super.key, 9 | }); 10 | 11 | final Recipe recipe; 12 | 13 | @override 14 | Widget build(BuildContext context) => AlertDialog( 15 | icon: const Icon(Icons.delete_forever), 16 | iconColor: Theme.of(context).colorScheme.error, 17 | title: Text(translate('recipe_edit.delete.title')), 18 | content: Text( 19 | translate( 20 | 'recipe_edit.delete.dialog', 21 | args: {'recipe': recipe.name}, 22 | ), 23 | ), 24 | actions: [ 25 | TextButton( 26 | onPressed: Navigator.of(context).pop, 27 | child: Text(MaterialLocalizations.of(context).cancelButtonLabel), 28 | ), 29 | TextButton( 30 | onPressed: () => Navigator.of(context).pop(true), 31 | child: Text(MaterialLocalizations.of(context).deleteButtonTooltip), 32 | ), 33 | ], 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/widget/alerts/recipe_edit_alert.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | 5 | class CancelEditAlert extends StatelessWidget { 6 | const CancelEditAlert({ 7 | this.recipe, 8 | super.key, 9 | }); 10 | 11 | final Recipe? recipe; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final key = recipe == null ? 'dismiss_create' : 'dismiss_edit'; 16 | return AlertDialog( 17 | title: Text(translate('recipe_form.dismiss_create.title')), 18 | content: Text( 19 | translate( 20 | 'recipe_form.$key.dialog', 21 | args: {'recipe': recipe?.name}, 22 | ), 23 | ), 24 | actions: [ 25 | TextButton( 26 | onPressed: Navigator.of(context).pop, 27 | child: Text(MaterialLocalizations.of(context).cancelButtonLabel), 28 | ), 29 | TextButton( 30 | onPressed: () => Navigator.of(context).pop(true), 31 | child: Text(translate('alert.discard')), 32 | ), 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/widget/animated_time_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/util/duration_utils.dart'; 5 | 6 | class AnimatedTimeProgressBar extends StatelessWidget { 7 | const AnimatedTimeProgressBar({ 8 | required this.timer, 9 | super.key, 10 | }); 11 | final Timer timer; 12 | 13 | @override 14 | Widget build(BuildContext context) => TweenAnimationBuilder( 15 | duration: timer.remaining, 16 | tween: Tween( 17 | begin: timer.progress, 18 | end: 1, 19 | ), 20 | builder: (context, value, child) { 21 | if (value == 1.0) { 22 | return Text(translate('timer.done')); 23 | } 24 | 25 | return Column( 26 | children: [ 27 | Row( 28 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 29 | children: [ 30 | Text(timer.remaining.formatSeconds()), 31 | child!, 32 | ], 33 | ), 34 | ClipRRect( 35 | borderRadius: BorderRadius.circular(4), 36 | child: LinearProgressIndicator( 37 | value: value, 38 | color: Theme.of(context).colorScheme.primaryContainer, 39 | semanticsLabel: timer.title, 40 | ), 41 | ), 42 | ], 43 | ); 44 | }, 45 | child: Text(timer.duration.formatSeconds()), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/widget/category_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/screens/recipes_list_screen.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; 6 | 7 | class CategoryCard extends StatelessWidget { 8 | const CategoryCard( 9 | this.category, 10 | this.imageID, { 11 | super.key, 12 | }); 13 | final Category category; 14 | final String? imageID; 15 | 16 | static const double _spacer = 8; 17 | static const _labelPadding = EdgeInsets.symmetric(horizontal: 8); 18 | 19 | static TextStyle _nameStyle(BuildContext context) => 20 | Theme.of(context).textTheme.labelSmall!; 21 | 22 | static TextStyle _itemStyle(BuildContext context) => 23 | Theme.of(context).textTheme.labelSmall!; 24 | 25 | static double hightExtend(BuildContext context) => 26 | _spacer + 27 | _itemStyle(context).fontSize! + 28 | _itemStyle(context).fontSize! + 29 | 2 * _labelPadding.horizontal; 30 | 31 | @override 32 | Widget build(BuildContext context) => LayoutBuilder( 33 | builder: (context, constraints) { 34 | final size = constraints.maxWidth; 35 | 36 | final itemsText = translatePlural( 37 | 'categories.items', 38 | category.recipeCount, 39 | ); 40 | 41 | return GestureDetector( 42 | child: Card( 43 | color: Theme.of(context).colorScheme.secondaryContainer, 44 | child: Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | ClipRRect( 48 | borderRadius: BorderRadius.circular(12), 49 | child: RecipeImage( 50 | id: imageID, 51 | size: Size.square(size), 52 | ), 53 | ), 54 | const SizedBox(height: _spacer), 55 | Padding( 56 | padding: _labelPadding, 57 | child: Text( 58 | category.name, 59 | maxLines: 1, 60 | style: _nameStyle(context), 61 | ), 62 | ), 63 | Padding( 64 | padding: _labelPadding, 65 | child: Text( 66 | itemsText, 67 | style: _itemStyle(context), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | onTap: () async => Navigator.push( 74 | context, 75 | MaterialPageRoute( 76 | builder: (context) => 77 | RecipesListScreen(category: category.name), 78 | ), 79 | ), 80 | ); 81 | }, 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/widget/checkbox_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CheckboxFormField extends FormField { 4 | CheckboxFormField({ 5 | super.key, 6 | Widget? title, 7 | super.onSaved, 8 | super.validator, 9 | bool super.initialValue = false, 10 | AutovalidateMode autoValidateMode = AutovalidateMode.disabled, 11 | }) : super( 12 | autovalidateMode: autoValidateMode, 13 | builder: (state) => CheckboxListTile( 14 | dense: state.hasError, 15 | title: title, 16 | value: state.value, 17 | onChanged: state.didChange, 18 | subtitle: state.hasError 19 | ? Builder( 20 | builder: (context) => Text( 21 | state.errorText!, 22 | style: TextStyle( 23 | color: Theme.of(context).colorScheme.error, 24 | ), 25 | ), 26 | ) 27 | : null, 28 | controlAffinity: ListTileControlAffinity.leading, 29 | ), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/widget/drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/screens/my_settings_screen.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; 7 | import 'package:nextcloud_cookbook_flutter/src/screens/timer_screen.dart'; 8 | import 'package:nextcloud_cookbook_flutter/src/widget/drawer_item.dart'; 9 | import 'package:nextcloud_cookbook_flutter/src/widget/user_image.dart'; 10 | 11 | class MainDrawer extends StatelessWidget { 12 | const MainDrawer({ 13 | super.key, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) => Drawer( 18 | child: ListView( 19 | padding: EdgeInsets.zero, 20 | children: [ 21 | DrawerHeader( 22 | decoration: BoxDecoration( 23 | color: Theme.of(context).colorScheme.primaryContainer, 24 | ), 25 | child: const Center(child: UserImage()), 26 | ), 27 | DrawerItem( 28 | icon: Icons.alarm_add_outlined, 29 | title: translate('timer.title'), 30 | onTap: () async { 31 | Navigator.pop(context); 32 | await Navigator.push( 33 | context, 34 | MaterialPageRoute( 35 | builder: (context) => const TimerScreen(), 36 | ), 37 | ); 38 | }, 39 | ), 40 | DrawerItem( 41 | icon: Icons.cloud_download_outlined, 42 | title: translate('categories.drawer.import'), 43 | onTap: () async { 44 | Navigator.pop(context); 45 | await Navigator.push( 46 | context, 47 | MaterialPageRoute( 48 | builder: (context) => const RecipeImportScreen(), 49 | ), 50 | ); 51 | }, 52 | ), 53 | DrawerItem( 54 | icon: Icons.settings_outlined, 55 | title: translate('categories.drawer.settings'), 56 | onTap: () async { 57 | await Navigator.push( 58 | context, 59 | MaterialPageRoute( 60 | builder: (context) => const MySettingsScreen(), 61 | ), 62 | ); 63 | }, 64 | ), 65 | DrawerItem( 66 | icon: Icons.exit_to_app_outlined, 67 | title: translate('app_bar.logout'), 68 | onTap: () { 69 | BlocProvider.of(context) 70 | .add(const LoggedOut()); 71 | }, 72 | ), 73 | ], 74 | ), 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/widget/drawer_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DrawerItem extends StatelessWidget { 4 | const DrawerItem({ 5 | required this.title, 6 | this.icon, 7 | this.onTap, 8 | super.key, 9 | }); 10 | 11 | final String title; 12 | final IconData? icon; 13 | final GestureTapCallback? onTap; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final leading = icon != null 18 | ? Icon( 19 | icon, 20 | semanticLabel: title, 21 | ) 22 | : null; 23 | 24 | return SizedBox( 25 | height: 56, 26 | child: ListTile( 27 | style: ListTileStyle.drawer, 28 | shape: const RoundedRectangleBorder( 29 | borderRadius: BorderRadius.all( 30 | Radius.circular(28), 31 | ), 32 | ), 33 | splashColor: Theme.of(context).colorScheme.secondaryContainer, 34 | leading: leading, 35 | title: Text(title), 36 | onTap: onTap, 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/widget/input/integer_text_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | 4 | class IntegerTextFormField extends StatelessWidget { 5 | IntegerTextFormField({ 6 | super.key, 7 | int? initialValue, 8 | this.enabled, 9 | this.decoration, 10 | this.onSaved, 11 | this.onChanged, 12 | this.textInputAction, 13 | this.minValue, 14 | this.maxValue, 15 | this.textAlign = TextAlign.end, 16 | }) : assert(initialValue != null || minValue != null), 17 | initialValue = initialValue ?? minValue!, 18 | assert(minValue == null || initialValue! >= minValue), 19 | assert((minValue == null || maxValue == null) || minValue <= maxValue); 20 | final int initialValue; 21 | final bool? enabled; 22 | final InputDecoration? decoration; 23 | final ValueChanged? onSaved; 24 | final ValueChanged? onChanged; 25 | final TextInputAction? textInputAction; 26 | final int? minValue; 27 | final int? maxValue; 28 | final TextAlign textAlign; 29 | 30 | @override 31 | Widget build(BuildContext context) => TextFormField( 32 | enabled: enabled, 33 | initialValue: initialValue.toString(), 34 | decoration: decoration, 35 | textAlign: textAlign, 36 | keyboardType: TextInputType.number, 37 | textInputAction: textInputAction, 38 | onSaved: (newValue) { 39 | if (newValue == null) { 40 | return; 41 | } 42 | 43 | onSaved?.call(int.parse(newValue)); 44 | }, 45 | onChanged: (value) { 46 | final int$ = int.tryParse(value); 47 | if (int$ != null) { 48 | onChanged?.call(int$); 49 | } 50 | }, 51 | validator: (value) { 52 | if (value == null || !ensureMinMax(int.tryParse(value))) { 53 | return translate('form.validators.invalid_number'); 54 | } 55 | 56 | return null; 57 | }, 58 | ); 59 | 60 | bool ensureMinMax(int? value) { 61 | if (value == null) { 62 | return false; 63 | } 64 | 65 | if (minValue != null && value < minValue!) { 66 | return false; 67 | } 68 | if (maxValue != null && value > maxValue!) { 69 | return false; 70 | } 71 | 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/recipe_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_translate/flutter_translate.dart'; 5 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/util/duration_utils.dart'; 7 | import 'package:url_launcher/url_launcher_string.dart'; 8 | 9 | part 'widget/ingredient_list.dart'; 10 | part 'widget/instruction_list.dart'; 11 | part 'widget/nutrition_list.dart'; 12 | part 'widget/rounded_box_item.dart'; 13 | part 'widget/duration_list.dart'; 14 | part 'widget/recipe_yield.dart'; 15 | part 'widget/tool_list.dart'; 16 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/duration_list.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class DurationList extends StatelessWidget { 4 | const DurationList({ 5 | required this.recipe, 6 | super.key, 7 | }); 8 | final Recipe recipe; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | const height = 35.0; 13 | const padding = EdgeInsets.symmetric(horizontal: 13); 14 | 15 | return Wrap( 16 | alignment: WrapAlignment.center, 17 | runSpacing: 10, 18 | spacing: 10, 19 | children: [ 20 | if (recipe.prepTime != null) 21 | RoundedBoxItem( 22 | name: translate('recipe.prep'), 23 | value: recipe.prepTime!.formatMinutes(), 24 | height: height, 25 | padding: padding, 26 | ), 27 | if (recipe.cookTime != null) 28 | RoundedBoxItem( 29 | name: translate('recipe.cook'), 30 | value: recipe.cookTime!.formatMinutes(), 31 | height: height, 32 | padding: padding, 33 | ), 34 | if (recipe.totalTime != null) 35 | RoundedBoxItem( 36 | name: translate('recipe.total'), 37 | value: recipe.totalTime!.formatMinutes(), 38 | height: height, 39 | padding: padding, 40 | ), 41 | ], 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/ingredient_list.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class IngredientList extends StatelessWidget { 4 | const IngredientList( 5 | this.recipe, { 6 | super.key, 7 | }); 8 | final Recipe recipe; 9 | 10 | @override 11 | Widget build(BuildContext context) => ExpansionTile( 12 | childrenPadding: const EdgeInsets.symmetric(horizontal: 8), 13 | title: Text(translate('recipe.fields.ingredients')), 14 | initiallyExpanded: true, 15 | expandedCrossAxisAlignment: CrossAxisAlignment.start, 16 | children: [ 17 | for (final ingredient in recipe.recipeIngredient) 18 | _IngredientListItem(ingredient) 19 | ], 20 | ); 21 | } 22 | 23 | class _IngredientListItem extends StatefulWidget { 24 | const _IngredientListItem(this.ingredient); 25 | final String ingredient; 26 | 27 | @override 28 | State<_IngredientListItem> createState() => __IngredientListItemState(); 29 | } 30 | 31 | class __IngredientListItemState extends State<_IngredientListItem> { 32 | bool selected = false; 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final style = Theme.of(context).textTheme.bodyLarge!; 37 | 38 | if (widget.ingredient.startsWith('##')) { 39 | return Text( 40 | widget.ingredient.replaceFirst(RegExp(r'##\s*'), '').trim(), 41 | style: const TextStyle( 42 | fontFeatures: [FontFeature.enable('smcp')], 43 | ), 44 | ); 45 | } 46 | 47 | return Padding( 48 | padding: const EdgeInsets.symmetric(horizontal: 24), 49 | child: GestureDetector( 50 | onTap: () => setState(() { 51 | selected = !selected; 52 | }), 53 | child: Row( 54 | children: [ 55 | Container( 56 | width: style.fontSize, 57 | height: style.fontSize, 58 | decoration: ShapeDecoration( 59 | shape: const CircleBorder( 60 | side: BorderSide(color: Colors.grey), 61 | ), 62 | color: selected 63 | ? Colors.green 64 | : Theme.of(context).colorScheme.onBackground, 65 | ), 66 | child: selected 67 | ? Icon( 68 | Icons.check_outlined, 69 | size: style.fontSize! * 0.75, 70 | ) 71 | : null, 72 | ), 73 | const SizedBox(width: 10), 74 | Expanded( 75 | child: Text( 76 | widget.ingredient, 77 | ), 78 | ), 79 | ], 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/instruction_list.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class InstructionList extends StatelessWidget { 4 | const InstructionList( 5 | this.recipe, { 6 | super.key, 7 | }); 8 | final Recipe recipe; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final instructions = recipe.recipeInstructions; 13 | return ExpansionTile( 14 | title: Text(translate('recipe.fields.instructions')), 15 | initiallyExpanded: true, 16 | children: [ 17 | for (int i = 0; i < instructions.length; i++) 18 | _InstructionListTitem(instructions[i], i) 19 | ], 20 | ); 21 | } 22 | } 23 | 24 | class _InstructionListTitem extends StatefulWidget { 25 | const _InstructionListTitem(this.instruction, this.index); 26 | final String instruction; 27 | final int index; 28 | 29 | @override 30 | State<_InstructionListTitem> createState() => _InstructionListTitemState(); 31 | } 32 | 33 | class _InstructionListTitemState extends State<_InstructionListTitem> { 34 | bool selected = false; 35 | 36 | @override 37 | Widget build(BuildContext context) => Padding( 38 | padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5), 39 | child: GestureDetector( 40 | onTap: () => setState(() { 41 | selected = !selected; 42 | }), 43 | child: Row( 44 | crossAxisAlignment: CrossAxisAlignment.start, 45 | children: [ 46 | Container( 47 | width: 40, 48 | height: 40, 49 | margin: const EdgeInsets.only(right: 15, top: 2.5), 50 | decoration: ShapeDecoration( 51 | shape: const CircleBorder( 52 | side: BorderSide(color: Colors.grey), 53 | ), 54 | color: selected 55 | ? Colors.green 56 | : Theme.of(context).colorScheme.background, 57 | ), 58 | child: selected 59 | ? const Icon(Icons.check_outlined) 60 | : Center(child: Text((widget.index + 1).toString())), 61 | ), 62 | Expanded( 63 | child: Text( 64 | widget.instruction, 65 | ), 66 | ), 67 | ], 68 | ), 69 | ), 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/nutrition_list.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class NutritionList extends StatelessWidget { 4 | const NutritionList( 5 | this.nutrition, { 6 | super.key, 7 | }); 8 | final Map nutrition; 9 | 10 | @override 11 | Widget build(BuildContext context) => ExpansionTile( 12 | title: Text(translate('recipe.fields.nutrition.title')), 13 | children: [ 14 | Padding( 15 | padding: const EdgeInsets.symmetric(horizontal: 16), 16 | child: Wrap( 17 | spacing: 10, 18 | runSpacing: 10, 19 | children: [ 20 | for (final entry in nutrition.entries) 21 | RoundedBoxItem( 22 | name: translate( 23 | 'recipe.fields.nutrition.items.${entry.key}', 24 | ), 25 | value: entry.value, 26 | height: 30, 27 | padding: const EdgeInsets.symmetric(horizontal: 10), 28 | ), 29 | ], 30 | ), 31 | ), 32 | ], 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/recipe_yield.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class RecipeYield extends StatelessWidget { 4 | const RecipeYield({ 5 | required this.recipe, 6 | super.key, 7 | }); 8 | final Recipe recipe; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final style = 13 | Theme.of(context).textTheme.bodyMedium!.apply(fontWeightDelta: 3); 14 | 15 | return Row( 16 | children: [ 17 | Text( 18 | "${translate('recipe.fields.servings.else')}: ${recipe.recipeYield}", 19 | style: style, 20 | ), 21 | if (recipe.url.isNotEmpty) ...[ 22 | const Spacer(), 23 | ElevatedButton( 24 | onPressed: () async => launchUrlString(recipe.url), 25 | child: Text(translate('recipe.fields.source_button')), 26 | ), 27 | ], 28 | ], 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/rounded_box_item.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class RoundedBoxItem extends StatelessWidget { 4 | const RoundedBoxItem({ 5 | required this.name, 6 | required this.value, 7 | this.height, 8 | this.padding, 9 | super.key, 10 | }); 11 | final String name; 12 | final String value; 13 | final double? height; 14 | final EdgeInsets? padding; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | const radius = Radius.circular(10); 19 | final theme = Theme.of(context); 20 | 21 | return IntrinsicWidth( 22 | child: Column( 23 | children: [ 24 | Container( 25 | height: height, 26 | padding: padding, 27 | decoration: BoxDecoration( 28 | borderRadius: const BorderRadius.vertical(top: radius), 29 | border: Border.all(color: theme.hintColor), 30 | color: theme.colorScheme.primary.withOpacity(0.2), 31 | ), 32 | child: Center( 33 | child: Text( 34 | name, 35 | style: const TextStyle(fontWeight: FontWeight.bold), 36 | ), 37 | ), 38 | ), 39 | Container( 40 | height: height, 41 | padding: padding, 42 | decoration: BoxDecoration( 43 | borderRadius: const BorderRadius.vertical(bottom: radius), 44 | border: Border.all( 45 | color: theme.hintColor.withOpacity(0.6), 46 | ), 47 | ), 48 | child: Center( 49 | child: Text(value), 50 | ), 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/widget/recipe/widget/tool_list.dart: -------------------------------------------------------------------------------- 1 | part of '../recipe_screen.dart'; 2 | 3 | class ToolList extends StatelessWidget { 4 | const ToolList({ 5 | required this.recipe, 6 | super.key, 7 | }); 8 | 9 | final Recipe recipe; 10 | 11 | @override 12 | Widget build(BuildContext context) => ExpansionTile( 13 | title: Text(translate('recipe.fields.tools')), 14 | children: [ 15 | for (final tool in recipe.tool) 16 | Align( 17 | alignment: Alignment.centerLeft, 18 | child: Padding( 19 | padding: const EdgeInsets.symmetric(horizontal: 24), 20 | child: Text( 21 | '- ${tool.trim()}', 22 | ), 23 | ), 24 | ), 25 | ], 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/widget/recipe_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 4 | 5 | class RecipeImage extends StatelessWidget { 6 | const RecipeImage({ 7 | required this.size, 8 | required this.id, 9 | super.key, 10 | }); 11 | final Size size; 12 | 13 | final String? id; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | const boxFit = BoxFit.cover; 18 | final color = Colors.grey[400]!; 19 | 20 | return SizedBox.fromSize( 21 | size: size, 22 | child: FutureBuilder( 23 | // ignore: discarded_futures 24 | future: id != null ? DataRepository().fetchImage(id!, size) : null, 25 | builder: (context, snapshot) { 26 | if (snapshot.hasData) { 27 | if (snapshot.data!.isSvg) { 28 | return ColoredBox( 29 | color: color, 30 | child: SvgPicture.memory( 31 | snapshot.data!.data, 32 | fit: boxFit, 33 | ), 34 | ); 35 | } else { 36 | return Image.memory( 37 | snapshot.data!.data, 38 | fit: boxFit, 39 | ); 40 | } 41 | } 42 | 43 | if (snapshot.connectionState == ConnectionState.done) { 44 | return ColoredBox( 45 | color: color, 46 | child: SvgPicture.asset( 47 | 'assets/icon.svg', 48 | fit: boxFit, 49 | ), 50 | ); 51 | } 52 | 53 | return ColoredBox( 54 | color: color, 55 | child: const Center( 56 | child: CircularProgressIndicator(), 57 | ), 58 | ); 59 | }, 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/widget/recipe_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:nc_cookbook_api/nc_cookbook_api.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/screens/recipe_screen.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; 6 | 7 | class RecipeListItem extends StatelessWidget { 8 | const RecipeListItem({ 9 | required this.recipe, 10 | super.key, 11 | }); 12 | final RecipeStub recipe; 13 | 14 | @override 15 | Widget build(BuildContext context) => ListTile( 16 | leading: ClipRRect( 17 | borderRadius: BorderRadius.circular(5), 18 | child: RecipeImage( 19 | id: recipe.recipeId.oneOf.value.toString(), 20 | size: const Size.square(80), 21 | ), 22 | ), 23 | title: Text(recipe.name), 24 | subtitle: Row( 25 | children: [ 26 | _RecipeListDate( 27 | Icons.edit_calendar_outlined, 28 | recipe.dateCreated, 29 | ), 30 | if (recipe.dateModified != null && 31 | recipe.dateModified != recipe.dateCreated) ...[ 32 | _RecipeListDate( 33 | Icons.edit_outlined, 34 | recipe.dateModified!, 35 | ), 36 | ], 37 | ], 38 | ), 39 | onTap: () async { 40 | await Navigator.push( 41 | context, 42 | MaterialPageRoute( 43 | builder: (context) => RecipeScreen( 44 | recipeId: recipe.recipeId.oneOf.value.toString(), 45 | ), 46 | ), 47 | ); 48 | }, 49 | ); 50 | } 51 | 52 | class _RecipeListDate extends StatelessWidget { 53 | const _RecipeListDate( 54 | this.icon, 55 | this.data, 56 | ); 57 | 58 | final DateTime data; 59 | final IconData? icon; 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | final textStyle = Theme.of(context).textTheme.bodySmall!; 64 | final colorScheme = Theme.of(context).colorScheme; 65 | final content = DateFormat(DateFormat.YEAR_NUM_MONTH_DAY).format(data); 66 | 67 | return Card( 68 | color: colorScheme.secondaryContainer, 69 | child: Padding( 70 | padding: const EdgeInsets.symmetric(horizontal: 4), 71 | child: Row( 72 | mainAxisSize: MainAxisSize.min, 73 | children: [ 74 | Icon( 75 | icon, 76 | size: textStyle.fontSize, 77 | color: colorScheme.onSecondaryContainer, 78 | ), 79 | const SizedBox(width: 4), 80 | Text( 81 | content, 82 | style: textStyle.copyWith( 83 | color: colorScheme.onSecondaryContainer, 84 | ), 85 | overflow: TextOverflow.fade, 86 | ), 87 | ], 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/widget/timer_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/screens/recipe_screen.dart'; 5 | import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; 6 | import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; 7 | 8 | class TimerListItem extends StatelessWidget { 9 | const TimerListItem({ 10 | required this.animation, 11 | required this.item, 12 | this.onDismissed, 13 | this.dense = false, 14 | this.enabled = true, 15 | super.key, 16 | }); 17 | 18 | final Animation animation; 19 | final VoidCallback? onDismissed; 20 | final Timer item; 21 | final bool dense; 22 | final bool enabled; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final image = ClipRRect( 27 | borderRadius: BorderRadius.circular(5), 28 | child: RecipeImage( 29 | id: item.recipeId, 30 | size: const Size.square(80), 31 | ), 32 | ); 33 | final progressBar = AnimatedTimeProgressBar( 34 | timer: item, 35 | ); 36 | 37 | Future onPressed() async { 38 | await Navigator.push( 39 | context, 40 | MaterialPageRoute( 41 | builder: (context) => RecipeScreen(recipeId: item.recipeId), 42 | ), 43 | ); 44 | } 45 | 46 | return SizeTransition( 47 | sizeFactor: animation, 48 | child: ListTile( 49 | leading: dense ? null : image, 50 | title: dense ? progressBar : Text(item.title), 51 | subtitle: dense ? null : progressBar, 52 | trailing: IconButton( 53 | icon: const Icon(Icons.cancel_outlined), 54 | tooltip: translate('timer.button.cancel'), 55 | onPressed: enabled ? onDismissed : null, 56 | ), 57 | onTap: (enabled && !dense) ? onPressed : null, 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/widget/user_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; 5 | 6 | class UserImage extends StatelessWidget { 7 | const UserImage({ 8 | super.key, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final url = DataRepository().getUserAvatarUrl(); 14 | final appAuthentication = UserRepository().currentAppAuthentication; 15 | 16 | return ClipOval( 17 | child: CachedNetworkImage( 18 | cacheManager: CustomCacheManager().instance, 19 | cacheKey: 'avatar', 20 | fit: BoxFit.fill, 21 | httpHeaders: { 22 | 'Authorization': appAuthentication.basicAuth, 23 | 'Accept': 'image/jpeg' 24 | }, 25 | imageUrl: url, 26 | placeholder: (context, url) => ColoredBox( 27 | color: Colors.grey[400]!, 28 | child: const Center( 29 | child: CircularProgressIndicator(), 30 | ), 31 | ), 32 | errorWidget: (context, url, error) => ColoredBox( 33 | color: Colors.grey[400]!, 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/models/app_authentication_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; 5 | 6 | void main() { 7 | const server = 'https://example.com'; 8 | const loginName = 'admin'; 9 | const password = 'password'; 10 | final basicAuth = 'Basic ${base64Encode( 11 | utf8.encode( 12 | '$loginName:$password', 13 | ), 14 | )}'; 15 | const isSelfSignedCertificate = false; 16 | 17 | final auth = AppAuthentication( 18 | server: server, 19 | loginName: loginName, 20 | basicAuth: basicAuth, 21 | isSelfSignedCertificate: isSelfSignedCertificate, 22 | ); 23 | 24 | final encodedJson = 25 | '"{\\"server\\":\\"$server\\",\\"loginName\\":\\"$loginName\\",\\"basicAuth\\":\\"$basicAuth\\",\\"isSelfSignedCertificate\\":$isSelfSignedCertificate}"'; 26 | final jsonBasicAuth = 27 | '{"server":"$server","loginName":"$loginName","basicAuth":"$basicAuth","isSelfSignedCertificate":$isSelfSignedCertificate}'; 28 | const jsonPassword = 29 | '{"server":"$server","loginName":"$loginName","appPassword":"$password","isSelfSignedCertificate":$isSelfSignedCertificate}'; 30 | 31 | group(AppAuthentication, () { 32 | test('toJson', () { 33 | expect(jsonEncode(auth.toJsonString()), equals(encodedJson)); 34 | }); 35 | 36 | test('fromJson', () { 37 | expect( 38 | AppAuthentication.fromJsonString(jsonBasicAuth), 39 | equals(auth), 40 | ); 41 | expect( 42 | AppAuthentication.fromJsonString(jsonPassword), 43 | equals(auth), 44 | ); 45 | }); 46 | 47 | test('password', () { 48 | expect(auth.password, equals(password)); 49 | }); 50 | 51 | test('parseBasicAuth', () { 52 | expect( 53 | AppAuthentication.parseBasicAuth(loginName, password), 54 | equals(basicAuth), 55 | ); 56 | }); 57 | 58 | test('toJson does not contain password', () { 59 | expect(auth.toString(), isNot(contains(basicAuth))); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /test/models/timer_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; 5 | 6 | void main() { 7 | const title = 'title'; 8 | const body = 'body'; 9 | const duration = Duration(minutes: 5); 10 | final done = DateTime.now().add(duration); 11 | const recipeId = '12345678'; 12 | const id = 0; 13 | 14 | final timer = Timer.restoreOld(done, id, recipeId, title, duration, recipeId); 15 | 16 | final json = 17 | '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":"$recipeId"}'; 18 | final orderedJson = 19 | '{"title":"$title","body":"$body","duration":${duration.inMinutes},"id":$id,"done":${done.millisecondsSinceEpoch},"recipeId":"$recipeId"}'; 20 | final oldJson = 21 | '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":$recipeId}'; 22 | 23 | final newJson = '{"recipe":null,"done":"${done.toIso8601String()}","id":$id}'; 24 | 25 | group(Timer, () { 26 | test('toJson', () { 27 | expect(jsonEncode(timer.toJson()), equals(newJson)); 28 | }); 29 | 30 | test('fromJson', () { 31 | expect( 32 | Timer.fromJson(jsonDecode(json) as Map), 33 | isA(), 34 | ); 35 | expect( 36 | Timer.fromJson(jsonDecode(orderedJson) as Map), 37 | isA(), 38 | ); 39 | expect( 40 | Timer.fromJson(jsonDecode(oldJson) as Map), 41 | isA(), 42 | ); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/util/url_validator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; 3 | 4 | void main() { 5 | group('URLUtils', () { 6 | test('.isValid() validates a url', () { 7 | const emptyUrl = ''; 8 | expect(URLUtils.isValid(emptyUrl), false); 9 | const validUrl = 'http://foo.bar'; 10 | expect(URLUtils.isValid(validUrl), true); 11 | const noProtocol = 'foo.bar'; 12 | expect(URLUtils.isValid(noProtocol), true); 13 | const punycodeUrl = 'https://öüäööß.foo.bar/'; 14 | expect(URLUtils.isValid(punycodeUrl), true); 15 | const missingTLD = 'https://foo/bar'; 16 | expect(URLUtils.isValid(missingTLD), false); 17 | }); 18 | 19 | test('.isSanitized() check sanitized', () { 20 | const dirtyUrl = 'foo.bar/cloud/'; 21 | expect(URLUtils.isSanitized(dirtyUrl), false); 22 | 23 | const cleanUrl = 'http://foo.bar'; 24 | expect(URLUtils.isSanitized(cleanUrl), true); 25 | }); 26 | 27 | test('.sanitizeUrl() sanitizes URL', () { 28 | const insecureUrl = 'http://foo.bar'; 29 | expect(URLUtils.sanitizeUrl(insecureUrl), equals('http://foo.bar')); 30 | const secureUrl = 'https://foo.bar/'; 31 | expect(URLUtils.sanitizeUrl(secureUrl), equals('https://foo.bar')); 32 | const plainDomain = 'foo.bar/'; 33 | expect(URLUtils.sanitizeUrl(plainDomain), equals('https://foo.bar')); 34 | const subdirUrl = 'foo.bar/cloud/'; 35 | expect(URLUtils.sanitizeUrl(subdirUrl), equals('https://foo.bar/cloud')); 36 | }); 37 | }); 38 | } 39 | --------------------------------------------------------------------------------