diff --git a/.gitignore b/.gitignore index 09360f3..c9e953d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ .pub/ /build/ +personal_info.* +credential.json + # Web related # Symbolication related diff --git a/README.md b/README.md index fd95d2a..cc5dea4 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,97 @@ -# flutter_rust_bridge_template +# Booky -This repository serves as a template for Flutter projects calling into native Rust -libraries via `flutter_rust_bridge`. +Booky is an application to help publish second-hand book. +It enable taking multiple picture of the book(s). Add books state (brand new, worn out), add the weight (for shipping), then extract the ISBN from the barcode in the pictures to have additional metadata by scrapping some website. -## Getting Started +Metadata include: +- Title +- Author +- Blurb. A book blurb is a short promotional description, whereas a synopsis summarizes the twists, turns, and conclusion of the story. +- Keywords or genres -To begin, ensure that you have a working installation of the following items: -- [Flutter SDK](https://docs.flutter.dev/get-started/install) -- [Rust language](https://rustup.rs/) -- `flutter_rust_bridge_codegen` [cargo package](https://cjycode.com/flutter_rust_bridge/integrate/deps.html#build-time-dependencies) -- Appropriate [Rust targets](https://rust-lang.github.io/rustup/cross-compilation.html) for cross-compiling to your device -- For Android targets: - - Install [cargo-ndk](https://github.com/bbqsrc/cargo-ndk#installing) - - Install [Android NDK 22](https://github.com/android/ndk/wiki/Unsupported-Downloads#r22b), then put its path in one of the `gradle.properties`, e.g.: +## Enrichment +### Example using Babelio as source +#### Input +```rust +let isbn = 9782266071529; ``` -echo "ANDROID_NDK=.." >> ~/.gradle/gradle.properties -``` - -- For iOS targets: - - Install [cargo-xcode](https://gitlab.com/kornelski/cargo-xcode#installation) -- [Web dependencies](http://cjycode.com/flutter_rust_bridge/template/setup_web.html) for the Web - -Then go ahead and run `flutter run` (for web, run `dart run flutter_rust_bridge:serve` instead). When you're ready, refer to our documentation -[here](https://fzyzcjy.github.io/flutter_rust_bridge/index.html) to learn how to write and use binding code. - -Once you have edited `api.rs` to incorporate your own Rust code, the bridge files `bridge_definitions.dart` and `bridge_generated.dart` are generated using the following command (note: append ` --wasm` to add web support): -### Windows -``` -flutter_rust_bridge_codegen --rust-input native\src\api.rs --dart-output .\lib\bridge_generated.dart --dart-decl-output .\lib\bridge_definitions.dart +#### Output +```rust +BookMetaData { + title: "Le nom de la bête", + author: { + surname: "Daniel", + name: "Easterman", + }, + blurb: "Janvier 1999. Peu à peu, les pays arabes ont sombré dans l'intégrisme. Les attentats terroristes se multiplient en Europe attisant la haine et le racisme. Au Caire, un coup d'état fomenté par les fondamentalistes permet à leur chef Al-Kourtoubi de s'installer au pouvoir et d'instaurer la terreur. Le réseau des agents secrets britanniques en Égypte ayant été anéanti, Michael Hunt est obligé de reprendre du service pour enquêter sur place. Aidé par son frère Paul, prêtre catholique et agent du Vatican, il apprend que le Pape doit se rendre à Jérusalem pour participer à une conférence œcuménique. Au courant de ce projet, le chef des fondamentalistes a prévu d'enlever le saint père.Dans ce récit efficace et à l'action soutenue, le héros lutte presque seul contre des groupes fanatiques puissants et sans grand espoir de réussir. Comme dans tous ses autres livres, Daniel Easterman, spécialiste de l'islam, part du constat que le Mal est puissant et il dénonce l'intolérance et les nationalismes qui engendrent violence et chaos.--Claude Mesplède
\t\t", + key_words: [ + "roman", "fantastique", "policier historique", "romans policiers et polars", "thriller", "terreur", "action", "démocratie", "mystique", "islam", "intégrisme religieux", "catholicisme", "religion", "terrorisme", "extrémisme", "egypte", "médias", "thriller religieux", "littérature irlandaise", "irlande" + ], +} ``` -### Linux/MacOS/any other Unix -``` -flutter_rust_bridge_codegen --rust-input native/src/api.rs --dart-output ./lib/bridge_generated.dart --dart-decl-output ./lib/bridge_definitions.dart +### Sources + +| Source | Metadata (in addition to title and authors) | Notes | +|-------------------------------------------------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Babelio](https://www.babelio.com/) | blurb, keyword | No API available. No plan to build one.
Babelio seem to block the IP if it detect this bot is doing some scrapping | +| [Decitre](https://www.decitre.fr/) | blurb, keywords in commentaries | | +| [GoodReads](https://www.goodreads.com/) | blurb, genres in english | An API was available, but GoodRead does not create new developer key. [See this](https://help.goodreads.com/s/article/Does-Goodreads-support-the-use-of-APIs) | +| [Google Books](https://www.google.fr/books/) | blurb, genres | [A real API](https://developers.google.com/books/docs/overview) is available to look up a book by ISBN
Some book can't be search by ISBN, even though a search by title can find them, and they display the right ISBN | +| [ISBSearcher](https://www.isbnsearcher.com/) | blurb, main category in english | | +| [Label Emmaus](https://www.label-emmaus.co/) | blurb, genres | | +| [OpenLibrary](https://openlibrary.org/) | blurb are not translated | Its is based on physical books, it is not really a book database | +| [Chasse Aux Livre](https://www.chasse-aux-livres.fr/) | price only | it is not possible to parse with Selenium | +| [AbeBooks](https://www.abebooks.fr/) | Seems to have good french blurb | | + +#### GoogleBooks +GoogleBooks has some inconsistencies: +https://www.googleapis.com/books/v1/volumes?q=isbn:9782744170812 +says te publishedDate is 2004. +But https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ +says the publishedDate is 2005. + +In the first response, we don't have a publisher, in the second we have. +In the first response, the title use a big C for "Cité", but in the second, it use a small 'c' + +## Contributing +### Build the barcode detector binary +Clone the 3 OpenCV repo: +- https://github.com/opencv/opencv.git (main repo) +- https://github.com/opencv/opencv_contrib.git (contain the barcode contrib module) +- https://github.com/opencv/opencv_extra.git (optionnal, contain the test data to test OpenCV) + +```shell +$ cd / +$ mkdir build +$ cd build/ +build/ $ cmake -DOPENCV_EXTRA_MODULES_PATH=/modules .. ``` -## Scaffolding in existing projects - -If you would like to generate boilerplate for using `flutter_rust_bridge` in your existing projects, -check out the [`flutter_rust_bridge` brick](https://brickhub.dev/bricks/flutter_rust_bridge/) -for more details. - -## Disclaimer - -This template is not affiliated with flutter_rust_bridge. Please file issues and PRs related to the template here, -not flutter_rust_bridge. - -## License - -Copyright 2022 Viet Dinh. - -This template is licensed under either of -- [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) ([LICENSE-APACHE](LICENSE-APACHE)) -- [MIT license](https://opensource.org/licenses/MIT) ([LICENSE-MIT](LICENSE-MIT)) - -at your option. +You can test the barcode module with: +```shell +build/ $ make opencv_test_barcode +build/ $ OPENCV_TEST_DATA_PATH=/testdata/ bin/opencv_test_barcode +``` -The [SPDX](https://spdx.dev/) license identifier for this project is `MIT OR Apache-2.0`. +### Install the rust/android toolchain +#### flutter_rust_bridge_template +Follow the instruction of flutter_rust_bridge_template. Here is an extract + +> To begin, ensure that you have a working installation of the following items: +> - [Flutter SDK](https://docs.flutter.dev/get-started/install) +> - [Rust language](https://rustup.rs/) +> - `flutter_rust_bridge_codegen` [cargo package](https://cjycode.com/flutter_rust_bridge/integrate/deps.html#build-time-dependencies) +> - Appropriate [Rust targets](https://rust-lang.github.io/rustup/cross-compilation.html) for cross-compiling to your device +> - For Android targets: +> - Install [cargo-ndk](https://github.com/bbqsrc/cargo-ndk#installing) +> - Install [Android NDK 22](https://github.com/android/ndk/wiki/Unsupported-Downloads#r22b), then put its path in one of the `gradle.properties`, e.g.: +> +> ``` +> echo "ANDROID_NDK=.." >> ~/.gradle/gradle.properties +> ``` + +#### super_native_extension +Follow this tutorial: https://pub.dev/packages/super_clipboard \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..fc356db --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# TODO + +* [ ] Grab the ISBN in real-time with ML Kit +* [ ] Search with Selenium in headless mode +* [ ] Price auto fill +* [ ] Compress the images to upload them quicker +* [ ] Launch the scrapping asynchronously to avoid waiting for the provider (notably BooksPrice) diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..456dfc6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,29 +1,19 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + use_key_in_widget_constructors: false + avoid_print: false + prefer_single_quotes: true + prefer_interpolation_to_compose_strings: false + prefer_is_empty: false + avoid_function_literals_in_foreach_calls: false +analyzer: + enable-experiment: + - records + - patterns + - sealed-class + language: + strict-casts: true + strict-inference: true + strict-raw-types: true \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index c1b36a9..9430ef7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + ndkVersion "25.2.9519653" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +47,7 @@ android { applicationId "com.example.flutter_rust_bridge_template" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d555ba0..91be786 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..ddbbffb 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +ANDROID_NDK=/home/julien/Android/Sdk/ndk/ diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 9625e10..0000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 11.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index b7d7efb..0000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,560 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A3671AD929957A9600604FF0 /* libnative_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A3671AD529957A8000604FF0 /* libnative_static.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - A3671AD229957A8000604FF0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A3671ACD29957A7F00604FF0 /* native.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA603EE7FF4DEAC3B2E0A336; - remoteInfo = "native-cdylib"; - }; - A3671AD429957A8000604FF0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A3671ACD29957A7F00604FF0 /* native.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA60F0FEBE702969E816930C; - remoteInfo = "native-staticlib"; - }; - A3671AD629957A8A00604FF0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A3671ACD29957A7F00604FF0 /* native.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = CA60F0FEBE701C83950DBC35; - remoteInfo = "native-staticlib"; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A3671ACD29957A7F00604FF0 /* native.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = native.xcodeproj; path = ../native/native.xcodeproj; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A3671AD929957A9600604FF0 /* libnative_static.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - A3671ACD29957A7F00604FF0 /* native.xcodeproj */, - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - A3671AD829957A9600604FF0 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - A3671ACE29957A7F00604FF0 /* Products */ = { - isa = PBXGroup; - children = ( - A3671AD329957A8000604FF0 /* native.dylib */, - A3671AD529957A8000604FF0 /* libnative_static.a */, - ); - name = Products; - sourceTree = ""; - }; - A3671AD829957A9600604FF0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - A3671AD729957A8A00604FF0 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectReferences = ( - { - ProductGroup = A3671ACE29957A7F00604FF0 /* Products */; - ProjectRef = A3671ACD29957A7F00604FF0 /* native.xcodeproj */; - }, - ); - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXReferenceProxy section */ - A3671AD329957A8000604FF0 /* native.dylib */ = { - isa = PBXReferenceProxy; - fileType = "compiled.mach-o.dylib"; - path = native.dylib; - remoteRef = A3671AD229957A8000604FF0 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - A3671AD529957A8000604FF0 /* libnative_static.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libnative_static.a; - remoteRef = A3671AD429957A8000604FF0 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - A3671AD729957A8A00604FF0 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "native-staticlib"; - targetProxy = A3671AD629957A8A00604FF0 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeTemplate; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeTemplate; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeTemplate; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index c87d15a..0000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index 1958642..0000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - print("dummy_value=\(dummy_method_to_enforce_bundling())"); - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d93..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b00..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe73094..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32a..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index 182d522..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,51 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Flutter Rust Bridge Template - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - flutter_rust_bridge_template - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index ffb33c6..0000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1,2 +0,0 @@ -#import "GeneratedPluginRegistrant.h" -#import "bridge_generated.h" diff --git a/ios/Runner/bridge_generated.h b/ios/Runner/bridge_generated.h deleted file mode 100644 index ae8c386..0000000 --- a/ios/Runner/bridge_generated.h +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -typedef struct _Dart_Handle* Dart_Handle; - -typedef struct DartCObject DartCObject; - -typedef int64_t DartPort; - -typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message); - -typedef struct DartCObject *WireSyncReturn; - -void store_dart_post_cobject(DartPostCObjectFnType ptr); - -Dart_Handle get_dart_object(uintptr_t ptr); - -void drop_dart_object(uintptr_t ptr); - -uintptr_t new_dart_opaque(Dart_Handle handle); - -intptr_t init_frb_dart_api_dl(void *obj); - -void wire_platform(int64_t port_); - -void wire_rust_release_mode(int64_t port_); - -void free_WireSyncReturn(WireSyncReturn ptr); - -static int64_t dummy_method_to_enforce_bundling(void) { - int64_t dummy_var = 0; - dummy_var ^= ((int64_t) (void*) wire_platform); - dummy_var ^= ((int64_t) (void*) wire_rust_release_mode); - dummy_var ^= ((int64_t) (void*) free_WireSyncReturn); - dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); - dummy_var ^= ((int64_t) (void*) get_dart_object); - dummy_var ^= ((int64_t) (void*) drop_dart_object); - dummy_var ^= ((int64_t) (void*) new_dart_opaque); - return dummy_var; -} \ No newline at end of file diff --git a/justfile b/justfile index 77f38c2..9faa856 100644 --- a/justfile +++ b/justfile @@ -1,16 +1,15 @@ -default: gen lint +default: gen fmt gen: flutter pub get flutter_rust_bridge_codegen \ --rust-input native/src/api.rs \ --dart-output lib/bridge_generated.dart \ - --c-output ios/Runner/bridge_generated.h \ --dart-decl-output lib/bridge_definitions.dart \ - --wasm - cp ios/Runner/bridge_generated.h macos/Runner/bridge_generated.h + # --wasm + # cp ios/Runner/bridge_generated.h macos/Runner/bridge_generated.h -lint: +fmt: cd native && cargo fmt dart format . diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index fadd6b6..bf214fa 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -1,31 +1,76 @@ // AUTO GENERATED FILE, DO NOT EDIT. -// Generated by `flutter_rust_bridge`@ 1.62.1. -// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member +// Generated by `flutter_rust_bridge`@ 1.68.0. +// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member, prefer_is_empty, unnecessary_const -import 'bridge_generated.io.dart' - if (dart.library.html) 'bridge_generated.web.dart'; -import 'dart:convert'; import 'dart:async'; -import 'package:meta/meta.dart'; +import 'dart:convert'; + import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'package:meta/meta.dart'; abstract class Native { - Future platform({dynamic hint}); + Future getMetadataFromProvider( + {required ProviderEnum provider, required String isbn, dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; + + Future publishAd({required Ad ad, required LbcCredential credential, dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; +} - FlutterRustBridgeTaskConstMeta get kPlatformConstMeta; +class Ad { + String title; + String description; + int priceCent; + List imgsPath; + + Ad({ + required this.title, + required this.description, + required this.priceCent, + required this.imgsPath, + }); +} + +class Author { + final String firstName; + final String lastName; + + const Author({ + required this.firstName, + required this.lastName, + }); +} + +class BookMetaDataFromProvider { + String? title; + List authors; + String? blurb; + List keywords; + Float32List marketPrice; + + BookMetaDataFromProvider({ + this.title, + required this.authors, + this.blurb, + required this.keywords, + required this.marketPrice, + }); +} - Future rustReleaseMode({dynamic hint}); +class LbcCredential { + String lbcToken; + String datadomeCookie; - FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta; + LbcCredential({ + required this.lbcToken, + required this.datadomeCookie, + }); } -enum Platform { - Unknown, - Android, - Ios, - Windows, - Unix, - MacIntel, - MacApple, - Wasm, +enum ProviderEnum { + Babelio, + GoogleBooks, + BooksPrice, } diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 7501e4b..c53f603 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -1,14 +1,19 @@ // AUTO GENERATED FILE, DO NOT EDIT. -// Generated by `flutter_rust_bridge`@ 1.62.1. -// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member +// Generated by `flutter_rust_bridge`@ 1.68.0. +// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member, prefer_is_empty, unnecessary_const import "bridge_definitions.dart"; import 'dart:convert'; import 'dart:async'; import 'package:meta/meta.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; -import 'bridge_generated.io.dart' - if (dart.library.html) 'bridge_generated.web.dart'; + +import 'dart:convert'; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; + +import 'dart:ffi' as ffi; class NativeImpl implements Native { final NativePlatform _platform; @@ -19,36 +24,43 @@ class NativeImpl implements Native { factory NativeImpl.wasm(FutureOr module) => NativeImpl(module as ExternalLibrary); NativeImpl.raw(this._platform); - Future platform({dynamic hint}) { + Future getMetadataFromProvider( + {required ProviderEnum provider, required String isbn, dynamic hint}) { + var arg0 = api2wire_provider_enum(provider); + var arg1 = _platform.api2wire_String(isbn); return _platform.executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => _platform.inner.wire_platform(port_), - parseSuccessData: _wire2api_platform, - constMeta: kPlatformConstMeta, - argValues: [], + callFfi: (port_) => + _platform.inner.wire_get_metadata_from_provider(port_, arg0, arg1), + parseSuccessData: _wire2api_opt_box_autoadd_book_meta_data_from_provider, + constMeta: kGetMetadataFromProviderConstMeta, + argValues: [provider, isbn], hint: hint, )); } - FlutterRustBridgeTaskConstMeta get kPlatformConstMeta => + FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta => const FlutterRustBridgeTaskConstMeta( - debugName: "platform", - argNames: [], + debugName: "get_metadata_from_provider", + argNames: ["provider", "isbn"], ); - Future rustReleaseMode({dynamic hint}) { + Future publishAd( + {required Ad ad, required LbcCredential credential, dynamic hint}) { + var arg0 = _platform.api2wire_box_autoadd_ad(ad); + var arg1 = _platform.api2wire_box_autoadd_lbc_credential(credential); return _platform.executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => _platform.inner.wire_rust_release_mode(port_), + callFfi: (port_) => _platform.inner.wire_publish_ad(port_, arg0, arg1), parseSuccessData: _wire2api_bool, - constMeta: kRustReleaseModeConstMeta, - argValues: [], + constMeta: kPublishAdConstMeta, + argValues: [ad, credential], hint: hint, )); } - FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta => + FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta => const FlutterRustBridgeTaskConstMeta( - debugName: "rust_release_mode", - argNames: [], + debugName: "publish_ad", + argNames: ["ad", "credential"], ); void dispose() { @@ -56,19 +68,399 @@ class NativeImpl implements Native { } // Section: wire2api + String _wire2api_String(dynamic raw) { + return raw as String; + } + + List _wire2api_StringList(dynamic raw) { + return (raw as List).cast(); + } + + Author _wire2api_author(dynamic raw) { + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return Author( + firstName: _wire2api_String(arr[0]), + lastName: _wire2api_String(arr[1]), + ); + } + + BookMetaDataFromProvider _wire2api_book_meta_data_from_provider(dynamic raw) { + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return BookMetaDataFromProvider( + title: _wire2api_opt_String(arr[0]), + authors: _wire2api_list_author(arr[1]), + blurb: _wire2api_opt_String(arr[2]), + keywords: _wire2api_StringList(arr[3]), + marketPrice: _wire2api_float_32_list(arr[4]), + ); + } + bool _wire2api_bool(dynamic raw) { return raw as bool; } - int _wire2api_i32(dynamic raw) { + BookMetaDataFromProvider _wire2api_box_autoadd_book_meta_data_from_provider( + dynamic raw) { + return _wire2api_book_meta_data_from_provider(raw); + } + + double _wire2api_f32(dynamic raw) { + return raw as double; + } + + Float32List _wire2api_float_32_list(dynamic raw) { + return raw as Float32List; + } + + List _wire2api_list_author(dynamic raw) { + return (raw as List).map(_wire2api_author).toList(); + } + + String? _wire2api_opt_String(dynamic raw) { + return raw == null ? null : _wire2api_String(raw); + } + + BookMetaDataFromProvider? + _wire2api_opt_box_autoadd_book_meta_data_from_provider(dynamic raw) { + return raw == null + ? null + : _wire2api_box_autoadd_book_meta_data_from_provider(raw); + } + + int _wire2api_u8(dynamic raw) { return raw as int; } - Platform _wire2api_platform(dynamic raw) { - return Platform.values[raw]; + Uint8List _wire2api_uint_8_list(dynamic raw) { + return raw as Uint8List; } } // Section: api2wire +@protected +int api2wire_i32(int raw) { + return raw; +} + +@protected +int api2wire_provider_enum(ProviderEnum raw) { + return api2wire_i32(raw.index); +} + +@protected +int api2wire_u8(int raw) { + return raw; +} + +// Section: finalizer + +class NativePlatform extends FlutterRustBridgeBase { + NativePlatform(ffi.DynamicLibrary dylib) : super(NativeWire(dylib)); + +// Section: api2wire + + @protected + ffi.Pointer api2wire_String(String raw) { + return api2wire_uint_8_list(utf8.encoder.convert(raw)); + } + + @protected + ffi.Pointer api2wire_StringList(List raw) { + final ans = inner.new_StringList_0(raw.length); + for (var i = 0; i < raw.length; i++) { + ans.ref.ptr[i] = api2wire_String(raw[i]); + } + return ans; + } + + @protected + ffi.Pointer api2wire_box_autoadd_ad(Ad raw) { + final ptr = inner.new_box_autoadd_ad_0(); + _api_fill_to_wire_ad(raw, ptr.ref); + return ptr; + } + + @protected + ffi.Pointer api2wire_box_autoadd_lbc_credential( + LbcCredential raw) { + final ptr = inner.new_box_autoadd_lbc_credential_0(); + _api_fill_to_wire_lbc_credential(raw, ptr.ref); + return ptr; + } + + @protected + ffi.Pointer api2wire_uint_8_list(Uint8List raw) { + final ans = inner.new_uint_8_list_0(raw.length); + ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); + return ans; + } // Section: finalizer + +// Section: api_fill_to_wire + + void _api_fill_to_wire_ad(Ad apiObj, wire_Ad wireObj) { + wireObj.title = api2wire_String(apiObj.title); + wireObj.description = api2wire_String(apiObj.description); + wireObj.price_cent = api2wire_i32(apiObj.priceCent); + wireObj.imgs_path = api2wire_StringList(apiObj.imgsPath); + } + + void _api_fill_to_wire_box_autoadd_ad( + Ad apiObj, ffi.Pointer wireObj) { + _api_fill_to_wire_ad(apiObj, wireObj.ref); + } + + void _api_fill_to_wire_box_autoadd_lbc_credential( + LbcCredential apiObj, ffi.Pointer wireObj) { + _api_fill_to_wire_lbc_credential(apiObj, wireObj.ref); + } + + void _api_fill_to_wire_lbc_credential( + LbcCredential apiObj, wire_LbcCredential wireObj) { + wireObj.lbc_token = api2wire_String(apiObj.lbcToken); + wireObj.datadome_cookie = api2wire_String(apiObj.datadomeCookie); + } +} + +// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. + +/// generated by flutter_rust_bridge +class NativeWire implements FlutterRustBridgeWireBase { + @internal + late final dartApi = DartApiDl(init_frb_dart_api_dl); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + NativeWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + NativeWire.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + void store_dart_post_cobject( + DartPostCObjectFnType ptr, + ) { + return _store_dart_post_cobject( + ptr, + ); + } + + late final _store_dart_post_cobjectPtr = + _lookup>( + 'store_dart_post_cobject'); + late final _store_dart_post_cobject = _store_dart_post_cobjectPtr + .asFunction(); + + Object get_dart_object( + int ptr, + ) { + return _get_dart_object( + ptr, + ); + } + + late final _get_dart_objectPtr = + _lookup>( + 'get_dart_object'); + late final _get_dart_object = + _get_dart_objectPtr.asFunction(); + + void drop_dart_object( + int ptr, + ) { + return _drop_dart_object( + ptr, + ); + } + + late final _drop_dart_objectPtr = + _lookup>( + 'drop_dart_object'); + late final _drop_dart_object = + _drop_dart_objectPtr.asFunction(); + + int new_dart_opaque( + Object handle, + ) { + return _new_dart_opaque( + handle, + ); + } + + late final _new_dart_opaquePtr = + _lookup>( + 'new_dart_opaque'); + late final _new_dart_opaque = + _new_dart_opaquePtr.asFunction(); + + int init_frb_dart_api_dl( + ffi.Pointer obj, + ) { + return _init_frb_dart_api_dl( + obj, + ); + } + + late final _init_frb_dart_api_dlPtr = + _lookup)>>( + 'init_frb_dart_api_dl'); + late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr + .asFunction)>(); + + void wire_get_metadata_from_provider( + int port_, + int provider, + ffi.Pointer isbn, + ) { + return _wire_get_metadata_from_provider( + port_, + provider, + isbn, + ); + } + + late final _wire_get_metadata_from_providerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Int32, ffi.Pointer)>>( + 'wire_get_metadata_from_provider'); + late final _wire_get_metadata_from_provider = + _wire_get_metadata_from_providerPtr + .asFunction)>(); + + void wire_publish_ad( + int port_, + ffi.Pointer ad, + ffi.Pointer credential, + ) { + return _wire_publish_ad( + port_, + ad, + credential, + ); + } + + late final _wire_publish_adPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_publish_ad'); + late final _wire_publish_ad = _wire_publish_adPtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer new_StringList_0( + int len, + ) { + return _new_StringList_0( + len, + ); + } + + late final _new_StringList_0Ptr = _lookup< + ffi.NativeFunction Function(ffi.Int32)>>( + 'new_StringList_0'); + late final _new_StringList_0 = _new_StringList_0Ptr + .asFunction Function(int)>(); + + ffi.Pointer new_box_autoadd_ad_0() { + return _new_box_autoadd_ad_0(); + } + + late final _new_box_autoadd_ad_0Ptr = + _lookup Function()>>( + 'new_box_autoadd_ad_0'); + late final _new_box_autoadd_ad_0 = + _new_box_autoadd_ad_0Ptr.asFunction Function()>(); + + ffi.Pointer new_box_autoadd_lbc_credential_0() { + return _new_box_autoadd_lbc_credential_0(); + } + + late final _new_box_autoadd_lbc_credential_0Ptr = + _lookup Function()>>( + 'new_box_autoadd_lbc_credential_0'); + late final _new_box_autoadd_lbc_credential_0 = + _new_box_autoadd_lbc_credential_0Ptr + .asFunction Function()>(); + + ffi.Pointer new_uint_8_list_0( + int len, + ) { + return _new_uint_8_list_0( + len, + ); + } + + late final _new_uint_8_list_0Ptr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int32)>>('new_uint_8_list_0'); + late final _new_uint_8_list_0 = _new_uint_8_list_0Ptr + .asFunction Function(int)>(); + + void free_WireSyncReturn( + WireSyncReturn ptr, + ) { + return _free_WireSyncReturn( + ptr, + ); + } + + late final _free_WireSyncReturnPtr = + _lookup>( + 'free_WireSyncReturn'); + late final _free_WireSyncReturn = + _free_WireSyncReturnPtr.asFunction(); +} + +class _Dart_Handle extends ffi.Opaque {} + +class wire_uint_8_list extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + +class wire_StringList extends ffi.Struct { + external ffi.Pointer> ptr; + + @ffi.Int32() + external int len; +} + +class wire_Ad extends ffi.Struct { + external ffi.Pointer title; + + external ffi.Pointer description; + + @ffi.Int32() + external int price_cent; + + external ffi.Pointer imgs_path; +} + +class wire_LbcCredential extends ffi.Struct { + external ffi.Pointer lbc_token; + + external ffi.Pointer datadome_cookie; +} + +typedef DartPostCObjectFnType = ffi.Pointer< + ffi.NativeFunction)>>; +typedef DartPort = ffi.Int64; diff --git a/lib/bundle.dart b/lib/bundle.dart new file mode 100644 index 0000000..02fff96 --- /dev/null +++ b/lib/bundle.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter_rust_bridge_template/common.dart'; +import 'package:path/path.dart' as path; + +class Bundle { + Bundle(this.directory); + + final Directory directory; + + Iterable get images { + return directory + .listSync() + .whereType() + .where((file) => path.extension(file.path) == '.jpg') + .sorted((f1, f2) => f1.lastModifiedSync().compareTo(f2.lastModifiedSync())); + } + + Metadata get metadata { + final metadataFile = File(path.join(directory.path, 'metadata.json')); + return Metadata.fromJson(jsonDecode(metadataFile.readAsStringSync()) as Map); + } +} diff --git a/lib/camera/camera.dart b/lib/camera/camera.dart new file mode 100644 index 0000000..c5cccaa --- /dev/null +++ b/lib/camera/camera.dart @@ -0,0 +1,472 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:gallery_saver/gallery_saver.dart'; +import 'package:path/path.dart' as path; +import 'package:permission_handler/permission_handler.dart'; + +import '../common.dart' as common; +import 'draggable_widget.dart'; + +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({Key? key}) : super(key: key); + + @override + State createState() { + return _CameraExampleHomeState(); + } +} + +/// Returns a suitable camera icon for [direction]. +IconData getCameraLensIcon(CameraLensDirection direction) { + switch (direction) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; +} + +void _logError(String code, String? message) { + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); +} + +class _CameraExampleHomeState extends State with WidgetsBindingObserver, TickerProviderStateMixin { + CameraController? controller; + XFile? imageFile; + late String bundleName; + + Directory get getBundleDir => Directory(path.join(common.bookyDir.path, bundleName)); + + void _generateNewFolderPath() { + bundleName = DateTime.now().toIso8601String().replaceAll(':', '_'); + } + + @override + void initState() { + super.initState(); + _generateNewFolderPath(); + WidgetsBinding.instance.addObserver(this); + + Future(() async { + try { + // WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + _onNewCameraSelected(_cameras.first); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + // #docregion AppLifecycle + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + _onNewCameraSelected(cameraController.description); + } + } + // #enddocregion AppLifecycle + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Booky Camera app'), actions: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + child: const Text('Change camera'), + onTap: () async { + await Future.delayed(const Duration(seconds: 0), () async { + await showDialog( + context: context, + builder: (BuildContext _) => SimpleDialog( + title: const Text('Select camera'), + children: _cameras + .where((c) => c.lensDirection == CameraLensDirection.back) + .map((c) => SimpleDialogOption( + onPressed: () => _onNewCameraSelected(c), + child: Text('Camera ${c.name}'), + )) + .toList()), + ); + }); + }) + ], + ), + ]), + body: Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: BottomWidget( + directory: getBundleDir, + onSubmit: () { + setState(() { + _generateNewFolderPath(); + }); + Navigator.pop(context); + }), + ), + ], + ), + ); + } + + /// Display the preview from the camera (or a message if the preview is not available). + Widget _cameraPreviewWidget() { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return const Text( + 'Tap a camera', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + return CameraPreview( + controller!, + child: LayoutBuilder( + builder: (context, boxConstraints) => GestureDetector( + onTapDown: (TapDownDetails details) async { + _onViewFinderTap(details, boxConstraints); + // The auto focus is not instantaneous. We must wait a little while before taking the picture + // In release mode, if we + // wait 100 ms : blurry + // wait 300 ms : sharp + // The optimum delay shall lie between the bounds + await Future.delayed(const Duration(milliseconds: 300)); + _onTakePictureButtonPressed(); + }, + ), + ), + ); + } + } + + void showInSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + } + + void _onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final Offset offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + Future _onNewCameraSelected(CameraDescription cameraDescription) async { + final CameraController? oldController = controller; + if (oldController != null) { + // `controller` needs to be set to null before getting disposed, + // to avoid a race condition when we use the controller that is being + // disposed. This happens when camera permission dialog shows up, + // which triggers `didChangeAppLifecycleState`, which disposes and + // re-creates the controller. + controller = null; + await oldController.dispose(); + } + + final CameraController cameraController = CameraController( + cameraDescription, + ResolutionPreset.max, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError) { + showInSnackBar('Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + } on CameraException catch (e) { + switch (e.code) { + case 'CameraAccessDenied': + showInSnackBar('You have denied camera access.'); + break; + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable camera access.'); + break; + case 'CameraAccessRestricted': + // iOS only + showInSnackBar('Camera access is restricted.'); + break; + case 'AudioAccessDenied': + showInSnackBar('You have denied audio access.'); + break; + case 'AudioAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable audio access.'); + break; + case 'AudioAccessRestricted': + // iOS only + showInSnackBar('Audio access is restricted.'); + break; + default: + _showCameraException(e); + break; + } + } + + if (mounted) { + setState(() {}); + } + } + + void _onTakePictureButtonPressed() { + takePicture().then((XFile? file) async { + if (mounted) { + if (file != null) { + GallerySaver.saveImage(file.path, albumName: 'booky/$bundleName', toDcim: true).then((bool? success) { + if (success != true) { + showInSnackBar('Error when saving image'); + } else { + setState(() { + imageFile = file; + }); + } + }); + } + } + }); + } + + Future onCaptureOrientationLockButtonPressed() async { + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } on CameraException catch (e) { + _showCameraException(e); + } + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + final XFile file = await cameraController.takePicture(); + return file; + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(CameraException e) { + _logError(e.code, e.description); + showInSnackBar('Error: ${e.code}\n${e.description}'); + } +} + +class BottomWidget extends StatefulWidget { + const BottomWidget({required this.directory, required this.onSubmit}); + final Directory directory; + final void Function() onSubmit; + + @override + State createState() => _BottomWidgetState(); +} + +class _BottomWidgetState extends State { + @override + Widget build(BuildContext context) { + try { + final images = widget.directory.listSync().where((file) => path.extension(file.path) == '.jpg'); + return Row( + children: [ + _thumbnailWidget(images), + _addMetadataButton(context: context, directory: widget.directory, onSubmit: widget.onSubmit), + ], + ); + } on PathNotFoundException { + return const Text('Tap the screen to take a picture'); + } + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget(Iterable images) { + return Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: images + .map((imgFile) => SizedBox( + width: 64, + height: 64, + child: DraggableWidget( + // Use a key otherwise if we delete an image, the image that will take its place will inherit the state of the deleted image + key: ValueKey(imgFile.path), + child: Image.file(File(imgFile.path)), + onVerticalDrag: () => setState(() { + imgFile.deleteSync(); + })), + )) + .toList(), + ), + ), + ); + } + + Widget _addMetadataButton( + {required BuildContext context, required Directory directory, required void Function() onSubmit}) => + IconButton( + icon: const Icon(Icons.keyboard_arrow_right), + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => MetadataWidget(directory: directory, onSubmit: onSubmit))); +} + +class MetadataWidget extends StatefulWidget { + const MetadataWidget({ + required this.directory, + required this.onSubmit, + }); + final Directory directory; + final void Function() onSubmit; + + @override + State createState() => _MetadataWidgetState(); +} + +class _MetadataWidgetState extends State { + var metadata = common.Metadata(); + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: const Text('Add the final metadata'), + children: [ + TextFormField( + initialValue: '', + autofocus: true, + onChanged: (newText) => setState(() => metadata.weightGrams = int.parse(newText)), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + icon: Icon(Icons.scale), + labelText: 'Weight in grams', + ), + style: const TextStyle(fontSize: 20), + ), + DropdownButton( + hint: const Text('Book state'), + value: metadata.itemState, + items: common.ItemState.values.map((s) => DropdownMenuItem(value: s, child: Text(s.loc))).toList(), + onChanged: (state) => setState(() { + metadata.itemState = state; + })), + IconButton( + icon: const Icon(Icons.save), + onPressed: () async { + final managePerm = await Permission.manageExternalStorage.request(); + print('managePerm = $managePerm'); + File(path.join(widget.directory.path, 'metadata.json')).writeAsStringSync(jsonEncode(metadata.toJson())); + widget.onSubmit(); + }) + ], + ); + } +} + +/// CameraApp is the Main Application. +class CameraApp extends StatelessWidget { + /// Default Constructor + const CameraApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: CameraExampleHome(), + ); + } +} + +List _cameras = []; +/* + +Future main() async { + runApp(const MaterialApp(home: Explorer())); + // Fetch the available cameras before initializing the app. + */ +/*try { + WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp());*/ /* + +} +*/ diff --git a/lib/camera/draggable_widget.dart b/lib/camera/draggable_widget.dart new file mode 100644 index 0000000..325996e --- /dev/null +++ b/lib/camera/draggable_widget.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class DraggableWidget extends StatefulWidget { + const DraggableWidget({required super.key, required this.child, required this.onVerticalDrag}); + + final Widget child; + final void Function() onVerticalDrag; + + @override + State createState() => _DraggableWidgetState(); +} + +class _DraggableWidgetState extends State { + bool showDismiss = false; + Offset? startPosition; + @override + Widget build(BuildContext context) { + return GestureDetector( + child: showDismiss + ? Stack( + fit: StackFit.expand, + children: [widget.child, ColoredBox(color: Colors.white.withOpacity(0.8))], + ) + : widget.child, + onVerticalDragStart: (details) { + startPosition = details.globalPosition; + }, + onVerticalDragUpdate: (details) { + final dy = (startPosition! - details.globalPosition).dy; + const maxDy = 50; + if (dy > maxDy && !showDismiss) { + setState(() => showDismiss = true); + } else if (dy < maxDy && showDismiss) { + setState(() => showDismiss = false); + } + }, + onVerticalDragEnd: (details) { + if (showDismiss) { + widget.onVerticalDrag(); + } + }, + ); + } +} diff --git a/lib/common.dart b/lib/common.dart new file mode 100644 index 0000000..4ef9a58 --- /dev/null +++ b/lib/common.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'common.g.dart'; + +final bookyDir = Platform.isAndroid + ? Directory('/storage/emulated/0/DCIM/booky/') + : Directory('/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/booky/'); + +enum ItemState { + brandNew, + veryGood, + good, + medium; + + String get loc { + switch (this) { + case ItemState.brandNew: + return 'Brand New'; + case ItemState.veryGood: + return 'Very Good'; + case ItemState.good: + return 'Good'; + case ItemState.medium: + return 'Medium'; + } + } +} + +@JsonSerializable() +class Metadata { + Metadata({this.weightGrams, this.itemState}); + int? weightGrams; + ItemState? itemState; + + factory Metadata.fromJson(Map json) => _$MetadataFromJson(json); + + Map toJson() => _$MetadataToJson(this); +} diff --git a/lib/common.g.dart b/lib/common.g.dart new file mode 100644 index 0000000..915ee66 --- /dev/null +++ b/lib/common.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'common.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Metadata _$MetadataFromJson(Map json) => Metadata( + weightGrams: json['weightGrams'] as int?, + itemState: $enumDecodeNullable(_$ItemStateEnumMap, json['itemState']), + ); + +Map _$MetadataToJson(Metadata instance) => { + 'weightGrams': instance.weightGrams, + 'itemState': _$ItemStateEnumMap[instance.itemState], + }; + +const _$ItemStateEnumMap = { + ItemState.brandNew: 'brandNew', + ItemState.veryGood: 'veryGood', + ItemState.good: 'good', + ItemState.medium: 'medium', +}; diff --git a/lib/copiable_text_field.dart b/lib/copiable_text_field.dart new file mode 100644 index 0000000..10c164d --- /dev/null +++ b/lib/copiable_text_field.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class CopiableTextField extends StatelessWidget { + const CopiableTextField(this.textFormField); + final TextFormField textFormField; + + @override + Widget build(BuildContext context) => Row( + children: [ + IconButton( + onPressed: () async { + final item = DataWriterItem(); + item.add(Formats.plainText(textFormField.controller!.text)); + await ClipboardWriter.instance.write([item]); + }, + icon: const Icon(Icons.copy)), + Expanded(child: textFormField), + ], + ); +} diff --git a/lib/drag_and_drop.dart b/lib/drag_and_drop.dart new file mode 100644 index 0000000..be36a4d --- /dev/null +++ b/lib/drag_and_drop.dart @@ -0,0 +1,207 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:super_clipboard/super_clipboard.dart'; +import 'package:super_drag_and_drop/super_drag_and_drop.dart'; + +class SelectImages extends StatelessWidget { + const SelectImages({required this.onSelect}); + final void Function(List paths) onSelect; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Drop the images to create a new ad'), + ), + body: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.blueGrey.shade200), + borderRadius: BorderRadius.circular(14), + ), + child: _DropZone(onSelect: onSelect), + ), + ); +} + +extension _ReadValue on DataReader { + Future readValue(ValueFormat format) { + final c = Completer(); + final progress = getValue(format, (value) { + c.complete(value); + }, onError: (e) { + c.completeError(e); + }); + if (progress == null) { + c.complete(null); + } + return c.future; + } +} + +class _DropZone extends StatefulWidget { + const _DropZone({required this.onSelect}); + final void Function(List paths) onSelect; + + @override + State createState() => _DropZoneState(); +} + +class _DropZoneState extends State<_DropZone> { + @override + Widget build(BuildContext context) { + return DropRegion( + formats: const [ + ...Formats.standardFormats, + ], + hitTestBehavior: HitTestBehavior.opaque, + onDropOver: _onDropOver, + onPerformDrop: _onPerformDrop, + onDropLeave: _onDropLeave, + child: Stack( + children: [ + Positioned.fill(child: _content), + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + opacity: _isDragOver ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: _preview, + ), + ), + ), + ], + ), + ); + } + + DropOperation _onDropOver(DropOverEvent event) { + setState(() { + _isDragOver = true; + _preview = Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(13), + color: Colors.black.withOpacity(0.2), + ), + child: Padding( + padding: const EdgeInsets.all(50), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Text('${event.session.items.length} images selected')), + ), + ), + ), + ), + ); + }); + return event.session.allowedOperations.firstOrNull ?? DropOperation.none; + } + + Future _onPerformDrop(PerformDropEvent event) async { + final pathsRaw = await event.session.items.first.dataReader!.readValue(Formats.plainText); + final paths = pathsRaw!.split('\n'); + final imgsPath = paths.where((e) => e.isNotEmpty).map((rawPath) { + print('A${rawPath}B'); + var path = rawPath; + const mtpPrefix = 'mtp://'; + if (rawPath.startsWith(mtpPrefix)) { + final p = rawPath.substring(mtpPrefix.length).trim(); + // final deviceName = p.substring(0, p.indexOf('/')); + // final pathInDevice = p.substring(p.indexOf('/')); + path = '/run/user/1000/gvfs/mtp:host=' + p.replaceAll('%20', ' '); + } + const filePrefix = 'file://'; + if (rawPath.startsWith(filePrefix)) { + path = rawPath.substring(filePrefix.length).trim(); + } + print('path = $path'); + return path; + }).toList(); + // The good way would be to read the Format.uri + // But it convert the device name to lowercase + // Because the device name can be mixed case it is thus not possible to retrieve the original device name + /*print('len = ${event.session.items}'); + final futureImgPaths = event.session.items.map((item) async { + final dataReader = item.dataReader!; + + for (final f in Formats.standardFormats.whereType()) { + print('f($f) = ${await dataReader.readValue(f)}'); + } + + /*final format = dataReader!.getFormats([Formats.plainText]).first; + switch (format) { + case Formats.plainText: + final text = (await dataReader.readValue(Formats.plainText))!; + break; + }*/ + print('plainText = ${await dataReader.readValue(Formats.plainText)}'); + final uri = await dataReader.readValue(Formats.uri); + // print('uri = ${uri?.uri.toFilePath(windows: false)}'); + + // final text = (await dataReader.readValue(Formats.plainText))!; + final text = uri!.uri.toString(); + print('text is $text'); + const mtpPrefix = 'mtp://'; + var path = text; + if (text.startsWith(mtpPrefix)) { + final p = text.substring(mtpPrefix.length); + final deviceName = p.substring(0, p.indexOf('/')); + final pathInDevice = p.substring(p.indexOf('/')); + path = '/run/user/1000/gvfs/mtp:host=' + deviceName.toUpperCase() + pathInDevice.replaceAll('%20', ' '); + } + print('path = $path'); + return path; + }).toList();*/ + + // final imgPaths = await Future.wait(futureImgPaths); + widget.onSelect(imgsPath); + /* + final dataReader = event.session.items.first.dataReader!; + final sugg = await dataReader.getSuggestedName(); + print('sugg = $sugg'); + + final formats = dataReader.getFormats(Formats.standardFormats); + print("PerformDropEvent = ${formats}"); + final imgsPaths = formats.map((format) async { + switch (format) { + case Formats.plainText: + final text = (await dataReader.readValue(Formats.plainText))!; + print('text is $text'); + const mtpPrefix = 'mtp://'; + var path = text; + if (text.startsWith(mtpPrefix)) { + path = '/run/user/1000/gvfs/mtp:host=' + text.substring(mtpPrefix.length).replaceAll('%20', ' '); + } + print('path = $path'); + break; + default: + print('format not handled'); + } + });*/ + } + + void _onDropLeave(DropEvent event) { + setState(() { + _isDragOver = false; + }); + } + + bool _isDragOver = false; + + Widget _preview = const SizedBox(); + final Widget _content = const Center( + child: Text( + 'Drop images here', + style: TextStyle( + color: Colors.grey, + fontSize: 32, + ), + ), + ); +} diff --git a/lib/draggable_files_widget.dart b/lib/draggable_files_widget.dart new file mode 100644 index 0000000..8cb6c77 --- /dev/null +++ b/lib/draggable_files_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:super_drag_and_drop/super_drag_and_drop.dart'; +import 'package:super_native_extensions/raw_drag_drop.dart' as raw; +import 'package:super_native_extensions/widgets.dart'; + +class DraggableFilesWidget extends StatelessWidget { + const DraggableFilesWidget({required this.uris, required this.child}); + + final Iterable uris; + final Widget child; + + @override + Widget build(BuildContext context) => FallbackSnapshotWidget( + child: Builder( + builder: (context) => BaseDraggableWidget( + hitTestBehavior: HitTestBehavior.deferToChild, + child: child, + dragConfiguration: (location, session) async { + Future getSnapshot(Offset location) async { + final snapshotter = Snapshotter.of(context)!; + final dragSnapshot = await snapshotter.getSnapshot(location, SnapshotType.drag); + + raw.TargetedImage? liftSnapshot; + if (defaultTargetPlatform == TargetPlatform.iOS) { + liftSnapshot = await snapshotter.getSnapshot(location, SnapshotType.lift); + } + + final snapshot = dragSnapshot ?? liftSnapshot ?? await snapshotter.getSnapshot(location, null); + + if (snapshot == null) { + return null; + } + + return DragImage(image: snapshot, liftImage: liftSnapshot); + } + + final dragImage = (await getSnapshot(const Offset(0, 0)))!; + // final r = dragImage!.image.rect; + // print('r = $r'); + + return DragConfiguration( + items: uris + .map((uri) => DragConfigurationItem( + item: DragItem()..add(Formats.uri(NamedUri(uri))), image: dragImage)) + .toList(), + allowedOperations: [DropOperation.copy], + ); + }, + )), + ); +} diff --git a/lib/enrichment/ad_editing.dart b/lib/enrichment/ad_editing.dart new file mode 100644 index 0000000..ed3875d --- /dev/null +++ b/lib/enrichment/ad_editing.dart @@ -0,0 +1,178 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; +import 'package:path/path.dart' as path; + +import '../copiable_text_field.dart'; +import '../draggable_files_widget.dart'; +import '../ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import '../helpers.dart'; +import 'enrichment.dart'; + +class AdEditingWidget extends StatefulWidget { + const AdEditingWidget({required this.step, required this.onSubmit}); + final AdEditingStep step; + final void Function() onSubmit; + + @override + State createState() => _AdEditingWidgetState(); +} + +String vecFmt(Iterable it) { + final vec = it.toList(); + if (vec.length == 0) return ''; + if (vec.length == 1) return 'de ${vec[0]}'; + if (vec.length == 2) return 'de ${vec[0]} et ${vec[1]}'; + throw UnimplementedError('More than 2 authors'); +} + +String _bookFormatTitleAndAuthor(String title, Iterable authors) { + return '"$title" ${vecFmt(authors.map((a) => a.toText()))}'; +} + +class _AdEditingWidgetState extends State { + late Ad ad; + + @override + void initState() { + super.initState(); + final metadataFromIsbn = widget.step.metadata.entries; + + var title = ''; + if (metadataFromIsbn.length == 1) { + final onlyMetadata = metadataFromIsbn.single.value; + title = _bookFormatTitleAndAuthor(onlyMetadata.title!, onlyMetadata.authors); + } + var description = _getDescription(metadataFromIsbn); + + description += '\n\n' + personal_info.customMessage; + + final keywords = metadataFromIsbn.map((entry) => entry.value.keywords).expand((kw) => kw).toSet().join(', '); + if (keywords.isNotEmpty) { + description += '\n\nMots-clés:\n' + keywords; + } + + final totalPrice = metadataFromIsbn.map((e) => e.value.priceCent ?? 0).sum; + + ad = Ad( + title: title, + description: description, + priceCent: totalPrice, + imgsPath: widget.step.bundle.images.map((e) => e.path).toList()); + } + + String _getDescription(Iterable> metadataFromIsbn) { + if (metadataFromIsbn.length == 1) { + final blurb = metadataFromIsbn.single.value.blurb; + if (blurb == null) return ''; + return 'Résumé:\n' + blurb; + } else { + final bookTitles = metadataFromIsbn + .map((entry) => _bookFormatTitleAndAuthor(entry.value.title!, entry.value.authors)) + .join('\n'); + final blurbs = metadataFromIsbn + .map((entry) => + _bookFormatTitleAndAuthor(entry.value.title!, entry.value.authors) + ':\n' + entry.value.blurb!) + .join('\n'); + final description = bookTitles + '\n\nRésumés:\n' + blurbs; + return description; + } + } + + @override + Widget build(BuildContext context) { + final metadata = widget.step.bundle.metadata; + return Scaffold( + appBar: AppBar(title: const Text('Ad editing')), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: SingleChildScrollView( + child: Column( + children: [ + CopiableTextField(TextFormField( + controller: TextEditingController(text: ad.title), + onChanged: (newText) => setState(() => ad.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Ad title', + ), + style: const TextStyle(fontSize: 30), + )), + TextFormField( + initialValue: metadata.itemState?.loc, + decoration: const InputDecoration( + icon: Icon(Icons.diamond), + labelText: 'State', + ), + style: const TextStyle(fontSize: 20), + ), + CopiableTextField(TextFormField( + controller: TextEditingController(text: ad.description), + maxLines: null, + scrollPhysics: const NeverScrollableScrollPhysics(), + onChanged: (newText) => setState(() => ad.description = newText), + decoration: const InputDecoration( + icon: Icon(Icons.text_snippet), + labelText: 'Ad description', + ), + )), + CopiableTextField(TextFormField( + controller: TextEditingController(text: ad.priceCent.divide(100).toString()), + onChanged: (newText) => + setState(() => ad.priceCent = double.tryParse(newText)! /*?*/ .multiply(100).round()), + decoration: const InputDecoration( + icon: Icon(Icons.euro), + labelText: 'Price', + ), + style: const TextStyle(fontSize: 20), + )), + TextFormField( + initialValue: metadata.weightGrams?.toString(), + decoration: const InputDecoration( + icon: Icon(Icons.scale), + labelText: 'Weight (grams)', + ), + style: const TextStyle(fontSize: 20), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row(children: [ + const Icon( + Icons.collections, + color: Colors.grey, + ), + const SizedBox(width: 16), + DraggableFilesWidget( + uris: ad.imgsPath.map((path) => Uri.file(path)), + child: Column( + children: [ + Row( + children: ad.imgsPath.map((img) => ImageWidget(File(img))).toList(), + ), + const Text('Drag and drop images') + ], + ), + ), + ]), + ), + ElevatedButton( + onPressed: () { + final d = widget.step.bundle.directory; + final segments = path.split(d.path); + segments[segments.length - 2] = 'booky_done'; + d.renameSync(path.joinAll(segments)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Moved'), + )); + widget.onSubmit(); + }, + child: const Text('Mark as published')) + ], + ), + ), + ), + ); + } +} diff --git a/lib/enrichment/bundle_selection.dart b/lib/enrichment/bundle_selection.dart new file mode 100644 index 0000000..86910a3 --- /dev/null +++ b/lib/enrichment/bundle_selection.dart @@ -0,0 +1,115 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; + +import '../bundle.dart'; +import '../common.dart' as common; +import '../helpers.dart'; +import 'enrichment.dart'; + +class BundleSelection extends StatefulWidget { + const BundleSelection({required this.onSubmit}); + + final void Function(ISBNDecodingStep newStep) onSubmit; + + @override + State createState() => _BundleSelectionState(); +} + +class _BundleSelectionState extends State { + @override + Widget build(BuildContext context) { + return Scaffold(appBar: AppBar(title: const Text('Bundle Section')), body: _getBody()); + } + + Widget _getBody() { + try { + final bundleDirs = + common.bookyDir.listSync().whereType().sorted((d1, d2) => d1.path.compareTo(d2.path)); + + return GridView.extent( + maxCrossAxisExtent: 500, + childAspectRatio: 2, + children: bundleDirs + .map((d) => Padding( + padding: const EdgeInsets.all(2.0), + child: GestureDetector( + child: BundleWidget(d, onDelete: () { + setState(() {}); + }), + onTap: () => widget.onSubmit(ISBNDecodingStep(bundle: Bundle(d))), + ), + )) + .toList(), + ); + } on PathNotFoundException { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Device not connected', + style: TextStyle(fontSize: 30), + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() {}); + }, + ), + ], + ), + ); + } + } +} + +class BundleWidget extends StatelessWidget { + const BundleWidget(this.directory, {required this.onDelete}); + + final Directory directory; + final void Function() onDelete; + + @override + Widget build(BuildContext context) { + return Card( + // decoration: const BoxDecoration(color: Colors.blue), + child: Column( + children: [ + Text(path.basename(directory.path)), + Expanded( + child: Row( + children: [ + ...directory + .listSync() + .whereType() + .where((f) => path.extension(f.path) == '.jpg') + .sorted((f1, f2) => f1.lastModifiedSync().compareTo(f2.lastModifiedSync())) + .map((f) => Padding( + padding: const EdgeInsets.all(8.0), + child: ImageWidget(f), + )) + .toList(), + const Expanded(child: SizedBox.expand()), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + final segments = path.split(directory.path); + segments[segments.length - 2] = 'booky_deleted'; + directory.renameSync(path.joinAll(segments)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Deleted'), + )); + onDelete(); + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/enrichment/enrichment.dart b/lib/enrichment/enrichment.dart new file mode 100644 index 0000000..a98f98b --- /dev/null +++ b/lib/enrichment/enrichment.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/helpers.dart'; + +import '../bundle.dart'; +import 'ad_editing.dart'; +import 'bundle_selection.dart'; +import 'isbn_decoding.dart'; +import 'metadata_collecting.dart'; + +sealed class BookyStep {} + +class BundleSelectionStep implements BookyStep {} + +class ISBNDecodingStep implements BookyStep { + Bundle bundle; + ISBNDecodingStep({required this.bundle}); +} + +class MetadataCollectingStep implements BookyStep { + Bundle bundle; + Set isbns = {}; + MetadataCollectingStep({required this.bundle, required this.isbns}); +} + +class AdEditingStep implements BookyStep { + Bundle bundle; + + Map metadata = {}; + + AdEditingStep({required this.bundle, required this.metadata}); +} + +class EnrichmentApp extends StatefulWidget { + const EnrichmentApp({Key? key}) : super(key: key); + + @override + State createState() => _EnrichmentAppState(); +} + +class _EnrichmentAppState extends State { + BookyStep step = BundleSelectionStep(); + /*AdEditingStep( + bundle: Bundle( + Directory('/home/julien/Perso/LeBonCoin/chain_automatisation/open_cv_test/test_images/booky_example/normal')), + metadata: { + 'myisbn': BookMetaDataManual( + title: 'Mock title', + authors: [const Author(firstName: 'Mock firstname', lastName: 'mock lastname')], + blurb: 'This is a mock blurb', + keywords: ['kw1', 'kw2', 'kw3'], + priceCent: 1234) + }, + );*/ + /* MetadataCollectingStep(imgsPaths: [ + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg', + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg', + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194753.jpg', + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194758.jpg' + ], isbns: { + '9782253029854', + // '9782277223634', + });*/ + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'BookAdPublisher', + theme: ThemeData(primarySwatch: Colors.blue), + home: switch (step) { + BundleSelectionStep() => + BundleSelection(onSubmit: (ISBNDecodingStep newStep) => setState(() => step = newStep)), + ISBNDecodingStep() => ISBNDecodingWidget( + step: step as ISBNDecodingStep, + onSubmit: (MetadataCollectingStep newStep) => setState(() => step = newStep), + onBack: () => setState(() => step = BundleSelectionStep())), + MetadataCollectingStep() => MetadataCollectingWidget( + step: step as MetadataCollectingStep, + onSubmit: (AdEditingStep newStep) => setState(() => step = newStep)), + AdEditingStep() => + AdEditingWidget(step: step as AdEditingStep, onSubmit: () => setState(() => step = BundleSelectionStep())), + BookyStep() => throw UnimplementedError('Not possible') + }); + } +} diff --git a/lib/enrichment/isbn_decoding.dart b/lib/enrichment/isbn_decoding.dart new file mode 100644 index 0000000..8365e70 --- /dev/null +++ b/lib/enrichment/isbn_decoding.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:kt_dart/collection.dart'; + +import '../helpers.dart'; +import 'enrichment.dart'; + +class ISBNDecodingWidget extends StatefulWidget { + const ISBNDecodingWidget({required this.step, required this.onSubmit, required this.onBack}); + final ISBNDecodingStep step; + final void Function(MetadataCollectingStep newStep) onSubmit; + final void Function() onBack; + + @override + State createState() => _ISBNDecodingWidgetState(); +} + +class _ISBNDecodingWidgetState extends State { + KtMutableMap>> isbns = KtMutableMap.empty(); + + @override + void initState() { + super.initState(); + widget.step.bundle.images.forEach((image) { + final imgPath = image.path; + isbns[imgPath] = Future(() async { + final decoderProcess = await Process.run( + '/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode', + ['-in=' + imgPath]); + if (decoderProcess.exitCode != 0) { + print('stdout is ${decoderProcess.stdout}'); + print('stderr is ${decoderProcess.stderr}'); + throw Exception('decoder status is ${decoderProcess.exitCode}'); + } + final s = decoderProcess.stdout as String; + return s.split('\n').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: widget.onBack, + ), + title: const Text('ISBN decoding')), + body: Column( + children: [ + Wrap( + children: widget.step.bundle.images + .map((imgPath) => Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SizedBox(height: 300, child: ImageWidget(imgPath)), + FutureBuilder( + future: isbns[imgPath.path]!, + builder: (context, snap) { + if (snap.hasData == false) { + return const CircularProgressIndicator(); + } + return Column(children: snap.data!.map((isbn) => Text(isbn)).toList()); + }) + ], + ), + ), + )) + .toList(), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: Future.wait(isbns.values.iter), + builder: (context, snap) { + return ElevatedButton( + onPressed: () { + final isbnSet = snap.data!.expand((e) => e).toSet(); + print('isbnSet = $isbnSet'); + widget.onSubmit(MetadataCollectingStep(bundle: widget.step.bundle, isbns: isbnSet)); + }, + child: const Text('Validate ISBNs')); + }), + ) + ], + ), + ); + } +} diff --git a/lib/enrichment/metadata_collecting.dart b/lib/enrichment/metadata_collecting.dart new file mode 100644 index 0000000..f1182b7 --- /dev/null +++ b/lib/enrichment/metadata_collecting.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rust_bridge_template/helpers.dart'; + +import '../ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import 'enrichment.dart'; + +const noneText = Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + +class SelectableTextAndUse extends StatelessWidget { + const SelectableTextAndUse(this.s, {required this.onUse}); + final String s; + final void Function(String) onUse; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TextButton(onPressed: () => onUse(s), child: const Text('Use')), + SelectableText(s), + ], + ); + } +} + +class MetadataCollectingWidget extends StatefulWidget { + MetadataCollectingWidget({required this.step, required this.onSubmit}); + final MetadataCollectingStep step; + final void Function(AdEditingStep newStep) onSubmit; + + final titleTextFieldController = TextEditingController(); + final authorsTextFieldController = TextEditingController(); + final blurbTextFieldController = TextEditingController(); + final priceTextFieldController = TextEditingController(); + + @override + State createState() => _MetadataCollectingWidgetState(); +} + +class Metadatas { + final Map> mdFromProviders; + BookMetaDataManual manual; + Metadatas({required this.mdFromProviders, required this.manual}); +} + +class _MetadataCollectingWidgetState extends State { + Map metadata = {}; + + void replaceIfBetterString(String? providerStr, String manualStr, void Function() onReplace) { + if (providerStr == null || manualStr.length > providerStr.length) return; + onReplace(); + } + + void _updateManualTitle(String isbn, String newTitle) { + setState(() { + metadata[isbn]!.manual.title = newTitle; + widget.titleTextFieldController.text = newTitle; + }); + } + + void _updateManualAuthors(String isbn, String newAuthor) { + setState(() { + metadata[isbn]!.manual.authors = [Author(firstName: '', lastName: newAuthor)]; + widget.authorsTextFieldController.text = newAuthor; + }); + } + + void _updateManualBlurb(String isbn, String newBlurb) { + setState(() { + metadata[isbn]!.manual.blurb = newBlurb; + widget.blurbTextFieldController.text = newBlurb; + }); + } + + @override + void initState() { + super.initState(); + widget.step.isbns.forEach((isbn) { + metadata.putIfAbsent( + isbn, + () => Metadatas( + manual: BookMetaDataManual(title: '', authors: [], blurb: '', keywords: [], priceCent: null), + mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { + final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); + md.then((value) { + if (value != null) { + replaceIfBetterString(value.title, metadata[isbn]!.manual.title!, () { + _updateManualTitle(isbn, value.title!); + }); + + // TODO: handle list of authors + final joinedAuthors = value.authors.toText(); + replaceIfBetterString(joinedAuthors, metadata[isbn]!.manual.authors.toText(), () { + _updateManualAuthors(isbn, joinedAuthors); + }); + replaceIfBetterString(value.blurb, metadata[isbn]!.manual.blurb!, () { + _updateManualBlurb(isbn, value.blurb!); + }); + } + }); + return MapEntry(provider, md); + })))); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Metadata Collecting'), + ), + body: SingleChildScrollView( + child: Column( + children: [ + ...widget.step.isbns.map((isbn) { + final manual = metadata[isbn]!.manual; + const columnHeaderStyle = TextStyle(fontSize: 20, fontWeight: FontWeight.bold); + return Card( + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SelectableText('ISBN: $isbn', style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold)), + Table( + children: [ + TableRow( + children: [ + const Text('Manual', style: columnHeaderStyle), + const Text('Babelio', style: columnHeaderStyle), + const Text('GoogleBooks', style: columnHeaderStyle), + const Text('BooksPrice', style: columnHeaderStyle), + ].map((e) => Center(child: e)).toList()), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + controller: widget.titleTextFieldController, + onChanged: (newText) => setState(() => manual.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), + )), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( + future: e.value, + builder: (data) => data == null ? noneText : SelectableText(data.title ?? ''))), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + controller: widget.authorsTextFieldController, + onChanged: (newText) => setState(() => manual.authors = + newText.split('\n').map((line) => Author(firstName: '', lastName: line)).toList()), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Authors', + ), + ), + ), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( + future: e.value, + builder: (data) { + final authors = data?.authors; + if (authors == null || authors.isEmpty) { + return noneText; + } + return SelectableText(authors.toText()); + })), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + controller: widget.blurbTextFieldController, + onChanged: (newText) => setState(() => metadata[isbn]!.manual.blurb = newText), + maxLines: null, + decoration: const InputDecoration( + icon: Icon(Icons.description), + labelText: 'Book blurb', + ), + )), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( + future: e.value, + builder: (data) { + final blurb = data?.blurb; + if (blurb == null) { + return noneText; + } + return SelectableTextAndUse( + blurb, + onUse: (b) => _updateManualBlurb(isbn, b), + ); + })), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + controller: widget.priceTextFieldController, + onChanged: (newText) => setState(() => metadata[isbn]!.manual.priceCent = + double.parse(newText).multiply(100).round()), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9]+[,.]{0,1}[0-9]*')), + ], + decoration: const InputDecoration( + icon: Icon(Icons.euro), + labelText: 'Price', + ), + )), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( + future: e.value, + builder: (data) { + final marketPrices = data?.marketPrice.toList()?..sort(); + if (marketPrices == null || marketPrices.isEmpty) { + return noneText; + } + return SelectableText( + '${marketPrices.first.toStringAsFixed(2)} - ${marketPrices.last.toStringAsFixed(2)}', + ); + })), + ]), + ], + ), + ], + ), + ), + ); + }).toList(), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () { + widget.onSubmit(AdEditingStep( + bundle: widget.step.bundle, + metadata: metadata.map((key, value) => MapEntry(key, value.manual)))); + }, + child: const Text('Validate Metadatas')), + ) + ], + ), + ), + ); + } +} diff --git a/lib/helpers.dart b/lib/helpers.dart new file mode 100644 index 0000000..ecd8114 --- /dev/null +++ b/lib/helpers.dart @@ -0,0 +1,91 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'bridge_definitions.dart'; + +class ImageWidget extends StatelessWidget { + const ImageWidget(this.image); + final File image; + + @override + Widget build(BuildContext context) { + return Image.file( + image, + fit: BoxFit.fitHeight, + isAntiAlias: true, + filterQuality: FilterQuality.medium, + ); + } +} + +class FutureWidget extends StatelessWidget { + const FutureWidget({required this.future, required this.builder}); + final Future future; + final Widget Function(T) builder; + + @override + Widget build(BuildContext context) { + return FutureBuilder(future: future, builder: (context, snap) => AsyncSnapshotWidget(snap: snap, builder: builder)); + } +} + +class AsyncSnapshotWidget extends StatelessWidget { + const AsyncSnapshotWidget({required this.snap, required this.builder}); + final AsyncSnapshot snap; + final Widget Function(T data) builder; + + @override + Widget build(BuildContext context) { + switch (snap.connectionState) { + case ConnectionState.waiting: + return const CircularProgressIndicator(); + case ConnectionState.done: + return builder(snap.data as T); + default: + return const Text('???'); + } + } +} + +extension AuthorExt on Author { + String toText() => [firstName, lastName].where((s) => s.isNotEmpty).join(' '); +} + +extension AuthorsExt on List { + String toText() => map((a) => a.toText()).join('\n'); +} + +extension IntExt on int { + int divide(int other) => this ~/ other; +} + +extension DoubleExt on double { + double multiply(double other) => this * other; +} + +class BookMetaDataManual { + String? title; + List authors; + String? blurb; + List keywords; + int? priceCent; + + BookMetaDataManual({ + this.title, + required this.authors, + this.blurb, + required this.keywords, + required this.priceCent, + }); +} + +/* +extension BookMetadataExt on BookMetaData { + BookMetaData deepCopy() => BookMetaData( + title: '$title', + authors: List.from(authors), + blurb: '$blurb', + keywords: List.from(keywords), + marketPrice: Float32List.fromList(marketPrice)); +}*/ diff --git a/lib/main.dart b/lib/main.dart index d151ca4..52c6d9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,148 +1,36 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import 'package:flutter_rust_bridge_template/camera/camera.dart'; + +import 'enrichment/enrichment.dart'; void main() { - runApp(const MyApp()); + runApp(const BookyApp()); } -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } +enum BookyAppActivity { + camera, + enrichment, } -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; +class BookyApp extends StatefulWidget { + const BookyApp(); @override - State createState() => _MyHomePageState(); + State createState() => _BookyAppState(); } -class _MyHomePageState extends State { - // These futures belong to the state and are only initialized once, - // in the initState method. - late Future platform; - late Future isRelease; - - @override - void initState() { - super.initState(); - platform = api.platform(); - isRelease = api.rustReleaseMode(); - } +class _BookyAppState extends State { + BookyAppActivity activity = Platform.isAndroid ? BookyAppActivity.camera : BookyAppActivity.enrichment; @override Widget build(BuildContext context) { - // This method is rerun every time setState is called. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("You're running on"), - // To render the results of a Future, a FutureBuilder is used which - // turns a Future into an AsyncSnapshot, which can be used to - // extract the error state, the loading state and the data if - // available. - // - // Here, the generic type that the FutureBuilder manages is - // explicitly named, because if omitted the snapshot will have the - // type of AsyncSnapshot. - FutureBuilder>( - // We await two unrelated futures here, so the type has to be - // List. - future: Future.wait([platform, isRelease]), - builder: (context, snap) { - final style = Theme.of(context).textTheme.headline4; - if (snap.error != null) { - // An error has been encountered, so give an appropriate response and - // pass the error details to an unobstructive tooltip. - debugPrint(snap.error.toString()); - return Tooltip( - message: snap.error.toString(), - child: Text('Unknown OS', style: style), - ); - } - - // Guard return here, the data is not ready yet. - final data = snap.data; - if (data == null) return const CircularProgressIndicator(); - - // Finally, retrieve the data expected in the same order provided - // to the FutureBuilder.future. - final Platform platform = data[0]; - final release = data[1] ? 'Release' : 'Debug'; - final text = const { - Platform.Android: 'Android', - Platform.Ios: 'iOS', - Platform.MacApple: 'MacOS with Apple Silicon', - Platform.MacIntel: 'MacOS', - Platform.Windows: 'Windows', - Platform.Unix: 'Unix', - Platform.Wasm: 'the Web', - }[platform] ?? - 'Unknown OS'; - return Text('$text ($release)', style: style); - }, - ) - ], - ), - ), - ); + switch (activity) { + case BookyAppActivity.camera: + return const CameraApp(); + case BookyAppActivity.enrichment: + return const EnrichmentApp(); + } } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..819251b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,14 @@ #include "generated_plugin_registrant.h" +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); + irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); + g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); + super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..cd11e59 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + irondash_engine_context + super_native_extensions ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index d99ce60..0000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,639 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - A38F8BAA29957A110018A868 /* libnative_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A38F8BA0299579880018A868 /* libnative_static.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; - A38F8B9D299579880018A868 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A38F8B98299579880018A868 /* native.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA603EE7FF4DEAC3B2E0A336; - remoteInfo = "native-cdylib"; - }; - A38F8B9F299579880018A868 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A38F8B98299579880018A868 /* native.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA60F0FEBE702969E816930C; - remoteInfo = "native-staticlib"; - }; - A38F8BA829957A0D0018A868 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A38F8B98299579880018A868 /* native.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = CA60F0FEBE701C83950DBC35; - remoteInfo = "native-staticlib"; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* flutter_rust_bridge_template.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_rust_bridge_template.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A38F8B98299579880018A868 /* native.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = native.xcodeproj; path = ../native/native.xcodeproj; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A38F8BAA29957A110018A868 /* libnative_static.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - A38F8B98299579880018A868 /* native.xcodeproj */, - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* flutter_rust_bridge_template.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - A38F8B99299579880018A868 /* Products */ = { - isa = PBXGroup; - children = ( - A38F8B9E299579880018A868 /* native.dylib */, - A38F8BA0299579880018A868 /* libnative_static.a */, - ); - name = Products; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - A38F8BA929957A0D0018A868 /* PBXTargetDependency */, - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* flutter_rust_bridge_template.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectReferences = ( - { - ProductGroup = A38F8B99299579880018A868 /* Products */; - ProjectRef = A38F8B98299579880018A868 /* native.xcodeproj */; - }, - ); - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXReferenceProxy section */ - A38F8B9E299579880018A868 /* native.dylib */ = { - isa = PBXReferenceProxy; - fileType = "compiled.mach-o.dylib"; - path = native.dylib; - remoteRef = A38F8B9D299579880018A868 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - A38F8BA0299579880018A868 /* libnative_static.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libnative_static.a; - remoteRef = A38F8B9F299579880018A868 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; - A38F8BA929957A0D0018A868 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "native-staticlib"; - targetProxy = A38F8BA829957A0D0018A868 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = Runner/bridge_generated.h; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = Runner/bridge_generated.h; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = Runner/bridge_generated.h; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 6ec83e6..0000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index 35578dd..0000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - dummy_method_to_enforce_bundling() - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eb..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb5722..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e7..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632c..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a..0000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index b7d4028..0000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = flutter_rust_bridge_template - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeTemplate - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a3..0000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index 4789daa..0000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837..0000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/macos/Runner/bridge_generated.h b/macos/Runner/bridge_generated.h deleted file mode 100644 index ae8c386..0000000 --- a/macos/Runner/bridge_generated.h +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -typedef struct _Dart_Handle* Dart_Handle; - -typedef struct DartCObject DartCObject; - -typedef int64_t DartPort; - -typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message); - -typedef struct DartCObject *WireSyncReturn; - -void store_dart_post_cobject(DartPostCObjectFnType ptr); - -Dart_Handle get_dart_object(uintptr_t ptr); - -void drop_dart_object(uintptr_t ptr); - -uintptr_t new_dart_opaque(Dart_Handle handle); - -intptr_t init_frb_dart_api_dl(void *obj); - -void wire_platform(int64_t port_); - -void wire_rust_release_mode(int64_t port_); - -void free_WireSyncReturn(WireSyncReturn ptr); - -static int64_t dummy_method_to_enforce_bundling(void) { - int64_t dummy_var = 0; - dummy_var ^= ((int64_t) (void*) wire_platform); - dummy_var ^= ((int64_t) (void*) wire_rust_release_mode); - dummy_var ^= ((int64_t) (void*) free_WireSyncReturn); - dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); - dummy_var ^= ((int64_t) (void*) get_dart_object); - dummy_var ^= ((int64_t) (void*) drop_dart_object); - dummy_var ^= ((int64_t) (void*) new_dart_opaque); - return dummy_var; -} \ No newline at end of file diff --git a/native/.gitignore b/native/.gitignore new file mode 100644 index 0000000..8d19f7f --- /dev/null +++ b/native/.gitignore @@ -0,0 +1,2 @@ +personal_info.rs +config.rs \ No newline at end of file diff --git a/native/Cargo.toml b/native/Cargo.toml index 667e107..af08ea5 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,3 +11,19 @@ crate-type = ["cdylib", "staticlib"] [dependencies] anyhow = "1" flutter_rust_bridge = "1" +base64 = "0.21.0" +itertools = "0.10.5" +regex = "1.7.1" +reqwest = { version = "0.11.14", default-features = false, features = ["blocking", "json", "rustls-tls", "multipart"] } +scraper = "0.16.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.91" +mockito = "1.0.0" +thirtyfour = "0.31.0" +tokio = { version = "1.20", features = ["fs", "macros", "rt-multi-thread", "io-util", "sync"] } + +# [dev-dependencies] +color-eyre = "0.6.2" +hyper = { version = "0.14", features = ["server", "tcp"] } +futures = "0.3.28" +html2text = "0.5.1" diff --git a/native/mock/google_books/get_volume_https:_slash__slash_www.googleapis.com_slash_books_slash_v1_slash_volumes_slash_HY_FNwAACAAJ b/native/mock/google_books/get_volume_https:_slash__slash_www.googleapis.com_slash_books_slash_v1_slash_volumes_slash_HY_FNwAACAAJ new file mode 100644 index 0000000..1d54583 --- /dev/null +++ b/native/mock/google_books/get_volume_https:_slash__slash_www.googleapis.com_slash_books_slash_v1_slash_volumes_slash_HY_FNwAACAAJ @@ -0,0 +1,64 @@ +{ + "kind": "books#volume", + "id": "HY_FNwAACAAJ", + "etag": "4DT81Rt3Ygg", + "selfLink": "https://www.googleapis.com/books/v1/volumes/HY_FNwAACAAJ", + "volumeInfo": { + "title": "L'essence du Tao", + "authors": [ + "Pamela J. Ball" + ], + "publisher": "Pocket", + "publishedDate": "2007", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "2266162772" + }, + { + "type": "ISBN_13", + "identifier": "9782266162777" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 283, + "printedPageCount": 283, + "dimensions": { + "height": "18.00 cm", + "width": "11.00 cm", + "thickness": "1.00 cm" + }, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "infoLink": "https://play.google.com/store/books/details?id=HY_FNwAACAAJ&source=gbs_api", + "canonicalVolumeLink": "https://play.google.com/store/books/details?id=HY_FNwAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + } +} \ No newline at end of file diff --git a/native/mock/google_books/search_by_isbn_9782266162777 b/native/mock/google_books/search_by_isbn_9782266162777 new file mode 100644 index 0000000..736d47c --- /dev/null +++ b/native/mock/google_books/search_by_isbn_9782266162777 @@ -0,0 +1,64 @@ +{ + "kind": "books#volumes", + "totalItems": 1, + "items": [ + { + "kind": "books#volume", + "id": "HY_FNwAACAAJ", + "etag": "PrJ4VYAFw40", + "selfLink": "https://www.googleapis.com/books/v1/volumes/HY_FNwAACAAJ", + "volumeInfo": { + "title": "L'essence du Tao", + "authors": [ + "Pamela Ball" + ], + "publishedDate": "2007-11-02", + "description": "Le Tao est moins une religion qu'un principe de vie universel, une recherche de la sagesse. C'est la \" Voie\" telle que les grands philosophes chinois, Lao Tse, Chuang Tse surtout, l'ont définie il y a plus de deux mille ans : une façon d'être; un ensemble de clés pour une existence harmonieuse et paisible. Pamela Bali nous aide à trouver le chemin qui est le nôtre par le biais de pratiques et de préceptes simples propres au Tao. Après en avoir brossé un bref historique, l'auteur développe les pratiques du Tao, son principe libérateur, évoquant aussi bien la méditation que le Li Chi, le Chi Cung, le Feng Shui ou art du placement, et l'interprétation du I Ching ou Livre des mutations. Un ouvrage clair, accessible et lumineux.", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "2266162772" + }, + { + "type": "ISBN_13", + "identifier": "9782266162777" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 283, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&dq=isbn:9782266162777&hl=&cd=1&source=gbs_api", + "infoLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&dq=isbn:9782266162777&hl=&source=gbs_api", + "canonicalVolumeLink": "https://books.google.com/books/about/L_essence_du_Tao.html?hl=&id=HY_FNwAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + } + } + ] +} \ No newline at end of file diff --git a/native/src/api.rs b/native/src/api.rs index 976bb9d..dfdb8da 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,59 +1,32 @@ -// This is the entry point of your Rust library. -// When adding new code to your project, note that only items used -// here will be transformed to their Dart equivalents. +use crate::cached_client::CachedClient; +use crate::common::{Ad, BookMetaDataFromProvider}; +use crate::common::{LbcCredential, Provider}; +use crate::publisher::Publisher; +use crate::{babelio, booksprice, google_books, leboncoin}; -// A plain enum without any fields. This is similar to Dart- or C-style enums. -// flutter_rust_bridge is capable of generating code for enums with fields -// (@freezed classes in Dart and tagged unions in C). -pub enum Platform { - Unknown, - Android, - Ios, - Windows, - Unix, - MacIntel, - MacApple, - Wasm, +pub enum ProviderEnum { + Babelio, + GoogleBooks, + BooksPrice, } -// A function definition in Rust. Similar to Dart, the return type must always be named -// and is never inferred. -pub fn platform() -> Platform { - // This is a macro, a special expression that expands into code. In Rust, all macros - // end with an exclamation mark and can be invoked with all kinds of brackets (parentheses, - // brackets and curly braces). However, certain conventions exist, for example the - // vector macro is almost always invoked as vec![..]. - // - // The cfg!() macro returns a boolean value based on the current compiler configuration. - // When attached to expressions (#[cfg(..)] form), they show or hide the expression at compile time. - // Here, however, they evaluate to runtime values, which may or may not be optimized out - // by the compiler. A variety of configurations are demonstrated here which cover most of - // the modern oeprating systems. Try running the Flutter application on different machines - // and see if it matches your expected OS. - // - // Furthermore, in Rust, the last expression in a function is the return value and does - // not have the trailing semicolon. This entire if-else chain forms a single expression. - if cfg!(windows) { - Platform::Windows - } else if cfg!(target_os = "android") { - Platform::Android - } else if cfg!(target_os = "ios") { - Platform::Ios - } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { - Platform::MacApple - } else if cfg!(target_os = "macos") { - Platform::MacIntel - } else if cfg!(target_family = "wasm") { - Platform::Wasm - } else if cfg!(unix) { - Platform::Unix - } else { - Platform::Unknown +pub fn get_metadata_from_provider( + provider: ProviderEnum, + isbn: String, +) -> Option { + match provider { + ProviderEnum::Babelio => babelio::Babelio {}.get_book_metadata_from_isbn(&isbn), + ProviderEnum::GoogleBooks => google_books::GoogleBooks { + client: Box::new(CachedClient { + http_client: reqwest::blocking::Client::builder().build().unwrap(), + }), + } + .get_book_metadata_from_isbn(&isbn), + ProviderEnum::BooksPrice => booksprice::BooksPrice {}.get_book_metadata_from_isbn(&isbn), } } -// The convention for Rust identifiers is the snake_case, -// and they are automatically converted to camelCase on the Dart side. -pub fn rust_release_mode() -> bool { - cfg!(not(debug_assertions)) +pub fn publish_ad(ad: Ad, credential: LbcCredential) -> bool { + let lbc_publisher = leboncoin::Leboncoin {}; + Publisher::publish(&lbc_publisher, ad, credential) } diff --git a/native/src/babelio.rs b/native/src/babelio.rs new file mode 100644 index 0000000..4f40ce4 --- /dev/null +++ b/native/src/babelio.rs @@ -0,0 +1,93 @@ +use crate::{cached_client::CachedClient, common}; +mod parser; +mod request; + +pub struct Babelio; + +impl common::Provider for Babelio { + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + let client = reqwest::blocking::Client::builder().build().unwrap(); + let cached_client = CachedClient { + http_client: client, + }; + let book_url = request::get_book_url(&cached_client, isbn)?; + let book_page = request::get_book_page(&cached_client, book_url); + let mut res = parser::extract_title_author_keywords(&book_page)?; + + if let Some(blurb_res) = parser::extract_blurb(&book_page) { + let raw_blurb = match blurb_res { + parser::BlurbRes::SmallBlurb(blurb) => blurb, + parser::BlurbRes::BigBlurb(id_obj) => { + request::get_book_blurb_see_more(&cached_client, &id_obj) + } + }; + res.blurb = Some(parser::parse_blurb(&raw_blurb)); + } + + Some(res) + } +} + +#[cfg(test)] +mod tests { + use crate::common::{Author, BookMetaDataFromProvider, Provider}; + + use super::*; + + #[test] + fn get_metadata_from_normal_book() { + let isbn = "9782266071529"; + let md = Babelio {}.get_book_metadata_from_isbn(isbn); + assert_eq!(md, Some(BookMetaDataFromProvider { + title: Some("Le nom de la bête".to_string()), + authors: vec![Author{first_name:"Daniel".to_string(), last_name: "Easterman".to_string()}], + blurb: Some("Janvier 1999. Peu à peu, les pays arabes ont sombré dans l'intégrisme. Les attentats terroristes se multiplient en Europe attisant la haine et le racisme. Au Caire, un coup d'état fomenté par les fondamentalistes permet à leur chef Al-Kourtoubi de s'installer au pouvoir et d'instaurer la terreur. Le réseau des agents secrets britanniques en Égypte ayant été anéanti, Michael Hunt est obligé de reprendre du service pour enquêter sur place. Aidé par son frère Paul, prêtre catholique et agent du Vatican, il apprend que le Pape doit se rendre à Jérusalem pour participer à une conférence œcuménique. Au courant de ce projet, le chef des fondamentalistes a prévu d'enlever le saint père.Dans ce récit efficace et à l'action soutenue, le héros lutte presque seul contre des groupes fanatiques puissants et sans grand espoir de réussir. Comme dans tous ses autres livres, Daniel Easterman, spécialiste de l'islam, part du constat que le Mal est puissant et il dénonce l'intolérance et les nationalismes qui engendrent violence et chaos.--Claude Mesplède".to_string()), + keywords: + [ + "roman", + "fantastique", + "policier historique", + "romans policiers et polars", + "thriller", + "terreur", + "action", + "démocratie", + "mystique", + "islam", + "intégrisme religieux", + "catholicisme", + "religion", + "terrorisme", + "extrémisme", + "egypte", + "médias", + "thriller religieux", + "littérature irlandaise", + "irlande" + ] + .map(|s| s.to_string()) + .to_vec(), + market_price: vec![], + })); + } + + #[test] + fn get_metadata_from_book_with_see_more_bug() { + let isbn = "9782070541898"; + let md = Babelio {}.get_book_metadata_from_isbn(isbn); + assert_eq!(md, Some(BookMetaDataFromProvider { + title: Some("À la croisée des mondes, tome 2 : La tour des anges".to_string()), + authors: vec![Author{first_name:"Philip".to_string(), last_name: "Pullman".to_string()}], + blurb: Some(r#"Le jeune Will, à la recherche de son père disparu depuis de longues années, est persuadé d’avoir tué un homme. Dans sa fuite, il franchit une brèche presque invisible qui lui permet de passer dans un monde parallèle. +Là, à Cittàgazze, la ville au-delà de l’Aurore, il rencontre Lyra, l’héroïne des "Royaumes du Nord". Elle aussi cherche à rejoindre son père, elle aussi est investie d’une mission dont elle ne connaît pas encore toute l’importance. +Ensemble, les deux enfants devront lutter contre les forces obscures du mal et, pour accomplir leur quête, pénétrer dans la mystérieuse tour des Anges…"#.to_string()), + keywords: + [ + "aventure", "saga", "roman", "fantasy", "fantastique", "littérature jeunesse", "jeunesse", "steampunk", "littérature pour adolescents", "enfants", "magie", "amitié", "enfance", "science-fiction", "univers parallèles", "religion", "adolescence", "littérature anglaise", "littérature britannique", "20ème siècle", + ] + .map(|s| s.to_string()) + .to_vec(), + market_price: vec![], + })); + } +} diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs new file mode 100644 index 0000000..4544d81 --- /dev/null +++ b/native/src/babelio/parser.rs @@ -0,0 +1,267 @@ +use crate::common::{html_select, BookMetaDataFromProvider}; +use itertools::Itertools; + +#[derive(PartialEq, Debug)] +pub enum BlurbRes { + SmallBlurb(String), + BigBlurb(String), +} + +pub fn extract_blurb(html: &str) -> Option { + let doc = scraper::Html::parse_document(html); + + let selector = + scraper::Selector::parse("#d_bio").expect("#d_bio should be a valid CSS selector"); + let mut res = doc.select(&selector); + + let d_bio = match res.next() { + None => return None, + Some(e) => e, + }; + + // Some books do not follow the general structure: https://www.babelio.com/livres/Pullman--la-croisee-des-mondes-tome-2--La-tour-des-anges/59278 + // It looks like a bug from Babelio because the style span do not close + // So I must use a css-style selector instead of going down the DOM tree + let s = scraper::Selector::parse("a[onclick^=\"javascript\"]").unwrap(); + let mut onclick_elements = d_bio.select(&s); + let on_click_element = onclick_elements.next(); + if let Some(_) = onclick_elements.next() { + panic!("There should be one or zero element with onclick attribute in the d_bio element"); + } + match on_click_element { + None => { + let dbio_second_to_last_child = d_bio + .children() + .rev() + .nth(1) + .expect("d_bio should have a second to last children (the style span)"); + Some(BlurbRes::SmallBlurb( + dbio_second_to_last_child + .value() + .as_text() + .unwrap() + .to_string(), + )) + } + Some(on_click_element) => { + let on_click = on_click_element + .value() + .attr("onclick") + .expect(" should have a 'onclick' attribute"); + let re = regex::Regex::new(r"javascript:voir_plus_a\('#d_bio',1,(\d+)\);").unwrap(); + + let single_capture = re + .captures_iter(on_click) + .next() + .expect("The onclick should match with the regex"); + let id_obj = &single_capture[1]; + Some(BlurbRes::BigBlurb(String::from(id_obj))) + } + } +} + +fn extract_author(author_scope: scraper::ElementRef) -> crate::common::Author { + let author_span = author_scope + .first_child() + .expect("author_scope shoud have a first child ") + .first_child() + .expect("author scope > a shoud have a first child "); + let mut children = author_span.children(); + let first_element = children + .next() + .expect("author scope > a > span shoud have a first child"); + let first_name; + let last_name_element; + if let Some(text) = first_element.value().as_text() { + first_name = text.trim().to_string(); + last_name_element = children + .next() + .expect("author scope > a > span shoud have a second child which is the last name"); + } else { + first_name = "".to_string(); + last_name_element = first_element; + } + + let last_name = last_name_element + .first_child() + .unwrap() + .value() + .as_text() + .expect("should be a text") + .trim() + .to_string(); + crate::common::Author { + first_name, + last_name, + } +} + +pub fn extract_title_author_keywords(html: &str) -> Option { + let doc = scraper::Html::parse_document(html); + + let book_select = html_select("div[itemscope][itemtype=\"https://schema.org/Book\"]"); + let res = doc.select(&book_select); + let book_scope = match res.exactly_one() { + Ok(book_scope) => book_scope, + Err(_) => { + eprintln!("Response should contain a element whose with id is itemscope and itemtype=\"https://schema.org/Book\""); + return None; + } + }; + let title_select = html_select("[itemprop=\"name\"]"); + let mut res2 = book_scope.select(&title_select).into_iter(); + let title = res2 + .next() + .expect("There should be at least one element with itemprop=\"name\"") + .first_child() + .unwrap() + .first_child() + .unwrap() + .value() + .as_text() + .unwrap() + .trim() + .to_string(); + + let binding = + html_select("[itemprop=\"author\"][itemscope][itemtype=\"https://schema.org/Person\"]"); + let r = book_scope.select(&binding); + + let authors = r.map(extract_author).collect_vec(); + + let keywords_scope = book_scope + .select(&html_select("[class=\"tags\"]")) + .exactly_one() + .unwrap(); + let keywords = keywords_scope + .children() + .filter_map(|c| { + Some( + c.first_child()? + .value() + .as_text() + .expect("c should be a text") + .trim() + .to_string(), + ) + }) + .collect(); + Some(BookMetaDataFromProvider { + title: Some(title), + authors, + keywords, + ..Default::default() + }) +} + +pub fn parse_blurb(raw_blurb: &str) -> String { + let text = html2text::from_read(raw_blurb.as_bytes(), usize::MAX); + text.trim().to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_id_obj_from_file() { + let html = std::fs::read_to_string("src/babelio/test/get_book.html").unwrap(); + let id_obj = extract_blurb(&html); + assert_eq!(id_obj, Some(BlurbRes::BigBlurb("827593".to_string()))); + } + + #[test] + fn test_parse_blurb_with_special_charset() { + let html = std::fs::read_to_string("src/babelio/test/get_book_blurb_see_more_179245.html") + .unwrap(); + let text = parse_blurb(&html); + assert_eq!(text, "La ville entière est sous le choc. Adam, un jeune autiste de neuf ans, a été retrouvé dans les bois à côté du corps sans vie d'une camarade d'école sauvagement poignardée. Quelques heures auparavant, les deux enfants avaient échappé à la vigilance des adultes pendant la récréation et s'étaient évanouis dans la nature. Tous les espoirs d'identifier le coupable reposent désormais sur le témoignage d'Adam. Mais, replié sur lui-même, il ne réagit pas et refuse de communiquer. Commence alors pour Cara, sa mère, un subtil exercice d'interprétation : saura-t-elle déchiffrer les silences de son fils et aider les enquêteurs à débusquer le meurtrier ? Thriller psychologique, Au fond des yeux raconte avec pudeur et justesse le courageux combat d'une mère contre les préjugés et l'isolement."); + } + + #[test] + pub fn extract_title_author_keywords_from_file() { + let html = std::fs::read_to_string("src/babelio/test/get_book_minimal.html").unwrap(); + let title_author_keywords = extract_title_author_keywords(&html); + assert_eq!( + title_author_keywords, + Some(BookMetaDataFromProvider { + title: Some("Le nom de la bête".to_string()), + authors: vec![crate::common::Author { + first_name: "Daniel".to_string(), + last_name: "Easterman".to_string() + }], + blurb: None, + keywords: [ + "roman", + "fantastique", + "policier historique", + "romans policiers et polars", + "thriller", + "terreur", + "action", + "démocratie", + "mystique", + "islam", + "intégrisme religieux", + "catholicisme", + "religion", + "terrorisme", + "extrémisme", + "egypte", + "médias", + "thriller religieux", + "littérature irlandaise", + "irlande" + ] + .map(|s| s.to_string()) + .to_vec(), + market_price: vec![], + }) + ); + } + + #[test] + pub fn extract_title_author_keywords_from_file_9782253143321() { + let html = std::fs::read_to_string( + "src/babelio/test/9782253143321_le_livre_tibetain_des_morts.html", + ) + .unwrap(); + let title_author_keywords = extract_title_author_keywords(&html); + assert_eq!( + title_author_keywords, + Some(BookMetaDataFromProvider { + title: Some("Bardo-Thödol : Le livre tibétain des morts".to_string()), + authors: vec![crate::common::Author { + first_name: "".to_string(), + last_name: "Padmasambhava".to_string() + }], + blurb: None, + keywords: [ + "document", + "classique", + "histoire", + "mystique", + "zen", + "mort", + "croyances", + "pensées philosophiques", + "libération", + "réincarnation", + "Médiumnité", + "religion", + "spiritualité", + "Bouddhistes", + "bouddhisme tibétain", + "vie après la mort", + "bouddhisme", + "voyage initiatique", + "ésotérisme", + "philosophie" + ] + .map(|s| s.to_string()) + .to_vec(), + market_price: vec![], + }) + ); + } +} diff --git a/native/src/babelio/request.rs b/native/src/babelio/request.rs new file mode 100644 index 0000000..62d7e28 --- /dev/null +++ b/native/src/babelio/request.rs @@ -0,0 +1,65 @@ +use crate::cached_client::{CachedClient, Client}; +use itertools::Itertools; + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +struct BabelioISBNResponse { + id_oeuvre: String, + titre: String, + couverture: String, + id: String, + id_auteur: String, + prenoms: String, + nom: String, + ca_copies: String, + ca_note: String, + id_edition: String, + r#type: String, + url: String, +} + +pub fn get_book_url(client: &dyn Client, isbn: &str) -> Option { + let raw_search_results = client.make_request( + format!("babelio/get_book_url_{}.html", isbn).as_str(), + &|http_client| { + http_client + .post("https://www.babelio.com/aj_recherche.php") + .body(format!("{{\"isMobile\":false,\"term\":\"{}\"}}", isbn)) + .send() + .unwrap() + .text() + .unwrap() + }, + ); + let parsed: Vec = serde_json::from_str(&raw_search_results).unwrap(); + let s = parsed.iter().exactly_one().ok()?.url.clone(); + Some(s) +} + +pub fn get_book_page(client: &CachedClient, url: String) -> String { + client.make_request( + format!("babelio/get_book_page_{}.html", url.replace("/", "_slash_")).as_str(), + &|http_client| { + let resp = http_client + .get(format!("https://www.babelio.com{url}")) + .send() + .unwrap(); + resp.text().unwrap() + }, + ) +} + +pub fn get_book_blurb_see_more(client: &CachedClient, id_obj: &str) -> String { + client.make_request( + format!("babelio/get_book_blurb_see_more_{}.html", id_obj).as_str(), + &|http_client| { + let params = std::collections::HashMap::from([("type", "1"), ("id_obj", id_obj)]); + + let voir_plus_resp = http_client + .post("https://www.babelio.com/aj_voir_plus_a.php") + .form(¶ms) + .send() + .unwrap(); + voir_plus_resp.text().unwrap() + }, + ) +} diff --git a/native/src/babelio/test/9782253143321_le_livre_tibetain_des_morts.html b/native/src/babelio/test/9782253143321_le_livre_tibetain_des_morts.html new file mode 100644 index 0000000..56c99db --- /dev/null +++ b/native/src/babelio/test/9782253143321_le_livre_tibetain_des_morts.html @@ -0,0 +1,1807 @@ + + + + + + + + + + + + + + + + Bardo-Thödol : Le livre tibétain des morts - Babelio + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+ +

Padma Sambhava (Auteur présumé) Karma-glin-pa (Éditeur + scientifique)Robert A. + F. Thurman (Éditeur scientifique)Gilles Poulain (Traducteur)Rose-Anne Huart + (Traducteur) +
+ + EAN : 9782253143321
+ 412 pages
Le Livre de Poche + + (01/11/1997) + +
4.13/5 +   + 39 notes + + + +
+ Résumé :
+
+ Le Bardo Thödol ou Livre tibétain des morts est un texte du bouddhisme tibétain décrivant les états de + conscience et les perceptions se succédant pendant la période qui s’étend de la mort à la renaissance. + L’étude de son vivant ou la récitation du principal chapitre par un lama lors de l’agonie ou après la + mort est censée aider à la libération du cycle des réincarnations, ou du moins à obtenir une meilleure + réincarnation.

+ Like a Star @ heaven Les thöd... >Voir plus + +
+
+ +
+
+ +
+
+ Acheter ce livre sur + +
+ + + + + + + + +
FnacAmazonRakutenCulturaMomox
Toutes les offres à + partir de 0.90€ +
+
+
+
+ étiquettes + + + + + Ajouter des étiquettes +
+ +
+ Critiques, Analyses et Avis (3) + + + + Ajouter une critique +
+
+ + +
+
FredMartineau
+ +
+ 31 mai 2015 +
+ +
+
+ + J'ai trouvé intéressant d'appréhender leur façon d'aborder la mort, de découvrir le Bardo, cette étape + intermédiaire de 49 jours durant laquelle l'âme choisit son chemin vers le prochain cycle. On y + retrouve des concepts s'approchant de ceux de la religion chrétienne, que certains lamas définirent + comme un bouddhisme imparfait. L'origine de la Trinité serait bien antérieure à l'emprunt chrétien, un + exemple supplémentaire de l'absurdité de ceux revendiquant leur construction théologique comme des + vérité irréfragables commencement de toute spiritualité... + + + +
+
+
Commenter +  J’apprécie        +   140
+
+
+
+ + +
+
karmon34
+ +
+ 11 juin 2013 +
+ +
+
+ + Après la mort apparente, le défunt traverse le Chikai Bardo, au cours duquel il ne comprend pas qu'il + est mort et se trouve dans un état proche du sommeil. La récitation de la première partie du livre est + destinée à lui faire prendre conscience de son état, à lui épargner les regrets et à le préparer à la + Libération finale, qui se manifeste comme la vision de la « Claire Lumière primordiale ». Si, mal préparé et apeurée il ne saisit pas cette + occasion d'entrer dans le nirvana, le défunt se trouvera dans le Chonyid Bardo. + + +
+ Lien : http://www.buddhachannel.tv/.. +
+
+
Commenter +  J’apprécie        +   70
+
+
+
+ + +
+
catherinejean
+ +
+ 08 février 2023 +
+ +
+
+ + Un beau texte sacré sur la mort et l'éveil!

+ Le Bardo Thödol, ou Livre des morts, est lu + au défunt à haute voix pendant 49 jours pour l'encourager et le guider dans son cheminement. Au moment + de la mort, nous errons dans des états intermédiaires appelés « Bardo ». Ainsi le Livre des morts + contient notamment la description des transformations de la conscience et des perceptions au cours des + états intermédiaires (« Bardo ») qui se succèdent de la mort à la renaissance.

+ Tout au long de la lecture du Bardo Thodöl, l'emphase est mise sur la compassion. La compassion, + l'amour sincère et désintéressé pour les autres est nécessaire pour atteindre la libération. Selon le + bouddhisme tibétain, si on ne se soucie pas des autres, on ne peut jamais connaître son propre + esprit.

+ En fait, le Livre des morts nous enseigne à nous préparer à la mort. Pour cela, il nous faut prendre + conscience de l'état d'endormissement, « de rêve », dans lequel nous vivons notre vie.

+ Un très beau texte de référence de la spiritualité et de la culture tibétaine. + + +
+ Lien : https://www.catherinejeanaut.. +
+
+
Commenter +  J’apprécie        +   20
+
+
+

+
+ Citations et extraits (6) + Voir plus + + Ajouter une citation +
+
+
+
Danieljean
+ +
29 juillet + 2015
+ +
+
+ + + Les deux premiers mantra sont dénommés chant du vajra. Ils ne sont pas en sanskrit, comme la plupart des + mantra, mais dans une langue d'une autre dimension appelée langue de l'Oddiyana et pafois langue des + dakini. On trouve plusieurs versions du chant du vajra : soit sous deux formes distinctes comme ici, + l'une, masculine, étant liée à Samantabhadra, et l'autre féminine, à Samantabhadri; soit sous une forme + unique qui combine les deux précédentes, comme c'est le cas dans le Tantra de l'Union du soleil et de la + lune, le Longchen Nyinghthik ou dans les écrits et terma de Namkhai Norbu Rinpoche. Chaque groupe de + syllabe du chant du vajra correspond à un aspect de l'enseignement dzogchen et à un point énergétique du + corps du yogi. On trouve dans le Longchen Nyingthik la signification du chant du vajra unifié : +

+ Sans naissance ni cessation,
+ Sans allées, ni venues, il embrasse toutes choses.
+ Grande félicité, suprême doctrine immuable,
+ Semblable au ciel, liberté absolue sans oripeaux,
+ Sans origine ni support,
+ Sans lieu ni prise, grand phénomène
+ Libre depuis l'origine, immensité s'étendant à l'infini,
+ Sans entraves, il n'a pas à être libéré;
+ Immensité de l'espace céleste,
+ Grand phénomène flamboyant, mandala du soleil et de la lune,
+ Il manifeste la présence spontanée :
+ Montagne de diamant, vaste lotus,
+ Lion solaire, chant de la sagesse,
+ Grand son, mélodie sans pareil,
+ Plénitude de qualités jusqu'aux confins de l'espace,
+ Eveil parfait, champ où s'égalisent tous les Eveil parfaits,
+ Vaste Samantabhadra, cime de l'enseignement,
+ Et, dans la matrice spacieuse du ciel de Samantabhadri,
+ Clarté spatiale, Présence spontanée, Grande Perfection de toujours + +
+
+
+
Commenter +  J’apprécie        +   130
+
+
+
+
+
+
Danieljean
+ +
05 octobre + 2017
+ +
+
+ + + Dans cette claire vacuité
+ où les pensées passées se sont évanouies
+ sans trace aucune,
+ Dans cette fraîcheur
+ où les pensées à venir ne sont pas encore :
+ A l’instant où s’établit le mode naturel sans fabrications,
+ Voici cette conscience qui, à ce moment,
+ est en elle-même tout ordinaire,
+ Et dès que vous tournez votre regard nu sur vous-même,
+ Ce regard qui n’a rien à voir débouche sur la clarté,
+ La Présence dans son évidence, nue et vive,
+ C’est une pure vacuité qui n’a été créée d’aucune manière.
+ Un état inaltéré où clarté et vide sont indivisibles,
+ Ni éternel puisque rien n’y existe vraiment
+ Ni néant puisqu’il est clair et vif.
+ Il ne se réduit pas à l’un,
+ étant présent et limpide en toutes choses.
+ Et n’est pas le multiple,
+ car tout y est d’une saveur unique dans l’inséparabilité,
+ Telle est cette Présence intrinsèque
+ et elle n’est rien d’autre. + +
+
+
+
Commenter +  J’apprécie        +   130
+
+
+
+
+
+
Danieljean
+ +
19 juillet + 2015
+ +
+
+ + + Ce mot [de mort qui figure dans le titre courant de ce livre] dévie totalement le sens de l'œuvre qui + réside dans l'idée de libération c'est-à-dire libération des illusions de notre conscience égocentrique + qui oscille perpétuellement entre naissance et mort, être et ne pas être, espoir et doute, sans parvenir + à l'éveil, à la paix du nirvana, cet état stable, loin des illusions du samsara et des états + intermédiaires. + +
+
+
+
Commenter +  J’apprécie        +   150
+
+
+
+
+
+
karmon34
+ +
11 juin + 2013
+ +
+
+ + + Noble fils (un tel), maintenant que ta respiration a presque cessé, voici pour toi le moment de chercher + une voie car la lumière fondamentale qui apparaît lors du premier état intermédiaire va poindre. Ton + Lama t’avait déjà montré cette lumière, la Vérité en Soi (Dharmata) vide et nue, comme l’espace sans + limites et n’ayant pas de centre, lucide ; c’est l’esprit vierge et sans tache. Voici le moment de le + reconnaître. Demeure donc ainsi en elle. Moi aussi je te la ferai découvrir ". + +
+
+
+
Commenter +  J’apprécie        +   90
+
+
+
+
+
+
Danieljean
+ +
01 octobre + 2018
+ +
+
+ + + La vie passe aussi vite que les nuages d'automne ;
+ Parents et amis sont comme les badauds d'un marché ;
+ Le démon de la mort rôde, furtif, comme les ombres du crépuscule ;
+ L'au-delà est [pour nous] comme un poisson transparent en eau trouble ;
+ Le monde, comme le rêve de la nuit passée ;
+ Les plaisirs des sens, comme une fête illusoire ;
+ Et les activités ordinaires aussi futiles
+ Que les ondes se succédant à la surface de l'eau. + +
+
+
+
Commenter +  J’apprécie        +   100
+
+
+
+

+
+
+ Dans la catégorie : + + BouddhismeVoir plus +
>Religion comparée. Autres religions>Religions d'origine hindoue>Bouddhisme (225) + + +
+ autres livres classés : bouddhismeVoir plus
+ + +
+ Notre sélection Non-fiction + Voir plus +
+ +
+
+ +
+
+ Acheter ce livre sur + +
+ + + + + + + + +
FnacAmazonRakutenCulturaMomox
Toutes les offres à + partir de 0.90€ +
+
+ +
+

+
+ + + + +

+ +

+

+ +
+
+ Quiz + Voir plus +
+
+

Jésus qui est-il ?

+

Jésus était-il vraiment Juif ?

+
+
Oui
+
Non
+
Plutôt Zen
+
Catholique
+
+

+
10 questions
+ 1675 lecteurs ont répondu +
+ + Thèmes : + christianisme + , religion + , bibleCréer un quiz sur ce livre +
+
+
+
+
+

+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/native/src/babelio/test/get_book.html b/native/src/babelio/test/get_book.html new file mode 100644 index 0000000..0157c2a --- /dev/null +++ b/native/src/babelio/test/get_book.html @@ -0,0 +1,581 @@ +Le nom de la bête - Daniel Easterman - Babelio

Bernard Ferry (Traducteur)
+ + EAN : 9782266071529
+ 5423 pages
Pocket + + (12/03/1999) + +
3.42/5 +   + 53 notes +
+ Résumé :
+ Janvier 1999. Peu à peu, les pays arabes ont sombré dans l'intégrisme. Les attentats terroristes se multiplient en Europe attisant la haine et le racisme. Au Caire, un coup d'état fomenté par les fondamentalistes permet à leur chef Al-Kourtoubi de s'installer au pouvoir et d'instaurer la terreur. Le réseau des agents secrets britanniques en Égypte ayant été anéanti, Michael Hunt est obligé de reprendre du service pour enquêter sur place. Aidé par son frère Paul, prê... >Voir plus
+ Acheter ce livre sur + +
FnacAmazonRakutenCulturaMomox
Toutes les offres à partir de 1.45€
+ étiquettes + + + + + Ajouter des étiquettes
+ Critiques, Analyses et Avis (5) + + + + Ajouter une critique
MacAlpine
  + 10 mars 2010
+ + 1999 : l'Egypte est plongée dans le chaos et la terreur. Porté au pouvoir à la faveur d'un coup d'état sanguinaire, l'homme fort du nouveau régime est al Kourtoubi, chef intégriste musulman. Naturellement, le monde entier a les yeux rivés sur lui. Les services secrets britanniques dépêchent au Caire Michael Hunt, l'un de leurs meilleurs agents. Avec l'aide de partenaires tout à fait inattendus - un prêtre du Vatican et une jeune archéologue - il tentera de déjouer un machiavélique complot international qui a pour but de déstabiliser l'Occident et d'anéantir la démocratie. + + +
Commenter  J’apprécie          70
Chiwi
  + 03 février 2013
+ + A la relecture de ce thriller (je l'avais lu il y a presque treize ans) je suis partagé.
D'un côté il y a un aspect réaliste qui fait froid dans le dos. Les descriptions des exactions des islamistes font écho à celles que l'on peut voir dans les médias. Il y avait comme une prémonition de ce qui allait arriver avec le terrorisme islamiste.
D'un autre côté Easterman introduit des éléments qui font que le roman prend à certains moments un tournant ésotérique voire un peu mystique. L'utilisation d'éléments de l'Apocalypse rend des fois le roman un peu délirant. le dirigeant islamiste est un intégriste pur et dur mais n'hésite pas à se présenter comme l'Antéchrist. C'est un mélange des genres que je trouve un peu gros.
Le personnage de Michael Hunt, héros du roman, est trop ambigu. Catholique, il refuse le divorce à sa femme alors qu'ils ne vivent plus ensemble et on ne sait pas bien pourquoi. Mais lorsqu'il rencontre une jeune archéologue, il n'hésite pas à coucher avec et pratiquer sans remords l'adultère. Au fil du roman, il donne l'impression de toujours être hésitant, de ne pas savoir sur quel pied danser, de subir les évènements. Cela est particulièrement vrai au moment du dénouement final.
Don le Nom de la Bête est un thriller qui vaut le coup pour les aspects géopolitiques, toujours actuels, mais qui est peu fouillé en ce qui concerne la psychologie des personnages.
+ Lire la suite
Commenter  J’apprécie          10
anthony44
  + 16 novembre 2014
Le Nom de la Bete est un roman de l'écrivain Irlandais Daniel Easterman. Michael Hunt, ancien agent des services secrets des Etats Unis doit reprendre du service afin de lutter contre un régime Islamique mené par un dangereux chef intégriste.
+Le roman a été écrit (il me semble) en 1997 et on peut dire qu'il y'avait une sorte de prémonition quand l'auteur a écrit ce récit d'aventure. On entend énormément parler d'intégrisme religieux dans les médias. le roman est composé d'énormément de rebondissements ce qui tient en haleine le lecteur. Malheureusement, on ne s'identifie pas vraiment aux personnages qui sont assez stéréotypés et la fin est vite bâclée je trouve.
+J'aime beaucoup les livres de cet auteur (Daniel Easterman) mais celui là bien qu'honnête ne m'a pas passionné malgré une intrigue intéressante et malheureusement actuelle. Un petit en-cas avant de passer à un autre gros livre.
Commenter  J’apprécie          10
bfauriaux
  + 26 septembre 2021
+ + Une plongee en Egypte pour un thriller contemporain qui nous replonge en 1999 et l'arrivée des integristes au pouvoir.Un agrnt consulaire britannique est envoye sur place et est charge de dejouer le complot qui doit viser jusqu'au pape afin de déstabiliser le monde europeen.Un thriller sans temps mort qui se devore d'un trait.A deguster sans moderation. + + +
Commenter  J’apprécie          10
Ellioth
  + 07 janvier 2012
+ + Encore un théme cher à Eastermann, l'extrémisme religieux, en particulier islamique. Descente aux enfers pour les héros du livre, le lecteur est encore rivé à ses pages, pour une action qui ne perd pas un instant de son intensité....on ne sort pas indemne de ce bouquin. Eastermann pousse encore les lecteurs à s'interroger sur notre monde... + + +
Commenter  J’apprécie          10

+ Citations et extraits (13) + Voir plus + + Ajouter une citation
rkhettaouirkhettaoui   08 janvier 2016
+ Les chefs de bureau ne voyagent pas, ils ne mettent en danger ni leur vie ni leurs connaissances. Ils observent à distance, et si nécessaire se retranchent derrière les voiles que le service a prévus pour eux. Il y en avait sept pour se dissimuler, autant que pour la danse : l’Honneur, la Discrétion, la Sécurité, la Diplomatie, le Secret, le Tact et la Connerie. C’est surtout ce dernier voile qui sert à masquer la nudité des serviteurs du royaume. + +
+ Lire la suite
Commenter  J’apprécie          20
rkhettaouirkhettaoui   08 janvier 2016
+ Tous les Arabes, tous les Iraniens, tous les Turcs étaient considérés comme des terroristes potentiels. Ou en activité. Des gens devenaient paranoïaques, ils les voyaient tous, hommes, femmes, enfants, en train de poser des bombes dans les rues. Des musulmans se faisaient agresser. Simplement parce qu’ils étaient barbus et portaient d’autres vêtements que les vêtements occidentaux. + +
Commenter  J’apprécie          20
rkhettaouirkhettaoui   08 janvier 2016
+ Une épidémie de peste peut être dévastatrice. Et s’il s’agit d’un virus mutant, rien ne pourra l’enrayer + +
Commenter  J’apprécie          70
rkhettaouirkhettaoui   08 janvier 2016
+ L’immunité diplomatique n’a aucune valeur pour eux. Les Iraniens ont déjà créé un précédent. L’art de la diplomatie est une ruse occidentale, une manière d’échapper aux lois des peuples autochtones. + +
Commenter  J’apprécie          20
rkhettaouirkhettaoui   08 janvier 2016
+ — Dans ce cas, il lui faudrait un garde du corps en permanence.
+— On le lui a dit, mais elle ne veut pas en entendre parler. Elle pense que ce serait lui accorder trop d’importance. Elle estime être plus en sûreté sans protection, parce que ça signifie qu’elle ne fait pas partie de ceux qu’on protège. Les gens importants ont des gardes du corps, mais ça ne les empêche pas d’être enlevés. En outre, elle dit que si elle bénéficiait d’une protection, en cas d’enlèvement elle serait traitée comme une personnalité, ce que précisément elle ne veut pas. + +
+ Lire la suite
Commenter  J’apprécie          00

+ autres livres classés : thrillerVoir plus
+ Notre sélection Polar et thriller + Voir plus
+ Acheter ce livre sur + +
FnacAmazonRakutenCulturaMomox
Toutes les offres à partir de 1.45€





+ Quiz + Voir plus

Retrouvez le bon adjectif dans le titre - (6 - polars et thrillers )

Roger-Jon Ellory : " **** le silence"

seul
profond
terrible
intense

20 questions
+ 2498 lecteurs ont répondu +
+ + Thèmes : + littérature + , thriller + , romans policiers et polarsCréer un quiz sur ce livre

\ No newline at end of file diff --git a/native/src/babelio/test/get_book_blurb_see_more_179245.html b/native/src/babelio/test/get_book_blurb_see_more_179245.html new file mode 100644 index 0000000..c7c5ee5 --- /dev/null +++ b/native/src/babelio/test/get_book_blurb_see_more_179245.html @@ -0,0 +1,4 @@ +
+ La ville entière est sous le choc. Adam, un jeune autiste de neuf ans, a été retrouvé dans les bois à côté du corps sans vie d'une camarade d'école sauvagement poignardée. Quelques heures auparavant, les deux enfants avaient échappé à la vigilance des adultes pendant la récréation et s'étaient évanouis dans la nature. Tous les espoirs d'identifier le coupable reposent désormais sur le témoignage d'Adam. Mais, replié sur lui-même, il ne réagit pas et refuse de communiquer. Commence alors pour Cara, sa mère, un subtil exercice d'interprétation : saura-t-elle déchiffrer les silences de son fils et aider les enquêteurs à débusquer le meurtrier ? Thriller psychologique, Au fond des yeux raconte avec pudeur et justesse le courageux combat d'une mère contre les préjugés et l'isolement.
+
+
\ No newline at end of file diff --git a/native/src/babelio/test/get_book_minimal.html b/native/src/babelio/test/get_book_minimal.html new file mode 100644 index 0000000..08d0afc --- /dev/null +++ b/native/src/babelio/test/get_book_minimal.html @@ -0,0 +1,147 @@ + + + + + + + +
+
+
+
+ +
+
+ +

Bernard Ferry (Traducteur) +
+ + EAN : 9782266071529
+ 5423 pages
Pocket + + (12/03/1999) + +
3.42/5 +   + 53 notes + + + +
+ Résumé :
+
+ Janvier 1999. Peu à peu, les pays arabes ont sombré dans l'intégrisme. Les attentats terroristes se + multiplient en Europe attisant la haine et le racisme. Au Caire, un coup d'état fomenté par les + fondamentalistes permet à leur chef Al-Kourtoubi de s'installer au pouvoir et d'instaurer la terreur. + Le réseau des agents secrets britanniques en Égypte ayant été anéanti, Michael Hunt est obligé de + reprendre du service pour enquêter sur place. Aidé par son frère Paul, prê... >Voir plus + +
+
+ +
+ + +
+ étiquettes + + + + + Ajouter des étiquettes +
+ +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/native/src/babelio/test/get_see_more.html b/native/src/babelio/test/get_see_more.html new file mode 100644 index 0000000..3e2b424 --- /dev/null +++ b/native/src/babelio/test/get_see_more.html @@ -0,0 +1 @@ +Janvier 1999. Peu à peu, les pays arabes ont sombré dans l'intégrisme. Les attentats terroristes se multiplient en Europe attisant la haine et le racisme. Au Caire, un coup d'état fomenté par les fondamentalistes permet à leur chef Al-Kourtoubi de s'installer au pouvoir et d'instaurer la terreur. Le réseau des agents secrets britanniques en Égypte ayant été anéanti, Michael Hunt est obligé de reprendre du service pour enquêter sur place. Aidé par son frère Paul, prêtre catholique et agent du Vatican, il apprend que le Pape doit se rendre à Jérusalem pour participer à une conférence œcuménique. Au courant de ce projet, le chef des fondamentalistes a prévu d'enlever le saint père.Dans ce récit efficace et à l'action soutenue, le héros lutte presque seul contre des groupes fanatiques puissants et sans grand espoir de réussir. Comme dans tous ses autres livres, Daniel Easterman, spécialiste de l'islam, part du constat que le Mal est puissant et il dénonce l'intolérance et les nationalismes qui engendrent violence et chaos.--Claude Mesplède
\ No newline at end of file diff --git a/native/src/babelio/test/search_by_isbn b/native/src/babelio/test/search_by_isbn new file mode 100644 index 0000000..e69de29 diff --git a/native/src/booksprice.rs b/native/src/booksprice.rs new file mode 100644 index 0000000..e8be244 --- /dev/null +++ b/native/src/booksprice.rs @@ -0,0 +1,19 @@ +use crate::common::{self, BookMetaDataFromProvider}; + +mod request; +mod selenium_common; + +pub struct BooksPrice; + +impl common::Provider for BooksPrice { + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + let prices = request::extract_price_from_isbn(isbn); + Some(BookMetaDataFromProvider { + title: None, + authors: vec![], + blurb: None, + keywords: vec![], + market_price: prices.unwrap(), + }) + } +} diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs new file mode 100644 index 0000000..ee1a12d --- /dev/null +++ b/native/src/booksprice/request.rs @@ -0,0 +1,95 @@ +//! Requires chromedriver running on port 9515: +//! +//! chromedriver --port=9515 + +use std::time::Duration; + +use thirtyfour::prelude::*; +use tokio; + +#[tokio::main] +pub async fn extract_price_from_isbn( + isbn: &str, +) -> Result, thirtyfour::prelude::WebDriverError> { + let caps = DesiredCapabilities::chrome(); + let driver = WebDriver::new("http://localhost:9515", caps).await?; + + extract_price_from_url( + driver, + &format!( + "https://www.booksprice.com/comparePrice.do?l=y&searchType=compare&inputData={}", + isbn + ), + ) + .await +} + +async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { + c.goto(&url).await?; + + let wait_res = c + .query(By::XPath("//*[@id='chart']")) + .wait(Duration::from_secs(10), Duration::from_secs(1)) + .exists() + .await; + + let entries = c + .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) + .await?; + + let prices = futures::future::try_join_all(entries.iter().map(|e| async { + let price_text = e + .find(By::XPath("td[@title='Total']/a/em")) + .await + .unwrap() + .text() + .await; + + price_text.map(|price_text| { + use regex::Regex; + let re = Regex::new(r"\$ (\d+\.?\d+)").unwrap(); + let r = re.captures(&price_text).unwrap(); + r.get(1).unwrap().as_str().parse::().unwrap() + }) + })) + .await + .unwrap(); + + c.close_window().await; + + Ok(prices) +} + +#[cfg(test)] +mod tests { + use crate::booksprice::selenium_common::handle_test_error; + use crate::booksprice::selenium_common::make_capabilities; + use crate::booksprice::selenium_common::make_url; + use crate::booksprice::selenium_common::setup_server; + + use crate::{local_tester, tester_inner}; + + use super::*; + + async fn parse_booksprices_from_9782884747974( + c: WebDriver, + port: u16, + ) -> Result<(), WebDriverError> { + use crate::booksprice::selenium_common; + + let prices = extract_price_from_url( + c, + &selenium_common::url_from_path(port, "9782884747974.html"), + ) + .await + .unwrap(); + + assert_eq!(prices, vec![16.55, 21.85, 23.75, 27.17, 28.15, 43.20]); + Ok(()) + } + + #[test] + fn test_selenium() { + local_tester!(parse_booksprices_from_9782884747974, "chrome"); + } +} diff --git a/native/src/booksprice/selenium_common.rs b/native/src/booksprice/selenium_common.rs new file mode 100644 index 0000000..e537a40 --- /dev/null +++ b/native/src/booksprice/selenium_common.rs @@ -0,0 +1,201 @@ +#![allow(dead_code)] +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response, Server, StatusCode}; +use std::convert::Infallible; +use std::future::Future; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::Path; +use thirtyfour::prelude::*; +use tokio::fs::read_to_string; + +const ASSETS_DIR: &str = "tests/test_html"; + +pub fn make_capabilities(s: &str) -> Capabilities { + match s { + "firefox" => { + let mut caps = DesiredCapabilities::firefox(); + caps.set_headless().unwrap(); + caps.into() + } + "chrome" => { + let mut caps = DesiredCapabilities::chrome(); + caps.set_headless().unwrap(); + caps.set_no_sandbox().unwrap(); + caps.set_disable_gpu().unwrap(); + caps.set_disable_dev_shm_usage().unwrap(); + caps.into() + } + browser => unimplemented!("unsupported browser backend {}", browser), + } +} + +pub fn make_url(s: &str) -> &'static str { + match s { + "firefox" => "http://localhost:4444", + "chrome" => "http://localhost:9515", + browser => unimplemented!("unsupported browser backend {}", browser), + } +} + +pub fn handle_test_error( + res: Result, Box>, +) -> bool { + match res { + Ok(Ok(_)) => true, + Ok(Err(e)) => { + eprintln!("test future failed to resolve: {:?}", e); + false + } + Err(e) => { + if let Some(e) = e.downcast_ref::() { + eprintln!("test future panicked: {:?}", e); + } else { + eprintln!("test future panicked; an assertion probably failed"); + } + false + } + } +} + +#[macro_export] +macro_rules! tester { + ($f:ident, $endpoint:expr) => {{ + use common::{make_capabilities, make_url}; + let url = make_url($endpoint); + let caps = make_capabilities($endpoint); + tester_inner!($f, WebDriver::new(url, caps)); + }}; +} + +#[macro_export] +macro_rules! tester_inner { + ($f:ident, $connector:expr) => {{ + // use std::sync::{Arc, Mutex}; + use std::thread; + + let c = $connector; + + // we'll need the session_id from the thread + // NOTE: even if it panics, so can't just return it + // let session_id = Arc::new(Mutex::new(None)); + + // run test in its own thread to catch panics + // let sid = session_id.clone(); + let res = thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let c = rt.block_on(c).expect("failed to construct test WebDriver"); + // let _sid = c.session_id().clone(); + // *sid.lock().unwrap() = Some(_sid); + // make sure we close, even if an assertion fails + let client = c.clone(); + let x = rt.block_on(async move { + let r = tokio::spawn($f(c)).await; + let _ = client.quit().await; + r + }); + drop(rt); + x.expect("test panicked") + }) + .join(); + let success = handle_test_error(res); + assert!(success); + }}; +} + +#[macro_export] +macro_rules! local_tester { + ($f:ident, $endpoint:expr) => {{ + use thirtyfour::prelude::*; + + let port = setup_server(); + let url = make_url($endpoint); + let caps = make_capabilities($endpoint); + let f = move |c: WebDriver| async move { $f(c, port).await }; + tester_inner!(f, WebDriver::new(url, caps)); + }}; +} + +/// Sets up the server and returns the port it bound to. +pub fn setup_server() -> u16 { + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let _ = rt.block_on(async { + let (socket_addr, server) = start_server(); + tx.send(socket_addr.port()) + .expect("To be able to send port"); + server.await.expect("To start the server") + }); + }); + + rx.recv().expect("To get the bound port.") +} + +/// Configures and starts the server +fn start_server() -> ( + SocketAddr, + impl Future> + 'static, +) { + let socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + + let server = Server::bind(&socket_addr).serve(make_service_fn(move |_| async { + Ok::<_, Infallible>(service_fn(handle_file_request)) + })); + + let addr = server.local_addr(); + (addr, server) +} + +/// Tries to return the requested html file +async fn handle_file_request(req: Request) -> Result, Infallible> { + let uri_path = req.uri().path().trim_matches(&['/', '\\'][..]); + + // tests only contain html files + // needed because the content-type: text/html is returned + if !uri_path.ends_with(".html") { + return Ok(file_not_found()); + } + + // this does not protect against a directory traversal attack + // but in this case it's not a risk + let asset_file = Path::new(ASSETS_DIR).join(uri_path); + + let ctn = match read_to_string(&asset_file).await { + Ok(ctn) => ctn, + Err(err) => { + eprintln!( + "could not find file {:#?}, error is {}", + asset_file.to_str(), + err + ); + return Ok(file_not_found()); + } + }; + + let res = Response::builder() + .header("content-type", "text/html") + .header("content-length", ctn.len()) + .body(ctn.into()) + .unwrap(); + + Ok(res) +} + +/// Response returned when a file is not found or could not be read +fn file_not_found() -> Response { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap() +} + +pub fn url_from_path(port: u16, path: &str) -> String { + format!("http://localhost:{}/{}", port, path) +} diff --git a/native/src/bridge_generated.io.rs b/native/src/bridge_generated.io.rs index a4ec960..628e8de 100644 --- a/native/src/bridge_generated.io.rs +++ b/native/src/bridge_generated.io.rs @@ -2,23 +2,144 @@ use super::*; // Section: wire functions #[no_mangle] -pub extern "C" fn wire_platform(port_: i64) { - wire_platform_impl(port_) +pub extern "C" fn wire_get_metadata_from_provider( + port_: i64, + provider: i32, + isbn: *mut wire_uint_8_list, +) { + wire_get_metadata_from_provider_impl(port_, provider, isbn) } #[no_mangle] -pub extern "C" fn wire_rust_release_mode(port_: i64) { - wire_rust_release_mode_impl(port_) +pub extern "C" fn wire_publish_ad( + port_: i64, + ad: *mut wire_Ad, + credential: *mut wire_LbcCredential, +) { + wire_publish_ad_impl(port_, ad, credential) } // Section: allocate functions +#[no_mangle] +pub extern "C" fn new_StringList_0(len: i32) -> *mut wire_StringList { + let wrap = wire_StringList { + ptr: support::new_leak_vec_ptr(<*mut wire_uint_8_list>::new_with_null_ptr(), len), + len, + }; + support::new_leak_box_ptr(wrap) +} + +#[no_mangle] +pub extern "C" fn new_box_autoadd_ad_0() -> *mut wire_Ad { + support::new_leak_box_ptr(wire_Ad::new_with_null_ptr()) +} + +#[no_mangle] +pub extern "C" fn new_box_autoadd_lbc_credential_0() -> *mut wire_LbcCredential { + support::new_leak_box_ptr(wire_LbcCredential::new_with_null_ptr()) +} + +#[no_mangle] +pub extern "C" fn new_uint_8_list_0(len: i32) -> *mut wire_uint_8_list { + let ans = wire_uint_8_list { + ptr: support::new_leak_vec_ptr(Default::default(), len), + len, + }; + support::new_leak_box_ptr(ans) +} + // Section: related functions // Section: impl Wire2Api +impl Wire2Api for *mut wire_uint_8_list { + fn wire2api(self) -> String { + let vec: Vec = self.wire2api(); + String::from_utf8_lossy(&vec).into_owned() + } +} +impl Wire2Api> for *mut wire_StringList { + fn wire2api(self) -> Vec { + let vec = unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(Wire2Api::wire2api).collect() + } +} +impl Wire2Api for wire_Ad { + fn wire2api(self) -> Ad { + Ad { + title: self.title.wire2api(), + description: self.description.wire2api(), + price_cent: self.price_cent.wire2api(), + imgs_path: self.imgs_path.wire2api(), + } + } +} +impl Wire2Api for *mut wire_Ad { + fn wire2api(self) -> Ad { + let wrap = unsafe { support::box_from_leak_ptr(self) }; + Wire2Api::::wire2api(*wrap).into() + } +} +impl Wire2Api for *mut wire_LbcCredential { + fn wire2api(self) -> LbcCredential { + let wrap = unsafe { support::box_from_leak_ptr(self) }; + Wire2Api::::wire2api(*wrap).into() + } +} + +impl Wire2Api for wire_LbcCredential { + fn wire2api(self) -> LbcCredential { + LbcCredential { + lbc_token: self.lbc_token.wire2api(), + datadome_cookie: self.datadome_cookie.wire2api(), + } + } +} + +impl Wire2Api> for *mut wire_uint_8_list { + fn wire2api(self) -> Vec { + unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + } + } +} // Section: wire structs +#[repr(C)] +#[derive(Clone)] +pub struct wire_StringList { + ptr: *mut *mut wire_uint_8_list, + len: i32, +} + +#[repr(C)] +#[derive(Clone)] +pub struct wire_Ad { + title: *mut wire_uint_8_list, + description: *mut wire_uint_8_list, + price_cent: i32, + imgs_path: *mut wire_StringList, +} + +#[repr(C)] +#[derive(Clone)] +pub struct wire_LbcCredential { + lbc_token: *mut wire_uint_8_list, + datadome_cookie: *mut wire_uint_8_list, +} + +#[repr(C)] +#[derive(Clone)] +pub struct wire_uint_8_list { + ptr: *mut u8, + len: i32, +} + // Section: impl NewWithNullPtr pub trait NewWithNullPtr { @@ -31,6 +152,38 @@ impl NewWithNullPtr for *mut T { } } +impl NewWithNullPtr for wire_Ad { + fn new_with_null_ptr() -> Self { + Self { + title: core::ptr::null_mut(), + description: core::ptr::null_mut(), + price_cent: Default::default(), + imgs_path: core::ptr::null_mut(), + } + } +} + +impl Default for wire_Ad { + fn default() -> Self { + Self::new_with_null_ptr() + } +} + +impl NewWithNullPtr for wire_LbcCredential { + fn new_with_null_ptr() -> Self { + Self { + lbc_token: core::ptr::null_mut(), + datadome_cookie: core::ptr::null_mut(), + } + } +} + +impl Default for wire_LbcCredential { + fn default() -> Self { + Self::new_with_null_ptr() + } +} + // Section: sync execution mode utility #[no_mangle] diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index 9f63b66..719b492 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -9,7 +9,7 @@ clippy::too_many_arguments )] // AUTO GENERATED FILE, DO NOT EDIT. -// Generated by `flutter_rust_bridge`@ 1.62.1. +// Generated by `flutter_rust_bridge`@ 1.68.0. use crate::api::*; use core::panic::UnwindSafe; @@ -19,26 +19,47 @@ use std::sync::Arc; // Section: imports +use crate::common::Ad; +use crate::common::Author; +use crate::common::BookMetaDataFromProvider; +use crate::common::LbcCredential; + // Section: wire functions -fn wire_platform_impl(port_: MessagePort) { +fn wire_get_metadata_from_provider_impl( + port_: MessagePort, + provider: impl Wire2Api + UnwindSafe, + isbn: impl Wire2Api + UnwindSafe, +) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "platform", + debug_name: "get_metadata_from_provider", port: Some(port_), mode: FfiCallMode::Normal, }, - move || move |task_callback| Ok(platform()), + move || { + let api_provider = provider.wire2api(); + let api_isbn = isbn.wire2api(); + move |task_callback| Ok(get_metadata_from_provider(api_provider, api_isbn)) + }, ) } -fn wire_rust_release_mode_impl(port_: MessagePort) { +fn wire_publish_ad_impl( + port_: MessagePort, + ad: impl Wire2Api + UnwindSafe, + credential: impl Wire2Api + UnwindSafe, +) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "rust_release_mode", + debug_name: "publish_ad", port: Some(port_), mode: FfiCallMode::Normal, }, - move || move |task_callback| Ok(rust_release_mode()), + move || { + let api_ad = ad.wire2api(); + let api_credential = credential.wire2api(); + move |task_callback| Ok(publish_ad(api_ad, api_credential)) + }, ) } // Section: wrapper structs @@ -63,36 +84,58 @@ where (!self.is_null()).then(|| self.wire2api()) } } -// Section: impl IntoDart -impl support::IntoDart for Platform { - fn into_dart(self) -> support::DartAbi { +impl Wire2Api for i32 { + fn wire2api(self) -> i32 { + self + } +} + +impl Wire2Api for i32 { + fn wire2api(self) -> ProviderEnum { match self { - Self::Unknown => 0, - Self::Android => 1, - Self::Ios => 2, - Self::Windows => 3, - Self::Unix => 4, - Self::MacIntel => 5, - Self::MacApple => 6, - Self::Wasm => 7, + 0 => ProviderEnum::Babelio, + 1 => ProviderEnum::GoogleBooks, + 2 => ProviderEnum::BooksPrice, + _ => unreachable!("Invalid variant for ProviderEnum: {}", self), } + } +} +impl Wire2Api for u8 { + fn wire2api(self) -> u8 { + self + } +} + +// Section: impl IntoDart + +impl support::IntoDart for Author { + fn into_dart(self) -> support::DartAbi { + vec![self.first_name.into_dart(), self.last_name.into_dart()].into_dart() + } +} +impl support::IntoDartExceptPrimitive for Author {} + +impl support::IntoDart for BookMetaDataFromProvider { + fn into_dart(self) -> support::DartAbi { + vec![ + self.title.into_dart(), + self.authors.into_dart(), + self.blurb.into_dart(), + self.keywords.into_dart(), + self.market_price.into_dart(), + ] .into_dart() } } +impl support::IntoDartExceptPrimitive for BookMetaDataFromProvider {} + // Section: executor support::lazy_static! { pub static ref FLUTTER_RUST_BRIDGE_HANDLER: support::DefaultHandler = Default::default(); } -/// cbindgen:ignore -#[cfg(target_family = "wasm")] -#[path = "bridge_generated.web.rs"] -mod web; -#[cfg(target_family = "wasm")] -pub use web::*; - #[cfg(not(target_family = "wasm"))] #[path = "bridge_generated.io.rs"] mod io; diff --git a/native/src/cached_client.rs b/native/src/cached_client.rs new file mode 100644 index 0000000..41a5b92 --- /dev/null +++ b/native/src/cached_client.rs @@ -0,0 +1,58 @@ +pub trait Client { + fn make_request( + &self, + cache_file_path: &str, + _make_request: &dyn Fn(&reqwest::blocking::Client) -> String, + ) -> String; +} + +pub struct MockClient { + pub dir: &'static str, +} + +impl Client for MockClient { + fn make_request( + &self, + cache_file_path: &str, + _make_request: &dyn Fn(&reqwest::blocking::Client) -> String, + ) -> String { + let cache_file_path = format!("{}/{}", self.dir, cache_file_path); + let html = std::fs::read_to_string(&cache_file_path); + + match html { + Ok(f) => { + println!("Read request from cache {}", &cache_file_path); + f + } + Err(e) => panic!("Cannot find mock file {}. Error is {}", cache_file_path, e), + } + } +} + +pub struct CachedClient { + pub http_client: reqwest::blocking::Client, +} + +impl Client for CachedClient { + fn make_request( + &self, + cache_file_path: &str, + _make_request: &dyn Fn(&reqwest::blocking::Client) -> String, + ) -> String { + let cache_file_path = format!("{}/{}", crate::config::CACHE_PATH, cache_file_path); + let html = std::fs::read_to_string(&cache_file_path); + match html { + Ok(f) => { + println!("Read request from cache {}", &cache_file_path); + f + } + Err(_) => { + println!("No file name {} in the cache", &cache_file_path); + let resp = _make_request(&self.http_client); + let write_res = std::fs::write(&cache_file_path, &resp); + write_res.expect(format!("Can't write to file {}", cache_file_path).as_str()); + resp + } + } + } +} diff --git a/native/src/common.rs b/native/src/common.rs new file mode 100644 index 0000000..28ad14e --- /dev/null +++ b/native/src/common.rs @@ -0,0 +1,41 @@ +#[derive(Default, Debug, PartialEq)] +pub struct BookMetaDataFromProvider { + pub title: Option, + pub authors: Vec, + // A book blurb is a short promotional description. + // A synopsis summarizes the twists, turns, and conclusion of the story. + pub blurb: Option, + pub keywords: Vec, + + pub market_price: Vec, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct Author { + pub first_name: String, + pub last_name: String, +} + +pub fn html_select(sel: &str) -> scraper::Selector { + scraper::Selector::parse(sel).unwrap() +} + +pub trait Provider { + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option; +} + +pub struct LbcCredential { + pub lbc_token: String, + pub datadome_cookie: String, +} + +pub struct Ad { + pub title: String, + pub description: String, + pub price_cent: i32, + pub imgs_path: Vec, +} + +pub fn url_to_path(url: &str) -> String { + url.replace("/", "_slash_") +} diff --git a/native/src/google_books.rs b/native/src/google_books.rs new file mode 100644 index 0000000..08b49ac --- /dev/null +++ b/native/src/google_books.rs @@ -0,0 +1,105 @@ +use crate::common; +mod parser; +mod request; +use crate::cached_client::Client; +use itertools::Itertools; + +pub struct GoogleBooks { + pub client: Box, +} + +fn merge(first: Option, other: Option, resolver: F) -> Option +where + F: FnOnce(T, T) -> T, +{ + if let None = first { + return other; + } + if let None = other { + return first; + } + Some(resolver(first.unwrap(), other.unwrap())) +} + +fn longest_string_merger(first: Option, other: Option) -> Option { + merge( + first, + other, + |s1, s2| if s1.len() > s2.len() { s1 } else { s2 }, + ) +} + +fn merge_vec( + v1: Vec, + v2: Vec, +) -> Vec { + v1.iter() + .chain(&v2) + .unique() + .map(|f| (*f).clone()) + .collect_vec() +} + +fn merge_bmd( + bmd1: common::BookMetaDataFromProvider, + bmd2: common::BookMetaDataFromProvider, +) -> common::BookMetaDataFromProvider { + common::BookMetaDataFromProvider { + title: longest_string_merger(bmd1.title, bmd2.title), + // Some authors are not display the same way in the first and second request. Sometimes GoogleBooks display the middle name, sometimes not + // So a basic merge would result in diplicate authors + // authors: merge_vec(bmd1.authors, bmd2.authors), + authors: bmd1.authors, + blurb: longest_string_merger(bmd1.blurb, bmd2.blurb), + keywords: merge_vec(bmd1.keywords, bmd2.keywords), + market_price: vec![], + } +} + +impl common::Provider for GoogleBooks { + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + // TODO: For some books (eg 9782703305033), the description is better on the first page than in the second + // The number of authors can be different too ! + let isbn_search_response = request::search_by_isbn(&self.client, isbn); + + let metadata_from_isbn_search = + parser::extract_metadata_from_isbn_response(&isbn_search_response); + + let self_link = parser::extract_self_link_from_isbn_response(&isbn_search_response)?; + let book_page = request::get_volume(&self.client, &self_link); + + let metadata_from_self_link_response = + parser::extract_metadata_from_self_link_response(&book_page); + + Some(merge_bmd( + metadata_from_isbn_search, + metadata_from_self_link_response, + )) + } +} + +#[cfg(test)] +mod tests { + use crate::cached_client::MockClient; + use crate::common::BookMetaDataFromProvider; + use crate::common::Provider; + + use super::*; + + #[test] + fn get_book_metadata_from_isbn_9782266162777() { + let g = GoogleBooks { + client: Box::new(MockClient { + dir: "mock/google_books", + }), + }; + let md = g.get_book_metadata_from_isbn("9782266162777"); + assert_eq!(md, Some(BookMetaDataFromProvider { + title: Some("L'essence du Tao".to_owned()), + authors: vec![common::Author{first_name: "".to_owned(), last_name: "Pamela Ball".to_owned()}], + blurb: Some("Le Tao est moins une religion qu'un principe de vie universel, une recherche de la sagesse. C'est la \" Voie\" telle que les grands philosophes chinois, Lao Tse, Chuang Tse surtout, l'ont définie il y a plus de deux mille ans : une façon d'être; un ensemble de clés pour une existence harmonieuse et paisible. Pamela Bali nous aide à trouver le chemin qui est le nôtre par le biais de pratiques et de préceptes simples propres au Tao. Après en avoir brossé un bref historique, l'auteur développe les pratiques du Tao, son principe libérateur, évoquant aussi bien la méditation que le Li Chi, le Chi Cung, le Feng Shui ou art du placement, et l'interprétation du I Ching ou Livre des mutations. Un ouvrage clair, accessible et lumineux.".to_string()), + keywords: vec![], + market_price: vec![], + })) + } +} diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs new file mode 100644 index 0000000..f27a2aa --- /dev/null +++ b/native/src/google_books/parser.rs @@ -0,0 +1,238 @@ +use itertools::Itertools; + +use crate::common::{self, BookMetaDataFromProvider}; + +pub fn extract_self_link_from_isbn_response(html: &str) -> Option { + let s: structs::Root = serde_json::from_str(html).unwrap(); + s.items.map(|items| items[0].self_link.to_string()) +} +pub fn extract_metadata_from_isbn_response(html: &str) -> common::BookMetaDataFromProvider { + let s: structs::Root = serde_json::from_str(html).unwrap(); + let a = s.items.map(|items| { + let first_book = &items[0].volume_info; + + let authors = first_book + .authors + .iter() + .map(|s| common::Author { + first_name: "".to_string(), + last_name: s.to_string(), + }) + .collect_vec(); + let blurb = items[0] + .volume_info + .description + .clone() + .map(|d| d.to_string()); + BookMetaDataFromProvider { + authors, + blurb, + ..Default::default() + } + }); + a.unwrap_or(BookMetaDataFromProvider { + ..Default::default() + }) +} + +pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaDataFromProvider { + let s: structs::Item = serde_json::from_str(html).unwrap(); + let first_book = &s.volume_info; + common::BookMetaDataFromProvider { + title: Some(first_book.title.to_string()), + authors: first_book + .authors + .iter() + .map(|s| common::Author { + first_name: "".to_string(), + last_name: s.to_string(), + }) + .collect_vec(), + + blurb: first_book.description.clone(), + ..Default::default() + } +} + +#[cfg(test)] +mod tests { + use crate::common::BookMetaDataFromProvider; + + use super::*; + + #[test] + fn extract_self_link_from_file() { + let html = + std::fs::read_to_string("src/google_books/test/9782744170812/isbn_response.html") + .unwrap(); + let self_link = extract_self_link_from_isbn_response(&html); + assert_eq!( + self_link, + Some("https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ".to_string()) + ) + } + + #[test] + fn extract_metadata_from_file() { + let html = + std::fs::read_to_string("src/google_books/test/9782744170812/self_link_response.html") + .unwrap(); + let metadata = extract_metadata_from_self_link_response(&html); + assert_eq!(metadata, BookMetaDataFromProvider{ + title: Some("La cité de Dieu".to_string()), + authors:vec![common::Author{first_name: "".to_string(), last_name: "Paulo Lins".to_string()}], + blurb: Some("Au Brésil, l'évolution d'un bidonville entre les années 1960 et 1980, à travers l'histoire de deux garçons qui suivent des voies différentes : l'un fait des études et s'efforce de devenir photographe, l'autre crée son premier gang et devient, quelques années plus tard, le maître de la cité.".to_string()), + ..Default::default() + }); + } + + #[test] + fn extract_self_link_from_file_2() { + let html = + std::fs::read_to_string("src/google_books/test/9782266162777/isbn_response.html") + .unwrap(); + let self_link = extract_self_link_from_isbn_response(&html); + assert_eq!( + self_link, + Some("https://www.googleapis.com/books/v1/volumes/HY_FNwAACAAJ".to_string()) + ) + } + + #[test] + fn extract_metadata_from_file_2() { + let html = + std::fs::read_to_string("src/google_books/test/9782266162777/self_link_response.html") + .unwrap(); + let metadata = extract_metadata_from_self_link_response(&html); + assert_eq!( + metadata, + BookMetaDataFromProvider { + title: Some("L'essence du Tao".to_string()), + authors: vec![common::Author { + first_name: "".to_string(), + last_name: "Pamela J. Ball".to_string() + }], + ..Default::default() + } + ); + } +} + +mod structs { + use serde::{Deserialize, Serialize}; + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Root<'a> { + pub kind: &'a str, + pub total_items: i64, + pub items: Option>>, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Item<'a> { + // pub kind: &'a str, + pub id: &'a str, + // pub etag: &'a str, + pub self_link: &'a str, + pub volume_info: VolumeInfo<'a>, + // pub sale_info: SaleInfo<'a>, + // pub access_info: AccessInfo<'a>, + // pub search_info: Option>, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct VolumeInfo<'a> { + pub title: &'a str, + pub subtitle: Option<&'a str>, + pub authors: Vec<&'a str>, + // pub publisher: Option<&'a str>, + // pub published_date: &'a str, + // Should be an owned String in case the description contain escape characters like (\") + // TODO: change all &str to String + pub description: Option, + // pub industry_identifiers: Vec>, + // pub reading_modes: ReadingModes, + // pub page_count: i64, + // pub print_type: &'a str, + // pub categories: Option>, + // pub maturity_rating: &'a str, + // pub image_links: Option>, + // pub language: &'a str, + // pub preview_link: &'a str, + // pub info_link: &'a str, + // pub canonical_volume_link: &'a str, + } + + /* #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + struct IndustryIdentifier<'a> { + #[serde(rename = "type")] + pub type_field: &'a str, + pub identifier: &'a str, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ReadingModes { + pub text: bool, + pub image: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PanelizationSummary { + pub contains_epub_bubbles: bool, + pub contains_image_bubbles: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ImageLinks<'a> { + pub small_thumbnail: &'a str, + pub thumbnail: &'a str, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct SaleInfo<'a> { + pub country: &'a str, + pub saleability: &'a str, + pub is_ebook: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct AccessInfo<'a> { + pub country: &'a str, + pub viewability: &'a str, + pub embeddable: bool, + pub public_domain: bool, + pub text_to_speech_permission: &'a str, + pub epub: Epub, + pub pdf: Pdf, + pub web_reader_link: &'a str, + pub access_view_status: &'a str, + pub quote_sharing_allowed: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Epub { + pub is_available: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Pdf { + pub is_available: bool, + } + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct SearchInfo<'a> { + pub text_snippet: &'a str, + }*/ +} diff --git a/native/src/google_books/request.rs b/native/src/google_books/request.rs new file mode 100644 index 0000000..ae29895 --- /dev/null +++ b/native/src/google_books/request.rs @@ -0,0 +1,17 @@ +use crate::cached_client::Client; + +pub fn search_by_isbn(client: &Box, isbn: &str) -> String { + client.make_request(&format!("search_by_isbn_{}", isbn), &|client| { + client + .get(format!( + "https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn}" + )) + .send() + .unwrap() + .text() + .unwrap() + }) +} +pub fn get_volume(client: &Box, url: &str) -> String { + client.make_request(&format!("get_volume_{}", crate::common::url_to_path(url)), &|http_client| http_client.get(url).send().unwrap().text().unwrap()) +} diff --git a/native/src/google_books/test/9782266162777/isbn_response.html b/native/src/google_books/test/9782266162777/isbn_response.html new file mode 100644 index 0000000..56d1645 --- /dev/null +++ b/native/src/google_books/test/9782266162777/isbn_response.html @@ -0,0 +1,64 @@ +{ + "kind": "books#volumes", + "totalItems": 1, + "items": [ + { + "kind": "books#volume", + "id": "HY_FNwAACAAJ", + "etag": "PyqPNtbM744", + "selfLink": "https://www.googleapis.com/books/v1/volumes/HY_FNwAACAAJ", + "volumeInfo": { + "title": "L'essence du Tao", + "authors": [ + "Pamela Ball" + ], + "publishedDate": "2007-11-02", + "description": "Le Tao est moins une religion qu'un principe de vie universel, une recherche de la sagesse. C'est la \" Voie\" telle que les grands philosophes chinois, Lao Tse, Chuang Tse surtout, l'ont définie il y a plus de deux mille ans : une façon d'être; un ensemble de clés pour une existence harmonieuse et paisible. Pamela Bali nous aide à trouver le chemin qui est le nôtre par le biais de pratiques et de préceptes simples propres au Tao. Après en avoir brossé un bref historique, l'auteur développe les pratiques du Tao, son principe libérateur, évoquant aussi bien la méditation que le Li Chi, le Chi Cung, le Feng Shui ou art du placement, et l'interprétation du I Ching ou Livre des mutations. Un ouvrage clair, accessible et lumineux.", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "2266162772" + }, + { + "type": "ISBN_13", + "identifier": "9782266162777" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 283, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&dq=isbn:9782266162777&hl=&cd=1&source=gbs_api", + "infoLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&dq=isbn:9782266162777&hl=&source=gbs_api", + "canonicalVolumeLink": "https://books.google.com/books/about/L_essence_du_Tao.html?hl=&id=HY_FNwAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + } + } + ] +} \ No newline at end of file diff --git a/native/src/google_books/test/9782266162777/self_link_response.html b/native/src/google_books/test/9782266162777/self_link_response.html new file mode 100644 index 0000000..be5e2c8 --- /dev/null +++ b/native/src/google_books/test/9782266162777/self_link_response.html @@ -0,0 +1,64 @@ +{ + "kind": "books#volume", + "id": "HY_FNwAACAAJ", + "etag": "xbuWWeqB6KI", + "selfLink": "https://www.googleapis.com/books/v1/volumes/HY_FNwAACAAJ", + "volumeInfo": { + "title": "L'essence du Tao", + "authors": [ + "Pamela J. Ball" + ], + "publisher": "Pocket", + "publishedDate": "2007", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "2266162772" + }, + { + "type": "ISBN_13", + "identifier": "9782266162777" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 283, + "printedPageCount": 283, + "dimensions": { + "height": "18.00 cm", + "width": "11.00 cm", + "thickness": "1.00 cm" + }, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "infoLink": "https://play.google.com/store/books/details?id=HY_FNwAACAAJ&source=gbs_api", + "canonicalVolumeLink": "https://play.google.com/store/books/details?id=HY_FNwAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=HY_FNwAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + } +} \ No newline at end of file diff --git a/native/src/google_books/test/9782744170812/isbn_response.html b/native/src/google_books/test/9782744170812/isbn_response.html new file mode 100644 index 0000000..b551f83 --- /dev/null +++ b/native/src/google_books/test/9782744170812/isbn_response.html @@ -0,0 +1,67 @@ +{ + "kind": "books#volumes", + "totalItems": 1, + "items": [ + { + "kind": "books#volume", + "id": "DQUFSQAACAAJ", + "etag": "QyXi2Vzw2K0", + "selfLink": "https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ", + "volumeInfo": { + "title": "La Cité de Dieu", + "subtitle": "roman", + "authors": [ + "Paulo Lins" + ], + "publishedDate": "2004", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "274417081X" + }, + { + "type": "ISBN_13", + "identifier": "9782744170812" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 413, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=DQUFSQAACAAJ&dq=isbn:9782744170812&hl=&cd=1&source=gbs_api", + "infoLink": "http://books.google.fr/books?id=DQUFSQAACAAJ&dq=isbn:9782744170812&hl=&source=gbs_api", + "canonicalVolumeLink": "https://books.google.com/books/about/La_Cit%C3%A9_de_Dieu.html?hl=&id=DQUFSQAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=DQUFSQAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + }, + "searchInfo": { + "textSnippet": "Au Brésil, l'évolution d'un bidonville entre les années 1960 et 1980, à travers l'histoire de deux garçons qui suivent des voies différentes : l'un fait des études et s'efforce de devenir photographe, l'autre crée son premier gang ..." + } + } + ] +} \ No newline at end of file diff --git a/native/src/google_books/test/9782744170812/self_link_response.html b/native/src/google_books/test/9782744170812/self_link_response.html new file mode 100644 index 0000000..90564ce --- /dev/null +++ b/native/src/google_books/test/9782744170812/self_link_response.html @@ -0,0 +1,63 @@ +{ + "kind": "books#volume", + "id": "DQUFSQAACAAJ", + "etag": "pZz4IWl62XA", + "selfLink": "https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ", + "volumeInfo": { + "title": "La cité de Dieu", + "authors": [ + "Paulo Lins" + ], + "publisher": "Gallimard", + "publishedDate": "2005", + "description": "Au Brésil, l'évolution d'un bidonville entre les années 1960 et 1980, à travers l'histoire de deux garçons qui suivent des voies différentes : l'un fait des études et s'efforce de devenir photographe, l'autre crée son premier gang et devient, quelques années plus tard, le maître de la cité.", + "industryIdentifiers": [ + { + "type": "ISBN_10", + "identifier": "274417081X" + }, + { + "type": "ISBN_13", + "identifier": "9782744170812" + } + ], + "readingModes": { + "text": false, + "image": false + }, + "pageCount": 581, + "printedPageCount": 581, + "dimensions": { + "height": "22.00 cm" + }, + "printType": "BOOK", + "maturityRating": "NOT_MATURE", + "allowAnonLogging": false, + "contentVersion": "preview-1.0.0", + "language": "fr", + "previewLink": "http://books.google.fr/books?id=DQUFSQAACAAJ&hl=&source=gbs_api", + "infoLink": "https://play.google.com/store/books/details?id=DQUFSQAACAAJ&source=gbs_api", + "canonicalVolumeLink": "https://play.google.com/store/books/details?id=DQUFSQAACAAJ" + }, + "saleInfo": { + "country": "FR", + "saleability": "NOT_FOR_SALE", + "isEbook": false + }, + "accessInfo": { + "country": "FR", + "viewability": "NO_PAGES", + "embeddable": false, + "publicDomain": false, + "textToSpeechPermission": "ALLOWED", + "epub": { + "isAvailable": false + }, + "pdf": { + "isAvailable": false + }, + "webReaderLink": "http://play.google.com/books/reader?id=DQUFSQAACAAJ&hl=&source=gbs_api", + "accessViewStatus": "NONE", + "quoteSharingAllowed": false + } +} \ No newline at end of file diff --git a/native/src/image_tools.rs b/native/src/image_tools.rs new file mode 100644 index 0000000..0040c24 --- /dev/null +++ b/native/src/image_tools.rs @@ -0,0 +1,19 @@ +use std::{path::Path, process::Command}; + +pub fn downsize_image(widht: u32, height: u32, input_filepath: &Path, output_filepath: &Path) { + let output = Command::new("convert") + .arg(input_filepath.to_str().unwrap()) + .arg("-resize") + .arg(format!("{}x{}^>", widht, height)) + .arg(output_filepath.to_str().unwrap()) + .output() + .unwrap(); + if output.status.success() { + return; + } + println!("status: {}", output.status); + println!("stdout: {:?}", &std::str::from_utf8(&output.stdout)); + println!("stderr: {:?}", &std::str::from_utf8(&output.stderr)); + + assert!(output.status.success()); +} diff --git a/native/src/jwt_decoder.rs b/native/src/jwt_decoder.rs new file mode 100644 index 0000000..d24e1b4 --- /dev/null +++ b/native/src/jwt_decoder.rs @@ -0,0 +1,23 @@ +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +struct JWT { + exp: u64, +} +use std::time::SystemTime; + +pub fn check_jwt_expiration(jwt: &str) -> () { + let parts = jwt.split('.').collect_vec(); + let p = parts[1]; + println!("p = {}", p); + let decoded = + base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, p).unwrap(); + let jj: JWT = serde_json::from_slice(&decoded).unwrap(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + if jj.exp < now { + panic!("TOKEN is expired"); + } +} diff --git a/native/src/leboncoin.rs b/native/src/leboncoin.rs new file mode 100644 index 0000000..826aa13 --- /dev/null +++ b/native/src/leboncoin.rs @@ -0,0 +1,54 @@ +extern crate reqwest; +pub(crate) mod personal_info; +use crate::publisher::Publisher; +pub struct Leboncoin; +use crate::image_tools; + +mod parser; +mod request; + +use itertools::Itertools; +use std::path::Path; + + +impl Publisher for Leboncoin { + fn publish(&self, ad: crate::common::Ad, credential: crate::common::LbcCredential) -> bool { + crate::jwt_decoder::check_jwt_expiration(&credential.lbc_token); + let img_lbc_refs = ad + .imgs_path + .clone() + .into_iter() + .map(|img_filepath| { + let input_path = Path::new(&img_filepath); + let compressed_img_filepath = Path::new("compressed/") + .join(input_path.file_name().unwrap().to_str().unwrap()); + image_tools::downsize_image(800, 800, &input_path, &compressed_img_filepath); + let imgs_upload_response = request::upload_file(&compressed_img_filepath, &credential); + let imgs_lbc_ref = parser::parse_file_upload(&imgs_upload_response); + Image { + name: imgs_lbc_ref.filename, + url: imgs_lbc_ref.url, + } + }) + .collect_vec(); + // let img_lbc_refs = vec![]; + + let send_answer: String = request::send(ad, img_lbc_refs, &credential); + let ad_id = parser::parse_send(&send_answer); + let submit_answer = request::submit(ad_id, credential).unwrap(); + let submit_ret = parser::parse_submit(&submit_answer); + println!("submit_ret = {:#?}", submit_ret); + match submit_ret { + parser::SubmitResult::Submitted => true, + parser::SubmitResult::Captcha(_) => false, + } + } +} + +use serde::{Deserialize, Serialize}; +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Image { + pub name: String, + pub url: String, +} diff --git a/native/src/leboncoin/parser.rs b/native/src/leboncoin/parser.rs new file mode 100644 index 0000000..b7fcb22 --- /dev/null +++ b/native/src/leboncoin/parser.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +//////////// + +pub fn parse_file_upload(imgs_upload_response: &str) -> ImageSubmitResponse { + let r: ImageSubmitResponse = serde_json::from_str(imgs_upload_response).unwrap(); + r +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageSubmitResponse { + pub filename: String, + pub url: String, +} + +//////////// + +pub fn parse_send(send_response: &str) -> i64 { + let s: structs::SendResponse = serde_json::from_str(send_response).unwrap(); + s.ad_id +} + +mod structs { + use serde::{Deserialize, Serialize}; + + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct SendResponse { + pub status: String, + #[serde(rename = "ad_id")] + pub ad_id: i64, + #[serde(rename = "action_id")] + pub action_id: i64, + pub step: String, + #[serde(rename = "transaction_step")] + pub transaction_step: String, + } +} + +////////////// + +#[derive(Debug)] +pub enum SubmitResult { + Submitted, + Captcha(String), +} + +pub fn parse_submit(submit_response: &str) -> SubmitResult { + if submit_response == "{}" { + return SubmitResult::Submitted; + } + let s: SubmitResponse = serde_json::from_str(submit_response).unwrap(); + SubmitResult::Captcha(s.url) +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubmitResponse { + pub url: String, +} diff --git a/native/src/leboncoin/request.rs b/native/src/leboncoin/request.rs new file mode 100644 index 0000000..1e7682e --- /dev/null +++ b/native/src/leboncoin/request.rs @@ -0,0 +1,313 @@ +use std::path::Path; + +use reqwest; +use serde::{Deserialize, Serialize}; + +use crate::{common::LbcCredential, leboncoin::personal_info}; + +use super::Image; + +pub fn send(ad: crate::common::Ad, images: Vec, credential: &LbcCredential) -> String { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert("authority", "api.leboncoin.fr".parse().unwrap()); + headers.insert("accept", "*/*".parse().unwrap()); + headers.insert( + "accept-language", + "en-US,en;q=0.9,fr;q=0.8".parse().unwrap(), + ); + headers.insert( + "authorization", + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), + ); + headers.insert("cache-control", "no-cache".parse().unwrap()); + headers.insert("content-type", "application/json".parse().unwrap()); + headers.insert("origin", "https://www.leboncoin.fr".parse().unwrap()); + headers.insert("pragma", "no-cache".parse().unwrap()); + headers.insert( + "referer", + "https://www.leboncoin.fr/deposer-une-annonce" + .parse() + .unwrap(), + ); + headers.insert( + "sec-ch-ua", + "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\"" + .parse() + .unwrap(), + ); + headers.insert("sec-ch-ua-mobile", "?0".parse().unwrap()); + headers.insert("sec-ch-ua-platform", "\"Linux\"".parse().unwrap()); + headers.insert("sec-fetch-dest", "empty".parse().unwrap()); + headers.insert("sec-fetch-mode", "cors".parse().unwrap()); + headers.insert( + reqwest::header::COOKIE, + credential.datadome_cookie.parse().unwrap(), + ); + headers.insert("sec-fetch-site", "same-site".parse().unwrap()); + headers.insert("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36".parse().unwrap()); + + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + let body = SendStruct { + subject: ad.title, + body: ad.description, + category_id: "27".to_string(), + ad_type: "sell".to_string(), + images, + attributes: Attributes { + title_adparams_prediction_id: "2a242efc-e50f-4c3c-9486-9c6ee59225dd".to_string(), + item_condition: "3".to_string(), + donation: "0".to_string(), + price_reco: "2|13|10|90|64ca29e6-269d-4d89-b945-a0dcd5eaf992".to_string(), + shipping_cost: "".to_string(), + }, + extended_attributes: ExtendedAttributes { + shipping: Shipping { + version: 2, + shipping_types: [ + "mondial_relay", + "colissimo", + "face_to_face", + "courrier_suivi", + ] + .map(|s| s.to_string()) + .to_vec(), + estimated_parcel_weight: 600, + }, + }, + location: Location { + address: "".to_string(), + city: personal_info::CITY.to_string(), + country: personal_info::COUNTRY.to_string(), + district: "".to_string(), + geo_provider: "here".to_string(), + geo_source: "city".to_string(), + label: personal_info::LABEL.to_string(), + lat: personal_info::LAT, + lng: personal_info::LNG, + zipcode: personal_info::ZIPCODE.to_string(), + }, + email: personal_info::EMAIL.to_string(), + phone: personal_info::PHONE.to_string(), + escrow_firstname: personal_info::ESCROW_FIRSTNAME.to_string(), + escrow_lastname: personal_info::ESCROW_LASTNAME.to_string(), + price_cents: ad.price_cent.to_string(), + price: (ad.price_cent / 100).to_string(), + no_salesmen: true, + }; + + let res = client + .post("https://api.leboncoin.fr/api/adsubmit/v2/classifieds?with_variation=true") + .headers(headers) + .body(serde_json::to_string(&body).unwrap()) + .send() + .unwrap() + .text() + .unwrap(); + + println!("request send : {:#?}", res); + res +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendStruct { + pub subject: String, + pub body: String, + #[serde(rename = "category_id")] + pub category_id: String, + #[serde(rename = "ad_type")] + pub ad_type: String, + pub images: Vec, + pub attributes: Attributes, + #[serde(rename = "extended_attributes")] + pub extended_attributes: ExtendedAttributes, + pub location: Location, + pub email: String, + pub phone: String, + pub escrow_firstname: String, + pub escrow_lastname: String, + #[serde(rename = "price_cents")] + pub price_cents: String, + pub price: String, + #[serde(rename = "no_salesmen")] + pub no_salesmen: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attributes { + #[serde(rename = "title_adparams_prediction_id")] + pub title_adparams_prediction_id: String, + #[serde(rename = "item_condition")] + pub item_condition: String, + pub donation: String, + #[serde(rename = "price_reco")] + pub price_reco: String, + #[serde(rename = "shipping_cost")] + pub shipping_cost: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExtendedAttributes { + pub shipping: Shipping, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Shipping { + pub version: i64, + #[serde(rename = "shipping_types")] + pub shipping_types: Vec, + #[serde(rename = "estimated_parcel_weight")] + pub estimated_parcel_weight: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Location { + pub address: String, + pub city: String, + pub country: String, + pub district: String, + #[serde(rename = "geo_provider")] + pub geo_provider: String, + #[serde(rename = "geo_source")] + pub geo_source: String, + pub label: String, + pub lat: f64, + pub lng: f64, + pub zipcode: String, +} + +pub fn submit(ad_id: i64, credential: LbcCredential) -> Result> { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert("authority", "api.leboncoin.fr".parse().unwrap()); + headers.insert("accept", "*/*".parse().unwrap()); + headers.insert( + "accept-language", + "en-US,en;q=0.9,fr;q=0.8".parse().unwrap(), + ); + headers.insert( + "authorization", + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), + ); + headers.insert("cache-control", "no-cache".parse().unwrap()); + headers.insert("content-type", "application/json".parse().unwrap()); + headers.insert("origin", "https://www.leboncoin.fr".parse().unwrap()); + headers.insert("pragma", "no-cache".parse().unwrap()); + headers.insert( + "referer", + "https://www.leboncoin.fr/deposer-une-annonce/options" + .parse() + .unwrap(), + ); + headers.insert( + "sec-ch-ua", + "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\"" + .parse() + .unwrap(), + ); + headers.insert("sec-ch-ua-mobile", "?0".parse().unwrap()); + headers.insert("sec-ch-ua-platform", "\"Linux\"".parse().unwrap()); + headers.insert("sec-fetch-dest", "empty".parse().unwrap()); + headers.insert("sec-fetch-mode", "cors".parse().unwrap()); + headers.insert( + reqwest::header::COOKIE, + credential.datadome_cookie.parse().unwrap(), + ); + headers.insert("sec-fetch-site", "same-site".parse().unwrap()); + headers.insert("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36".parse().unwrap()); + + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + let body = SubmitBody { + ads: vec![SubmitAd { + ad_type: "sell".to_string(), + ad_id, + options: vec![], + action_id: 1, + transaction_type: "new_ad".to_string(), + }], + pricing_id: "87275b3e0eae7a906b6ef915156f8295".to_string(), + user_journey: "deposit".to_string(), + }; + let res = client + .post("https://api.leboncoin.fr/api/services/v1/submit") + .headers(headers) + .body(serde_json::to_string(&body).unwrap()) + .send()? + .text()?; + println!("request submit : {}", res); + + Ok(res) +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubmitBody { + pub ads: Vec, + #[serde(rename = "pricing_id")] + pub pricing_id: String, + #[serde(rename = "user_journey")] + pub user_journey: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubmitAd { + #[serde(rename = "ad_type")] + pub ad_type: String, + #[serde(rename = "ad_id")] + pub ad_id: i64, + pub options: Vec, + #[serde(rename = "action_id")] + pub action_id: i64, + #[serde(rename = "transaction_type")] + pub transaction_type: String, +} + +pub fn upload_file(img_path: &Path, credential: &LbcCredential) -> String { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + "authorization", + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), + ); + headers.insert( + "sec-ch-ua", + "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\"" + .parse() + .unwrap(), + ); + headers.insert("sec-ch-ua-mobile", "?0".parse().unwrap()); + headers.insert("sec-ch-ua-platform", "\"Linux\"".parse().unwrap()); + headers.insert("sec-fetch-dest", "empty".parse().unwrap()); + headers.insert("sec-fetch-mode", "cors".parse().unwrap()); + headers.insert( + reqwest::header::COOKIE, + credential.datadome_cookie.parse().unwrap(), + ); + headers.insert("sec-fetch-site", "same-site".parse().unwrap()); + headers.insert("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36".parse().unwrap()); + + let form = reqwest::blocking::multipart::Form::new() + .file("file", img_path) + .expect(&format!("Could not find file at path: {:?}", img_path)); + + let client = reqwest::blocking::Client::new(); + let res = client + .post("https://api.leboncoin.fr/api/pintad/v1/public/upload/image") + .headers(headers) + .multipart(form) + .send() + .unwrap() + .text() + .unwrap(); + println!("upload_file response = {}", res); + res +} diff --git a/native/src/lib.rs b/native/src/lib.rs index dfdd872..691c70e 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,2 +1,15 @@ + +// #[macro_use] +// extern crate thirtyfour; mod api; +mod babelio; mod bridge_generated; +mod cached_client; +mod common; +mod config; +mod google_books; +mod image_tools; +mod jwt_decoder; +mod leboncoin; +mod publisher; +mod booksprice; diff --git a/native/src/publisher.rs b/native/src/publisher.rs new file mode 100644 index 0000000..6008400 --- /dev/null +++ b/native/src/publisher.rs @@ -0,0 +1,5 @@ +use crate::common::{Ad, LbcCredential}; + +pub trait Publisher { + fn publish(&self, ad: Ad, credential: LbcCredential) -> bool; +} diff --git a/native/tests/test_html/9782884747974.html b/native/tests/test_html/9782884747974.html new file mode 100644 index 0000000..55071ca --- /dev/null +++ b/native/tests/test_html/9782884747974.html @@ -0,0 +1,424 @@ + +2884747974 Book Price Comparison + + + + + + + + + + + + +
+
+
BooksPrice.com

book price comparison

+
+

 

+
+
+ +
+
+
+
+

Histoire d'une maison

+

+Viollet-le-Duc, Eugène-Emmanuel Bressani, Martin  +

+

+9782884747974 / Paperback + + + / INFOLIO + +

+ + +

+ +
+
+ +
Set Email Price Alert + + + + +
+ +
+ +
+ + +
+
+ + + + +
+ + +
+ +
+
+ + +
 
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Store NameConditionTermPriceShippingTotal Price Go to Store (#ad)
+
  +New + [+] + + $16.55Free + + $ + 16.55 + + + + + + + + + Buy at +
+ +Abebooks
  +New + + $17.86$3.99 $ + 21.85 + + + + + + + + + Buy at +
+ +Amazon
  +New + + $19.76$3.99 $ + 23.75 + + + + + + + + + Buy at +
+ +Amazon
  +Used (Very Good) + + $27.17Free + + $ + 27.17 + + + + + + + + + Buy at +
+ +Amazon
  +Used (Mint) + + $28.15Free + + $ + 28.15 + + + + + + + + + Buy at +
+ +Amazon
  +Used (Very Good) + [+] + + $10.97$32.23 $ + 43.20 + + + + + + + + + Buy at +
+ +Abebooks
+ +
+
+ + + +
+ + + + + +
+

* Shipping cost is an estimate. Please check the +shipping cost at the site before making the purchase!

+

The prices shown may have risen since the time we +last updated them.
+The actual price of the product on the sellers site at the time of +purchase will govern the sale.
+It is not technically possible for the prices displayed above to be +updated in real-time.

+
+
+
+ +
 
+
+ + + + + + + + + + +. + + \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e7f3e0b..dd9fdbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,12 @@ name: flutter_rust_bridge_template description: A new Flutter project. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: - sdk: ">=2.17.5 <3.0.0" + sdk: '>=3.0.0-305.0.dev <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -29,36 +17,31 @@ environment: dependencies: flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + collection: ^1.17.1 ffi: ^2.0.1 flutter_rust_bridge: ^1.45.0 + json_annotation: ^4.8.0 + jwt_decoder: ^2.0.1 meta: ^1.8.0 + path: ^1.8.3 + super_clipboard: ^0.3.0+2 + super_drag_and_drop: ^0.3.0+2 + super_native_extensions: ^0.3.0+2 + kt_dart: ^1.1.0 + camera: ^0.10.3+2 + gallery_saver: ^2.3.2 + permission_handler: ^10.2.0 + dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^2.0.0 - ffigen: ^7.2.4 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec + ffigen: ^7.2.7 + build_runner: ^2.4.1 + json_serializable: ^6.6.1 -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index 8aaa46a..0000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index b749bfe..0000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48..0000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d7..0000000 Binary files a/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c566..0000000 Binary files a/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 4b7bd51..0000000 --- a/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - flutter_rust_bridge_template - - - - - - - - - - diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100644 index a589f20..0000000 --- a/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "flutter_rust_bridge_template", - "short_name": "flutter_rust_bridge_template", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt deleted file mode 100644 index 8394b51..0000000 --- a/windows/CMakeLists.txt +++ /dev/null @@ -1,102 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(flutter_rust_bridge_template LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "flutter_rust_bridge_template") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - -include(./rust.cmake) - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt deleted file mode 100644 index 930d207..0000000 --- a/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,104 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d468..0000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100644 index b93c4c3..0000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c..0000000 --- a/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc deleted file mode 100644 index 45b3617..0000000 --- a/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "flutter_rust_bridge_template" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "flutter_rust_bridge_template" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "flutter_rust_bridge_template.exe" "\0" - VALUE "ProductName", "flutter_rust_bridge_template" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp deleted file mode 100644 index b25e363..0000000 --- a/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652..0000000 --- a/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp deleted file mode 100644 index 6be2cdc..0000000 --- a/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"flutter_rust_bridge_template", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/windows/runner/resource.h b/windows/runner/resource.h deleted file mode 100644 index 66a65d1..0000000 --- a/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20c..0000000 Binary files a/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest deleted file mode 100644 index a42ea76..0000000 --- a/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp deleted file mode 100644 index f5bf9fa..0000000 --- a/windows/runner/utils.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/windows/runner/utils.h b/windows/runner/utils.h deleted file mode 100644 index 3879d54..0000000 --- a/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp deleted file mode 100644 index 041a385..0000000 --- a/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h deleted file mode 100644 index c86632d..0000000 --- a/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/windows/rust.cmake b/windows/rust.cmake deleted file mode 100644 index a7c5321..0000000 --- a/windows/rust.cmake +++ /dev/null @@ -1,21 +0,0 @@ -# We include Corrosion inline here, but ideally in a project with -# many dependencies we would need to install Corrosion on the system. -# See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install -# Once done, uncomment this line: -# find_package(Corrosion REQUIRED) - -include(FetchContent) - -FetchContent_Declare( - Corrosion - GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git - GIT_TAG origin/master # Optionally specify a version tag or branch here -) - -FetchContent_MakeAvailable(Corrosion) - -corrosion_import_crate(MANIFEST_PATH ../native/Cargo.toml IMPORTED_CRATES imported_crates) -target_link_libraries(${BINARY_NAME} PRIVATE ${imported_crates}) -foreach(imported_crate ${imported_crates}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) -endforeach()