From 8e818428bfa7ed91cd43f1164013e85f3fb80882 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 8 Mar 2023 21:10:16 +0100 Subject: [PATCH 01/81] gen code --- android/gradle.properties | 1 + lib/bridge_definitions.dart | 6 +- lib/bridge_generated.dart | 167 ++++++++++++++++++++++++++++++++- lib/main.dart | 1 + native/src/bridge_generated.rs | 9 +- pubspec.yaml | 2 +- 6 files changed, 169 insertions(+), 17 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..5c76181 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-bundle/ diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index fadd6b6..a2ace8d 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -1,9 +1,7 @@ // 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'; diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 7501e4b..227be62 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; @@ -72,3 +77,157 @@ class NativeImpl implements Native { // Section: api2wire // Section: finalizer + +class NativePlatform extends FlutterRustBridgeBase { + NativePlatform(ffi.DynamicLibrary dylib) : super(NativeWire(dylib)); + +// Section: api2wire + +// Section: finalizer + +// Section: api_fill_to_wire +} + +// 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_platform( + int port_, + ) { + return _wire_platform( + port_, + ); + } + + late final _wire_platformPtr = + _lookup>( + 'wire_platform'); + late final _wire_platform = + _wire_platformPtr.asFunction(); + + void wire_rust_release_mode( + int port_, + ) { + return _wire_rust_release_mode( + port_, + ); + } + + late final _wire_rust_release_modePtr = + _lookup>( + 'wire_rust_release_mode'); + late final _wire_rust_release_mode = + _wire_rust_release_modePtr.asFunction(); + + 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 {} + +typedef DartPostCObjectFnType = ffi.Pointer< + ffi.NativeFunction)>>; +typedef DartPort = ffi.Int64; diff --git a/lib/main.dart b/lib/main.dart index d151ca4..f4f0614 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; void main() { diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index 9f63b66..dac5a8e 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; @@ -86,13 +86,6 @@ 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/pubspec.yaml b/pubspec.yaml index e7f3e0b..298476d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 - ffigen: ^7.2.4 + ffigen: ^7.2.7 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From ec9c83359a0dc81ec64f52b9acf257b22dc0e293 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 9 Mar 2023 19:20:33 +0100 Subject: [PATCH 02/81] Import BookMetadataFinder rust code and basic UI --- lib/bridge_definitions.dart | 34 +- lib/bridge_generated.dart | 152 +++-- lib/main.dart | 129 ++-- native/.gitignore | 1 + native/Cargo.toml | 8 + native/src/api.rs | 160 +++-- native/src/babelio.rs | 91 +++ native/src/babelio/parser.rs | 212 +++++++ native/src/babelio/request.rs | 69 +++ native/src/babelio/test/get_book.html | 581 ++++++++++++++++++ native/src/babelio/test/get_book_minimal.html | 147 +++++ native/src/babelio/test/get_see_more.html | 1 + native/src/babelio/test/search_by_isbn | 0 native/src/bridge_generated.io.rs | 61 +- native/src/bridge_generated.rs | 49 +- native/src/cached_client.rs | 22 + native/src/common.rs | 30 + native/src/google_books.rs | 17 + native/src/google_books/parser.rs | 174 ++++++ native/src/google_books/request.rs | 13 + .../src/google_books/test/isbn_response.html | 67 ++ .../google_books/test/self_link_response.html | 63 ++ native/src/image_tools.rs | 11 + native/src/jwt_decoder.rs | 23 + native/src/leboncoin.rs | 52 ++ native/src/leboncoin/parser.rs | 61 ++ native/src/leboncoin/request.rs | 347 +++++++++++ native/src/lib.rs | 8 + native/src/publisher.rs | 5 + 29 files changed, 2390 insertions(+), 198 deletions(-) create mode 100644 native/.gitignore create mode 100644 native/src/babelio.rs create mode 100644 native/src/babelio/parser.rs create mode 100644 native/src/babelio/request.rs create mode 100644 native/src/babelio/test/get_book.html create mode 100644 native/src/babelio/test/get_book_minimal.html create mode 100644 native/src/babelio/test/get_see_more.html create mode 100644 native/src/babelio/test/search_by_isbn create mode 100644 native/src/cached_client.rs create mode 100644 native/src/common.rs create mode 100644 native/src/google_books.rs create mode 100644 native/src/google_books/parser.rs create mode 100644 native/src/google_books/request.rs create mode 100644 native/src/google_books/test/isbn_response.html create mode 100644 native/src/google_books/test/self_link_response.html create mode 100644 native/src/image_tools.rs create mode 100644 native/src/jwt_decoder.rs create mode 100644 native/src/leboncoin.rs create mode 100644 native/src/leboncoin/parser.rs create mode 100644 native/src/leboncoin/request.rs create mode 100644 native/src/publisher.rs diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index a2ace8d..29fd663 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -2,28 +2,28 @@ // 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 '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}); - - FlutterRustBridgeTaskConstMeta get kPlatformConstMeta; + Future getMetadataFromImages({required List imgsPath, dynamic hint}); - Future rustReleaseMode({dynamic hint}); - - FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta; + FlutterRustBridgeTaskConstMeta get kGetMetadataFromImagesConstMeta; } -enum Platform { - Unknown, - Android, - Ios, - Windows, - Unix, - MacIntel, - MacApple, - Wasm, +class Ad { + String title; + String description; + int priceCent; + List imgsPath; + + Ad({ + required this.title, + required this.description, + required this.priceCent, + required this.imgsPath, + }); } diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 227be62..7ae6c5e 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -24,36 +24,23 @@ class NativeImpl implements Native { factory NativeImpl.wasm(FutureOr module) => NativeImpl(module as ExternalLibrary); NativeImpl.raw(this._platform); - Future platform({dynamic hint}) { + Future getMetadataFromImages( + {required List imgsPath, dynamic hint}) { + var arg0 = _platform.api2wire_StringList(imgsPath); 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_images(port_, arg0), + parseSuccessData: _wire2api_ad, + constMeta: kGetMetadataFromImagesConstMeta, + argValues: [imgsPath], hint: hint, )); } - FlutterRustBridgeTaskConstMeta get kPlatformConstMeta => + FlutterRustBridgeTaskConstMeta get kGetMetadataFromImagesConstMeta => const FlutterRustBridgeTaskConstMeta( - debugName: "platform", - argNames: [], - ); - - Future rustReleaseMode({dynamic hint}) { - return _platform.executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => _platform.inner.wire_rust_release_mode(port_), - parseSuccessData: _wire2api_bool, - constMeta: kRustReleaseModeConstMeta, - argValues: [], - hint: hint, - )); - } - - FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta => - const FlutterRustBridgeTaskConstMeta( - debugName: "rust_release_mode", - argNames: [], + debugName: "get_metadata_from_images", + argNames: ["imgsPath"], ); void dispose() { @@ -61,21 +48,46 @@ class NativeImpl implements Native { } // Section: wire2api - bool _wire2api_bool(dynamic raw) { - return raw as bool; + String _wire2api_String(dynamic raw) { + return raw as String; + } + + List _wire2api_StringList(dynamic raw) { + return (raw as List).cast(); + } + + Ad _wire2api_ad(dynamic raw) { + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return Ad( + title: _wire2api_String(arr[0]), + description: _wire2api_String(arr[1]), + priceCent: _wire2api_i32(arr[2]), + imgsPath: _wire2api_StringList(arr[3]), + ); } int _wire2api_i32(dynamic raw) { return raw as int; } - Platform _wire2api_platform(dynamic raw) { - return Platform.values[raw]; + int _wire2api_u8(dynamic raw) { + return raw as int; + } + + Uint8List _wire2api_uint_8_list(dynamic raw) { + return raw as Uint8List; } } // Section: api2wire +@protected +int api2wire_u8(int raw) { + return raw; +} + // Section: finalizer class NativePlatform extends FlutterRustBridgeBase { @@ -83,6 +95,26 @@ class NativePlatform extends FlutterRustBridgeBase { // 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_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 @@ -183,33 +215,51 @@ class NativeWire implements FlutterRustBridgeWireBase { late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr .asFunction)>(); - void wire_platform( + void wire_get_metadata_from_images( int port_, + ffi.Pointer imgs_path, ) { - return _wire_platform( + return _wire_get_metadata_from_images( port_, + imgs_path, ); } - late final _wire_platformPtr = - _lookup>( - 'wire_platform'); - late final _wire_platform = - _wire_platformPtr.asFunction(); + late final _wire_get_metadata_from_imagesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_get_metadata_from_images'); + late final _wire_get_metadata_from_images = _wire_get_metadata_from_imagesPtr + .asFunction)>(); - void wire_rust_release_mode( - int port_, + ffi.Pointer new_StringList_0( + int len, ) { - return _wire_rust_release_mode( - port_, + 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_uint_8_list_0( + int len, + ) { + return _new_uint_8_list_0( + len, ); } - late final _wire_rust_release_modePtr = - _lookup>( - 'wire_rust_release_mode'); - late final _wire_rust_release_mode = - _wire_rust_release_modePtr.asFunction(); + 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, @@ -228,6 +278,20 @@ class NativeWire implements FlutterRustBridgeWireBase { 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; +} + typedef DartPostCObjectFnType = ffi.Pointer< ffi.NativeFunction)>>; typedef DartPort = ffi.Int64; diff --git a/lib/main.dart b/lib/main.dart index f4f0614..29b4a08 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,52 +50,25 @@ class MyHomePage extends StatefulWidget { } 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; - + late Future ad; @override void initState() { super.initState(); - platform = api.platform(); - isRelease = api.rustReleaseMode(); + ad = api.getMetadataFromImages(imgsPath: [ + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg' + ]); //.then((ad) => print('ad = $ad')); } @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 @@ -104,10 +77,8 @@ class _MyHomePageState extends State { // 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]), + FutureBuilder( + future: ad, builder: (context, snap) { final style = Theme.of(context).textTheme.headline4; if (snap.error != null) { @@ -116,29 +87,14 @@ class _MyHomePageState extends State { debugPrint(snap.error.toString()); return Tooltip( message: snap.error.toString(), - child: Text('Unknown OS', style: style), + child: Text('Error during image decoding', 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); + final ad = snap.data; + if (ad == null) return const Text("Extracting info from images"); + + return AdPage(ad: ad); }, ) ], @@ -147,3 +103,66 @@ class _MyHomePageState extends State { ); } } + +extension IntExt on int { + int divide(int other) => this ~/ other; +} + +extension DoubleExt on double { + double multiply(double other) => this * other; +} + +class AdPage extends StatefulWidget { + AdPage({super.key, required Ad ad}) : initialAd = ad; + + final Ad initialAd; + + @override + State createState() => _AdPageState(); +} + +class _AdPageState extends State { + late Ad ad; + + @override + void initState() { + super.initState(); + ad = widget.initialAd; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TextFormField( + initialValue: ad.title, + onChanged: (newText) { + setState(() { + ad.title = newText; + }); + }, + ), + TextFormField( + initialValue: ad.description, + maxLines: 20, + onChanged: (newText) { + setState(() { + ad.description = newText; + }); + }, + ), + TextFormField( + initialValue: ad.priceCent /*?*/ .divide(100).toString(), + onChanged: (newText) => setState(() => ad.priceCent = double.tryParse(newText)! /*?*/ .multiply(100).round()), + ), + ElevatedButton( + onPressed: ad.priceCent == null + ? null + : () { + print('Try to publish'); + }, + child: const Text("Publish")) + ], + ); + } +} diff --git a/native/.gitignore b/native/.gitignore new file mode 100644 index 0000000..107eafc --- /dev/null +++ b/native/.gitignore @@ -0,0 +1 @@ +personal_info.rs diff --git a/native/Cargo.toml b/native/Cargo.toml index 667e107..a013ab0 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,3 +11,11 @@ crate-type = ["cdylib", "staticlib"] [dependencies] anyhow = "1" flutter_rust_bridge = "1" +#reqwest = "0.11.14" +base64 = "0.21.0" +itertools = "0.10.5" +regex = "1.7.1" +reqwest = { version = "0.11.14", features = ["blocking", "json", "multipart"] } +scraper = "0.14.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.91" \ No newline at end of file diff --git a/native/src/api.rs b/native/src/api.rs index 976bb9d..65e1ea0 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,59 +1,111 @@ -// 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. - -// 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, -} +use std::process::Command; +use itertools::Itertools; +use crate::{babelio, common, google_books, leboncoin}; +use crate::common::{Ad, BookMetaData}; + +pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { + let isbns: Vec = imgs_path + .clone() + .into_iter() + .map(|picture_path| { + println!("{picture_path}"); + let output = Command::new( + "/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode", + ) + .arg("-in=".to_string() + &picture_path) + .output() + .expect("failed to execute process"); + let output = std::str::from_utf8(&output.stdout).unwrap(); + println!("output is {:?}", output); + output + .split_ascii_whitespace() + .map(|x| x.to_string()) + .collect_vec() + }) + .flatten() + .unique() + .collect(); + + println!("isbns {:?}", isbns); + + let book_metadata_providers: Vec> = vec![ + Box::new(babelio::Babelio {}), + Box::new(google_books::GoogleBooks {}), + ]; -// 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 + let books: Vec = isbns + .iter() + .map(|isbn| { + for provider in &book_metadata_providers { + let res = provider.get_book_metadata_from_isbn(&isbn); + if let Some(r) = res { + return r; + } + } + panic!("No provider find any information on book {}", isbn) + /* book_metadata_providers[0] + .get_book_metadata_from_isbn(&isbn) + .unwrap() */ + }) + .collect(); + let books_titles = books.iter().map(book_format_title_and_author).join("\n"); + let blurbs = books + .iter() + .map(|b| { + format!( + "{}:\n{}\n", + book_format_title_and_author(b), + b.blurb.as_ref().unwrap() + ) + }) + .join("\n"); + let keywords = books.iter().flat_map(|b| &b.keywords).unique().join(", "); + + let custom_message = leboncoin::personal_info::CUSTOM_MESSAGE; + + let mut ad_description = books_titles + "\n\nRésumé:\n" + &blurbs + "\n" + &custom_message; + if !keywords.is_empty() { + ad_description = ad_description + "\n\nMots-clés:\n" + &keywords; } + + println!("ad_description: {:#?}", ad_description); + println!("ad_description: {}", ad_description); + + common::Ad { + title: if books.len() == 1 { + books.first().unwrap().title.clone() + } else { + todo!() + }, + description: ad_description, + price_cent: 1000, + imgs_path, + } + + /*let publisher = leboncoin::Leboncoin {}; + + + publisher::Publisher::publish(&publisher, ad);*/ } -// 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)) +fn book_format_title_and_author(book: &BookMetaData) -> String { + format!( + "\"{}\" {}", + book.title, + vec_fmt( + book.authors + .iter() + .map(|a| format!("{} {}", a.first_name, a.last_name)) + .collect_vec() + ) + ) +} + +fn vec_fmt(vec: Vec) -> String { + match vec.len() { + 0 => "".to_string(), + 1 => format!("de {}", vec[0]), + 2 => format!("de {} et {}", vec[0], vec[1]), + _ => panic!("More than 2 authors"), + } } diff --git a/native/src/babelio.rs b/native/src/babelio.rs new file mode 100644 index 0000000..9878b25 --- /dev/null +++ b/native/src/babelio.rs @@ -0,0 +1,91 @@ +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 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) + } + }; + + let mut res = parser::extract_title_author_keywords(&book_page); + res.blurb = parser::parse_blurb(&raw_blurb); + Some(res) + } +} + +#[cfg(test)] +mod tests { + use crate::common::{Author, BookMetaData, 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(BookMetaData { + title: "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\n".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(), + })); + } + + #[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(BookMetaData { + title: "À 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(), + })); + } +} diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs new file mode 100644 index 0000000..d12517e --- /dev/null +++ b/native/src/babelio/parser.rs @@ -0,0 +1,212 @@ +use crate::common::{html_select, BookMetaData}; +use itertools::Itertools; + +#[derive(PartialEq, Debug)] +pub enum BlurbRes { + SmallBlurb(String), + BigBlurb(String), +} + +pub fn extract_blurb(html: &str) -> BlurbRes { + let doc = scraper::Html::parse_document(html); + + let selector = scraper::Selector::parse("#d_bio").expect( + format!( + "Response should contain a element whose id is 'd_bio', html is {:?}", + html + ) + .as_str(), + ); + let mut res = doc.select(&selector); + + let d_bio = res.next().expect( + format!( + "There should be exactly one element with id 'd_bio', html {:?}", + html + ) + .as_str(), + ); + + // Some books do not folow the general strucuture: 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)"); + 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]; + BlurbRes::BigBlurb(String::from(id_obj)) + } + } +} + +pub fn extract_title_author_keywords(html: &str) -> BookMetaData { + 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 = res.exactly_one().expect(format!( + "Response should contain a element whose with id is itemscope and itemtype=\"https://schema.org/Book\", html is {:?}", + html + ) + .as_str()); + 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(|author_scope| { + 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 first_name = author_span + .first_child() + .expect("author scope > a > span shoud have a first child which is first name") + .value() + .as_text() + .expect("should be a text") + .trim() + .to_string(); + let last_name = author_span + .children() + .nth(1) + .expect("author scope > a > span shoud have a second child which is the last name") + .first_child() + .unwrap() + .value() + .as_text() + .expect("should be a text") + .trim() + .to_string(); + crate::common::Author { + first_name, + last_name, + } + }) + .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(); + BookMetaData { + title, + authors, + keywords, + ..Default::default() + } +} + +pub fn parse_blurb(raw_blurb: &str) -> Option { + Some(raw_blurb.trim().replace("
", "\n")) +} + +#[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, BlurbRes::BigBlurb("827593".to_string())); + } + #[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, + BookMetaData { + title: "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(), + } + ); + } +} diff --git a/native/src/babelio/request.rs b/native/src/babelio/request.rs new file mode 100644 index 0000000..79735a8 --- /dev/null +++ b/native/src/babelio/request.rs @@ -0,0 +1,69 @@ +use crate::cached_client::CachedClient; +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: &CachedClient, isbn: &str) -> Option { + let raw_search_results = client.get_from_cache( + format!("cache/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.get_from_cache( + format!( + "cache/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.get_from_cache( + format!("cache/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/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_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/bridge_generated.io.rs b/native/src/bridge_generated.io.rs index a4ec960..f335968 100644 --- a/native/src/bridge_generated.io.rs +++ b/native/src/bridge_generated.io.rs @@ -2,23 +2,74 @@ 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_images(port_: i64, imgs_path: *mut wire_StringList) { + wire_get_metadata_from_images_impl(port_, imgs_path) } +// Section: allocate functions + #[no_mangle] -pub extern "C" fn wire_rust_release_mode(port_: i64) { - wire_rust_release_mode_impl(port_) +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) } -// Section: allocate functions +#[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 *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_uint_8_list { + ptr: *mut u8, + len: i32, +} + // Section: impl NewWithNullPtr pub trait NewWithNullPtr { diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index dac5a8e..991de3c 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -19,26 +19,24 @@ use std::sync::Arc; // Section: imports +use crate::common::Ad; + // Section: wire functions -fn wire_platform_impl(port_: MessagePort) { +fn wire_get_metadata_from_images_impl( + port_: MessagePort, + imgs_path: impl Wire2Api> + UnwindSafe, +) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "platform", + debug_name: "get_metadata_from_images", port: Some(port_), mode: FfiCallMode::Normal, }, - move || move |task_callback| Ok(platform()), - ) -} -fn wire_rust_release_mode_impl(port_: MessagePort) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap( - WrapInfo { - debug_name: "rust_release_mode", - port: Some(port_), - mode: FfiCallMode::Normal, + move || { + let api_imgs_path = imgs_path.wire2api(); + move |task_callback| Ok(get_metadata_from_images(api_imgs_path)) }, - move || move |task_callback| Ok(rust_release_mode()), ) } // Section: wrapper structs @@ -63,23 +61,28 @@ where (!self.is_null()).then(|| self.wire2api()) } } + +impl Wire2Api for u8 { + fn wire2api(self) -> u8 { + self + } +} + // Section: impl IntoDart -impl support::IntoDart for Platform { +impl support::IntoDart for Ad { fn into_dart(self) -> support::DartAbi { - 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, - } + vec![ + self.title.into_dart(), + self.description.into_dart(), + self.price_cent.into_dart(), + self.imgs_path.into_dart(), + ] .into_dart() } } +impl support::IntoDartExceptPrimitive for Ad {} + // Section: executor support::lazy_static! { diff --git a/native/src/cached_client.rs b/native/src/cached_client.rs new file mode 100644 index 0000000..55783ea --- /dev/null +++ b/native/src/cached_client.rs @@ -0,0 +1,22 @@ +pub struct CachedClient { + pub http_client: reqwest::blocking::Client, +} + +impl CachedClient { + pub fn get_from_cache String>( + &self, + cache_file_path: &str, + make_request: F, + ) -> String { + let html = std::fs::read_to_string(cache_file_path); + match html { + Ok(f) => f, + Err(_) => { + 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..4723920 --- /dev/null +++ b/native/src/common.rs @@ -0,0 +1,30 @@ +#[derive(Default, Debug, PartialEq)] +pub struct BookMetaData { + pub title: String, + 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, +} + +#[derive(Debug, PartialEq)] +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 Ad { + pub title: String, + pub description: String, + pub price_cent: i32, + pub imgs_path: Vec, +} diff --git a/native/src/google_books.rs b/native/src/google_books.rs new file mode 100644 index 0000000..a2dd9aa --- /dev/null +++ b/native/src/google_books.rs @@ -0,0 +1,17 @@ +use crate::common; +mod parser; +mod request; + +pub struct GoogleBooks; + +impl common::Provider for GoogleBooks { + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + let client = reqwest::blocking::Client::builder().build().unwrap(); + let isbn_search_response = request::search_by_isbn(&client, isbn); + let self_link = parser::extract_self_link_from_isbn_response(&isbn_search_response); + let book_page = request::get_volume(&client, &self_link); + Some(parser::extract_metadata_from_self_link_response(&book_page)) + } +} + + diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs new file mode 100644 index 0000000..c563d33 --- /dev/null +++ b/native/src/google_books/parser.rs @@ -0,0 +1,174 @@ +use itertools::Itertools; + +use crate::common; + +pub fn extract_self_link_from_isbn_response(html: &str) -> String { + let s: structs::Root = serde_json::from_str(html).unwrap(); + s.items[0].self_link.to_string() +} + +pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { + let s: structs::Item = serde_json::from_str(html).unwrap(); + let first_book = &s.volume_info; + common::BookMetaData { + title: 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.map(|d| d.to_string()), + ..Default::default() + } +} + +#[cfg(test)] +mod tests { + use crate::common::BookMetaData; + + use super::*; + + #[test] + fn extract_self_link_from_file() { + let html = std::fs::read_to_string("src/google_books/test/isbn_response.html").unwrap(); + let self_link = extract_self_link_from_isbn_response(&html); + assert_eq!( + self_link, + "https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ" + ) + } + + #[test] + fn extract_metadata_from_file() { + let html = + std::fs::read_to_string("src/google_books/test/self_link_response.html").unwrap(); + let metadata = extract_metadata_from_self_link_response(&html); + assert_eq!(metadata, BookMetaData{ + title: "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() + }); + } +} + +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: Vec>, + } + + #[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, + pub description: Option<&'a str>, + 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")] + pub 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..f0df992 --- /dev/null +++ b/native/src/google_books/request.rs @@ -0,0 +1,13 @@ +pub fn search_by_isbn(client: &reqwest::blocking::Client, isbn: &str) -> String { + let resp = client + .get(format!( + "https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn}" + )) + .send() + .unwrap(); + resp.text().unwrap() +} +pub fn get_volume(client: &reqwest::blocking::Client, url: &str) -> String { + let resp = client.get(url).send().unwrap(); + resp.text().unwrap() +} diff --git a/native/src/google_books/test/isbn_response.html b/native/src/google_books/test/isbn_response.html new file mode 100644 index 0000000..b551f83 --- /dev/null +++ b/native/src/google_books/test/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/self_link_response.html b/native/src/google_books/test/self_link_response.html new file mode 100644 index 0000000..90564ce --- /dev/null +++ b/native/src/google_books/test/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..c904ecd --- /dev/null +++ b/native/src/image_tools.rs @@ -0,0 +1,11 @@ +use std::{path::Path, process::Command}; + +pub fn downsize_image(widht: u32, height: u32, input_filepath: &Path, output_filepath: &Path) { + Command::new("convert") + .arg(input_filepath.to_str().unwrap()) + .arg("-resize") + .arg(format!("{}x{}^>", widht, height)) + .arg(output_filepath.to_str().unwrap()) + .output() + .unwrap(); +} 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..e91b912 --- /dev/null +++ b/native/src/leboncoin.rs @@ -0,0 +1,52 @@ +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) -> bool { + crate::jwt_decoder::check_jwt_expiration(personal_info::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); + 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 send_answer: String = request::send(ad, img_lbc_refs); + let ad_id = parser::parse_send(&send_answer); + let submit_answer = request::submit(ad_id).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..7ecdbc7 --- /dev/null +++ b/native/src/leboncoin/request.rs @@ -0,0 +1,347 @@ +use std::path::Path; + +use reqwest; +use serde::{Deserialize, Serialize}; + +use crate::leboncoin::personal_info; + +use super::Image; + +pub fn send(ad: crate::common::Ad, images: Vec) -> 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 ", personal_info::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, + personal_info::DATA_DOME_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) -> 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 ", personal_info::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, + personal_info::DATA_DOME_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) -> String { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + "authorization", + ["Bearer ", personal_info::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, + personal_info::DATA_DOME_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) + .unwrap(); + + 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 +} + +//curl 'https://api.leboncoin.fr/api/pintad/v1/public/upload/image' -X POST -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H 'Accept: */*' -H 'Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3' -H 'Accept-Encoding: gzip, deflate, br' -H 'Referer: https://www.leboncoin.fr/annonce/2305203826/editer' -H 'api_key: ba0c2dad52b3ec' -H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxNTYxMiwiaWF0IjoxNjc3NjA4NDExLCJpZCI6IjliYzg5OWM1LTMxN2UtNDE1Ny1iMzEyLTAyMWQ1ZTQ3YTlkYSIsImluc3RhbGxfaWQiOiI3MDQ1YjhmMy0xMzYyLTRiN2UtYjhmZC1lY2Y0OWU4ODRjOGQiLCJqdGkiOiIyY2FhMzU5OS1jZDk3LTQxYTEtYmIzMC1hNmI2YjlmMDA1MzciLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6ImVjMDZjZTM0LThhMzItNDkyNC05NDc1LTc4MzU4MmY3ZGI3YiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiIyNGUyNDA2Mi0xNmE2LTQ4NWQtYjg0Yi1iNWY4MGViNjkzYTUiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.GRqf3gDdFiq1ukFhN_2i8HhWycirauVUM7rDoZZHsSgD-wv5VOwuKDWc6axDoPK3Wsbg_oFXfrSHX-bcDkE2SRaOqNB734eqD-fbceCG1ntf-afgeLWf-MPnas0n_ylOB2ZSK1LAG2aCXZSDm3ZEkXs_-KZhwQtsmqLgIte0PJUUk_qP4tYYDqLe3FvUeGIkrPAFHKxfnAXmKXf-kh9RvbykGiek9lqFT-Hg95X21eS3Z8HH2li-OMP4B2I-PQysOLuaAZ47wkjkt8PKgC6qG3rlitr28MRbkBYrsuo5ic9JMEKTlmbYa5WsyzZJL5F5Y3CdKTXxiQ4ae2kY2hRLgubk2Dihy8vdqLhitX-Fm_sGQSFnP7vy7iHhQCK5m4jLnD-p-sD_DAehNkGYF8lQqG44myb7XdmTtY9uoR_1Tv2LXYSncKQzEpCn-G6Pf0DJg4xb02CCxXWqB7oysooBgFzgPGdixJeBnFSX_8H0zbmoszUUW7Wqw_aSKv7aAQ3p2Foha3U7B4B-3lHPetec6wEo1eLvq5XXRbDAZuvIqbG6cvQHS5HDNkiBQIHfED3VwVOnextu0BADL7hYl4bOM50yNquNIoecPbEOC0Tij3JdYdGjHJ_ywDhsGwD08awZLIpPsJ1ppcaxMv3thoMsiInKqX5wLHNmTec13Lyn978' -H 'Content-Type: multipart/form-data; boundary=---------------------------153785532732722146451504606153' -H 'Origin: https://www.leboncoin.fr' -H 'Connection: keep-alive' -H 'Cookie: datadome=Ysu9fziXa098YNozRd6iS4oq62CcYXd0SZUP9cw4Rl5NMfVROqQrM7eT96i618vhu3M4l~JZWv80IUWEBCzBJdRnqF2Z-W6M0oyZXXOyLvLbDiGsWPQcoQw_UU7qXS-; __Secure-Install=7045b8f3-1362-4b7e-b8fd-ecf49e884c8d; __Secure-InstanceId=7045b8f3-1362-4b7e-b8fd-ecf49e884c8d; utag_main=v_id:0186993fcaf6000e3c437fda305405046001900900bd0$_sn:1$_ss:0$_pn:5%3Bexp-session$_st:1677610707192$ses_id:1677608340214%3Bexp-session; didomi_token=eyJ1c2VyX2lkIjoiMTg2OTkzZmMtYjVmNi02ZTFiLWI2YWQtZDFmMjhmOTJiODk4IiwiY3JlYXRlZCI6IjIwMjMtMDItMjhUMTg6MTk6MDIuMzQyWiIsInVwZGF0ZWQiOiIyMDIzLTAyLTI4VDE4OjE5OjAyLjM0MloiLCJ2ZW5kb3JzIjp7ImVuYWJsZWQiOlsiZ29vZ2xlIiwiYzpsYmNmcmFuY2UiLCJjOnJldmxpZnRlci1jUnBNbnA1eCIsImM6ZGlkb21pIl19LCJwdXJwb3NlcyI6eyJlbmFibGVkIjpbImV4cGVyaWVuY2V1dGlsaXNhdGV1ciIsIm1lc3VyZWF1ZGllbmNlIiwicGVyc29ubmFsaXNhdGlvbm1hcmtldGluZyIsInByaXgiXX0sInZlbmRvcnNfbGkiOnsiZW5hYmxlZCI6WyJnb29nbGUiXX0sInZlcnNpb24iOjIsImFjIjoiRExXQkFBRUlBSXdBV1FCLWdHRkFQeUFra0JKWUVBd0lrZ1NrQXR5QnhBRHB3SFZnUU1BaW9CSE9DU2NFdFlLREFVSWdvdEJYT0N3VUZ0NExqQVhMQXdHQmhFREUwR1dvLkRMV0JBQUVJQUl3QVdRQi1nR0ZBUHlBa2tCSllFQXdJa2dTa0F0eUJ4QURwd0hWZ1FNQWlvQkhPQ1NjRXRZS0RBVUlnb3RCWE9Dd1VGdDRMakFYTEF3R0JoRURFMEdXbyJ9; euconsent-v2=CPn5KgAPn5KgAAHABBENC5CgAPLAAH7AAAAAIsNB_G_dTyPi-f59YvtwYQ1P4VQnoyACjgaNgwwJiRLBMI0EhmAIKAHqAAACIBAkICJAAQBlCAHAAAAA4IEAASMMAAAAIRAIIgCAAEAAAmJICABZCxAAAQAQgkwAABQAgAICABMgSDAAAAAAFAAAAAgAAAAAAAAAAAAAQAAAAAAAAgAAAAAAAAAAAAAEEAQATDVuIAGxLHAmkDCIAACMIAgCgBABRQBCwQAEBIgAEEYACjAAAAAFAAAAAAAAEAMAAAAAgAQgAAAAcEAgAIAEAAAAEAgEAAAAACAAADAAAAAAAMAAAAAAgAIAAAKAQAABAAgAJAgACAAAAgAAAAAAAAAgEAAAAAAAAAAAAAAAAQAxQAGAAIJQjAAMAAQShIAAYAAglCAA.flgAD9gAAAAA; include_in_experiment=true; _hjSessionUser_2783207=eyJpZCI6IjBlOTAwNGIzLWVmNmUtNWM5ZC1iNzQ4LTU4NjFlMWVmMmUyMSIsImNyZWF0ZWQiOjE2Nzc2MDgzNDM2NzksImV4aXN0aW5nIjp0cnVlfQ==; _hjFirstSeen=1; _hjIncludedInSessionSample_2783207=1; _hjSession_2783207=eyJpZCI6ImE1MjQxMjgxLWY5YzQtNDU5YS05NzNkLWFiODc4YjM4MzU2MyIsImNyZWF0ZWQiOjE2Nzc2MDgzNDM2ODEsImluU2FtcGxlIjp0cnVlfQ==; _hjAbsoluteSessionInProgress=0; ry_ry-l3b0nco_realytics=eyJpZCI6InJ5XzI5RjFCQTE5LUQ3ODYtNEUxQy05NUNDLTYwMjE4QUVEOUI3NyIsImNpZCI6bnVsbCwiZXhwIjoxNzA5MTQ0MzQ0Nzc3LCJjcyI6bnVsbH0%3D; ry_ry-l3b0nco_so_realytics=eyJpZCI6InJ5XzI5RjFCQTE5LUQ3ODYtNEUxQy05NUNDLTYwMjE4QUVEOUI3NyIsImNpZCI6bnVsbCwib3JpZ2luIjp0cnVlLCJyZWYiOm51bGwsImNvbnQiOm51bGwsIm5zIjpmYWxzZX0%3D; _gcl_au=1.1.701707994.1677608345; cto_bundle=CNZ-SV9JaWtsRlAyVjVqODhGWWhVYldZN3BzOFl6eG5hJTJCMTZibnQ2R2RNakRCS3N5WDdWWFVleHpzRCUyQmNtQUQwUmt0bW5CdldxZUFLem02b1clMkZTMDdTdXQ3emtxYUtISDJqUzZEazclMkZZMFlFTzQzS2dRaU5NZnVMc3JtR2NMTFBBcGNBZ2hINGczcm44TFpQSSUyRmF1YiUyQnJ3OXclM0QlM0Q; __gads=ID=f3fe3612fb4470d8:T=1677608347:S=ALNI_MbMwkuo3rfJtYKI9L6tyyLC8sIwbg; __gpi=UID=00000be0047e7d68:T=1677608347:RT=1677608347:S=ALNI_Mav2FKNNTDv7opgvCIg2Kld9HMpbw; luat=eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxNTYxMiwiaWF0IjoxNjc3NjA4NDExLCJpZCI6IjliYzg5OWM1LTMxN2UtNDE1Ny1iMzEyLTAyMWQ1ZTQ3YTlkYSIsImluc3RhbGxfaWQiOiI3MDQ1YjhmMy0xMzYyLTRiN2UtYjhmZC1lY2Y0OWU4ODRjOGQiLCJqdGkiOiIyY2FhMzU5OS1jZDk3LTQxYTEtYmIzMC1hNmI2YjlmMDA1MzciLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6ImVjMDZjZTM0LThhMzItNDkyNC05NDc1LTc4MzU4MmY3ZGI3YiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiIyNGUyNDA2Mi0xNmE2LTQ4NWQtYjg0Yi1iNWY4MGViNjkzYTUiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.GRqf3gDdFiq1ukFhN_2i8HhWycirauVUM7rDoZZHsSgD-wv5VOwuKDWc6axDoPK3Wsbg_oFXfrSHX-bcDkE2SRaOqNB734eqD-fbceCG1ntf-afgeLWf-MPnas0n_ylOB2ZSK1LAG2aCXZSDm3ZEkXs_-KZhwQtsmqLgIte0PJUUk_qP4tYYDqLe3FvUeGIkrPAFHKxfnAXmKXf-kh9RvbykGiek9lqFT-Hg95X21eS3Z8HH2li-OMP4B2I-PQysOLuaAZ47wkjkt8PKgC6qG3rlitr28MRbkBYrsuo5ic9JMEKTlmbYa5WsyzZJL5F5Y3CdKTXxiQ4ae2kY2hRLgubk2Dihy8vdqLhitX-Fm_sGQSFnP7vy7iHhQCK5m4jLnD-p-sD_DAehNkGYF8lQqG44myb7XdmTtY9uoR_1Tv2LXYSncKQzEpCn-G6Pf0DJg4xb02CCxXWqB7oysooBgFzgPGdixJeBnFSX_8H0zbmoszUUW7Wqw_aSKv7aAQ3p2Foha3U7B4B-3lHPetec6wEo1eLvq5XXRbDAZuvIqbG6cvQHS5HDNkiBQIHfED3VwVOnextu0BADL7hYl4bOM50yNquNIoecPbEOC0Tij3JdYdGjHJ_ywDhsGwD08awZLIpPsJ1ppcaxMv3thoMsiInKqX5wLHNmTec13Lyn978; _schn=_27faqp; _scid=d6571009-7472-41bb-9cb9-8510b51a6a95; _fbp=fb.1.1677608908547.2034999500' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: same-site' -H 'TE: trailers' --data-binary $'-----------------------------153785532732722146451504606153\r\nContent-Disposition: form-data; name="file"; filename="20230228_192237.jpg"\r\nContent-Type: image/jpeg\r\n\r\n-----------------------------153785532732722146451504606153--\r\n' + +// curl 'https://api.leboncoin.fr/api/pintad/v1/public/upload/image' \ +// -H 'authority: api.leboncoin.fr' \ + +// -H 'accept: */*' \ +/* +-H 'accept-language: en-US,en;q=0.9,fr;q=0.8' \ +-H 'api_key: ba0c2dad52b3ec' \ +-H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxMTY1NywiaWF0IjoxNjc3NjA0NDU2LCJpZCI6IjQ1MTM1OTUxLTY0ZjQtNDFjZS05NGVjLWJkMzkzZDRjY2U2ZiIsImluc3RhbGxfaWQiOiIwNTA1NzA2YS05NDJhLTQzNjktYTdlYy02MGYxZDYxYWZiNjUiLCJqdGkiOiJlYmQzZDI2My1mMTMzLTQ3MjktYjVjOS1kNTA3ZmYwZjUxNDEiLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6IjcwN2M3M2ZiLTk2NzQtNDZlNC05N2NmLTRkZTk2MzQ3NTYwZiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiI3NGM1ZGY4Yy05MzQ3LTQ0MWQtYWViYi0zZWIzYjYyZTk1MjMiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.dZMGwYAei7ovgsB6REy1XjtTjqbgVj4oXQomz7teIj-z0KEajW1pyLyBp4EweGpyb1McgSu8BOS74da9GEeNZ60x9pOAn9KiS8VNfqMBYiwknURnwI8NJdf9KiB6k__SzXb05uTyDeazLK76MoUIAImT8LwzMrFvdZewmmkqyYqT4o4Bcn0tynDkRLv5dSZ87n4ca0AsOsHt6zgWipwpqsGBom0ysYnOzq-hCkyM1-3SsjR4ohVT--qqR2EIijO2-SGk90kmwDzR9aYwCZzzRAlUTFhpE6-zHO7TquAV9oIQAU2Wmq5HgzhEREjUhJOI0fqXy9xk1dPRzb1A__rDbAm8Nkfxq-mF1JcaRM-nB2Pb1VgDV8j6P2MtPC8TlKyr9dMQFzuTWpvFa8sMYg92f3i1oLwvbgsHu5nweMqrWItDAwja7v35T3IReejBwKGOXXEmsTlJEcq589b2AdtZwH82mcFfwn6QkTPbJVGv7YiSLbyGNCQLbUJ-FhLptq2fLZwJUTEye72u-WzY5yeCxs8ZaIfaQHTduJrVviMlEfam9rnUUU-cUdA7NJx8bg63FqOYhEH-hFHYeo5gSF5EqA97jBwC2KoApADf4t1q5EhUPw7gGR9U7qQuhoRiTLVFV4kEjIWeX1QRntOHkVXfuoWTaTE-X1A6XsBvkcINJho' \ +-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryZ9zBRAquVv1qb78o' \ +-H 'cookie: s=red1xa04ffeea4ed8b07c235277adc932a3e4d092859d; log_from=http%3A%2F%2Fwww2.leboncoin.fr%2Fdc%2Frules%3Fca%3D12_s; xtvrn=$266818$; ry_ry-l3b0nco_realytics=eyJpZCI6InJ5Xzg1M0VCQzVGLURCNjgtNDk1NC1COEE0LTM1OTY0RjhDN0U1RSIsImNpZCI6bnVsbCwiZXhwIjoxNjg0MzI0MDMzMDQ1LCJjcyI6bnVsbH0%3D; _pin_unauth=dWlkPU1HVXhPREF4TnpZdE1qTXhZUzAwTXpBd0xXSmpZVFF0TXpoallqRTNNamczTldVeg; _hjSessionUser_2783207=eyJpZCI6ImZjZTk5MmM1LTY0NmItNTViOS05OWY4LWRkYzc2YmE3ODc1YyIsImNyZWF0ZWQiOjE2NTI3ODgwMzQ1MjcsImV4aXN0aW5nIjp0cnVlfQ==; _scid=e7942114-373a-4d77-b41c-1717c1b77529; __Secure-Install=0505706a-942a-4369-a7ec-60f1d61afb65; __Secure-InstanceId=0505706a-942a-4369-a7ec-60f1d61afb65; didomi_token=eyJ1c2VyX2lkIjoiMTcyNmY1NWMtMjgwYS02Njc3LWFmMDAtZjk2YzM1MDM1NDQ3IiwiY3JlYXRlZCI6IjIwMjItMTItMDFUMTk6NTA6MDAuNzMwWiIsInVwZGF0ZWQiOiIyMDIyLTEyLTAxVDE5OjUwOjAwLjczMFoiLCJ2ZW5kb3JzIjp7ImVuYWJsZWQiOlsiZ29vZ2xlIiwiYzpsYmNmcmFuY2UiLCJjOnJldmxpZnRlci1jUnBNbnA1eCIsImM6ZGlkb21pIl19LCJwdXJwb3NlcyI6eyJlbmFibGVkIjpbInBlcnNvbm5hbGlzYXRpb25tYXJrZXRpbmciLCJwcml4IiwibWVzdXJlYXVkaWVuY2UiLCJleHBlcmllbmNldXRpbGlzYXRldXIiXX0sInZlbmRvcnNfbGkiOnsiZW5hYmxlZCI6WyJnb29nbGUiXX0sInZlcnNpb24iOjIsImFjIjoiRExXQkFBRUlBSXdBV1FCLWdHRkFQeUFra0JKWUVBd0lrZ1NrQXR5QnhBRHB3SFZnUU1BaW9CSE9DU2NFdFlLREFVSWdvdEJYT0N3VUZ0NExqQVhMQXdHQmhFREUwR1dvLkRMV0EtQUVJQUl3QV9RRENnSDVBU1NBa3NDQVlFU1FKU0FXNUE0Z0IwNERxd0lHQVJVQWpuQkpPQ1dzRkJnS0VRVVdncm5CWUtDMjhGeGdMbGdZREF3aUJpYURMVUFBQSJ9; euconsent-v2=CPjT1EAPjT1EAAHABBENCsCgAPLAAHLAAAAAIAtB_G_dTyPi-f59YvtwYQ1P4VQnoyACjgaNgwwJiRLBMI0EhmAIKAHqAAACIBAkICJAAQBlCAHAAAAA4IEAASMMAAAAIRAIIgCAAEAAAiJICABZCxAAAQAQgkwAABQAgAICABMgSDAAAAAAFAAAAAgAAAAAAAAAAAAAQAAAAAAAAggCACYatxAA2JY4E0gYRAAARhAEAUAIAKKAIWCAAgJEAAgjAAUYAAAAAoAAAAAAAAgBgAAAAEACEAAAADggEABAAgAAAAgEAgAAAAAQAAAYAAAAAABgAAAAAEABAAABQCAAAIAEABIEAAQAAAEAAAAAAAAAEAgAAAAAAAAAAAAAAACAGKAAwABBJYYABgACCSxAADAAEElg.flgADlgAAAAA; include_in_experiment=true; _gcl_au=1.1.396577628.1672180056; _fbp=fb.1.1674497618789.664585017; __gads=ID=3756ef2df471787f:T=1674498902:S=ALNI_MZYJfxMcpbJdw6KbbvraZ_khGHZnA; __gpi=UID=00000bc9e3cb6789:T=1674498902:RT=1674566983:S=ALNI_MbBn6iqQ8F-8Zpmb1WCKLM9NSFmxg; __gsas=ID=8ce0a4b086087df5:T=1674567077:S=ALNI_Mby9K0wXjz1NzkAZsVzbLFcccNtNw; adview_clickmeter=search__listing__4__8c78a2b2-4b20-497b-ae05-cf1e963b741d; luat=eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxMTY1NywiaWF0IjoxNjc3NjA0NDU2LCJpZCI6IjQ1MTM1OTUxLTY0ZjQtNDFjZS05NGVjLWJkMzkzZDRjY2U2ZiIsImluc3RhbGxfaWQiOiIwNTA1NzA2YS05NDJhLTQzNjktYTdlYy02MGYxZDYxYWZiNjUiLCJqdGkiOiJlYmQzZDI2My1mMTMzLTQ3MjktYjVjOS1kNTA3ZmYwZjUxNDEiLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6IjcwN2M3M2ZiLTk2NzQtNDZlNC05N2NmLTRkZTk2MzQ3NTYwZiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiI3NGM1ZGY4Yy05MzQ3LTQ0MWQtYWViYi0zZWIzYjYyZTk1MjMiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.dZMGwYAei7ovgsB6REy1XjtTjqbgVj4oXQomz7teIj-z0KEajW1pyLyBp4EweGpyb1McgSu8BOS74da9GEeNZ60x9pOAn9KiS8VNfqMBYiwknURnwI8NJdf9KiB6k__SzXb05uTyDeazLK76MoUIAImT8LwzMrFvdZewmmkqyYqT4o4Bcn0tynDkRLv5dSZ87n4ca0AsOsHt6zgWipwpqsGBom0ysYnOzq-hCkyM1-3SsjR4ohVT--qqR2EIijO2-SGk90kmwDzR9aYwCZzzRAlUTFhpE6-zHO7TquAV9oIQAU2Wmq5HgzhEREjUhJOI0fqXy9xk1dPRzb1A__rDbAm8Nkfxq-mF1JcaRM-nB2Pb1VgDV8j6P2MtPC8TlKyr9dMQFzuTWpvFa8sMYg92f3i1oLwvbgsHu5nweMqrWItDAwja7v35T3IReejBwKGOXXEmsTlJEcq589b2AdtZwH82mcFfwn6QkTPbJVGv7YiSLbyGNCQLbUJ-FhLptq2fLZwJUTEye72u-WzY5yeCxs8ZaIfaQHTduJrVviMlEfam9rnUUU-cUdA7NJx8bg63FqOYhEH-hFHYeo5gSF5EqA97jBwC2KoApADf4t1q5EhUPw7gGR9U7qQuhoRiTLVFV4kEjIWeX1QRntOHkVXfuoWTaTE-X1A6XsBvkcINJho; _hjSession_2783207=eyJpZCI6IjlmOWFiNjllLTk0MTctNDIwOS1iMTU0LWQwODM1OGU2MDVlZSIsImNyZWF0ZWQiOjE2Nzc2MDQ0NTc4NTgsImluU2FtcGxlIjp0cnVlfQ==; _hjAbsoluteSessionInProgress=0; ry_ry-l3b0nco_so_realytics=eyJpZCI6InJ5Xzg1M0VCQzVGLURCNjgtNDk1NC1COEE0LTM1OTY0RjhDN0U1RSIsImNpZCI6bnVsbCwib3JpZ2luIjpmYWxzZSwicmVmIjpudWxsLCJjb250IjpudWxsLCJucyI6ZmFsc2V9; _hjIncludedInSessionSample_2783207=1; cto_bundle=XggTMV9lYzVPa0lSRm5hMGZrMzB1ciUyRkhyamtZUmRmUHFhYkVVVDlMRWZMSUxRQW1PMGdOYyUyQkNwOE5QbFp3Y0U5NmV1aWtyZjg3b2xMZ09tOFVTdWFMenBPR2Y1anNhb0tteFZqUng5NDVBVmdzakRjdVh1cyUyRkdiYWxGVnZXTyUyRkl4dk9Q; datadome=4cwibCI1RYexaCD1wjZqJZ-6hUi16_fPyqPGEtsMSG-r3~8EoWMghXY6ZUZ3L~1GpA3vzRzDw__LqSlDp~FlYdAu_jP3M0N9vV8ZWBSbzQ0~ijJp6tNET4wW0fjfuKGn; utag_main=v_id:01792c6c39a9001726449a99708002069002106100bd0$_sn:149$_ss:0$_st:1677607622614$_pn:3%3Bexp-session$ses_id:1677604455561%3Bexp-session' \ +-H 'origin: https://www.leboncoin.fr' \ +-H 'referer: https://www.leboncoin.fr/deposer-une-annonce' \ +-H 'sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"' \ +-H 'sec-ch-ua-mobile: ?0' \ +-H 'sec-ch-ua-platform: "Linux"' \ +-H 'sec-fetch-dest: empty' \ +-H 'sec-fetch-mode: cors' \ +-H 'sec-fetch-site: same-site' \ +-H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' \ +--data-raw $'------WebKitFormBoundaryZ9zBRAquVv1qb78o\r\nContent-Disposition: form-data; name="file"; filename="20230204_194811.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryZ9zBRAquVv1qb78o--\r\n' \ +--compressed +*/ diff --git a/native/src/lib.rs b/native/src/lib.rs index dfdd872..66e43fd 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,2 +1,10 @@ mod api; mod bridge_generated; +mod babelio; +mod cached_client; +mod common; +mod google_books; +mod image_tools; +mod publisher; +mod jwt_decoder; +mod leboncoin; diff --git a/native/src/publisher.rs b/native/src/publisher.rs new file mode 100644 index 0000000..329cbf3 --- /dev/null +++ b/native/src/publisher.rs @@ -0,0 +1,5 @@ +use crate::common::Ad; + +pub trait Publisher { + fn publish(&self, ad: Ad) -> bool; +} From d57e930de3aab2d84d018c530629393d35e4a001 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 9 Mar 2023 19:20:58 +0100 Subject: [PATCH 03/81] Rework cache so it works with flutter run --- native/.gitignore | 1 + native/src/babelio/request.rs | 6 +++--- native/src/cached_client.rs | 11 ++++++++--- native/src/lib.rs | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/native/.gitignore b/native/.gitignore index 107eafc..8d19f7f 100644 --- a/native/.gitignore +++ b/native/.gitignore @@ -1 +1,2 @@ personal_info.rs +config.rs \ No newline at end of file diff --git a/native/src/babelio/request.rs b/native/src/babelio/request.rs index 79735a8..7895a40 100644 --- a/native/src/babelio/request.rs +++ b/native/src/babelio/request.rs @@ -19,7 +19,7 @@ struct BabelioISBNResponse { pub fn get_book_url(client: &CachedClient, isbn: &str) -> Option { let raw_search_results = client.get_from_cache( - format!("cache/babelio/get_book_url_{}.html", isbn).as_str(), + format!("babelio/get_book_url_{}.html", isbn).as_str(), |http_client| { http_client .post("https://www.babelio.com/aj_recherche.php") @@ -38,7 +38,7 @@ pub fn get_book_url(client: &CachedClient, isbn: &str) -> Option { pub fn get_book_page(client: &CachedClient, url: String) -> String { client.get_from_cache( format!( - "cache/babelio/get_book_page_{}.html", + "babelio/get_book_page_{}.html", url.replace("/", "_slash_") ) .as_str(), @@ -54,7 +54,7 @@ pub fn get_book_page(client: &CachedClient, url: String) -> String { pub fn get_book_blurb_see_more(client: &CachedClient, id_obj: &str) -> String { client.get_from_cache( - format!("cache/babelio/get_book_blurb_see_more_{}.html", id_obj).as_str(), + 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)]); diff --git a/native/src/cached_client.rs b/native/src/cached_client.rs index 55783ea..366cb18 100644 --- a/native/src/cached_client.rs +++ b/native/src/cached_client.rs @@ -8,12 +8,17 @@ impl CachedClient { cache_file_path: &str, make_request: F, ) -> String { - let html = std::fs::read_to_string(cache_file_path); + 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) => f, + 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); + 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/lib.rs b/native/src/lib.rs index 66e43fd..26a09fa 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -8,3 +8,4 @@ mod image_tools; mod publisher; mod jwt_decoder; mod leboncoin; +mod config; From 50c362d6b4cb1c9649ec4d5553d3156ed8464f64 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 9 Mar 2023 19:46:40 +0100 Subject: [PATCH 04/81] Better UI, display images --- lib/main.dart | 132 ++++++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 29b4a08..d013a62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; @@ -13,37 +15,15 @@ class MyApp extends StatelessWidget { @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'), + title: 'BookAdPublisher', + theme: ThemeData(primarySwatch: Colors.blue), + home: const MyHomePage(), ); } } 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; + const MyHomePage(); @override State createState() => _MyHomePageState(); @@ -54,20 +34,18 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - ad = api.getMetadataFromImages(imgsPath: [ - '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg' - ]); //.then((ad) => print('ad = $ad')); + ad = api.getMetadataFromImages( + imgsPath: ['/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg']); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: const Text('Create an automatic online book ad from picture'), ), body: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ // To render the results of a Future, a FutureBuilder is used which // turns a Future into an AsyncSnapshot, which can be used to @@ -113,7 +91,7 @@ extension DoubleExt on double { } class AdPage extends StatefulWidget { - AdPage({super.key, required Ad ad}) : initialAd = ad; + const AdPage({required Ad ad}) : initialAd = ad; final Ad initialAd; @@ -132,37 +110,65 @@ class _AdPageState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - TextFormField( - initialValue: ad.title, - onChanged: (newText) { - setState(() { - ad.title = newText; - }); - }, - ), - TextFormField( - initialValue: ad.description, - maxLines: 20, - onChanged: (newText) { - setState(() { - ad.description = newText; - }); - }, - ), - TextFormField( - initialValue: ad.priceCent /*?*/ .divide(100).toString(), - onChanged: (newText) => setState(() => ad.priceCent = double.tryParse(newText)! /*?*/ .multiply(100).round()), - ), - ElevatedButton( - onPressed: ad.priceCent == null - ? null - : () { - print('Try to publish'); - }, - child: const Text("Publish")) - ], + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextFormField( + initialValue: 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: ad.description, + maxLines: 20, + onChanged: (newText) => setState(() => ad.description = newText), + decoration: const InputDecoration( + icon: Icon(Icons.text_snippet), + labelText: 'Ad description', + ), + ), + TextFormField( + initialValue: 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), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row(children: [ + const Icon( + Icons.image, + color: Colors.grey, + ), + const SizedBox(width: 16), + ...ad.imgsPath + .map((imgPath) => Image.file( + File(imgPath), + height: 200, + isAntiAlias: true, + filterQuality: FilterQuality.medium, + )) + .toList(), + ]), + ), + ElevatedButton( + onPressed: ad.priceCent == null + ? null + : () { + print('Try to publish'); + }, + child: const Text("Publish")) + ], + ), ); } } From 87973a07e231b8290b5af440a68a46f76daac547 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 9 Mar 2023 23:29:01 +0100 Subject: [PATCH 05/81] Publish ad with images, simplify GoogleBooks, show better error for convert --- lib/bridge_definitions.dart | 4 ++ lib/bridge_generated.dart | 83 ++++++++++++++++++++++++++++++ lib/main.dart | 9 ++-- native/src/api.rs | 9 ++-- native/src/bridge_generated.io.rs | 52 +++++++++++++++++++ native/src/bridge_generated.rs | 18 +++++++ native/src/google_books.rs | 4 +- native/src/google_books/parser.rs | 15 +++--- native/src/google_books/request.rs | 6 +-- native/src/image_tools.rs | 11 +++- native/src/leboncoin.rs | 1 + native/src/leboncoin/request.rs | 2 +- 12 files changed, 188 insertions(+), 26 deletions(-) diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index 29fd663..8ce158f 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -12,6 +12,10 @@ abstract class Native { Future getMetadataFromImages({required List imgsPath, dynamic hint}); FlutterRustBridgeTaskConstMeta get kGetMetadataFromImagesConstMeta; + + Future publishAd({required Ad ad, dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; } class Ad { diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 7ae6c5e..6239de2 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -43,6 +43,23 @@ class NativeImpl implements Native { argNames: ["imgsPath"], ); + Future publishAd({required Ad ad, dynamic hint}) { + var arg0 = _platform.api2wire_box_autoadd_ad(ad); + return _platform.executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_publish_ad(port_, arg0), + parseSuccessData: _wire2api_unit, + constMeta: kPublishAdConstMeta, + argValues: [ad], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta => + const FlutterRustBridgeTaskConstMeta( + debugName: "publish_ad", + argNames: ["ad"], + ); + void dispose() { _platform.dispose(); } @@ -79,10 +96,19 @@ class NativeImpl implements Native { Uint8List _wire2api_uint_8_list(dynamic raw) { return raw as Uint8List; } + + void _wire2api_unit(dynamic raw) { + return; + } } // Section: api2wire +@protected +int api2wire_i32(int raw) { + return raw; +} + @protected int api2wire_u8(int raw) { return raw; @@ -109,6 +135,13 @@ class NativePlatform extends FlutterRustBridgeBase { 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_uint_8_list(Uint8List raw) { final ans = inner.new_uint_8_list_0(raw.length); @@ -118,6 +151,18 @@ class NativePlatform extends FlutterRustBridgeBase { // 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); + } } // ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names @@ -232,6 +277,23 @@ class NativeWire implements FlutterRustBridgeWireBase { late final _wire_get_metadata_from_images = _wire_get_metadata_from_imagesPtr .asFunction)>(); + void wire_publish_ad( + int port_, + ffi.Pointer ad, + ) { + return _wire_publish_ad( + port_, + ad, + ); + } + + late final _wire_publish_adPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_publish_ad'); + late final _wire_publish_ad = _wire_publish_adPtr + .asFunction)>(); + ffi.Pointer new_StringList_0( int len, ) { @@ -246,6 +308,16 @@ class NativeWire implements FlutterRustBridgeWireBase { 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_uint_8_list_0( int len, ) { @@ -292,6 +364,17 @@ class wire_StringList extends ffi.Struct { 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; +} + typedef DartPostCObjectFnType = ffi.Pointer< ffi.NativeFunction)>>; typedef DartPort = ffi.Int64; diff --git a/lib/main.dart b/lib/main.dart index d013a62..4d50e82 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,8 +34,10 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - ad = api.getMetadataFromImages( - imgsPath: ['/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg']); + ad = api.getMetadataFromImages(imgsPath: [ + '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/Camera/20230220_182059.jpg', + '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/Camera/20230220_182113.jpg' + ]); } @override @@ -164,7 +166,8 @@ class _AdPageState extends State { onPressed: ad.priceCent == null ? null : () { - print('Try to publish'); + print('Try to publish...'); + api.publishAd(ad: ad); }, child: const Text("Publish")) ], diff --git a/native/src/api.rs b/native/src/api.rs index 65e1ea0..5d94ca0 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -2,6 +2,7 @@ use std::process::Command; use itertools::Itertools; use crate::{babelio, common, google_books, leboncoin}; use crate::common::{Ad, BookMetaData}; +use crate::publisher::Publisher; pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { let isbns: Vec = imgs_path @@ -81,11 +82,11 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { price_cent: 1000, imgs_path, } +} - /*let publisher = leboncoin::Leboncoin {}; - - - publisher::Publisher::publish(&publisher, ad);*/ +pub fn publish_ad(ad: Ad) -> () { + let lbc_publisher = leboncoin::Leboncoin {}; + Publisher::publish(&lbc_publisher, ad); } fn book_format_title_and_author(book: &BookMetaData) -> String { diff --git a/native/src/bridge_generated.io.rs b/native/src/bridge_generated.io.rs index f335968..447aa2a 100644 --- a/native/src/bridge_generated.io.rs +++ b/native/src/bridge_generated.io.rs @@ -6,6 +6,11 @@ pub extern "C" fn wire_get_metadata_from_images(port_: i64, imgs_path: *mut wire wire_get_metadata_from_images_impl(port_, imgs_path) } +#[no_mangle] +pub extern "C" fn wire_publish_ad(port_: i64, ad: *mut wire_Ad) { + wire_publish_ad_impl(port_, ad) +} + // Section: allocate functions #[no_mangle] @@ -17,6 +22,11 @@ pub extern "C" fn new_StringList_0(len: i32) -> *mut wire_StringList { 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_uint_8_list_0(len: i32) -> *mut wire_uint_8_list { let ans = wire_uint_8_list { @@ -45,6 +55,22 @@ impl Wire2Api> for *mut wire_StringList { 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_uint_8_list { fn wire2api(self) -> Vec { @@ -63,6 +89,15 @@ pub struct wire_StringList { 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_uint_8_list { @@ -82,6 +117,23 @@ 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() + } +} + // Section: sync execution mode utility #[no_mangle] diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index 991de3c..56be752 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -39,6 +39,19 @@ fn wire_get_metadata_from_images_impl( }, ) } +fn wire_publish_ad_impl(port_: MessagePort, ad: impl Wire2Api + UnwindSafe) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "publish_ad", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_ad = ad.wire2api(); + move |task_callback| Ok(publish_ad(api_ad)) + }, + ) +} // Section: wrapper structs // Section: static checks @@ -62,6 +75,11 @@ where } } +impl Wire2Api for i32 { + fn wire2api(self) -> i32 { + self + } +} impl Wire2Api for u8 { fn wire2api(self) -> u8 { self diff --git a/native/src/google_books.rs b/native/src/google_books.rs index a2dd9aa..4b04879 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -8,9 +8,7 @@ impl common::Provider for GoogleBooks { fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { let client = reqwest::blocking::Client::builder().build().unwrap(); let isbn_search_response = request::search_by_isbn(&client, isbn); - let self_link = parser::extract_self_link_from_isbn_response(&isbn_search_response); - let book_page = request::get_volume(&client, &self_link); - Some(parser::extract_metadata_from_self_link_response(&book_page)) + Some(parser::extract_metadata_from_isbn_response(&isbn_search_response)) } } diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index c563d33..519137c 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -2,14 +2,11 @@ use itertools::Itertools; use crate::common; -pub fn extract_self_link_from_isbn_response(html: &str) -> String { - let s: structs::Root = serde_json::from_str(html).unwrap(); - s.items[0].self_link.to_string() -} +pub fn extract_metadata_from_isbn_response(html: &str) -> common::BookMetaData { + let owned_string = html.to_string(); + let s: structs::Root = serde_json::from_str(&owned_string).unwrap(); + let first_book = &s.items[0].volume_info; -pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { - let s: structs::Item = serde_json::from_str(html).unwrap(); - let first_book = &s.volume_info; common::BookMetaData { title: first_book.title.to_string(), authors: first_book @@ -21,7 +18,7 @@ pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaD }) .collect_vec(), - blurb: first_book.description.map(|d| d.to_string()), + blurb: first_book.description.to_owned(), ..Default::default() } } @@ -88,7 +85,7 @@ mod structs { pub authors: Vec<&'a str>, pub publisher: Option<&'a str>, pub published_date: &'a str, - pub description: Option<&'a str>, + pub description: Option, pub industry_identifiers: Vec>, pub reading_modes: ReadingModes, pub page_count: i64, diff --git a/native/src/google_books/request.rs b/native/src/google_books/request.rs index f0df992..8da9c9b 100644 --- a/native/src/google_books/request.rs +++ b/native/src/google_books/request.rs @@ -6,8 +6,4 @@ pub fn search_by_isbn(client: &reqwest::blocking::Client, isbn: &str) -> String .send() .unwrap(); resp.text().unwrap() -} -pub fn get_volume(client: &reqwest::blocking::Client, url: &str) -> String { - let resp = client.get(url).send().unwrap(); - resp.text().unwrap() -} +} \ No newline at end of file diff --git a/native/src/image_tools.rs b/native/src/image_tools.rs index c904ecd..27685c7 100644 --- a/native/src/image_tools.rs +++ b/native/src/image_tools.rs @@ -1,11 +1,20 @@ use std::{path::Path, process::Command}; +use std::process::ExitStatus; pub fn downsize_image(widht: u32, height: u32, input_filepath: &Path, output_filepath: &Path) { - Command::new("convert") + 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/leboncoin.rs b/native/src/leboncoin.rs index e91b912..8235aa8 100644 --- a/native/src/leboncoin.rs +++ b/native/src/leboncoin.rs @@ -30,6 +30,7 @@ impl Publisher for Leboncoin { } }) .collect_vec(); + // let img_lbc_refs = vec![]; let send_answer: String = request::send(ad, img_lbc_refs); let ad_id = parser::parse_send(&send_answer); diff --git a/native/src/leboncoin/request.rs b/native/src/leboncoin/request.rs index 7ecdbc7..d49ad99 100644 --- a/native/src/leboncoin/request.rs +++ b/native/src/leboncoin/request.rs @@ -306,7 +306,7 @@ pub fn upload_file(img_path: &Path) -> String { let form = reqwest::blocking::multipart::Form::new() .file("file", img_path) - .unwrap(); + .expect(&format!("Could not find file at path: {:?}", img_path)); let client = reqwest::blocking::Client::new(); let res = client From abd20ee4d53f81a4e2921507e076248151799b49 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 9 Mar 2023 23:47:49 +0100 Subject: [PATCH 06/81] clean --- native/src/google_books/parser.rs | 2 ++ native/src/image_tools.rs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index 519137c..ba577a6 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -85,6 +85,8 @@ mod structs { 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, diff --git a/native/src/image_tools.rs b/native/src/image_tools.rs index 27685c7..0040c24 100644 --- a/native/src/image_tools.rs +++ b/native/src/image_tools.rs @@ -1,5 +1,4 @@ use std::{path::Path, process::Command}; -use std::process::ExitStatus; pub fn downsize_image(widht: u32, height: u32, input_filepath: &Path, output_filepath: &Path) { let output = Command::new("convert") From cbc25516a1dc25c8c5ff22f3fc76baa06f4326ad Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 10 Mar 2023 21:28:25 +0100 Subject: [PATCH 07/81] WIP drag and drop --- lib/drag_and_drop.dart | 535 ++++++++++++++++++ lib/main.dart | 3 +- linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 8 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 lib/drag_and_drop.dart diff --git a/lib/drag_and_drop.dart b/lib/drag_and_drop.dart new file mode 100644 index 0000000..23d4b05 --- /dev/null +++ b/lib/drag_and_drop.dart @@ -0,0 +1,535 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.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 MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class DragableWidget extends StatefulWidget { + const DragableWidget({ + super.key, + required this.name, + required this.color, + required this.dragItemProvider, + }); + + final String name; + final Color color; + final DragItemProvider dragItemProvider; + + @override + State createState() => _DragableWidgetState(); +} + +class _DragableWidgetState extends State { + bool _dragging = false; + + Future provideDragItem(AsyncValueGetter snapshot, DragSession session) async { + final item = await widget.dragItemProvider(snapshot, session); + if (item != null) { + setState(() { + _dragging = session.dragging; + }); + session.dragStarted.addListener(() { + setState(() { + _dragging = true; + }); + }); + session.dragCompleted.addListener(() { + if (mounted) { + setState(() { + _dragging = false; + }); + } + }); + } + return item; + } + + @override + Widget build(BuildContext context) { + return DragItemWidget( + allowedOperations: () => [DropOperation.copy], + canAddItemToExistingSession: true, + dragItemProvider: provideDragItem, + child: DraggableWidget( + child: AnimatedOpacity( + opacity: _dragging ? 0.5 : 1, + duration: const Duration(milliseconds: 200), + child: Container( + decoration: BoxDecoration( + color: widget.color, + borderRadius: BorderRadius.circular(14), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Text( + widget.name, + style: const TextStyle(fontSize: 20, color: Colors.white), + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + } +} + +Future createImageData(Color color) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + final paint = Paint()..color = color; + canvas.drawOval(const Rect.fromLTWH(0, 0, 200, 200), paint); + final picture = recorder.endRecording(); + final image = await picture.toImage(200, 200); + final data = await image.toByteData(format: ui.ImageByteFormat.png); + return data!.buffer.asUint8List(); +} + +class HomeLayout extends StatelessWidget { + const HomeLayout({ + super.key, + required this.draggable, + required this.dropZone, + }); + + final List draggable; + final Widget dropZone; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < 500) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + child: Wrap( + direction: Axis.horizontal, + runSpacing: 8, + spacing: 10, + children: draggable, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: dropZone, + ), + ), + ], + ); + } else { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + textDirection: TextDirection.rtl, + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: draggable + .intersperse( + const SizedBox(height: 10), + ) + .toList(growable: false), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith(right: 0), + child: dropZone, + ), + ), + ], + ); + } + }), + ); + } +} + +extension IntersperseExtensions on Iterable { + Iterable intersperse(T element) sync* { + final iterator = this.iterator; + if (iterator.moveNext()) { + yield iterator.current; + while (iterator.moveNext()) { + yield element; + yield iterator.current; + } + } + } +} + +extension on DragSession { + Future hasLocalData(Object data) async { + final localData = await getLocalData() ?? []; + return localData.contains(data); + } +} + +class _MyHomePageState extends State { + void showMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(milliseconds: 1500), + ), + ); + } + + Future textDragItem( + AsyncValueGetter dragImage, + DragSession session, + ) async { + // For multi drag on iOS check if this item is already in the session + if (await session.hasLocalData('text-item')) { + return null; + } + final item = DragItem(image: await dragImage(), localData: 'text-item', suggestedName: 'PlainText.txt'); + item.add(Formats.plainText('Plain Text Value')); + return item; + } + + Future imageDragItem( + AsyncValueGetter dragImage, + DragSession session, + ) async { + // For multi drag on iOS check if this item is already in the session + if (await session.hasLocalData('image-item')) { + return null; + } + final item = DragItem( + image: await dragImage(), + localData: 'image-item', + suggestedName: 'Green.png', + ); + item.add(Formats.png(await createImageData(Colors.green))); + return item; + } + + Future lazyImageDragItem( + AsyncValueGetter dragImage, + DragSession session, + ) async { + // For multi drag on iOS check if this item is already in the session + if (await session.hasLocalData('lazy-image-item')) { + return null; + } + final item = DragItem( + image: await dragImage(), + localData: 'lazy-image-item', + suggestedName: 'LazyBlue.png', + ); + item.add(Formats.png.lazy(() async { + showMessage('Requested lazy image.'); + return await createImageData(Colors.blue); + })); + return item; + } + + Future virtualFileDragItem( + AsyncValueGetter dragImage, + DragSession session, + ) async { + // For multi drag on iOS check if this item is already in the session + if (await session.hasLocalData('virtual-file-item')) { + return null; + } + final item = DragItem( + image: await dragImage(), + localData: 'virtual-file-item', + suggestedName: 'VirtualFile.txt', + ); + if (!item.virtualFileSupported) { + return null; + } + item.addVirtualFile( + format: Formats.plainTextFile, + provider: (sinkProvider, progress) { + showMessage('Requesting virtual file content.'); + final line = utf8.encode('Line in virtual file\n'); + const lines = 10; + final sink = sinkProvider(fileSize: line.length * lines); + for (var i = 0; i < lines; ++i) { + sink.add(line); + } + sink.close(); + }, + ); + return item; + } + + Future multipleRepresentationsDragItem( + AsyncValueGetter dragImage, + DragSession session, + ) async { + // For multi drag on iOS check if this item is already in the session + if (await session.hasLocalData('multiple-representations-item')) { + return null; + } + final item = DragItem( + image: await dragImage(), + localData: 'multiple-representations-item', + ); + item.add(Formats.png(await createImageData(Colors.pink))); + item.add(Formats.plainText("Hello World")); + item.add(Formats.uri(NamedUri(Uri.parse('https://flutter.dev'), name: 'Flutter'))); + return item; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: HomeLayout( + draggable: [ + DragableWidget( + name: 'Text', + color: Colors.red, + dragItemProvider: textDragItem, + ), + DragableWidget( + name: 'Image', + color: Colors.green, + dragItemProvider: imageDragItem, + ), + DragableWidget( + name: 'Image 2', + color: Colors.blue, + dragItemProvider: lazyImageDragItem, + ), + DragableWidget( + name: 'Virtual', + color: Colors.amber.shade700, + dragItemProvider: virtualFileDragItem, + ), + DragableWidget( + name: 'Multiple', + color: Colors.pink, + dragItemProvider: multipleRepresentationsDragItem, + ), + ], + dropZone: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.blueGrey.shade200), + borderRadius: BorderRadius.circular(14), + ), + child: _DropZone(), + ), + ), + ); + } +} + +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 { + @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: ListView( + shrinkWrap: true, + children: event.session.items + .map((e) => _DropItemInfo(dropItem: e)) + .intersperse(Container( + height: 2, + color: Colors.white.withOpacity(0.7), + )) + .toList(growable: false), + ), + ), + ), + ), + ), + ); + }); + return event.session.allowedOperations.firstOrNull ?? DropOperation.none; + } + + Future _onPerformDrop(PerformDropEvent event) async { + final dataReader = event.session.items.first.dataReader!; + final sugg = await dataReader.getSuggestedName(); + print('sugg = $sugg'); + + final formats = dataReader.getFormats(Formats.standardFormats); + print("PerformDropEvent = ${formats}"); + formats.forEach((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'); + } + }); + + /* + // Obtain additional reader information first + final readers = await Future.wait( + event.session.items.map( + (e) => ReaderInfo.fromReader( + e.dataReader!, + localData: e.localData, + ), + ), + ); + + if (!mounted) { + return; + } + + buildWidgetsForReaders(context, readers, (value) { + setState(() { + _content = ListView( + padding: const EdgeInsets.all(10), + children: value.intersperse(const SizedBox(height: 10)).toList(growable: false), + ); + }); + }); + */ + } + + void _onDropLeave(DropEvent event) { + setState(() { + _isDragOver = false; + }); + } + + bool _isDragOver = false; + + Widget _preview = const SizedBox(); + Widget _content = const Center( + child: Text( + 'Drop here', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ); +} + +class _DropItemInfo extends StatelessWidget { + const _DropItemInfo({ + required this.dropItem, + }); + + final DropItem dropItem; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: DefaultTextStyle.merge( + style: const TextStyle(fontSize: 11.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (dropItem.localData != null) + Text.rich(TextSpan(children: [ + const TextSpan( + text: 'Local data: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '${dropItem.localData}'), + ])), + const SizedBox( + height: 4, + ), + Text.rich(TextSpan(children: [ + const TextSpan( + text: 'Native formats: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: dropItem.platformFormats.join(', ')), + ])), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 4d50e82..327702c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'drag_and_drop.dart' as drag_and_drop; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; void main() { @@ -17,7 +18,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'BookAdPublisher', theme: ThemeData(primarySwatch: Colors.blue), - home: const MyHomePage(), + home: const drag_and_drop.MyHomePage(title: 'title'), ); } } 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/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..35154be 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import irondash_engine_context +import super_native_extensions func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) } diff --git a/pubspec.yaml b/pubspec.yaml index 298476d..b2dfd2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: ffi: ^2.0.1 flutter_rust_bridge: ^1.45.0 meta: ^1.8.0 + super_drag_and_drop: ^0.2.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..94564b6 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + IrondashEngineContextPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); + SuperNativeExtensionsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..607cf1f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/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 From 2103eceaecdeac71bade0d09012ddb8b041e399f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 10 Mar 2023 21:54:53 +0100 Subject: [PATCH 08/81] drop clean --- lib/drag_and_drop.dart | 418 ++--------------------------------------- lib/main.dart | 2 +- 2 files changed, 15 insertions(+), 405 deletions(-) diff --git a/lib/drag_and_drop.dart b/lib/drag_and_drop.dart index 23d4b05..0393312 100644 --- a/lib/drag_and_drop.dart +++ b/lib/drag_and_drop.dart @@ -1,343 +1,26 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:ui' as ui; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.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 MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class DragableWidget extends StatefulWidget { - const DragableWidget({ - super.key, - required this.name, - required this.color, - required this.dragItemProvider, - }); - - final String name; - final Color color; - final DragItemProvider dragItemProvider; - - @override - State createState() => _DragableWidgetState(); -} - -class _DragableWidgetState extends State { - bool _dragging = false; - - Future provideDragItem(AsyncValueGetter snapshot, DragSession session) async { - final item = await widget.dragItemProvider(snapshot, session); - if (item != null) { - setState(() { - _dragging = session.dragging; - }); - session.dragStarted.addListener(() { - setState(() { - _dragging = true; - }); - }); - session.dragCompleted.addListener(() { - if (mounted) { - setState(() { - _dragging = false; - }); - } - }); - } - return item; - } +class SelectImages extends StatelessWidget { + const SelectImages(); @override - Widget build(BuildContext context) { - return DragItemWidget( - allowedOperations: () => [DropOperation.copy], - canAddItemToExistingSession: true, - dragItemProvider: provideDragItem, - child: DraggableWidget( - child: AnimatedOpacity( - opacity: _dragging ? 0.5 : 1, - duration: const Duration(milliseconds: 200), - child: Container( - decoration: BoxDecoration( - color: widget.color, - borderRadius: BorderRadius.circular(14), - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - child: Text( - widget.name, - style: const TextStyle(fontSize: 20, color: Colors.white), - textAlign: TextAlign.center, - ), - ), + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Drop the images to create a new ad'), ), - ), - ); - } -} - -Future createImageData(Color color) async { - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - final paint = Paint()..color = color; - canvas.drawOval(const Rect.fromLTWH(0, 0, 200, 200), paint); - final picture = recorder.endRecording(); - final image = await picture.toImage(200, 200); - final data = await image.toByteData(format: ui.ImageByteFormat.png); - return data!.buffer.asUint8List(); -} - -class HomeLayout extends StatelessWidget { - const HomeLayout({ - super.key, - required this.draggable, - required this.dropZone, - }); - - final List draggable; - final Widget dropZone; - - @override - Widget build(BuildContext context) { - return SafeArea( - child: LayoutBuilder(builder: (context, constraints) { - if (constraints.maxWidth < 500) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.all(16), - child: Wrap( - direction: Axis.horizontal, - runSpacing: 8, - spacing: 10, - children: draggable, - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0).copyWith(top: 0), - child: dropZone, - ), - ), - ], - ); - } else { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - textDirection: TextDirection.rtl, - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: IntrinsicWidth( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: draggable - .intersperse( - const SizedBox(height: 10), - ) - .toList(growable: false), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0).copyWith(right: 0), - child: dropZone, - ), - ), - ], - ); - } - }), - ); - } -} - -extension IntersperseExtensions on Iterable { - Iterable intersperse(T element) sync* { - final iterator = this.iterator; - if (iterator.moveNext()) { - yield iterator.current; - while (iterator.moveNext()) { - yield element; - yield iterator.current; - } - } - } -} - -extension on DragSession { - Future hasLocalData(Object data) async { - final localData = await getLocalData() ?? []; - return localData.contains(data); - } -} - -class _MyHomePageState extends State { - void showMessage(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(milliseconds: 1500), - ), - ); - } - - Future textDragItem( - AsyncValueGetter dragImage, - DragSession session, - ) async { - // For multi drag on iOS check if this item is already in the session - if (await session.hasLocalData('text-item')) { - return null; - } - final item = DragItem(image: await dragImage(), localData: 'text-item', suggestedName: 'PlainText.txt'); - item.add(Formats.plainText('Plain Text Value')); - return item; - } - - Future imageDragItem( - AsyncValueGetter dragImage, - DragSession session, - ) async { - // For multi drag on iOS check if this item is already in the session - if (await session.hasLocalData('image-item')) { - return null; - } - final item = DragItem( - image: await dragImage(), - localData: 'image-item', - suggestedName: 'Green.png', - ); - item.add(Formats.png(await createImageData(Colors.green))); - return item; - } - - Future lazyImageDragItem( - AsyncValueGetter dragImage, - DragSession session, - ) async { - // For multi drag on iOS check if this item is already in the session - if (await session.hasLocalData('lazy-image-item')) { - return null; - } - final item = DragItem( - image: await dragImage(), - localData: 'lazy-image-item', - suggestedName: 'LazyBlue.png', - ); - item.add(Formats.png.lazy(() async { - showMessage('Requested lazy image.'); - return await createImageData(Colors.blue); - })); - return item; - } - - Future virtualFileDragItem( - AsyncValueGetter dragImage, - DragSession session, - ) async { - // For multi drag on iOS check if this item is already in the session - if (await session.hasLocalData('virtual-file-item')) { - return null; - } - final item = DragItem( - image: await dragImage(), - localData: 'virtual-file-item', - suggestedName: 'VirtualFile.txt', - ); - if (!item.virtualFileSupported) { - return null; - } - item.addVirtualFile( - format: Formats.plainTextFile, - provider: (sinkProvider, progress) { - showMessage('Requesting virtual file content.'); - final line = utf8.encode('Line in virtual file\n'); - const lines = 10; - final sink = sinkProvider(fileSize: line.length * lines); - for (var i = 0; i < lines; ++i) { - sink.add(line); - } - sink.close(); - }, - ); - return item; - } - - Future multipleRepresentationsDragItem( - AsyncValueGetter dragImage, - DragSession session, - ) async { - // For multi drag on iOS check if this item is already in the session - if (await session.hasLocalData('multiple-representations-item')) { - return null; - } - final item = DragItem( - image: await dragImage(), - localData: 'multiple-representations-item', - ); - item.add(Formats.png(await createImageData(Colors.pink))); - item.add(Formats.plainText("Hello World")); - item.add(Formats.uri(NamedUri(Uri.parse('https://flutter.dev'), name: 'Flutter'))); - return item; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: HomeLayout( - draggable: [ - DragableWidget( - name: 'Text', - color: Colors.red, - dragItemProvider: textDragItem, - ), - DragableWidget( - name: 'Image', - color: Colors.green, - dragItemProvider: imageDragItem, - ), - DragableWidget( - name: 'Image 2', - color: Colors.blue, - dragItemProvider: lazyImageDragItem, - ), - DragableWidget( - name: 'Virtual', - color: Colors.amber.shade700, - dragItemProvider: virtualFileDragItem, - ), - DragableWidget( - name: 'Multiple', - color: Colors.pink, - dragItemProvider: multipleRepresentationsDragItem, - ), - ], - dropZone: Container( + body: Container( decoration: BoxDecoration( border: Border.all(color: Colors.blueGrey.shade200), borderRadius: BorderRadius.circular(14), ), child: _DropZone(), ), - ), - ); - } + ); } extension _ReadValue on DataReader { @@ -403,16 +86,10 @@ class _DropZoneState extends State<_DropZone> { constraints: const BoxConstraints(maxWidth: 400), child: ClipRRect( borderRadius: BorderRadius.circular(10), - child: ListView( - shrinkWrap: true, - children: event.session.items - .map((e) => _DropItemInfo(dropItem: e)) - .intersperse(Container( - height: 2, - color: Colors.white.withOpacity(0.7), - )) - .toList(growable: false), - ), + child: Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Text('${event.session.items.length} images selected')), ), ), ), @@ -445,31 +122,6 @@ class _DropZoneState extends State<_DropZone> { print('format not handled'); } }); - - /* - // Obtain additional reader information first - final readers = await Future.wait( - event.session.items.map( - (e) => ReaderInfo.fromReader( - e.dataReader!, - localData: e.localData, - ), - ), - ); - - if (!mounted) { - return; - } - - buildWidgetsForReaders(context, readers, (value) { - setState(() { - _content = ListView( - padding: const EdgeInsets.all(10), - children: value.intersperse(const SizedBox(height: 10)).toList(growable: false), - ); - }); - }); - */ } void _onDropLeave(DropEvent event) { @@ -481,55 +133,13 @@ class _DropZoneState extends State<_DropZone> { bool _isDragOver = false; Widget _preview = const SizedBox(); - Widget _content = const Center( + final Widget _content = const Center( child: Text( - 'Drop here', + 'Drop images here', style: TextStyle( color: Colors.grey, - fontSize: 16, + fontSize: 32, ), ), ); } - -class _DropItemInfo extends StatelessWidget { - const _DropItemInfo({ - required this.dropItem, - }); - - final DropItem dropItem; - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), - child: DefaultTextStyle.merge( - style: const TextStyle(fontSize: 11.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (dropItem.localData != null) - Text.rich(TextSpan(children: [ - const TextSpan( - text: 'Local data: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan(text: '${dropItem.localData}'), - ])), - const SizedBox( - height: 4, - ), - Text.rich(TextSpan(children: [ - const TextSpan( - text: 'Native formats: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan(text: dropItem.platformFormats.join(', ')), - ])), - ], - ), - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 327702c..e986901 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'BookAdPublisher', theme: ThemeData(primarySwatch: Colors.blue), - home: const drag_and_drop.MyHomePage(title: 'title'), + home: const drag_and_drop.SelectImages(), ); } } From c1a6ddba437d68f5906a5220cfa98378da348ab8 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 10 Mar 2023 23:07:42 +0100 Subject: [PATCH 09/81] Drag and Drop works --- lib/drag_and_drop.dart | 66 +++++++++++++++++++++++++++++++++++++++--- lib/main.dart | 26 ++++++++++++----- native/src/api.rs | 5 ++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/lib/drag_and_drop.dart b/lib/drag_and_drop.dart index 0393312..098c7fe 100644 --- a/lib/drag_and_drop.dart +++ b/lib/drag_and_drop.dart @@ -6,7 +6,8 @@ import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_drag_and_drop/super_drag_and_drop.dart'; class SelectImages extends StatelessWidget { - const SelectImages(); + const SelectImages({required this.onSelect}); + final void Function(List paths) onSelect; @override Widget build(BuildContext context) => Scaffold( @@ -18,7 +19,7 @@ class SelectImages extends StatelessWidget { border: Border.all(color: Colors.blueGrey.shade200), borderRadius: BorderRadius.circular(14), ), - child: _DropZone(), + child: _DropZone(onSelect: onSelect), ), ); } @@ -39,6 +40,9 @@ extension _ReadValue on DataReader { } class _DropZone extends StatefulWidget { + const _DropZone({required this.onSelect}); + final void Function(List paths) onSelect; + @override State createState() => _DropZoneState(); } @@ -100,13 +104,67 @@ class _DropZoneState extends State<_DropZone> { } 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', ' '); + } + 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}"); - formats.forEach((format) async { + final imgsPaths = formats.map((format) async { switch (format) { case Formats.plainText: final text = (await dataReader.readValue(Formats.plainText))!; @@ -121,7 +179,7 @@ class _DropZoneState extends State<_DropZone> { default: print('format not handled'); } - }); + });*/ } void _onDropLeave(DropEvent event) { diff --git a/lib/main.dart b/lib/main.dart index e986901..4b5a1fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,22 +9,35 @@ void main() { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + List imgsPaths = []; + @override Widget build(BuildContext context) { return MaterialApp( title: 'BookAdPublisher', theme: ThemeData(primarySwatch: Colors.blue), - home: const drag_and_drop.SelectImages(), + home: imgsPaths.isEmpty + ? drag_and_drop.SelectImages(onSelect: (List paths) { + setState(() { + imgsPaths = paths; + }); + }) + : MyHomePage(imgsPaths), ); } } class MyHomePage extends StatefulWidget { - const MyHomePage(); + const MyHomePage(this.imgsPaths); + final List imgsPaths; @override State createState() => _MyHomePageState(); @@ -35,10 +48,7 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - ad = api.getMetadataFromImages(imgsPath: [ - '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/Camera/20230220_182059.jpg', - '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/Camera/20230220_182113.jpg' - ]); + ad = api.getMetadataFromImages(imgsPath: widget.imgsPaths); } @override diff --git a/native/src/api.rs b/native/src/api.rs index 5d94ca0..2d6579c 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -16,6 +16,11 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { .arg("-in=".to_string() + &picture_path) .output() .expect("failed to execute process"); + if !output.status.success() { + println!("stdout is {:?}", std::str::from_utf8(&output.stdout).unwrap()); + println!("stderr is {:?}", std::str::from_utf8(&output.stderr).unwrap()); + panic!("output.status is {}", output.status) + } let output = std::str::from_utf8(&output.stdout).unwrap(); println!("output is {:?}", output); output From 785d4865656d23e6b5e8efa4b1e5aa86c8e60747 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 11 Mar 2023 09:47:45 +0100 Subject: [PATCH 10/81] Make babelio more resistant to no blurb --- native/src/api.rs | 10 +++++++--- native/src/babelio.rs | 19 +++++++++++-------- native/src/babelio/parser.rs | 31 +++++++++++-------------------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/native/src/api.rs b/native/src/api.rs index 2d6579c..536aeae 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -56,7 +56,7 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { .collect(); let books_titles = books.iter().map(book_format_title_and_author).join("\n"); let blurbs = books - .iter() + .iter().filter(|b| b.blurb.is_some()) .map(|b| { format!( "{}:\n{}\n", @@ -69,7 +69,11 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { let custom_message = leboncoin::personal_info::CUSTOM_MESSAGE; - let mut ad_description = books_titles + "\n\nRésumé:\n" + &blurbs + "\n" + &custom_message; + let mut ad_description = books_titles; + if !blurbs.is_empty() { + ad_description += &("\n\nRésumé:\n".to_owned() + &blurbs); + } + ad_description += &("\n\n".to_owned() + &custom_message); if !keywords.is_empty() { ad_description = ad_description + "\n\nMots-clés:\n" + &keywords; } @@ -81,7 +85,7 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { title: if books.len() == 1 { books.first().unwrap().title.clone() } else { - todo!() + "".to_string() }, description: ad_description, price_cent: 1000, diff --git a/native/src/babelio.rs b/native/src/babelio.rs index 9878b25..3ff3255 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -14,15 +14,18 @@ impl common::Provider for Babelio { let book_page = request::get_book_page(&cached_client, book_url); let 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) - } - }; - let mut res = parser::extract_title_author_keywords(&book_page); - res.blurb = parser::parse_blurb(&raw_blurb); + + if let Some(blurb_res) = blurb_res { + 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 = parser::parse_blurb(&raw_blurb); + } + Some(res) } } diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index d12517e..5d63474 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -7,27 +7,18 @@ pub enum BlurbRes { BigBlurb(String), } -pub fn extract_blurb(html: &str) -> BlurbRes { +pub fn extract_blurb(html: &str) -> Option { let doc = scraper::Html::parse_document(html); - let selector = scraper::Selector::parse("#d_bio").expect( - format!( - "Response should contain a element whose id is 'd_bio', html is {:?}", - html - ) - .as_str(), - ); + 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 = res.next().expect( - format!( - "There should be exactly one element with id 'd_bio', html {:?}", - html - ) - .as_str(), - ); + let d_bio = match res.next() { + None => return None, + Some(e) => e, + }; - // Some books do not folow the general strucuture: https://www.babelio.com/livres/Pullman--la-croisee-des-mondes-tome-2--La-tour-des-anges/59278 + // 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(); @@ -43,13 +34,13 @@ pub fn extract_blurb(html: &str) -> BlurbRes { .rev() .nth(1) .expect("d_bio should have a second to last children (the style span)"); - BlurbRes::SmallBlurb( + Some(BlurbRes::SmallBlurb( dbio_second_to_last_child .value() .as_text() .unwrap() .to_string(), - ) + )) } Some(on_click_element) => { let on_click = on_click_element @@ -63,7 +54,7 @@ pub fn extract_blurb(html: &str) -> BlurbRes { .next() .expect("The onclick should match with the regex"); let id_obj = &single_capture[1]; - BlurbRes::BigBlurb(String::from(id_obj)) + Some(BlurbRes::BigBlurb(String::from(id_obj))) } } } @@ -167,7 +158,7 @@ mod tests { 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, BlurbRes::BigBlurb("827593".to_string())); + assert_eq!(id_obj, Some(BlurbRes::BigBlurb("827593".to_string()))); } #[test] pub fn extract_title_author_keywords_from_file() { From 5d483dd7229cf677cdd01fd4d7878501f91c9fc0 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 11 Mar 2023 09:55:03 +0100 Subject: [PATCH 11/81] Undo part of commit 50c362d, which oversimplified GoogleBooks --- native/src/google_books.rs | 4 +++- native/src/google_books/parser.rs | 13 ++++++++----- native/src/google_books/request.rs | 6 +++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/native/src/google_books.rs b/native/src/google_books.rs index 4b04879..a2dd9aa 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -8,7 +8,9 @@ impl common::Provider for GoogleBooks { fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { let client = reqwest::blocking::Client::builder().build().unwrap(); let isbn_search_response = request::search_by_isbn(&client, isbn); - Some(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(&client, &self_link); + Some(parser::extract_metadata_from_self_link_response(&book_page)) } } diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index ba577a6..23202e4 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -2,11 +2,14 @@ use itertools::Itertools; use crate::common; -pub fn extract_metadata_from_isbn_response(html: &str) -> common::BookMetaData { - let owned_string = html.to_string(); - let s: structs::Root = serde_json::from_str(&owned_string).unwrap(); - let first_book = &s.items[0].volume_info; +pub fn extract_self_link_from_isbn_response(html: &str) -> String { + let s: structs::Root = serde_json::from_str(html).unwrap(); + s.items[0].self_link.to_string() +} +pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { + let s: structs::Item = serde_json::from_str(html).unwrap(); + let first_book = &s.volume_info; common::BookMetaData { title: first_book.title.to_string(), authors: first_book @@ -18,7 +21,7 @@ pub fn extract_metadata_from_isbn_response(html: &str) -> common::BookMetaData { }) .collect_vec(), - blurb: first_book.description.to_owned(), + blurb: first_book.description.clone(), ..Default::default() } } diff --git a/native/src/google_books/request.rs b/native/src/google_books/request.rs index 8da9c9b..f0df992 100644 --- a/native/src/google_books/request.rs +++ b/native/src/google_books/request.rs @@ -6,4 +6,8 @@ pub fn search_by_isbn(client: &reqwest::blocking::Client, isbn: &str) -> String .send() .unwrap(); resp.text().unwrap() -} \ No newline at end of file +} +pub fn get_volume(client: &reqwest::blocking::Client, url: &str) -> String { + let resp = client.get(url).send().unwrap(); + resp.text().unwrap() +} From c59fdc77537439084def2841d4813224b4b21db2 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 11 Mar 2023 10:24:40 +0100 Subject: [PATCH 12/81] Add GoogleBook comment --- native/src/google_books.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/google_books.rs b/native/src/google_books.rs index a2dd9aa..4d9c5de 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -6,6 +6,8 @@ pub struct GoogleBooks; 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 client = reqwest::blocking::Client::builder().build().unwrap(); let isbn_search_response = request::search_by_isbn(&client, isbn); let self_link = parser::extract_self_link_from_isbn_response(&isbn_search_response); @@ -13,5 +15,3 @@ impl common::Provider for GoogleBooks { Some(parser::extract_metadata_from_self_link_response(&book_page)) } } - - From 1105a53727a534b5e1909607dd64de0012447631 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 12 Mar 2023 12:30:48 +0100 Subject: [PATCH 13/81] Babelio return None instead of panic if there is no book itemscope --- native/src/babelio.rs | 6 ++---- native/src/babelio/parser.rs | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/native/src/babelio.rs b/native/src/babelio.rs index 3ff3255..3e20eb1 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -12,11 +12,9 @@ impl common::Provider for Babelio { }; let book_url = request::get_book_url(&cached_client, isbn)?; let book_page = request::get_book_page(&cached_client, book_url); - let blurb_res = parser::extract_blurb(&book_page); + let mut res = parser::extract_title_author_keywords(&book_page)?; - let mut res = parser::extract_title_author_keywords(&book_page); - - if let Some(blurb_res) = blurb_res { + 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) => { diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index 5d63474..d366eac 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -1,5 +1,5 @@ use crate::common::{html_select, BookMetaData}; -use itertools::Itertools; +use itertools::{Itertools}; #[derive(PartialEq, Debug)] pub enum BlurbRes { @@ -10,7 +10,8 @@ pub enum BlurbRes { 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 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() { @@ -59,16 +60,18 @@ pub fn extract_blurb(html: &str) -> Option { } } -pub fn extract_title_author_keywords(html: &str) -> BookMetaData { +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 = res.exactly_one().expect(format!( - "Response should contain a element whose with id is itemscope and itemtype=\"https://schema.org/Book\", html is {:?}", - html - ) - .as_str()); + 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 @@ -138,12 +141,12 @@ pub fn extract_title_author_keywords(html: &str) -> BookMetaData { ) }) .collect(); - BookMetaData { + Some(BookMetaData { title, authors, keywords, ..Default::default() - } + }) } pub fn parse_blurb(raw_blurb: &str) -> Option { @@ -166,7 +169,7 @@ mod tests { let title_author_keywords = extract_title_author_keywords(&html); assert_eq!( title_author_keywords, - BookMetaData { + Some(BookMetaData { title: "Le nom de la bête".to_string(), authors: vec![crate::common::Author { first_name: "Daniel".to_string(), @@ -197,7 +200,7 @@ mod tests { ] .map(|s| s.to_string()) .to_vec(), - } + }) ); } } From 3360a4d83a54fc059a84a77ec5a8c6b8c560ee69 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 12 Mar 2023 12:52:24 +0100 Subject: [PATCH 14/81] GoogleBooks better handle no matche for isbn --- native/src/google_books.rs | 2 +- native/src/google_books/parser.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/native/src/google_books.rs b/native/src/google_books.rs index 4d9c5de..fddea62 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -10,7 +10,7 @@ impl common::Provider for GoogleBooks { // The number of authors can be different too ! let client = reqwest::blocking::Client::builder().build().unwrap(); let isbn_search_response = request::search_by_isbn(&client, isbn); - let self_link = parser::extract_self_link_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(&client, &self_link); Some(parser::extract_metadata_from_self_link_response(&book_page)) } diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index 23202e4..ea041b8 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -2,9 +2,9 @@ use itertools::Itertools; use crate::common; -pub fn extract_self_link_from_isbn_response(html: &str) -> String { +pub fn extract_self_link_from_isbn_response(html: &str) -> Option { let s: structs::Root = serde_json::from_str(html).unwrap(); - s.items[0].self_link.to_string() + s.items.map(|items| items[0].self_link.to_string()) } pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { @@ -38,7 +38,7 @@ mod tests { let self_link = extract_self_link_from_isbn_response(&html); assert_eq!( self_link, - "https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ" + Some("https://www.googleapis.com/books/v1/volumes/DQUFSQAACAAJ".to_string()) ) } @@ -64,7 +64,7 @@ mod structs { pub struct Root<'a> { pub kind: &'a str, pub total_items: i64, - pub items: Vec>, + pub items: Option>>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] From b1f0cf1aad889d08972f7bf6d8502bc6c9b5cc20 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 15 Mar 2023 19:27:46 +0100 Subject: [PATCH 15/81] Big refacto --- .gitignore | 2 + analysis_options.yaml | 34 ++------ lib/ad_editing.dart | 102 ++++++++++++++++++++++ lib/bridge_definitions.dart | 33 ++++++- lib/bridge_generated.dart | 86 +++++++++++++------ lib/isbn_decoding.dart | 31 +++++++ lib/main.dart | 137 ++++++++++++------------------ lib/metadata_collecting.dart | 86 +++++++++++++++++++ native/src/api.rs | 36 +++++--- native/src/bridge_generated.io.rs | 8 +- native/src/bridge_generated.rs | 40 ++++++--- native/src/lib.rs | 6 +- pubspec.yaml | 2 +- 13 files changed, 438 insertions(+), 165 deletions(-) create mode 100644 lib/ad_editing.dart create mode 100644 lib/isbn_decoding.dart create mode 100644 lib/metadata_collecting.dart diff --git a/.gitignore b/.gitignore index 09360f3..38d2398 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ .pub/ /build/ +personal_info.* + # Web related # Symbolication related diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..da4804b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,29 +1,13 @@ -# 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 +analyzer: + enable-experiment: + - records + - patterns + - sealed-class \ No newline at end of file diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart new file mode 100644 index 0000000..a5b5a04 --- /dev/null +++ b/lib/ad_editing.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/main.dart'; +import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; + +import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; + +class AdEditingWidget extends StatefulWidget { + const AdEditingWidget({required this.step}); + final AdEditingStep step; + + @override + State createState() => _AdEditingWidgetState(); +} + +String vecFmt(List vec) { + 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(BookMetaData book) { + final authors = book.authors.map((a) => '${a.firstName} ${a.lastName}').toList(); + return '"${book.title}" ${vecFmt(authors)}'; +} + +class _AdEditingWidgetState extends State { + late Ad ad; + + @override + void initState() { + super.initState(); + ad.imgsPath = widget.step.imgsPaths; + final bookTitles = widget.step.metadata.entries.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); + final blurbs = widget.step.metadata.entries + .map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!) + .join('\n'); + ad.description = bookTitles + '\n\nRésumé:\n' + blurbs + '\n\n' + personal_info.customMessage; + final keywords = widget.step.metadata.entries.map((entry) => entry.value.keywords).join(', '); + if (keywords.isNotEmpty) { + ad.description += '\n\nMots-clés:\n' + keywords; + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextFormField( + initialValue: 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: ad.description, + maxLines: 20, + onChanged: (newText) => setState(() => ad.description = newText), + decoration: const InputDecoration( + icon: Icon(Icons.text_snippet), + labelText: 'Ad description', + ), + ), + TextFormField( + initialValue: 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), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row(children: [ + const Icon( + Icons.image, + color: Colors.grey, + ), + const SizedBox(width: 16), + ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), + ]), + ), + ElevatedButton( + onPressed: ad.priceCent == null + ? null + : () { + print('Try to publish...'); + api.publishAd(ad: ad); + }, + child: const Text("Publish")) + ], + ), + ); + } +} diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index 8ce158f..6287545 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -9,9 +9,9 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; import 'package:meta/meta.dart'; abstract class Native { - Future getMetadataFromImages({required List imgsPath, dynamic hint}); + Future getMetadataFromProvider({required ProviderEnum provider, required String isbn, dynamic hint}); - FlutterRustBridgeTaskConstMeta get kGetMetadataFromImagesConstMeta; + FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; Future publishAd({required Ad ad, dynamic hint}); @@ -31,3 +31,32 @@ class Ad { required this.imgsPath, }); } + +class Author { + final String firstName; + final String lastName; + + const Author({ + required this.firstName, + required this.lastName, + }); +} + +class BookMetaData { + String title; + List authors; + String? blurb; + List keywords; + + BookMetaData({ + required this.title, + required this.authors, + this.blurb, + required this.keywords, + }); +} + +enum ProviderEnum { + Babelio, + GoogleBooks, +} diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 6239de2..2e6a4cb 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -24,23 +24,24 @@ class NativeImpl implements Native { factory NativeImpl.wasm(FutureOr module) => NativeImpl(module as ExternalLibrary); NativeImpl.raw(this._platform); - Future getMetadataFromImages( - {required List imgsPath, dynamic hint}) { - var arg0 = _platform.api2wire_StringList(imgsPath); + 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_get_metadata_from_images(port_, arg0), - parseSuccessData: _wire2api_ad, - constMeta: kGetMetadataFromImagesConstMeta, - argValues: [imgsPath], + _platform.inner.wire_get_metadata_from_provider(port_, arg0, arg1), + parseSuccessData: _wire2api_opt_box_autoadd_book_meta_data, + constMeta: kGetMetadataFromProviderConstMeta, + argValues: [provider, isbn], hint: hint, )); } - FlutterRustBridgeTaskConstMeta get kGetMetadataFromImagesConstMeta => + FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta => const FlutterRustBridgeTaskConstMeta( - debugName: "get_metadata_from_images", - argNames: ["imgsPath"], + debugName: "get_metadata_from_provider", + argNames: ["provider", "isbn"], ); Future publishAd({required Ad ad, dynamic hint}) { @@ -73,20 +74,42 @@ class NativeImpl implements Native { return (raw as List).cast(); } - Ad _wire2api_ad(dynamic raw) { + 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]), + ); + } + + BookMetaData _wire2api_book_meta_data(dynamic raw) { final arr = raw as List; if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return Ad( + return BookMetaData( title: _wire2api_String(arr[0]), - description: _wire2api_String(arr[1]), - priceCent: _wire2api_i32(arr[2]), - imgsPath: _wire2api_StringList(arr[3]), + authors: _wire2api_list_author(arr[1]), + blurb: _wire2api_opt_String(arr[2]), + keywords: _wire2api_StringList(arr[3]), ); } - int _wire2api_i32(dynamic raw) { - return raw as int; + BookMetaData _wire2api_box_autoadd_book_meta_data(dynamic raw) { + return _wire2api_book_meta_data(raw); + } + + 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); + } + + BookMetaData? _wire2api_opt_box_autoadd_book_meta_data(dynamic raw) { + return raw == null ? null : _wire2api_box_autoadd_book_meta_data(raw); } int _wire2api_u8(dynamic raw) { @@ -109,6 +132,11 @@ 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; @@ -260,22 +288,26 @@ class NativeWire implements FlutterRustBridgeWireBase { late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr .asFunction)>(); - void wire_get_metadata_from_images( + void wire_get_metadata_from_provider( int port_, - ffi.Pointer imgs_path, + int provider, + ffi.Pointer isbn, ) { - return _wire_get_metadata_from_images( + return _wire_get_metadata_from_provider( port_, - imgs_path, + provider, + isbn, ); } - late final _wire_get_metadata_from_imagesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_get_metadata_from_images'); - late final _wire_get_metadata_from_images = _wire_get_metadata_from_imagesPtr - .asFunction)>(); + 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_, diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart new file mode 100644 index 0000000..9d25bc6 --- /dev/null +++ b/lib/isbn_decoding.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/main.dart'; + +class ISBNDecodingWidget extends StatefulWidget { + const ISBNDecodingWidget({required this.step, required this.onSubmit}); + final ISBNDecodingStep step; + final void Function(MetadataCollectingStep newStep) onSubmit; + + @override + State createState() => _ISBNDecodingWidgetState(); +} + +class _ISBNDecodingWidgetState extends State { + Map> isbns = {}; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: widget.step.imgsPaths + .map((imgPath) => Column( + children: [ + ImageWidget(imgPath), + ...isbns[imgPath]?.map((isbn) => Text(isbn)).toList() ?? [Text('no ISBN')], + ], + )) + .toList(), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 4b5a1fe..31988ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'ad_editing.dart'; import 'drag_and_drop.dart' as drag_and_drop; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import 'isbn_decoding.dart'; +import 'metadata_collecting.dart'; void main() { runApp(const MyApp()); @@ -16,22 +19,57 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -class _MyAppState extends State { +sealed class BookyStep {} + +class ImageSelectionStep implements BookyStep {} + +class ISBNDecodingStep implements BookyStep { + List imgsPaths = []; + ISBNDecodingStep({required this.imgsPaths}); +} + +class MetadataCollectingStep implements BookyStep { List imgsPaths = []; + Set isbns = {}; +} + +class AdEditingStep implements BookyStep { + List imgsPaths = []; + Set isbns = {}; + Map metadata = {}; +} +class _MyAppState extends State { + BookyStep step = ImageSelectionStep(); @override Widget build(BuildContext context) { return MaterialApp( - title: 'BookAdPublisher', - theme: ThemeData(primarySwatch: Colors.blue), - home: imgsPaths.isEmpty + title: 'BookAdPublisher', + theme: ThemeData(primarySwatch: Colors.blue), + home: switch (step) { + ImageSelectionStep() => drag_and_drop.SelectImages(onSelect: (List paths) { + setState(() { + step = ISBNDecodingStep(imgsPaths: paths); + }); + }), + ISBNDecodingStep() => ISBNDecodingWidget( + step: step as ISBNDecodingStep, + onSubmit: (MetadataCollectingStep newStep) => setState(() => step = newStep)), + MetadataCollectingStep() => MetadataCollectingWidget( + step: step as MetadataCollectingStep, + onSubmit: (AdEditingStep newStep) => setState(() => step = newStep)), + AdEditingStep() => AdEditingWidget( + step: step as AdEditingStep, onSubmit: (bool publishSuccess) => setState(() => step = newStep)), + BookyStep() => throw UnimplementedError('Not possible') + } + /* imgsPaths.isEmpty ? drag_and_drop.SelectImages(onSelect: (List paths) { setState(() { imgsPaths = paths; }); }) - : MyHomePage(imgsPaths), - ); + : MyHomePage(imgsPaths),*/ + ); } } @@ -48,7 +86,7 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - ad = api.getMetadataFromImages(imgsPath: widget.imgsPaths); + // ad = api.getMetadataFromImages(imgsPath: widget.imgsPaths); } @override @@ -103,86 +141,17 @@ extension DoubleExt on double { double multiply(double other) => this * other; } -class AdPage extends StatefulWidget { - const AdPage({required Ad ad}) : initialAd = ad; - - final Ad initialAd; - - @override - State createState() => _AdPageState(); -} - -class _AdPageState extends State { - late Ad ad; - - @override - void initState() { - super.initState(); - ad = widget.initialAd; - } +class ImageWidget extends StatelessWidget { + const ImageWidget(this.imgPath); + final String imgPath; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - TextFormField( - initialValue: 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: ad.description, - maxLines: 20, - onChanged: (newText) => setState(() => ad.description = newText), - decoration: const InputDecoration( - icon: Icon(Icons.text_snippet), - labelText: 'Ad description', - ), - ), - TextFormField( - initialValue: 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), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row(children: [ - const Icon( - Icons.image, - color: Colors.grey, - ), - const SizedBox(width: 16), - ...ad.imgsPath - .map((imgPath) => Image.file( - File(imgPath), - height: 200, - isAntiAlias: true, - filterQuality: FilterQuality.medium, - )) - .toList(), - ]), - ), - ElevatedButton( - onPressed: ad.priceCent == null - ? null - : () { - print('Try to publish...'); - api.publishAd(ad: ad); - }, - child: const Text("Publish")) - ], - ), + return Image.file( + File(imgPath), + height: 200, + isAntiAlias: true, + filterQuality: FilterQuality.medium, ); } } diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart new file mode 100644 index 0000000..21b6ccd --- /dev/null +++ b/lib/metadata_collecting.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import 'bridge_definitions.dart'; +import 'main.dart'; +import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; + + +class MetadataCollectingWidget extends StatefulWidget { + const MetadataCollectingWidget({required this.step, required this.onSubmit}); + final MetadataCollectingStep step; + final void Function(AdEditingStep newStep) onSubmit; + + @override + State createState() => _MetadataCollectingWidgetState(); +} + +class Metadatas { + + final Map> mdFromProviders; + final BookMetaData manual; // = BookMetaData(title: '', authors: [], keywords: []); + Metadatas({required this.mdFromProviders, required this.manual}); +} + +class _MetadataCollectingWidgetState extends State { + Map metadata = {}; + + @override + void initState() { + super.initState(); + widget.step.isbns.forEach((isbn) { + metadata.putIfAbsent(isbn, () => Metadatas(manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) => MapEntry(provider, api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!))))) ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: widget.step.isbns.map((isbn) { + // metadata[isbn]. + // if (metadata[isbn] == null) { + // metadata.a + // } + // if + return Column( + children: [ + // ImageWidget(imgPath), + Text('ISBN: $isbn'), + FutureBuilder( + future: metadata[isbn], + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); + } + final book = snap.data!; + + return Column(children: [ + TextFormField( + initialValue: book.title, + onChanged: (newText) => setState(() => metadata.update(key, (value) => null)[isbn].title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), + style: const TextStyle(fontSize: 30), + ), + TextFormField( + initialValue: book?.title, + onChanged: (newText) => setState(() => metadata[isbn].title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Book author', + ), + style: const TextStyle(fontSize: 30), + ), + ]), + }), + + // ...isbns[imgPath]?.map((isbn) => Text(isbn)).toList() ?? [Text('no ISBN')], + ], + ); + }).toList(), + ), + ); + } +} diff --git a/native/src/api.rs b/native/src/api.rs index 536aeae..1c65bfe 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,9 +1,25 @@ -use std::process::Command; -use itertools::Itertools; -use crate::{babelio, common, google_books, leboncoin}; +use crate::babelio::Babelio; +use crate::common::Provider; use crate::common::{Ad, BookMetaData}; +use crate::google_books::GoogleBooks; use crate::publisher::Publisher; +use crate::{babelio, common, google_books, leboncoin}; +use itertools::Itertools; +use std::process::Command; +enum ProviderEnum { + Babelio, + GoogleBooks, +} + +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 {}.get_book_metadata_from_isbn(&isbn), + } +} + +/* pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { let isbns: Vec = imgs_path .clone() @@ -39,19 +55,16 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { Box::new(google_books::GoogleBooks {}), ]; - let books: Vec = isbns + let books: Vec> = isbns .iter() .map(|isbn| { for provider in &book_metadata_providers { let res = provider.get_book_metadata_from_isbn(&isbn); if let Some(r) = res { - return r; + return Some(r); } } - panic!("No provider find any information on book {}", isbn) - /* book_metadata_providers[0] - .get_book_metadata_from_isbn(&isbn) - .unwrap() */ + None }) .collect(); let books_titles = books.iter().map(book_format_title_and_author).join("\n"); @@ -91,13 +104,13 @@ pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { price_cent: 1000, imgs_path, } -} +}*/ pub fn publish_ad(ad: Ad) -> () { let lbc_publisher = leboncoin::Leboncoin {}; Publisher::publish(&lbc_publisher, ad); } - +/* fn book_format_title_and_author(book: &BookMetaData) -> String { format!( "\"{}\" {}", @@ -119,3 +132,4 @@ fn vec_fmt(vec: Vec) -> String { _ => panic!("More than 2 authors"), } } +*/ diff --git a/native/src/bridge_generated.io.rs b/native/src/bridge_generated.io.rs index 447aa2a..e24e918 100644 --- a/native/src/bridge_generated.io.rs +++ b/native/src/bridge_generated.io.rs @@ -2,8 +2,12 @@ use super::*; // Section: wire functions #[no_mangle] -pub extern "C" fn wire_get_metadata_from_images(port_: i64, imgs_path: *mut wire_StringList) { - wire_get_metadata_from_images_impl(port_, imgs_path) +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] diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index 56be752..cec2d9f 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -20,22 +20,26 @@ use std::sync::Arc; // Section: imports use crate::common::Ad; +use crate::common::Author; +use crate::common::BookMetaData; // Section: wire functions -fn wire_get_metadata_from_images_impl( +fn wire_get_metadata_from_provider_impl( port_: MessagePort, - imgs_path: impl Wire2Api> + UnwindSafe, + provider: impl Wire2Api + UnwindSafe, + isbn: impl Wire2Api + UnwindSafe, ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "get_metadata_from_images", + debug_name: "get_metadata_from_provider", port: Some(port_), mode: FfiCallMode::Normal, }, move || { - let api_imgs_path = imgs_path.wire2api(); - move |task_callback| Ok(get_metadata_from_images(api_imgs_path)) + let api_provider = provider.wire2api(); + let api_isbn = isbn.wire2api(); + move |task_callback| Ok(get_metadata_from_provider(api_provider, api_isbn)) }, ) } @@ -80,6 +84,15 @@ impl Wire2Api for i32 { self } } +impl Wire2Api for i32 { + fn wire2api(self) -> ProviderEnum { + match self { + 0 => ProviderEnum::Babelio, + 1 => ProviderEnum::GoogleBooks, + _ => unreachable!("Invalid variant for ProviderEnum: {}", self), + } + } +} impl Wire2Api for u8 { fn wire2api(self) -> u8 { self @@ -88,18 +101,25 @@ impl Wire2Api for u8 { // Section: impl IntoDart -impl support::IntoDart for Ad { +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 BookMetaData { fn into_dart(self) -> support::DartAbi { vec![ self.title.into_dart(), - self.description.into_dart(), - self.price_cent.into_dart(), - self.imgs_path.into_dart(), + self.authors.into_dart(), + self.blurb.into_dart(), + self.keywords.into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for Ad {} +impl support::IntoDartExceptPrimitive for BookMetaData {} // Section: executor diff --git a/native/src/lib.rs b/native/src/lib.rs index 26a09fa..c8c5bf4 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,11 +1,11 @@ mod api; -mod bridge_generated; mod babelio; +mod bridge_generated; mod cached_client; mod common; +mod config; mod google_books; mod image_tools; -mod publisher; mod jwt_decoder; mod leboncoin; -mod config; +mod publisher; diff --git a/pubspec.yaml b/pubspec.yaml index b2dfd2e..aa1fe86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev 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 From 6999e83220f110cd87c243692f421e7a1cf90fa2 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 15 Mar 2023 19:46:10 +0100 Subject: [PATCH 16/81] compile --- analysis_options.yaml | 2 + lib/ad_editing.dart | 10 +++-- lib/common.dart | 18 ++++++++ lib/isbn_decoding.dart | 2 + lib/main.dart | 34 +++------------ lib/metadata_collecting.dart | 81 ++++++++++++++++++++---------------- pubspec.yaml | 2 + 7 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 lib/common.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index da4804b..8529660 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,6 +6,8 @@ linter: 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 diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index a5b5a04..e356508 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; +import 'common.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; class AdEditingWidget extends StatefulWidget { - const AdEditingWidget({required this.step}); + const AdEditingWidget({required this.step, required this.onSubmit}); final AdEditingStep step; + final void Function(bool newStep) onSubmit; @override State createState() => _AdEditingWidgetState(); @@ -14,8 +16,8 @@ class AdEditingWidget extends StatefulWidget { String vecFmt(List vec) { if (vec.length == 0) return ''; - if (vec.length == 1) return "de ${vec[0]}"; - if (vec.length == 2) return "de ${vec[0]} et ${vec[1]}"; + 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'); } @@ -94,7 +96,7 @@ class _AdEditingWidgetState extends State { print('Try to publish...'); api.publishAd(ad: ad); }, - child: const Text("Publish")) + child: const Text('Publish')) ], ), ); diff --git a/lib/common.dart b/lib/common.dart new file mode 100644 index 0000000..0c253be --- /dev/null +++ b/lib/common.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class ImageWidget extends StatelessWidget { + const ImageWidget(this.imgPath); + final String imgPath; + + @override + Widget build(BuildContext context) { + return Image.file( + File(imgPath), + height: 200, + isAntiAlias: true, + filterQuality: FilterQuality.medium, + ); + } +} diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 9d25bc6..05bc0a4 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; +import 'common.dart'; + class ISBNDecodingWidget extends StatefulWidget { const ISBNDecodingWidget({required this.step, required this.onSubmit}); final ISBNDecodingStep step; diff --git a/lib/main.dart b/lib/main.dart index 31988ae..e5b3545 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'ad_editing.dart'; @@ -59,17 +57,10 @@ class _MyAppState extends State { step: step as MetadataCollectingStep, onSubmit: (AdEditingStep newStep) => setState(() => step = newStep)), AdEditingStep() => AdEditingWidget( - step: step as AdEditingStep, onSubmit: (bool publishSuccess) => setState(() => step = newStep)), + step: step as AdEditingStep, + onSubmit: (bool publishSuccess) => print('onSubmit with bool = $publishSuccess')), BookyStep() => throw UnimplementedError('Not possible') - } - /* imgsPaths.isEmpty - ? drag_and_drop.SelectImages(onSelect: (List paths) { - setState(() { - imgsPaths = paths; - }); - }) - : MyHomePage(imgsPaths),*/ - ); + }); } } @@ -121,9 +112,9 @@ class _MyHomePageState extends State { } final ad = snap.data; - if (ad == null) return const Text("Extracting info from images"); + if (ad == null) return const Text('Extracting info from images'); - return AdPage(ad: ad); + return const Text('extract finish'); }, ) ], @@ -140,18 +131,3 @@ extension IntExt on int { extension DoubleExt on double { double multiply(double other) => this * other; } - -class ImageWidget extends StatelessWidget { - const ImageWidget(this.imgPath); - final String imgPath; - - @override - Widget build(BuildContext context) { - return Image.file( - File(imgPath), - height: 200, - isAntiAlias: true, - filterQuality: FilterQuality.medium, - ); - } -} diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 21b6ccd..b156144 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; -import 'bridge_definitions.dart'; -import 'main.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; - +import 'main.dart'; class MetadataCollectingWidget extends StatefulWidget { const MetadataCollectingWidget({required this.step, required this.onSubmit}); @@ -15,7 +13,6 @@ class MetadataCollectingWidget extends StatefulWidget { } class Metadatas { - final Map> mdFromProviders; final BookMetaData manual; // = BookMetaData(title: '', authors: [], keywords: []); Metadatas({required this.mdFromProviders, required this.manual}); @@ -28,53 +25,67 @@ class _MetadataCollectingWidgetState extends State { void initState() { super.initState(); widget.step.isbns.forEach((isbn) { - metadata.putIfAbsent(isbn, () => Metadatas(manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) => MapEntry(provider, api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!))))) ); + metadata.putIfAbsent( + isbn, + () => Metadatas( + manual: BookMetaData(title: '', authors: [], keywords: []), + mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) => MapEntry( + provider, api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!)))))); }); } @override Widget build(BuildContext context) { return Scaffold( - body: Row( + body: Column( children: widget.step.isbns.map((isbn) { // metadata[isbn]. // if (metadata[isbn] == null) { // metadata.a // } // if - return Column( + final manual = metadata[isbn]!.manual; + + return Row( children: [ // ImageWidget(imgPath), Text('ISBN: $isbn'), - FutureBuilder( - future: metadata[isbn], - builder: (context, snap) { - if (snap.connectionState != ConnectionState.done) { - return const CircularProgressIndicator(); - } - final book = snap.data!; + Column( + children: [ + TextFormField( + initialValue: manual.title, + onChanged: (newText) => setState(() => manual.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), + style: const TextStyle(fontSize: 30), + ), + TextFormField( + initialValue: manual.blurb, + onChanged: (newText) => setState(() => manual.blurb = newText), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Book blurb', + ), + style: const TextStyle(fontSize: 30), + ), + ], + ), + ...metadata[isbn]!.mdFromProviders.entries.map((entry) => Text(entry.key.name) + /*FutureBuilder( + future: metadata[isbn], + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); + } + final book = snap.data!; + + return Column(children: [ - return Column(children: [ - TextFormField( - initialValue: book.title, - onChanged: (newText) => setState(() => metadata.update(key, (value) => null)[isbn].title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.title), - labelText: 'Book title', - ), - style: const TextStyle(fontSize: 30), - ), - TextFormField( - initialValue: book?.title, - onChanged: (newText) => setState(() => metadata[isbn].title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Book author', - ), - style: const TextStyle(fontSize: 30), - ), - ]), - }), + ]), + })*/ + ), // ...isbns[imgPath]?.map((isbn) => Text(isbn)).toList() ?? [Text('no ISBN')], ], diff --git a/pubspec.yaml b/pubspec.yaml index aa1fe86..10b8a9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: flutter_rust_bridge: ^1.45.0 meta: ^1.8.0 super_drag_and_drop: ^0.2.3 + collection: ^1.17.1 + super_clipboard: ^0.2.3+1 dev_dependencies: flutter_test: From eb099d056aac6158bbd6edec1959dbbadee0f269 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 15 Mar 2023 21:22:26 +0100 Subject: [PATCH 17/81] Add isbn decoder --- lib/drag_and_drop.dart | 4 ++++ lib/isbn_decoding.dart | 47 +++++++++++++++++++++++++++++++++++++++++- native/src/api.rs | 6 ++++-- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/drag_and_drop.dart b/lib/drag_and_drop.dart index 098c7fe..be36a4d 100644 --- a/lib/drag_and_drop.dart +++ b/lib/drag_and_drop.dart @@ -116,6 +116,10 @@ class _DropZoneState extends State<_DropZone> { // 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(); diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 05bc0a4..3937cf1 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; @@ -15,6 +17,49 @@ class ISBNDecodingWidget extends StatefulWidget { class _ISBNDecodingWidgetState extends State { Map> isbns = {}; + @override + void initState() { + // TODO: implement initState + super.initState(); + print('initState'); + widget.step.imgsPaths.forEach((imgPath) { + Future.microtask(() async { + final decoder_process = await Process.run( + '/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode', + ['-in=' + imgPath]); + if (decoder_process.exitCode != 0) { + print('stdout is ${decoder_process.stdout}'); + print('stderr is ${decoder_process.stderr}'); + throw Exception('decoder status is ${decoder_process.exitCode}'); + } + // final s = String.fromCharCodes((decoder_process.stdout as List)); + final s = decoder_process.stdout as String; + print('s = $s'); + setState(() { + isbns[imgPath] = s.split(' '); + }); + }); + }); +/* + let output = Command::new( + "/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode", + ) + .arg("-in=".to_string() + &picture_path) + .output() + .expect("failed to execute process"); + if !output.status.success() { + println!("stdout is {:?}", std::str::from_utf8(&output.stdout).unwrap()); + println!("stderr is {:?}", std::str::from_utf8(&output.stderr).unwrap()); + panic!("output.status is {}", output.status) + } + let output = std::str::from_utf8(&output.stdout).unwrap(); + println!("output is {:?}", output); + output + .split_ascii_whitespace() + .map(|x| x.to_string()) + .collect_vec()*/ + } + @override Widget build(BuildContext context) { return Scaffold( @@ -23,7 +68,7 @@ class _ISBNDecodingWidgetState extends State { .map((imgPath) => Column( children: [ ImageWidget(imgPath), - ...isbns[imgPath]?.map((isbn) => Text(isbn)).toList() ?? [Text('no ISBN')], + ...isbns[imgPath]!.map((isbn) => Text(isbn)).toList(), ], )) .toList(), diff --git a/native/src/api.rs b/native/src/api.rs index 1c65bfe..6945840 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -7,7 +7,7 @@ use crate::{babelio, common, google_books, leboncoin}; use itertools::Itertools; use std::process::Command; -enum ProviderEnum { +pub enum ProviderEnum { Babelio, GoogleBooks, } @@ -15,7 +15,9 @@ enum ProviderEnum { 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 {}.get_book_metadata_from_isbn(&isbn), + ProviderEnum::GoogleBooks => { + google_books::GoogleBooks {}.get_book_metadata_from_isbn(&isbn) + } } } From dcd3aaa86041332ccfebd8a4b33cec00e3bced8b Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 15 Mar 2023 21:57:20 +0100 Subject: [PATCH 18/81] show basic metadatacollecting --- lib/isbn_decoding.dart | 65 ++++++++++++++++++------------------ lib/main.dart | 1 + lib/metadata_collecting.dart | 40 +++++++++++----------- 3 files changed, 54 insertions(+), 52 deletions(-) diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 3937cf1..e22dc5d 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -15,7 +15,7 @@ class ISBNDecodingWidget extends StatefulWidget { } class _ISBNDecodingWidgetState extends State { - Map> isbns = {}; + Map>> isbns = {}; @override void initState() { @@ -23,7 +23,7 @@ class _ISBNDecodingWidgetState extends State { super.initState(); print('initState'); widget.step.imgsPaths.forEach((imgPath) { - Future.microtask(() async { + isbns[imgPath] = Future(() async { final decoder_process = await Process.run( '/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode', ['-in=' + imgPath]); @@ -32,46 +32,45 @@ class _ISBNDecodingWidgetState extends State { print('stderr is ${decoder_process.stderr}'); throw Exception('decoder status is ${decoder_process.exitCode}'); } - // final s = String.fromCharCodes((decoder_process.stdout as List)); final s = decoder_process.stdout as String; - print('s = $s'); - setState(() { - isbns[imgPath] = s.split(' '); - }); + return s.split(' ').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); }); }); -/* - let output = Command::new( - "/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode", - ) - .arg("-in=".to_string() + &picture_path) - .output() - .expect("failed to execute process"); - if !output.status.success() { - println!("stdout is {:?}", std::str::from_utf8(&output.stdout).unwrap()); - println!("stderr is {:?}", std::str::from_utf8(&output.stderr).unwrap()); - panic!("output.status is {}", output.status) - } - let output = std::str::from_utf8(&output.stdout).unwrap(); - println!("output is {:?}", output); - output - .split_ascii_whitespace() - .map(|x| x.to_string()) - .collect_vec()*/ } @override Widget build(BuildContext context) { return Scaffold( body: Row( - children: widget.step.imgsPaths - .map((imgPath) => Column( - children: [ - ImageWidget(imgPath), - ...isbns[imgPath]!.map((isbn) => Text(isbn)).toList(), - ], - )) - .toList(), + children: [ + ...widget.step.imgsPaths + .map((imgPath) => Column( + children: [ + ImageWidget(imgPath), + FutureBuilder( + future: isbns[imgPath]!, + builder: (context, snap) { + if (snap.hasData == false) { + return const CircularProgressIndicator(); + } + return Column(children: snap.data!.map((isbn) => Text(isbn)).toList()); + }) + ], + )) + .toList(), + Spacer(), + FutureBuilder( + future: Future.wait(isbns.values), + builder: (context, snap) { + return ElevatedButton( + onPressed: () { + final isbnSet = snap.data!.expand((e) => e).toSet(); + print('isbnSet = $isbnSet'); + widget.onSubmit(MetadataCollectingStep(imgsPaths: widget.step.imgsPaths, isbns: isbnSet)); + }, + child: const Text('Validate ISBNs')); + }) + ], ), ); } diff --git a/lib/main.dart b/lib/main.dart index e5b3545..9cab584 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ class ISBNDecodingStep implements BookyStep { class MetadataCollectingStep implements BookyStep { List imgsPaths = []; Set isbns = {}; + MetadataCollectingStep({required this.imgsPaths, required this.isbns}); } class AdEditingStep implements BookyStep { diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index b156144..ef1769e 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -50,27 +50,29 @@ class _MetadataCollectingWidgetState extends State { children: [ // ImageWidget(imgPath), Text('ISBN: $isbn'), - Column( - children: [ - TextFormField( - initialValue: manual.title, - onChanged: (newText) => setState(() => manual.title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.title), - labelText: 'Book title', + Expanded( + child: Column( + children: [ + TextFormField( + initialValue: manual.title, + onChanged: (newText) => setState(() => manual.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), + style: const TextStyle(fontSize: 30), ), - style: const TextStyle(fontSize: 30), - ), - TextFormField( - initialValue: manual.blurb, - onChanged: (newText) => setState(() => manual.blurb = newText), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Book blurb', + TextFormField( + initialValue: manual.blurb, + onChanged: (newText) => setState(() => manual.blurb = newText), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Book blurb', + ), + style: const TextStyle(fontSize: 30), ), - style: const TextStyle(fontSize: 30), - ), - ], + ], + ), ), ...metadata[isbn]!.mdFromProviders.entries.map((entry) => Text(entry.key.name) /*FutureBuilder( From c53ef46a96287e9f10562db3d34abf81fd3a60a9 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 15 Mar 2023 23:54:34 +0100 Subject: [PATCH 19/81] Use gridview for MetadataCollectingStep --- lib/metadata_collecting.dart | 99 ++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index ef1769e..ef6ff0e 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -37,62 +37,53 @@ class _MetadataCollectingWidgetState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Column( - children: widget.step.isbns.map((isbn) { - // metadata[isbn]. - // if (metadata[isbn] == null) { - // metadata.a - // } - // if - final manual = metadata[isbn]!.manual; - - return Row( - children: [ - // ImageWidget(imgPath), - Text('ISBN: $isbn'), - Expanded( - child: Column( - children: [ - TextFormField( - initialValue: manual.title, - onChanged: (newText) => setState(() => manual.title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.title), - labelText: 'Book title', - ), - style: const TextStyle(fontSize: 30), - ), - TextFormField( - initialValue: manual.blurb, - onChanged: (newText) => setState(() => manual.blurb = newText), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Book blurb', - ), - style: const TextStyle(fontSize: 30), + body: SingleChildScrollView( + child: Column( + children: widget.step.isbns.map((isbn) { + final manual = metadata[isbn]!.manual; + return Card( + margin: EdgeInsets.all(10), + child: Row( + children: [ + Text('ISBN: $isbn'), + Expanded( + child: GridView.count( + crossAxisCount: 3, + shrinkWrap: true, + children: [ + Text('Manual'), + Text('Babelio'), + Text('GoogleBooks'), + TextFormField( + initialValue: manual.title, + onChanged: (newText) => setState(() => manual.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), + style: const TextStyle(fontSize: 30), + ), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => + FutureBuilder(future: e.value, builder: (context, snapMD) => Text(snapMD.data!.title))), + TextFormField( + initialValue: manual.blurb, + onChanged: (newText) => setState(() => manual.blurb = newText), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Book blurb', + ), + style: const TextStyle(fontSize: 30), + ), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( + future: e.value, builder: (context, snapMD) => Text(snapMD.data!.blurb ?? 'None'))) + ], ), - ], - ), - ), - ...metadata[isbn]!.mdFromProviders.entries.map((entry) => Text(entry.key.name) - /*FutureBuilder( - future: metadata[isbn], - builder: (context, snap) { - if (snap.connectionState != ConnectionState.done) { - return const CircularProgressIndicator(); - } - final book = snap.data!; - - return Column(children: [ - - ]), - })*/ ), - - // ...isbns[imgPath]?.map((isbn) => Text(isbn)).toList() ?? [Text('no ISBN')], - ], - ); - }).toList(), + ], + ), + ); + }).toList(), + ), ), ); } From 3ed60c5a12f79ebf06bc9140439dd1727f3db5f7 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 00:16:57 +0100 Subject: [PATCH 20/81] SelectableText non scroll grid view --- lib/metadata_collecting.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index ef6ff0e..4d2f6a9 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -14,7 +14,7 @@ class MetadataCollectingWidget extends StatefulWidget { class Metadatas { final Map> mdFromProviders; - final BookMetaData manual; // = BookMetaData(title: '', authors: [], keywords: []); + final BookMetaData manual; Metadatas({required this.mdFromProviders, required this.manual}); } @@ -45,10 +45,11 @@ class _MetadataCollectingWidgetState extends State { margin: EdgeInsets.all(10), child: Row( children: [ - Text('ISBN: $isbn'), + SelectableText('ISBN: $isbn'), Expanded( child: GridView.count( crossAxisCount: 3, + physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, children: [ Text('Manual'), @@ -63,8 +64,8 @@ class _MetadataCollectingWidgetState extends State { ), style: const TextStyle(fontSize: 30), ), - ...metadata[isbn]!.mdFromProviders.entries.map((e) => - FutureBuilder(future: e.value, builder: (context, snapMD) => Text(snapMD.data!.title))), + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( + future: e.value, builder: (context, snapMD) => SelectableText(snapMD.data!.title))), TextFormField( initialValue: manual.blurb, onChanged: (newText) => setState(() => manual.blurb = newText), @@ -75,7 +76,12 @@ class _MetadataCollectingWidgetState extends State { style: const TextStyle(fontSize: 30), ), ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( - future: e.value, builder: (context, snapMD) => Text(snapMD.data!.blurb ?? 'None'))) + future: e.value, + builder: (context, snapMD) { + final blurb = snapMD.data!.blurb; + if (blurb == null) return Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + return SelectableText(blurb); + })) ], ), ), From a99a42ca85c19ec0f28746dcc625c51866e8bd73 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 20:42:37 +0100 Subject: [PATCH 21/81] Use Table instead of GridView --- lib/metadata_collecting.dart | 69 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 4d2f6a9..15983b5 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -42,46 +42,49 @@ class _MetadataCollectingWidgetState extends State { children: widget.step.isbns.map((isbn) { final manual = metadata[isbn]!.manual; return Card( - margin: EdgeInsets.all(10), + margin: const EdgeInsets.all(10), child: Row( children: [ SelectableText('ISBN: $isbn'), Expanded( - child: GridView.count( - crossAxisCount: 3, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, + child: Table( children: [ - Text('Manual'), - Text('Babelio'), - Text('GoogleBooks'), - TextFormField( - initialValue: manual.title, - onChanged: (newText) => setState(() => manual.title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.title), - labelText: 'Book title', + const TableRow(children: [ + Text('Manual'), + Text('Babelio'), + Text('GoogleBooks'), + ]), + TableRow(children: [ + TextFormField( + initialValue: manual.title, + onChanged: (newText) => setState(() => manual.title = newText), + decoration: const InputDecoration( + icon: Icon(Icons.title), + labelText: 'Book title', + ), ), - style: const TextStyle(fontSize: 30), - ), - ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( - future: e.value, builder: (context, snapMD) => SelectableText(snapMD.data!.title))), - TextFormField( - initialValue: manual.blurb, - onChanged: (newText) => setState(() => manual.blurb = newText), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Book blurb', + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( + future: e.value, builder: (context, snapMD) => SelectableText(snapMD.data!.title))), + ]), + TableRow(children: [ + TextFormField( + initialValue: manual.blurb, + onChanged: (newText) => setState(() => manual.blurb = newText), + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Book blurb', + ), ), - style: const TextStyle(fontSize: 30), - ), - ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( - future: e.value, - builder: (context, snapMD) { - final blurb = snapMD.data!.blurb; - if (blurb == null) return Text('None', style: TextStyle(fontStyle: FontStyle.italic)); - return SelectableText(blurb); - })) + ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( + future: e.value, + builder: (context, snapMD) { + final blurb = snapMD.data!.blurb; + if (blurb == null) { + return const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + } + return SelectableText(blurb); + })), + ]), ], ), ), From e8390e435c33240d3e4dcd8e6aad808e001d9cb0 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 20:56:29 +0100 Subject: [PATCH 22/81] Mock Step2 --- lib/main.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 9cab584..140a3b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,7 +39,16 @@ class AdEditingStep implements BookyStep { } class _MyAppState extends State { - BookyStep step = ImageSelectionStep(); + BookyStep step = //ImageSelectionStep(); + 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( From d02b9128c78e26b5d54a7a1537c7316fd3200f32 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 22:33:43 +0100 Subject: [PATCH 23/81] Add FutureWidget --- lib/ad_editing.dart | 6 +- lib/common.dart | 35 ++++++++ lib/main.dart | 3 +- lib/metadata_collecting.dart | 160 +++++++++++++++++++++++------------ 4 files changed, 145 insertions(+), 59 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index e356508..3a64ca2 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -32,16 +32,16 @@ class _AdEditingWidgetState extends State { @override void initState() { super.initState(); - ad.imgsPath = widget.step.imgsPaths; final bookTitles = widget.step.metadata.entries.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); final blurbs = widget.step.metadata.entries .map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!) .join('\n'); - ad.description = bookTitles + '\n\nRésumé:\n' + blurbs + '\n\n' + personal_info.customMessage; + var description = bookTitles + '\n\nRésumé:\n' + blurbs + '\n\n' + personal_info.customMessage; final keywords = widget.step.metadata.entries.map((entry) => entry.value.keywords).join(', '); if (keywords.isNotEmpty) { - ad.description += '\n\nMots-clés:\n' + keywords; + description += '\n\nMots-clés:\n' + keywords; } + ad = Ad(title: '', description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); } @override diff --git a/lib/common.dart b/lib/common.dart index 0c253be..8d07977 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'bridge_definitions.dart'; + class ImageWidget extends StatelessWidget { const ImageWidget(this.imgPath); final String imgPath; @@ -16,3 +18,36 @@ class ImageWidget extends StatelessWidget { ); } } + +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!); + default: + return const Text('???'); + } + } +} + +extension AuthorsExt on List { + String toText() => map((a) => '${a.firstName} ${a.lastName}').join('\n'); +} diff --git a/lib/main.dart b/lib/main.dart index 140a3b5..a31beec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,8 +34,9 @@ class MetadataCollectingStep implements BookyStep { class AdEditingStep implements BookyStep { List imgsPaths = []; - Set isbns = {}; Map metadata = {}; + + AdEditingStep({required this.imgsPaths, required this.metadata}); } class _MyAppState extends State { diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 15983b5..783deab 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/common.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'main.dart'; @@ -14,7 +15,7 @@ class MetadataCollectingWidget extends StatefulWidget { class Metadatas { final Map> mdFromProviders; - final BookMetaData manual; + BookMetaData manual; Metadatas({required this.mdFromProviders, required this.manual}); } @@ -29,8 +30,13 @@ class _MetadataCollectingWidgetState extends State { isbn, () => Metadatas( manual: BookMetaData(title: '', authors: [], keywords: []), - mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) => MapEntry( - provider, api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!)))))); + mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { + final md = api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!); + /*if (provider == ProviderEnum.Babelio) { + md.then((value) => metadata[isbn]!.manual = value); + }*/ + return MapEntry(provider, md); + })))); }); } @@ -39,59 +45,103 @@ class _MetadataCollectingWidgetState extends State { return Scaffold( body: SingleChildScrollView( child: Column( - children: widget.step.isbns.map((isbn) { - final manual = metadata[isbn]!.manual; - return Card( - margin: const EdgeInsets.all(10), - child: Row( - children: [ - SelectableText('ISBN: $isbn'), - Expanded( - child: Table( - children: [ - const TableRow(children: [ - Text('Manual'), - Text('Babelio'), - Text('GoogleBooks'), - ]), - TableRow(children: [ - TextFormField( - initialValue: manual.title, - onChanged: (newText) => setState(() => manual.title = newText), - decoration: const InputDecoration( - icon: Icon(Icons.title), - labelText: 'Book title', - ), - ), - ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( - future: e.value, builder: (context, snapMD) => SelectableText(snapMD.data!.title))), - ]), - TableRow(children: [ - TextFormField( - initialValue: manual.blurb, - onChanged: (newText) => setState(() => manual.blurb = newText), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Book blurb', - ), - ), - ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureBuilder( - future: e.value, - builder: (context, snapMD) { - final blurb = snapMD.data!.blurb; - if (blurb == null) { - return const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); - } - return SelectableText(blurb); - })), - ]), - ], - ), + children: [ + ...widget.step.isbns.map((isbn) { + final manual = metadata[isbn]!.manual; + return Card( + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + SelectableText('ISBN: $isbn'), + Expanded( + child: Table( + children: [ + const TableRow(children: [ + Text('Manual'), + Text('Babelio'), + Text('GoogleBooks'), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + initialValue: data.title, + 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) => SelectableText(data.title))), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + initialValue: data.authors.toText(), + 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.isEmpty) { + return const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + } + return SelectableText(authors.toText()); + })), + ]), + TableRow(children: [ + FutureWidget( + future: metadata[isbn]!.mdFromProviders.entries.first.value, + builder: (data) => TextFormField( + initialValue: data.blurb, + onChanged: (newText) => setState(() => manual.blurb = newText), + 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 const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + } + return SelectableText(blurb); + })), + ]), + ], + ), + ), + ], ), - ], - ), - ); - }).toList(), + ), + ); + }).toList(), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () { + widget.onSubmit(AdEditingStep( + imgsPaths: widget.step.imgsPaths, + metadata: metadata.map((key, value) => MapEntry(key, value.manual)))); + }, + child: const Text('Validate Metadatas')), + ) + ], ), ), ); From 70704a4feef4a8066935676434effa7f3ddc9fe4 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 22:44:34 +0100 Subject: [PATCH 24/81] Going to AdEditing works --- lib/ad_editing.dart | 101 ++++++++++++++++++----------------- lib/isbn_decoding.dart | 2 +- lib/metadata_collecting.dart | 7 ++- 3 files changed, 58 insertions(+), 52 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 3a64ca2..e67ef54 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -46,58 +46,61 @@ class _AdEditingWidgetState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - TextFormField( - initialValue: 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: ad.description, - maxLines: 20, - onChanged: (newText) => setState(() => ad.description = newText), - decoration: const InputDecoration( - icon: Icon(Icons.text_snippet), - labelText: 'Ad description', + return Scaffold( + appBar: AppBar(title: const Text('Ad editing')), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextFormField( + initialValue: 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: 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', + TextFormField( + initialValue: ad.description, + maxLines: 20, + onChanged: (newText) => setState(() => ad.description = newText), + decoration: const InputDecoration( + icon: Icon(Icons.text_snippet), + labelText: 'Ad description', + ), ), - style: const TextStyle(fontSize: 20), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row(children: [ - const Icon( - Icons.image, - color: Colors.grey, + TextFormField( + initialValue: 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', ), - const SizedBox(width: 16), - ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), - ]), - ), - ElevatedButton( - onPressed: ad.priceCent == null - ? null - : () { - print('Try to publish...'); - api.publishAd(ad: ad); - }, - child: const Text('Publish')) - ], + style: const TextStyle(fontSize: 20), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row(children: [ + const Icon( + Icons.image, + color: Colors.grey, + ), + const SizedBox(width: 16), + ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), + ]), + ), + ElevatedButton( + onPressed: ad.priceCent == null + ? null + : () { + print('Try to publish...'); + api.publishAd(ad: ad); + }, + child: const Text('Publish')) + ], + ), ), ); } diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index e22dc5d..1883989 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -58,7 +58,7 @@ class _ISBNDecodingWidgetState extends State { ], )) .toList(), - Spacer(), + const Spacer(), FutureBuilder( future: Future.wait(isbns.values), builder: (context, snap) { diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 783deab..7c4af51 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -32,9 +32,9 @@ class _MetadataCollectingWidgetState extends State { manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { final md = api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!); - /*if (provider == ProviderEnum.Babelio) { + if (provider == ProviderEnum.Babelio) { md.then((value) => metadata[isbn]!.manual = value); - }*/ + } return MapEntry(provider, md); })))); }); @@ -43,6 +43,9 @@ class _MetadataCollectingWidgetState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + title: const Text('Metadata Collecting'), + ), body: SingleChildScrollView( child: Column( children: [ From a22da44700409d2a4cbabe16a96926fad505fa9a Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 16 Mar 2023 22:58:01 +0100 Subject: [PATCH 25/81] Remove Windows, macos and ios target --- ios/.gitignore | 34 - ios/Flutter/AppFrameworkInfo.plist | 26 - ios/Flutter/Debug.xcconfig | 1 - ios/Flutter/Release.xcconfig | 1 - ios/Runner.xcodeproj/project.pbxproj | 560 --------------- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 --- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - ios/Runner/AppDelegate.swift | 14 - .../AppIcon.appiconset/Contents.json | 122 ---- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 - ios/Runner/Base.lproj/Main.storyboard | 26 - ios/Runner/Info.plist | 51 -- ios/Runner/Runner-Bridging-Header.h | 2 - ios/Runner/bridge_generated.h | 40 -- macos/.gitignore | 7 - macos/Flutter/Flutter-Debug.xcconfig | 1 - macos/Flutter/Flutter-Release.xcconfig | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 14 - macos/Runner.xcodeproj/project.pbxproj | 639 ------------------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 --- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - macos/Runner/AppDelegate.swift | 10 - .../AppIcon.appiconset/Contents.json | 68 -- .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes macos/Runner/Base.lproj/MainMenu.xib | 343 ---------- macos/Runner/Configs/AppInfo.xcconfig | 14 - macos/Runner/Configs/Debug.xcconfig | 2 - macos/Runner/Configs/Release.xcconfig | 2 - macos/Runner/Configs/Warnings.xcconfig | 13 - macos/Runner/DebugProfile.entitlements | 12 - macos/Runner/Info.plist | 32 - macos/Runner/MainFlutterWindow.swift | 15 - macos/Runner/Release.entitlements | 8 - macos/Runner/bridge_generated.h | 40 -- web/favicon.png | Bin 917 -> 0 bytes web/icons/Icon-192.png | Bin 5292 -> 0 bytes web/icons/Icon-512.png | Bin 8252 -> 0 bytes web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes web/index.html | 59 -- web/manifest.json | 35 - windows/.gitignore | 17 - windows/CMakeLists.txt | 102 --- windows/flutter/CMakeLists.txt | 104 --- .../flutter/generated_plugin_registrant.cc | 17 - windows/flutter/generated_plugin_registrant.h | 15 - windows/flutter/generated_plugins.cmake | 25 - windows/runner/CMakeLists.txt | 40 -- windows/runner/Runner.rc | 121 ---- windows/runner/flutter_window.cpp | 66 -- windows/runner/flutter_window.h | 33 - windows/runner/main.cpp | 43 -- windows/runner/resource.h | 16 - windows/runner/resources/app_icon.ico | Bin 33772 -> 0 bytes windows/runner/runner.exe.manifest | 20 - windows/runner/utils.cpp | 64 -- windows/runner/utils.h | 19 - windows/runner/win32_window.cpp | 288 -------- windows/runner/win32_window.h | 102 --- windows/rust.cmake | 21 - 93 files changed, 3613 deletions(-) delete mode 100644 ios/.gitignore delete mode 100644 ios/Flutter/AppFrameworkInfo.plist delete mode 100644 ios/Flutter/Debug.xcconfig delete mode 100644 ios/Flutter/Release.xcconfig delete mode 100644 ios/Runner.xcodeproj/project.pbxproj delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 ios/Runner/AppDelegate.swift delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 ios/Runner/Base.lproj/Main.storyboard delete mode 100644 ios/Runner/Info.plist delete mode 100644 ios/Runner/Runner-Bridging-Header.h delete mode 100644 ios/Runner/bridge_generated.h delete mode 100644 macos/.gitignore delete mode 100644 macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 macos/Flutter/Flutter-Release.xcconfig delete mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 macos/Runner.xcodeproj/project.pbxproj delete mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner/AppDelegate.swift delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 macos/Runner/Configs/Debug.xcconfig delete mode 100644 macos/Runner/Configs/Release.xcconfig delete mode 100644 macos/Runner/Configs/Warnings.xcconfig delete mode 100644 macos/Runner/DebugProfile.entitlements delete mode 100644 macos/Runner/Info.plist delete mode 100644 macos/Runner/MainFlutterWindow.swift delete mode 100644 macos/Runner/Release.entitlements delete mode 100644 macos/Runner/bridge_generated.h delete mode 100644 web/favicon.png delete mode 100644 web/icons/Icon-192.png delete mode 100644 web/icons/Icon-512.png delete mode 100644 web/icons/Icon-maskable-192.png delete mode 100644 web/icons/Icon-maskable-512.png delete mode 100644 web/index.html delete mode 100644 web/manifest.json delete mode 100644 windows/.gitignore delete mode 100644 windows/CMakeLists.txt delete mode 100644 windows/flutter/CMakeLists.txt delete mode 100644 windows/flutter/generated_plugin_registrant.cc delete mode 100644 windows/flutter/generated_plugin_registrant.h delete mode 100644 windows/flutter/generated_plugins.cmake delete mode 100644 windows/runner/CMakeLists.txt delete mode 100644 windows/runner/Runner.rc delete mode 100644 windows/runner/flutter_window.cpp delete mode 100644 windows/runner/flutter_window.h delete mode 100644 windows/runner/main.cpp delete mode 100644 windows/runner/resource.h delete mode 100644 windows/runner/resources/app_icon.ico delete mode 100644 windows/runner/runner.exe.manifest delete mode 100644 windows/runner/utils.cpp delete mode 100644 windows/runner/utils.h delete mode 100644 windows/runner/win32_window.cpp delete mode 100644 windows/runner/win32_window.h delete mode 100644 windows/rust.cmake 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 dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 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 797d452e458972bab9d994556c8305db4c827017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c 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 6ed2d933e1120817fe9182483a228007b18ab6ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 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 4cd7b0099ca80c806f8fe495613e8d6c69460d76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX 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 fe730945a01f64a61e2235dbe3f45b08f7729182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c 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 502f463a9bc882b461c96aadf492d1729e49e725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU 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 0ec303439225b78712f49115768196d8d76f6790..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU 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 e9f5fea27c705180eb716271f41b582e76dcbd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 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 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 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H 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 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v 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 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v 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 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v 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/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 35154be..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import irondash_engine_context -import super_native_extensions - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) - SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) -} 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 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY 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 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 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 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 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 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 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 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/web/favicon.png b/web/favicon.png deleted file mode 100644 index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index b749bfef07473333cf1dd31e9eed89862a5d52aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 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 diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48dff1169879ba46840804b412fe02fefd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e525556d5d89141648c724331630325d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20998 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 MPgg&ebxsLQ025IeI{*Lx 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 94564b6..0000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - IrondashEngineContextPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); - SuperNativeExtensionsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); -} 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 607cf1f..0000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - irondash_engine_context - super_native_extensions -) - -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 c04e20caf6370ebb9253ad831cc31de4a9c965f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33772 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 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() From 40052c77e4e0e860c2ed16f4fbbf0c60384da2ff Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 00:08:54 +0100 Subject: [PATCH 26/81] Small adjustment in justfile --- justfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 . From ba4f122c23bd19284576695853bec3769256f5e6 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 00:18:10 +0100 Subject: [PATCH 27/81] AdEditing: Fix scrolling, add check before enabling publishing --- lib/ad_editing.dart | 97 +++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index e67ef54..f1e250e 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -50,56 +50,59 @@ class _AdEditingWidgetState extends State { appBar: AppBar(title: const Text('Ad editing')), body: Padding( padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - TextFormField( - initialValue: 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: ad.description, - maxLines: 20, - onChanged: (newText) => setState(() => ad.description = newText), - decoration: const InputDecoration( - icon: Icon(Icons.text_snippet), - labelText: 'Ad description', + child: SingleChildScrollView( + child: Column( + children: [ + TextFormField( + initialValue: 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: 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', + TextFormField( + initialValue: ad.description, + maxLines: null, + scrollPhysics: const NeverScrollableScrollPhysics(), + onChanged: (newText) => setState(() => ad.description = newText), + decoration: const InputDecoration( + icon: Icon(Icons.text_snippet), + labelText: 'Ad description', + ), ), - style: const TextStyle(fontSize: 20), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row(children: [ - const Icon( - Icons.image, - color: Colors.grey, + TextFormField( + initialValue: 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', ), - const SizedBox(width: 16), - ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), - ]), - ), - ElevatedButton( - onPressed: ad.priceCent == null - ? null - : () { - print('Try to publish...'); - api.publishAd(ad: ad); - }, - child: const Text('Publish')) - ], + style: const TextStyle(fontSize: 20), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row(children: [ + const Icon( + Icons.image, + color: Colors.grey, + ), + const SizedBox(width: 16), + ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), + ]), + ), + ElevatedButton( + onPressed: (ad.title.length < 2 || ad.description.length < 15 || ad.priceCent == null) + ? null + : () { + print('Try to publish...'); + api.publishAd(ad: ad); + }, + child: const Text('Publish')) + ], + ), ), ), ); From cbba5a4619c48af68f4b839ddcd47550052b9ce0 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 08:52:22 +0100 Subject: [PATCH 28/81] Fix lint --- lib/common.dart | 2 +- lib/isbn_decoding.dart | 12 ++++++------ lib/main.dart | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/common.dart b/lib/common.dart index 8d07977..89d7f29 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -41,7 +41,7 @@ class AsyncSnapshotWidget extends StatelessWidget { case ConnectionState.waiting: return const CircularProgressIndicator(); case ConnectionState.done: - return builder(snap.data!); + return builder(snap.data as T); default: return const Text('???'); } diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 1883989..78ef669 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -24,15 +24,15 @@ class _ISBNDecodingWidgetState extends State { print('initState'); widget.step.imgsPaths.forEach((imgPath) { isbns[imgPath] = Future(() async { - final decoder_process = await Process.run( + final decoderProcess = await Process.run( '/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode', ['-in=' + imgPath]); - if (decoder_process.exitCode != 0) { - print('stdout is ${decoder_process.stdout}'); - print('stderr is ${decoder_process.stderr}'); - throw Exception('decoder status is ${decoder_process.exitCode}'); + if (decoderProcess.exitCode != 0) { + print('stdout is ${decoderProcess.stdout}'); + print('stderr is ${decoderProcess.stderr}'); + throw Exception('decoder status is ${decoderProcess.exitCode}'); } - final s = decoder_process.stdout as String; + final s = decoderProcess.stdout as String; return s.split(' ').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); }); }); diff --git a/lib/main.dart b/lib/main.dart index a31beec..d9416dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -111,7 +111,7 @@ class _MyHomePageState extends State { FutureBuilder( future: ad, builder: (context, snap) { - final style = Theme.of(context).textTheme.headline4; + final style = Theme.of(context).textTheme.headlineMedium; if (snap.error != null) { // An error has been encountered, so give an appropriate response and // pass the error details to an unobstructive tooltip. From 1c3e2f88f27b4cde60d5b514f92563ebe168d91c Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 09:09:19 +0100 Subject: [PATCH 29/81] Clean and use book title as ad title if only one book --- lib/ad_editing.dart | 14 +++--- native/src/api.rs | 116 +------------------------------------------- pubspec.yaml | 31 ------------ 3 files changed, 9 insertions(+), 152 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index f1e250e..902f022 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -32,16 +32,18 @@ class _AdEditingWidgetState extends State { @override void initState() { super.initState(); - final bookTitles = widget.step.metadata.entries.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); - final blurbs = widget.step.metadata.entries - .map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!) - .join('\n'); + final metadataFromIsbn = widget.step.metadata.entries; + final bookTitles = metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); + final blurbs = + metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!).join('\n'); var description = bookTitles + '\n\nRésumé:\n' + blurbs + '\n\n' + personal_info.customMessage; - final keywords = widget.step.metadata.entries.map((entry) => entry.value.keywords).join(', '); + final keywords = metadataFromIsbn.map((entry) => entry.value.keywords).join(', '); if (keywords.isNotEmpty) { description += '\n\nMots-clés:\n' + keywords; } - ad = Ad(title: '', description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); + + final title = metadataFromIsbn.length == 1 ? metadataFromIsbn.first.value.title : ''; + ad = Ad(title: title, description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); } @override diff --git a/native/src/api.rs b/native/src/api.rs index 6945840..eaf4c6c 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,11 +1,7 @@ -use crate::babelio::Babelio; use crate::common::Provider; use crate::common::{Ad, BookMetaData}; -use crate::google_books::GoogleBooks; use crate::publisher::Publisher; -use crate::{babelio, common, google_books, leboncoin}; -use itertools::Itertools; -use std::process::Command; +use crate::{babelio, google_books, leboncoin}; pub enum ProviderEnum { Babelio, @@ -21,117 +17,7 @@ pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Optio } } -/* -pub fn get_metadata_from_images(imgs_path: Vec) -> Ad { - let isbns: Vec = imgs_path - .clone() - .into_iter() - .map(|picture_path| { - println!("{picture_path}"); - let output = Command::new( - "/home/julien/Perso/LeBonCoin/chain_automatisation/book_metadata_finder/detect_barcode", - ) - .arg("-in=".to_string() + &picture_path) - .output() - .expect("failed to execute process"); - if !output.status.success() { - println!("stdout is {:?}", std::str::from_utf8(&output.stdout).unwrap()); - println!("stderr is {:?}", std::str::from_utf8(&output.stderr).unwrap()); - panic!("output.status is {}", output.status) - } - let output = std::str::from_utf8(&output.stdout).unwrap(); - println!("output is {:?}", output); - output - .split_ascii_whitespace() - .map(|x| x.to_string()) - .collect_vec() - }) - .flatten() - .unique() - .collect(); - - println!("isbns {:?}", isbns); - - let book_metadata_providers: Vec> = vec![ - Box::new(babelio::Babelio {}), - Box::new(google_books::GoogleBooks {}), - ]; - - let books: Vec> = isbns - .iter() - .map(|isbn| { - for provider in &book_metadata_providers { - let res = provider.get_book_metadata_from_isbn(&isbn); - if let Some(r) = res { - return Some(r); - } - } - None - }) - .collect(); - let books_titles = books.iter().map(book_format_title_and_author).join("\n"); - let blurbs = books - .iter().filter(|b| b.blurb.is_some()) - .map(|b| { - format!( - "{}:\n{}\n", - book_format_title_and_author(b), - b.blurb.as_ref().unwrap() - ) - }) - .join("\n"); - let keywords = books.iter().flat_map(|b| &b.keywords).unique().join(", "); - - let custom_message = leboncoin::personal_info::CUSTOM_MESSAGE; - - let mut ad_description = books_titles; - if !blurbs.is_empty() { - ad_description += &("\n\nRésumé:\n".to_owned() + &blurbs); - } - ad_description += &("\n\n".to_owned() + &custom_message); - if !keywords.is_empty() { - ad_description = ad_description + "\n\nMots-clés:\n" + &keywords; - } - - println!("ad_description: {:#?}", ad_description); - println!("ad_description: {}", ad_description); - - common::Ad { - title: if books.len() == 1 { - books.first().unwrap().title.clone() - } else { - "".to_string() - }, - description: ad_description, - price_cent: 1000, - imgs_path, - } -}*/ - pub fn publish_ad(ad: Ad) -> () { let lbc_publisher = leboncoin::Leboncoin {}; Publisher::publish(&lbc_publisher, ad); } -/* -fn book_format_title_and_author(book: &BookMetaData) -> String { - format!( - "\"{}\" {}", - book.title, - vec_fmt( - book.authors - .iter() - .map(|a| format!("{} {}", a.first_name, a.last_name)) - .collect_vec() - ) - ) -} - -fn vec_fmt(vec: Vec) -> String { - match vec.len() { - 0 => "".to_string(), - 1 => format!("de {}", vec[0]), - 2 => format!("de {} et {}", vec[0], vec[1]), - _ => panic!("More than 2 authors"), - } -} -*/ diff --git a/pubspec.yaml b/pubspec.yaml index 10b8a9e..66d5678 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,20 +1,8 @@ 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: @@ -29,11 +17,6 @@ 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 ffi: ^2.0.1 flutter_rust_bridge: ^1.45.0 meta: ^1.8.0 @@ -44,24 +27,10 @@ dependencies: 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.7 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# 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: From 557e97ef513e972f856c7b03afdd2b054db39c1d Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 09:13:30 +0100 Subject: [PATCH 30/81] Clean --- lib/common.dart | 8 +++++ lib/main.dart | 82 +++++-------------------------------------------- 2 files changed, 15 insertions(+), 75 deletions(-) diff --git a/lib/common.dart b/lib/common.dart index 89d7f29..a1dd726 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -51,3 +51,11 @@ class AsyncSnapshotWidget extends StatelessWidget { extension AuthorsExt on List { String toText() => map((a) => '${a.firstName} ${a.lastName}').join('\n'); } + +extension IntExt on int { + int divide(int other) => this ~/ other; +} + +extension DoubleExt on double { + double multiply(double other) => this * other; +} diff --git a/lib/main.dart b/lib/main.dart index d9416dc..4c4dfe8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,13 +10,6 @@ void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - sealed class BookyStep {} class ImageSelectionStep implements BookyStep {} @@ -39,6 +32,13 @@ class AdEditingStep implements BookyStep { AdEditingStep({required this.imgsPaths, required this.metadata}); } +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + class _MyAppState extends State { BookyStep step = //ImageSelectionStep(); MetadataCollectingStep(imgsPaths: [ @@ -74,71 +74,3 @@ class _MyAppState extends State { }); } } - -class MyHomePage extends StatefulWidget { - const MyHomePage(this.imgsPaths); - final List imgsPaths; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - late Future ad; - @override - void initState() { - super.initState(); - // ad = api.getMetadataFromImages(imgsPath: widget.imgsPaths); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Create an automatic online book ad from picture'), - ), - body: Center( - child: Column( - children: [ - // 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( - future: ad, - builder: (context, snap) { - final style = Theme.of(context).textTheme.headlineMedium; - 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('Error during image decoding', style: style), - ); - } - - final ad = snap.data; - if (ad == null) return const Text('Extracting info from images'); - - return const Text('extract finish'); - }, - ) - ], - ), - ), - ); - } -} - -extension IntExt on int { - int divide(int other) => this ~/ other; -} - -extension DoubleExt on double { - double multiply(double other) => this * other; -} From e8d6ea16c53adc6321ee4c5712390fc940cc1db5 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 21:23:01 +0100 Subject: [PATCH 31/81] Better description if one book --- lib/ad_editing.dart | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 902f022..7bb6c2e 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -33,19 +33,30 @@ class _AdEditingWidgetState extends State { void initState() { super.initState(); final metadataFromIsbn = widget.step.metadata.entries; - final bookTitles = metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); - final blurbs = - metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!).join('\n'); - var description = bookTitles + '\n\nRésumé:\n' + blurbs + '\n\n' + personal_info.customMessage; - final keywords = metadataFromIsbn.map((entry) => entry.value.keywords).join(', '); + + final title = metadataFromIsbn.length == 1 ? metadataFromIsbn.first.value.title : ''; + var description = _getDescription(metadataFromIsbn); + + final keywords = metadataFromIsbn.map((entry) => entry.value.keywords).expand((kw) => kw).toSet().join(', '); if (keywords.isNotEmpty) { description += '\n\nMots-clés:\n' + keywords; } - final title = metadataFromIsbn.length == 1 ? metadataFromIsbn.first.value.title : ''; ad = Ad(title: title, description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); } + String _getDescription(Iterable> metadataFromIsbn) { + final blurbs = + metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!).join('\n'); + if (metadataFromIsbn.length == 1) { + return 'Résumé:\n' + metadataFromIsbn.single.value.blurb!; + } else { + final bookTitles = metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); + final description = bookTitles + '\n\nRésumés:\n' + blurbs + '\n\n' + personal_info.customMessage; + return description; + } + } + @override Widget build(BuildContext context) { return Scaffold( From b73299197d76d72b3c02e6ae52a2662f76503c96 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 21:52:33 +0100 Subject: [PATCH 32/81] publish_ad return bool --- lib/ad_editing.dart | 8 ++++++-- lib/bridge_definitions.dart | 2 +- lib/bridge_generated.dart | 12 ++++++------ native/src/api.rs | 4 ++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 7bb6c2e..0b6e652 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -109,9 +109,13 @@ class _AdEditingWidgetState extends State { ElevatedButton( onPressed: (ad.title.length < 2 || ad.description.length < 15 || ad.priceCent == null) ? null - : () { + : () async { print('Try to publish...'); - api.publishAd(ad: ad); + final res = await api.publishAd(ad: ad); + + if (!context.mounted) return; + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(res ? 'Success' : 'Failure'))); }, child: const Text('Publish')) ], diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index 6287545..a80a140 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -13,7 +13,7 @@ abstract class Native { FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; - Future publishAd({required Ad ad, dynamic hint}); + Future publishAd({required Ad ad, dynamic hint}); FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; } diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 2e6a4cb..9d28f06 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -44,11 +44,11 @@ class NativeImpl implements Native { argNames: ["provider", "isbn"], ); - Future publishAd({required Ad ad, dynamic hint}) { + Future publishAd({required Ad ad, dynamic hint}) { var arg0 = _platform.api2wire_box_autoadd_ad(ad); return _platform.executeNormal(FlutterRustBridgeTask( callFfi: (port_) => _platform.inner.wire_publish_ad(port_, arg0), - parseSuccessData: _wire2api_unit, + parseSuccessData: _wire2api_bool, constMeta: kPublishAdConstMeta, argValues: [ad], hint: hint, @@ -96,6 +96,10 @@ class NativeImpl implements Native { ); } + bool _wire2api_bool(dynamic raw) { + return raw as bool; + } + BookMetaData _wire2api_box_autoadd_book_meta_data(dynamic raw) { return _wire2api_book_meta_data(raw); } @@ -119,10 +123,6 @@ class NativeImpl implements Native { Uint8List _wire2api_uint_8_list(dynamic raw) { return raw as Uint8List; } - - void _wire2api_unit(dynamic raw) { - return; - } } // Section: api2wire diff --git a/native/src/api.rs b/native/src/api.rs index eaf4c6c..76a7c68 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -17,7 +17,7 @@ pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Optio } } -pub fn publish_ad(ad: Ad) -> () { +pub fn publish_ad(ad: Ad) -> bool { let lbc_publisher = leboncoin::Leboncoin {}; - Publisher::publish(&lbc_publisher, ad); + Publisher::publish(&lbc_publisher, ad) } From 8e351c5611f62f83d66dad5cc330def98cfb25cd Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 17 Mar 2023 22:44:45 +0100 Subject: [PATCH 33/81] Better handle BookMD = null, add begining of test when desc is better is GBook first request than in the second one --- lib/main.dart | 6 +- lib/metadata_collecting.dart | 33 ++++++---- native/src/google_books/parser.rs | 35 +++++++++- .../test/9782266162777/isbn_response.html | 64 +++++++++++++++++++ .../9782266162777/self_link_response.html | 64 +++++++++++++++++++ .../{ => 9782744170812}/isbn_response.html | 0 .../self_link_response.html | 0 7 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 native/src/google_books/test/9782266162777/isbn_response.html create mode 100644 native/src/google_books/test/9782266162777/self_link_response.html rename native/src/google_books/test/{ => 9782744170812}/isbn_response.html (100%) rename native/src/google_books/test/{ => 9782744170812}/self_link_response.html (100%) diff --git a/lib/main.dart b/lib/main.dart index 4c4dfe8..ffa9ced 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,8 +40,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = //ImageSelectionStep(); - MetadataCollectingStep(imgsPaths: [ + BookyStep step = ImageSelectionStep(); + /* 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', @@ -49,7 +49,7 @@ class _MyAppState extends State { ], isbns: { '9782253029854', '9782277223634', - }); + });*/ @override Widget build(BuildContext context) { return MaterialApp( diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 7c4af51..f46487c 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -4,6 +4,8 @@ import 'package:flutter_rust_bridge_template/common.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'main.dart'; +const noneText = Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + class MetadataCollectingWidget extends StatefulWidget { const MetadataCollectingWidget({required this.step, required this.onSubmit}); final MetadataCollectingStep step; @@ -14,7 +16,7 @@ class MetadataCollectingWidget extends StatefulWidget { } class Metadatas { - final Map> mdFromProviders; + final Map> mdFromProviders; BookMetaData manual; Metadatas({required this.mdFromProviders, required this.manual}); } @@ -31,9 +33,13 @@ class _MetadataCollectingWidgetState extends State { () => Metadatas( manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { - final md = api.getMetadataFromProvider(provider: provider, isbn: isbn).then((value) => value!); + final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); //.then((value) => value!); if (provider == ProviderEnum.Babelio) { - md.then((value) => metadata[isbn]!.manual = value); + md.then((value) { + if (value != null) { + metadata[isbn]!.manual = value; + } + }); } return MapEntry(provider, md); })))); @@ -70,21 +76,22 @@ class _MetadataCollectingWidgetState extends State { FutureWidget( future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( - initialValue: data.title, + initialValue: data?.title, 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) => SelectableText(data.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( - initialValue: data.authors.toText(), + initialValue: data?.authors.toText(), onChanged: (newText) => setState(() => manual.authors = newText .split('\n') .map((line) => Author(firstName: '', lastName: line)) @@ -98,9 +105,9 @@ class _MetadataCollectingWidgetState extends State { ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( future: e.value, builder: (data) { - final authors = data.authors; - if (authors.isEmpty) { - return const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + final authors = data?.authors; + if (authors == null || authors.isEmpty) { + return noneText; } return SelectableText(authors.toText()); })), @@ -109,7 +116,7 @@ class _MetadataCollectingWidgetState extends State { FutureWidget( future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( - initialValue: data.blurb, + initialValue: data?.blurb, onChanged: (newText) => setState(() => manual.blurb = newText), decoration: const InputDecoration( icon: Icon(Icons.description), @@ -119,9 +126,9 @@ class _MetadataCollectingWidgetState extends State { ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( future: e.value, builder: (data) { - final blurb = data.blurb; + final blurb = data?.blurb; if (blurb == null) { - return const Text('None', style: TextStyle(fontStyle: FontStyle.italic)); + return noneText; } return SelectableText(blurb); })), diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index ea041b8..2de3c74 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -34,7 +34,9 @@ mod tests { #[test] fn extract_self_link_from_file() { - let html = std::fs::read_to_string("src/google_books/test/isbn_response.html").unwrap(); + 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, @@ -45,7 +47,8 @@ mod tests { #[test] fn extract_metadata_from_file() { let html = - std::fs::read_to_string("src/google_books/test/self_link_response.html").unwrap(); + 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, BookMetaData{ title: "La cité de Dieu".to_string(), @@ -54,6 +57,34 @@ mod tests { ..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, BookMetaData{ + title: "L'essence du Tao".to_string(), + authors:vec![common::Author{first_name: "".to_string(), last_name: "Pamela J. Ball".to_string()}], + blurb: None, + // 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. + //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() + }); + } } mod structs { 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/isbn_response.html b/native/src/google_books/test/9782744170812/isbn_response.html similarity index 100% rename from native/src/google_books/test/isbn_response.html rename to native/src/google_books/test/9782744170812/isbn_response.html diff --git a/native/src/google_books/test/self_link_response.html b/native/src/google_books/test/9782744170812/self_link_response.html similarity index 100% rename from native/src/google_books/test/self_link_response.html rename to native/src/google_books/test/9782744170812/self_link_response.html From cbe554d0014b950ea584bce0f89cdf178e6cc698 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 19 Mar 2023 17:59:35 +0100 Subject: [PATCH 34/81] Convert title to Option, Add GB.extract_metadata_from_isbn_response and add test --- lib/metadata_collecting.dart | 2 +- native/src/babelio.rs | 4 +- native/src/babelio/parser.rs | 4 +- native/src/common.rs | 4 +- native/src/google_books.rs | 73 ++++++++++++++++++++++++++++++- native/src/google_books/parser.rs | 53 +++++++++++++++++----- 6 files changed, 121 insertions(+), 19 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index f46487c..54e711e 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -33,7 +33,7 @@ class _MetadataCollectingWidgetState extends State { () => Metadatas( manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { - final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); //.then((value) => value!); + final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); if (provider == ProviderEnum.Babelio) { md.then((value) { if (value != null) { diff --git a/native/src/babelio.rs b/native/src/babelio.rs index 3e20eb1..8c800e6 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -39,7 +39,7 @@ mod tests { let isbn = "9782266071529"; let md = Babelio {}.get_book_metadata_from_isbn(isbn); assert_eq!(md, Some(BookMetaData { - title: "Le nom de la bête".to_string(), + 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\n".to_string()), keywords: @@ -75,7 +75,7 @@ mod tests { let isbn = "9782070541898"; let md = Babelio {}.get_book_metadata_from_isbn(isbn); assert_eq!(md, Some(BookMetaData { - title: "À la croisée des mondes, tome 2 : La tour des anges".to_string(), + 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. diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index d366eac..ee1d6f5 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -142,7 +142,7 @@ pub fn extract_title_author_keywords(html: &str) -> Option { }) .collect(); Some(BookMetaData { - title, + title: Some(title), authors, keywords, ..Default::default() @@ -170,7 +170,7 @@ mod tests { assert_eq!( title_author_keywords, Some(BookMetaData { - title: "Le nom de la bête".to_string(), + 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() diff --git a/native/src/common.rs b/native/src/common.rs index 4723920..8019ee9 100644 --- a/native/src/common.rs +++ b/native/src/common.rs @@ -1,6 +1,6 @@ #[derive(Default, Debug, PartialEq)] pub struct BookMetaData { - pub title: String, + 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. @@ -8,7 +8,7 @@ pub struct BookMetaData { pub keywords: Vec, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Author { pub first_name: String, pub last_name: String, diff --git a/native/src/google_books.rs b/native/src/google_books.rs index fddea62..a5e5fdb 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -1,17 +1,88 @@ use crate::common; mod parser; mod request; +use itertools::Itertools; pub struct GoogleBooks; +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::BookMetaData, bmd2: common::BookMetaData) -> common::BookMetaData { + common::BookMetaData { + 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), + } +} + 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 client = reqwest::blocking::Client::builder().build().unwrap(); let isbn_search_response = request::search_by_isbn(&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(&client, &self_link); - Some(parser::extract_metadata_from_self_link_response(&book_page)) + + 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::common::BookMetaData; + use crate::common::Provider; + + use super::*; + + #[test] + fn get_book_metadata_from_isbn_9782266162777() { + let g = GoogleBooks{}; + let md = g.get_book_metadata_from_isbn("9782266162777"); + assert_eq!(md, Some(BookMetaData { 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![] })) } } diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index 2de3c74..8458ea1 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -1,17 +1,45 @@ use itertools::Itertools; -use crate::common; +use crate::common::{self, BookMetaData}; 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::BookMetaData { + 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()); + BookMetaData { + authors, + blurb, + ..Default::default() + } + }); + a.unwrap_or(BookMetaData { + ..Default::default() + }) +} pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { let s: structs::Item = serde_json::from_str(html).unwrap(); let first_book = &s.volume_info; common::BookMetaData { - title: first_book.title.to_string(), + title: Some(first_book.title.to_string()), authors: first_book .authors .iter() @@ -51,7 +79,7 @@ mod tests { .unwrap(); let metadata = extract_metadata_from_self_link_response(&html); assert_eq!(metadata, BookMetaData{ - title: "La cité de Dieu".to_string(), + 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() @@ -76,14 +104,17 @@ mod tests { 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, BookMetaData{ - title: "L'essence du Tao".to_string(), - authors:vec![common::Author{first_name: "".to_string(), last_name: "Pamela J. Ball".to_string()}], - blurb: None, - // 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. - //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() - }); + assert_eq!( + metadata, + BookMetaData { + 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() + } + ); } } From 26421ad571a4a773c040cfb6833bad24d9b450bb Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 19 Mar 2023 19:48:40 +0100 Subject: [PATCH 35/81] Rust: GB: remove all field not useful --- native/src/google_books/parser.rs | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index 8458ea1..3e8ee75 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -132,14 +132,14 @@ mod structs { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Item<'a> { - pub kind: &'a str, + // pub kind: &'a str, pub id: &'a str, - pub etag: &'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>, + // pub sale_info: SaleInfo<'a>, + // pub access_info: AccessInfo<'a>, + // pub search_info: Option>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -148,27 +148,27 @@ mod structs { pub title: &'a str, pub subtitle: Option<&'a str>, pub authors: Vec<&'a str>, - pub publisher: Option<&'a str>, - pub published_date: &'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)] + // 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")] - pub struct IndustryIdentifier<'a> { + struct IndustryIdentifier<'a> { #[serde(rename = "type")] pub type_field: &'a str, pub identifier: &'a str, @@ -234,5 +234,5 @@ mod structs { #[serde(rename_all = "camelCase")] pub struct SearchInfo<'a> { pub text_snippet: &'a str, - } + }*/ } From 3b6e135055c5b9dd76d3fc289751250a4c81986b Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 22 Mar 2023 22:06:19 +0100 Subject: [PATCH 36/81] Babelio handle authors with only last name --- native/src/babelio/parser.rs | 115 +- ...253143321_le_livre_tibetain_des_morts.html | 1807 +++++++++++++++++ 2 files changed, 1889 insertions(+), 33 deletions(-) create mode 100644 native/src/babelio/test/9782253143321_le_livre_tibetain_des_morts.html diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index ee1d6f5..0a94177 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -1,5 +1,5 @@ use crate::common::{html_select, BookMetaData}; -use itertools::{Itertools}; +use itertools::Itertools; #[derive(PartialEq, Debug)] pub enum BlurbRes { @@ -60,6 +60,42 @@ pub fn extract_blurb(html: &str) -> Option { } } +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); @@ -91,38 +127,7 @@ pub fn extract_title_author_keywords(html: &str) -> Option { html_select("[itemprop=\"author\"][itemscope][itemtype=\"https://schema.org/Person\"]"); let r = book_scope.select(&binding); - let authors = r - .map(|author_scope| { - 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 first_name = author_span - .first_child() - .expect("author scope > a > span shoud have a first child which is first name") - .value() - .as_text() - .expect("should be a text") - .trim() - .to_string(); - let last_name = author_span - .children() - .nth(1) - .expect("author scope > a > span shoud have a second child which is the last name") - .first_child() - .unwrap() - .value() - .as_text() - .expect("should be a text") - .trim() - .to_string(); - crate::common::Author { - first_name, - last_name, - } - }) - .collect_vec(); + let authors = r.map(extract_author).collect_vec(); let keywords_scope = book_scope .select(&html_select("[class=\"tags\"]")) @@ -203,4 +208,48 @@ mod tests { }) ); } + + #[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(BookMetaData { + 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(), + }) + ); + } } 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 From 22fa29402ab5a1c274f4412578f183336f2630f9 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 23 Mar 2023 00:54:09 +0100 Subject: [PATCH 37/81] All test pass without internet. GB with mock folder, Babelio with cache --- native/Cargo.toml | 3 +- ..._slash_v1_slash_volumes_slash_HY_FNwAACAAJ | 64 +++++++++++++++++++ .../google_books/search_by_isbn_9782266162777 | 64 +++++++++++++++++++ native/src/api.rs | 8 ++- native/src/babelio/request.rs | 22 +++---- native/src/cached_client.rs | 43 +++++++++++-- native/src/common.rs | 4 ++ native/src/google_books.rs | 17 +++-- native/src/google_books/request.rs | 26 ++++---- 9 files changed, 213 insertions(+), 38 deletions(-) create mode 100644 native/mock/google_books/get_volume_https:_slash__slash_www.googleapis.com_slash_books_slash_v1_slash_volumes_slash_HY_FNwAACAAJ create mode 100644 native/mock/google_books/search_by_isbn_9782266162777 diff --git a/native/Cargo.toml b/native/Cargo.toml index a013ab0..29de5cb 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -18,4 +18,5 @@ regex = "1.7.1" reqwest = { version = "0.11.14", features = ["blocking", "json", "multipart"] } scraper = "0.14.0" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.91" \ No newline at end of file +serde_json = "1.0.91" +mockito = "1.0.0" 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 76a7c68..1440673 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,3 +1,4 @@ +use crate::cached_client::CachedClient; use crate::common::Provider; use crate::common::{Ad, BookMetaData}; use crate::publisher::Publisher; @@ -11,9 +12,12 @@ pub enum ProviderEnum { 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 {}.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), } } diff --git a/native/src/babelio/request.rs b/native/src/babelio/request.rs index 7895a40..62d7e28 100644 --- a/native/src/babelio/request.rs +++ b/native/src/babelio/request.rs @@ -1,4 +1,4 @@ -use crate::cached_client::CachedClient; +use crate::cached_client::{CachedClient, Client}; use itertools::Itertools; #[derive(serde::Serialize, serde::Deserialize, Debug)] @@ -17,10 +17,10 @@ struct BabelioISBNResponse { url: String, } -pub fn get_book_url(client: &CachedClient, isbn: &str) -> Option { - let raw_search_results = client.get_from_cache( +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| { http_client .post("https://www.babelio.com/aj_recherche.php") .body(format!("{{\"isMobile\":false,\"term\":\"{}\"}}", isbn)) @@ -36,13 +36,9 @@ pub fn get_book_url(client: &CachedClient, isbn: &str) -> Option { } pub fn get_book_page(client: &CachedClient, url: String) -> String { - client.get_from_cache( - format!( - "babelio/get_book_page_{}.html", - url.replace("/", "_slash_") - ) - .as_str(), - |http_client| { + 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() @@ -53,9 +49,9 @@ pub fn get_book_page(client: &CachedClient, url: String) -> String { } pub fn get_book_blurb_see_more(client: &CachedClient, id_obj: &str) -> String { - client.get_from_cache( + client.make_request( format!("babelio/get_book_blurb_see_more_{}.html", id_obj).as_str(), - |http_client| { + &|http_client| { let params = std::collections::HashMap::from([("type", "1"), ("id_obj", id_obj)]); let voir_plus_resp = http_client diff --git a/native/src/cached_client.rs b/native/src/cached_client.rs index 366cb18..41a5b92 100644 --- a/native/src/cached_client.rs +++ b/native/src/cached_client.rs @@ -1,23 +1,54 @@ +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 CachedClient { - pub fn get_from_cache String>( +impl Client for CachedClient { + fn make_request( &self, cache_file_path: &str, - make_request: F, + _make_request: &dyn Fn(&reqwest::blocking::Client) -> String, ) -> String { - let cache_file_path = format!("{}/{}", crate::config::CACHE_PATH , cache_file_path); + 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 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 index 8019ee9..a8bedf5 100644 --- a/native/src/common.rs +++ b/native/src/common.rs @@ -28,3 +28,7 @@ pub struct Ad { 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 index a5e5fdb..67fd6cd 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -1,9 +1,12 @@ use crate::common; mod parser; mod request; +use crate::cached_client::Client; use itertools::Itertools; -pub struct GoogleBooks; +pub struct GoogleBooks { + pub client: Box, +} fn merge(first: Option, other: Option, resolver: F) -> Option where @@ -53,14 +56,13 @@ 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 client = reqwest::blocking::Client::builder().build().unwrap(); - let isbn_search_response = request::search_by_isbn(&client, isbn); + 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(&client, &self_link); + 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); @@ -74,6 +76,7 @@ impl common::Provider for GoogleBooks { #[cfg(test)] mod tests { + use crate::cached_client::MockClient; use crate::common::BookMetaData; use crate::common::Provider; @@ -81,7 +84,11 @@ mod tests { #[test] fn get_book_metadata_from_isbn_9782266162777() { - let g = GoogleBooks{}; + let g = GoogleBooks { + client: Box::new(MockClient { + dir: "mock/google_books", + }), + }; let md = g.get_book_metadata_from_isbn("9782266162777"); assert_eq!(md, Some(BookMetaData { 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![] })) } diff --git a/native/src/google_books/request.rs b/native/src/google_books/request.rs index f0df992..ae29895 100644 --- a/native/src/google_books/request.rs +++ b/native/src/google_books/request.rs @@ -1,13 +1,17 @@ -pub fn search_by_isbn(client: &reqwest::blocking::Client, isbn: &str) -> String { - let resp = client - .get(format!( - "https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn}" - )) - .send() - .unwrap(); - resp.text().unwrap() +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: &reqwest::blocking::Client, url: &str) -> String { - let resp = client.get(url).send().unwrap(); - resp.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()) } From 5750c5fbcb85461ae7b5180553097292f7fe3138 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 23 Mar 2023 22:05:35 +0100 Subject: [PATCH 38/81] Move credential to dart --- lib/ad_editing.dart | 32 ++++++++++++++--- lib/bridge_definitions.dart | 16 +++++++-- lib/bridge_generated.dart | 59 ++++++++++++++++++++++++++----- lib/metadata_collecting.dart | 3 +- native/src/api.rs | 6 ++-- native/src/bridge_generated.io.rs | 50 ++++++++++++++++++++++++-- native/src/bridge_generated.rs | 11 ++++-- native/src/common.rs | 5 +++ native/src/leboncoin.rs | 11 +++--- native/src/leboncoin/request.rs | 54 ++++++---------------------- native/src/publisher.rs | 4 +-- 11 files changed, 175 insertions(+), 76 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 0b6e652..d93c739 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -28,15 +28,18 @@ String _bookFormatTitleAndAuthor(BookMetaData book) { class _AdEditingWidgetState extends State { late Ad ad; + var credential = LbcCredential(lbcToken: '', datadomeCookie: ''); @override void initState() { super.initState(); final metadataFromIsbn = widget.step.metadata.entries; - final title = metadataFromIsbn.length == 1 ? metadataFromIsbn.first.value.title : ''; + final title = metadataFromIsbn.length == 1 ? (metadataFromIsbn.first.value.title ?? '') : ''; 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; @@ -46,13 +49,14 @@ class _AdEditingWidgetState extends State { } String _getDescription(Iterable> metadataFromIsbn) { - final blurbs = - metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!).join('\n'); if (metadataFromIsbn.length == 1) { return 'Résumé:\n' + metadataFromIsbn.single.value.blurb!; } else { final bookTitles = metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); - final description = bookTitles + '\n\nRésumés:\n' + blurbs + '\n\n' + personal_info.customMessage; + final blurbs = metadataFromIsbn + .map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!) + .join('\n'); + final description = bookTitles + '\n\nRésumés:\n' + blurbs; return description; } } @@ -106,12 +110,30 @@ class _AdEditingWidgetState extends State { ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), ]), ), + TextFormField( + initialValue: '', + onChanged: (newText) => setState(() => credential.lbcToken = newText), + decoration: const InputDecoration( + icon: Icon(Icons.key), + labelText: 'LBC Bearer token', + ), + style: const TextStyle(fontSize: 20), + ), + TextFormField( + initialValue: '', + onChanged: (newText) => setState(() => credential.datadomeCookie = newText), + decoration: const InputDecoration( + icon: Icon(Icons.cookie), + labelText: 'datadome cookie', + ), + style: const TextStyle(fontSize: 20), + ), ElevatedButton( onPressed: (ad.title.length < 2 || ad.description.length < 15 || ad.priceCent == null) ? null : () async { print('Try to publish...'); - final res = await api.publishAd(ad: ad); + final res = await api.publishAd(ad: ad, credential: credential); if (!context.mounted) return; ScaffoldMessenger.of(context) diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index a80a140..50af226 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -13,7 +13,7 @@ abstract class Native { FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; - Future publishAd({required Ad ad, dynamic hint}); + Future publishAd({required Ad ad, required LbcCredential credential, dynamic hint}); FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; } @@ -43,19 +43,29 @@ class Author { } class BookMetaData { - String title; + String? title; List authors; String? blurb; List keywords; BookMetaData({ - required this.title, + this.title, required this.authors, this.blurb, required this.keywords, }); } +class LbcCredential { + String lbcToken; + String datadomeCookie; + + LbcCredential({ + required this.lbcToken, + required this.datadomeCookie, + }); +} + enum ProviderEnum { Babelio, GoogleBooks, diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index 9d28f06..c209bb1 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -44,13 +44,15 @@ class NativeImpl implements Native { argNames: ["provider", "isbn"], ); - Future publishAd({required Ad ad, 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_publish_ad(port_, arg0), + callFfi: (port_) => _platform.inner.wire_publish_ad(port_, arg0, arg1), parseSuccessData: _wire2api_bool, constMeta: kPublishAdConstMeta, - argValues: [ad], + argValues: [ad, credential], hint: hint, )); } @@ -58,7 +60,7 @@ class NativeImpl implements Native { FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta => const FlutterRustBridgeTaskConstMeta( debugName: "publish_ad", - argNames: ["ad"], + argNames: ["ad", "credential"], ); void dispose() { @@ -89,7 +91,7 @@ class NativeImpl implements Native { if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); return BookMetaData( - title: _wire2api_String(arr[0]), + title: _wire2api_opt_String(arr[0]), authors: _wire2api_list_author(arr[1]), blurb: _wire2api_opt_String(arr[2]), keywords: _wire2api_StringList(arr[3]), @@ -170,6 +172,14 @@ class NativePlatform extends FlutterRustBridgeBase { 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); @@ -191,6 +201,17 @@ class NativePlatform extends FlutterRustBridgeBase { 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 @@ -312,19 +333,22 @@ class NativeWire implements FlutterRustBridgeWireBase { 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)>>('wire_publish_ad'); - late final _wire_publish_ad = _wire_publish_adPtr - .asFunction)>(); + 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, @@ -350,6 +374,17 @@ class NativeWire implements FlutterRustBridgeWireBase { 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, ) { @@ -407,6 +442,12 @@ class wire_Ad extends ffi.Struct { 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/metadata_collecting.dart b/lib/metadata_collecting.dart index 54e711e..9452a69 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -85,7 +85,7 @@ class _MetadataCollectingWidgetState extends State { )), ...metadata[isbn]!.mdFromProviders.entries.map((e) => FutureWidget( future: e.value, - builder: (data) => data == null ? noneText : SelectableText(data.title))), + builder: (data) => data == null ? noneText : SelectableText(data.title ?? ''))), ]), TableRow(children: [ FutureWidget( @@ -118,6 +118,7 @@ class _MetadataCollectingWidgetState extends State { builder: (data) => TextFormField( initialValue: data?.blurb, onChanged: (newText) => setState(() => manual.blurb = newText), + maxLines: null, decoration: const InputDecoration( icon: Icon(Icons.description), labelText: 'Book blurb', diff --git a/native/src/api.rs b/native/src/api.rs index 1440673..c692fec 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,5 +1,5 @@ use crate::cached_client::CachedClient; -use crate::common::Provider; +use crate::common::{Provider, LbcCredential}; use crate::common::{Ad, BookMetaData}; use crate::publisher::Publisher; use crate::{babelio, google_books, leboncoin}; @@ -21,7 +21,7 @@ pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Optio } } -pub fn publish_ad(ad: Ad) -> bool { +pub fn publish_ad(ad: Ad, credential: LbcCredential) -> bool { let lbc_publisher = leboncoin::Leboncoin {}; - Publisher::publish(&lbc_publisher, ad) + Publisher::publish(&lbc_publisher, ad, credential) } diff --git a/native/src/bridge_generated.io.rs b/native/src/bridge_generated.io.rs index e24e918..628e8de 100644 --- a/native/src/bridge_generated.io.rs +++ b/native/src/bridge_generated.io.rs @@ -11,8 +11,12 @@ pub extern "C" fn wire_get_metadata_from_provider( } #[no_mangle] -pub extern "C" fn wire_publish_ad(port_: i64, ad: *mut wire_Ad) { - wire_publish_ad_impl(port_, ad) +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 @@ -31,6 +35,11 @@ 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 { @@ -75,6 +84,21 @@ impl Wire2Api for *mut wire_Ad { 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 { @@ -102,6 +126,13 @@ pub struct wire_Ad { 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 { @@ -138,6 +169,21 @@ impl Default for wire_Ad { } } +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 cec2d9f..f99f0c4 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use crate::common::Ad; use crate::common::Author; use crate::common::BookMetaData; +use crate::common::LbcCredential; // Section: wire functions @@ -43,7 +44,11 @@ fn wire_get_metadata_from_provider_impl( }, ) } -fn wire_publish_ad_impl(port_: MessagePort, ad: impl Wire2Api + UnwindSafe) { +fn wire_publish_ad_impl( + port_: MessagePort, + ad: impl Wire2Api + UnwindSafe, + credential: impl Wire2Api + UnwindSafe, +) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { debug_name: "publish_ad", @@ -52,7 +57,8 @@ fn wire_publish_ad_impl(port_: MessagePort, ad: impl Wire2Api + UnwindSafe) }, move || { let api_ad = ad.wire2api(); - move |task_callback| Ok(publish_ad(api_ad)) + let api_credential = credential.wire2api(); + move |task_callback| Ok(publish_ad(api_ad, api_credential)) }, ) } @@ -84,6 +90,7 @@ impl Wire2Api for i32 { self } } + impl Wire2Api for i32 { fn wire2api(self) -> ProviderEnum { match self { diff --git a/native/src/common.rs b/native/src/common.rs index a8bedf5..7b1a52a 100644 --- a/native/src/common.rs +++ b/native/src/common.rs @@ -22,6 +22,11 @@ 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, diff --git a/native/src/leboncoin.rs b/native/src/leboncoin.rs index 8235aa8..826aa13 100644 --- a/native/src/leboncoin.rs +++ b/native/src/leboncoin.rs @@ -10,9 +10,10 @@ mod request; use itertools::Itertools; use std::path::Path; + impl Publisher for Leboncoin { - fn publish(&self, ad: crate::common::Ad) -> bool { - crate::jwt_decoder::check_jwt_expiration(personal_info::LBC_TOKEN); + 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() @@ -22,7 +23,7 @@ impl Publisher for Leboncoin { 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); + 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, @@ -32,9 +33,9 @@ impl Publisher for Leboncoin { .collect_vec(); // let img_lbc_refs = vec![]; - let send_answer: String = request::send(ad, img_lbc_refs); + 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).unwrap(); + 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 { diff --git a/native/src/leboncoin/request.rs b/native/src/leboncoin/request.rs index d49ad99..1e7682e 100644 --- a/native/src/leboncoin/request.rs +++ b/native/src/leboncoin/request.rs @@ -3,11 +3,11 @@ use std::path::Path; use reqwest; use serde::{Deserialize, Serialize}; -use crate::leboncoin::personal_info; +use crate::{common::LbcCredential, leboncoin::personal_info}; use super::Image; -pub fn send(ad: crate::common::Ad, images: Vec) -> String { +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()); @@ -17,10 +17,7 @@ pub fn send(ad: crate::common::Ad, images: Vec) -> String { ); headers.insert( "authorization", - ["Bearer ", personal_info::LBC_TOKEN] - .concat() - .parse() - .unwrap(), + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), ); headers.insert("cache-control", "no-cache".parse().unwrap()); headers.insert("content-type", "application/json".parse().unwrap()); @@ -44,7 +41,7 @@ pub fn send(ad: crate::common::Ad, images: Vec) -> String { headers.insert("sec-fetch-mode", "cors".parse().unwrap()); headers.insert( reqwest::header::COOKIE, - personal_info::DATA_DOME_COOKIE.parse().unwrap(), + 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()); @@ -186,7 +183,7 @@ pub struct Location { pub zipcode: String, } -pub fn submit(ad_id: i64) -> Result> { +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()); @@ -196,10 +193,7 @@ pub fn submit(ad_id: i64) -> Result> { ); headers.insert( "authorization", - ["Bearer ", personal_info::LBC_TOKEN] - .concat() - .parse() - .unwrap(), + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), ); headers.insert("cache-control", "no-cache".parse().unwrap()); headers.insert("content-type", "application/json".parse().unwrap()); @@ -223,7 +217,7 @@ pub fn submit(ad_id: i64) -> Result> { headers.insert("sec-fetch-mode", "cors".parse().unwrap()); headers.insert( reqwest::header::COOKIE, - personal_info::DATA_DOME_COOKIE.parse().unwrap(), + 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()); @@ -278,14 +272,11 @@ pub struct SubmitAd { pub transaction_type: String, } -pub fn upload_file(img_path: &Path) -> String { +pub fn upload_file(img_path: &Path, credential: &LbcCredential) -> String { let mut headers = reqwest::header::HeaderMap::new(); headers.insert( "authorization", - ["Bearer ", personal_info::LBC_TOKEN] - .concat() - .parse() - .unwrap(), + ["Bearer ", &credential.lbc_token].concat().parse().unwrap(), ); headers.insert( "sec-ch-ua", @@ -299,7 +290,7 @@ pub fn upload_file(img_path: &Path) -> String { headers.insert("sec-fetch-mode", "cors".parse().unwrap()); headers.insert( reqwest::header::COOKIE, - personal_info::DATA_DOME_COOKIE.parse().unwrap(), + 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()); @@ -320,28 +311,3 @@ pub fn upload_file(img_path: &Path) -> String { println!("upload_file response = {}", res); res } - -//curl 'https://api.leboncoin.fr/api/pintad/v1/public/upload/image' -X POST -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H 'Accept: */*' -H 'Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3' -H 'Accept-Encoding: gzip, deflate, br' -H 'Referer: https://www.leboncoin.fr/annonce/2305203826/editer' -H 'api_key: ba0c2dad52b3ec' -H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxNTYxMiwiaWF0IjoxNjc3NjA4NDExLCJpZCI6IjliYzg5OWM1LTMxN2UtNDE1Ny1iMzEyLTAyMWQ1ZTQ3YTlkYSIsImluc3RhbGxfaWQiOiI3MDQ1YjhmMy0xMzYyLTRiN2UtYjhmZC1lY2Y0OWU4ODRjOGQiLCJqdGkiOiIyY2FhMzU5OS1jZDk3LTQxYTEtYmIzMC1hNmI2YjlmMDA1MzciLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6ImVjMDZjZTM0LThhMzItNDkyNC05NDc1LTc4MzU4MmY3ZGI3YiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiIyNGUyNDA2Mi0xNmE2LTQ4NWQtYjg0Yi1iNWY4MGViNjkzYTUiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.GRqf3gDdFiq1ukFhN_2i8HhWycirauVUM7rDoZZHsSgD-wv5VOwuKDWc6axDoPK3Wsbg_oFXfrSHX-bcDkE2SRaOqNB734eqD-fbceCG1ntf-afgeLWf-MPnas0n_ylOB2ZSK1LAG2aCXZSDm3ZEkXs_-KZhwQtsmqLgIte0PJUUk_qP4tYYDqLe3FvUeGIkrPAFHKxfnAXmKXf-kh9RvbykGiek9lqFT-Hg95X21eS3Z8HH2li-OMP4B2I-PQysOLuaAZ47wkjkt8PKgC6qG3rlitr28MRbkBYrsuo5ic9JMEKTlmbYa5WsyzZJL5F5Y3CdKTXxiQ4ae2kY2hRLgubk2Dihy8vdqLhitX-Fm_sGQSFnP7vy7iHhQCK5m4jLnD-p-sD_DAehNkGYF8lQqG44myb7XdmTtY9uoR_1Tv2LXYSncKQzEpCn-G6Pf0DJg4xb02CCxXWqB7oysooBgFzgPGdixJeBnFSX_8H0zbmoszUUW7Wqw_aSKv7aAQ3p2Foha3U7B4B-3lHPetec6wEo1eLvq5XXRbDAZuvIqbG6cvQHS5HDNkiBQIHfED3VwVOnextu0BADL7hYl4bOM50yNquNIoecPbEOC0Tij3JdYdGjHJ_ywDhsGwD08awZLIpPsJ1ppcaxMv3thoMsiInKqX5wLHNmTec13Lyn978' -H 'Content-Type: multipart/form-data; boundary=---------------------------153785532732722146451504606153' -H 'Origin: https://www.leboncoin.fr' -H 'Connection: keep-alive' -H 'Cookie: datadome=Ysu9fziXa098YNozRd6iS4oq62CcYXd0SZUP9cw4Rl5NMfVROqQrM7eT96i618vhu3M4l~JZWv80IUWEBCzBJdRnqF2Z-W6M0oyZXXOyLvLbDiGsWPQcoQw_UU7qXS-; __Secure-Install=7045b8f3-1362-4b7e-b8fd-ecf49e884c8d; __Secure-InstanceId=7045b8f3-1362-4b7e-b8fd-ecf49e884c8d; utag_main=v_id:0186993fcaf6000e3c437fda305405046001900900bd0$_sn:1$_ss:0$_pn:5%3Bexp-session$_st:1677610707192$ses_id:1677608340214%3Bexp-session; didomi_token=eyJ1c2VyX2lkIjoiMTg2OTkzZmMtYjVmNi02ZTFiLWI2YWQtZDFmMjhmOTJiODk4IiwiY3JlYXRlZCI6IjIwMjMtMDItMjhUMTg6MTk6MDIuMzQyWiIsInVwZGF0ZWQiOiIyMDIzLTAyLTI4VDE4OjE5OjAyLjM0MloiLCJ2ZW5kb3JzIjp7ImVuYWJsZWQiOlsiZ29vZ2xlIiwiYzpsYmNmcmFuY2UiLCJjOnJldmxpZnRlci1jUnBNbnA1eCIsImM6ZGlkb21pIl19LCJwdXJwb3NlcyI6eyJlbmFibGVkIjpbImV4cGVyaWVuY2V1dGlsaXNhdGV1ciIsIm1lc3VyZWF1ZGllbmNlIiwicGVyc29ubmFsaXNhdGlvbm1hcmtldGluZyIsInByaXgiXX0sInZlbmRvcnNfbGkiOnsiZW5hYmxlZCI6WyJnb29nbGUiXX0sInZlcnNpb24iOjIsImFjIjoiRExXQkFBRUlBSXdBV1FCLWdHRkFQeUFra0JKWUVBd0lrZ1NrQXR5QnhBRHB3SFZnUU1BaW9CSE9DU2NFdFlLREFVSWdvdEJYT0N3VUZ0NExqQVhMQXdHQmhFREUwR1dvLkRMV0JBQUVJQUl3QVdRQi1nR0ZBUHlBa2tCSllFQXdJa2dTa0F0eUJ4QURwd0hWZ1FNQWlvQkhPQ1NjRXRZS0RBVUlnb3RCWE9Dd1VGdDRMakFYTEF3R0JoRURFMEdXbyJ9; euconsent-v2=CPn5KgAPn5KgAAHABBENC5CgAPLAAH7AAAAAIsNB_G_dTyPi-f59YvtwYQ1P4VQnoyACjgaNgwwJiRLBMI0EhmAIKAHqAAACIBAkICJAAQBlCAHAAAAA4IEAASMMAAAAIRAIIgCAAEAAAmJICABZCxAAAQAQgkwAABQAgAICABMgSDAAAAAAFAAAAAgAAAAAAAAAAAAAQAAAAAAAAgAAAAAAAAAAAAAEEAQATDVuIAGxLHAmkDCIAACMIAgCgBABRQBCwQAEBIgAEEYACjAAAAAFAAAAAAAAEAMAAAAAgAQgAAAAcEAgAIAEAAAAEAgEAAAAACAAADAAAAAAAMAAAAAAgAIAAAKAQAABAAgAJAgACAAAAgAAAAAAAAAgEAAAAAAAAAAAAAAAAQAxQAGAAIJQjAAMAAQShIAAYAAglCAA.flgAD9gAAAAA; include_in_experiment=true; _hjSessionUser_2783207=eyJpZCI6IjBlOTAwNGIzLWVmNmUtNWM5ZC1iNzQ4LTU4NjFlMWVmMmUyMSIsImNyZWF0ZWQiOjE2Nzc2MDgzNDM2NzksImV4aXN0aW5nIjp0cnVlfQ==; _hjFirstSeen=1; _hjIncludedInSessionSample_2783207=1; _hjSession_2783207=eyJpZCI6ImE1MjQxMjgxLWY5YzQtNDU5YS05NzNkLWFiODc4YjM4MzU2MyIsImNyZWF0ZWQiOjE2Nzc2MDgzNDM2ODEsImluU2FtcGxlIjp0cnVlfQ==; _hjAbsoluteSessionInProgress=0; ry_ry-l3b0nco_realytics=eyJpZCI6InJ5XzI5RjFCQTE5LUQ3ODYtNEUxQy05NUNDLTYwMjE4QUVEOUI3NyIsImNpZCI6bnVsbCwiZXhwIjoxNzA5MTQ0MzQ0Nzc3LCJjcyI6bnVsbH0%3D; ry_ry-l3b0nco_so_realytics=eyJpZCI6InJ5XzI5RjFCQTE5LUQ3ODYtNEUxQy05NUNDLTYwMjE4QUVEOUI3NyIsImNpZCI6bnVsbCwib3JpZ2luIjp0cnVlLCJyZWYiOm51bGwsImNvbnQiOm51bGwsIm5zIjpmYWxzZX0%3D; _gcl_au=1.1.701707994.1677608345; cto_bundle=CNZ-SV9JaWtsRlAyVjVqODhGWWhVYldZN3BzOFl6eG5hJTJCMTZibnQ2R2RNakRCS3N5WDdWWFVleHpzRCUyQmNtQUQwUmt0bW5CdldxZUFLem02b1clMkZTMDdTdXQ3emtxYUtISDJqUzZEazclMkZZMFlFTzQzS2dRaU5NZnVMc3JtR2NMTFBBcGNBZ2hINGczcm44TFpQSSUyRmF1YiUyQnJ3OXclM0QlM0Q; __gads=ID=f3fe3612fb4470d8:T=1677608347:S=ALNI_MbMwkuo3rfJtYKI9L6tyyLC8sIwbg; __gpi=UID=00000be0047e7d68:T=1677608347:RT=1677608347:S=ALNI_Mav2FKNNTDv7opgvCIg2Kld9HMpbw; luat=eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxNTYxMiwiaWF0IjoxNjc3NjA4NDExLCJpZCI6IjliYzg5OWM1LTMxN2UtNDE1Ny1iMzEyLTAyMWQ1ZTQ3YTlkYSIsImluc3RhbGxfaWQiOiI3MDQ1YjhmMy0xMzYyLTRiN2UtYjhmZC1lY2Y0OWU4ODRjOGQiLCJqdGkiOiIyY2FhMzU5OS1jZDk3LTQxYTEtYmIzMC1hNmI2YjlmMDA1MzciLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6ImVjMDZjZTM0LThhMzItNDkyNC05NDc1LTc4MzU4MmY3ZGI3YiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiIyNGUyNDA2Mi0xNmE2LTQ4NWQtYjg0Yi1iNWY4MGViNjkzYTUiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.GRqf3gDdFiq1ukFhN_2i8HhWycirauVUM7rDoZZHsSgD-wv5VOwuKDWc6axDoPK3Wsbg_oFXfrSHX-bcDkE2SRaOqNB734eqD-fbceCG1ntf-afgeLWf-MPnas0n_ylOB2ZSK1LAG2aCXZSDm3ZEkXs_-KZhwQtsmqLgIte0PJUUk_qP4tYYDqLe3FvUeGIkrPAFHKxfnAXmKXf-kh9RvbykGiek9lqFT-Hg95X21eS3Z8HH2li-OMP4B2I-PQysOLuaAZ47wkjkt8PKgC6qG3rlitr28MRbkBYrsuo5ic9JMEKTlmbYa5WsyzZJL5F5Y3CdKTXxiQ4ae2kY2hRLgubk2Dihy8vdqLhitX-Fm_sGQSFnP7vy7iHhQCK5m4jLnD-p-sD_DAehNkGYF8lQqG44myb7XdmTtY9uoR_1Tv2LXYSncKQzEpCn-G6Pf0DJg4xb02CCxXWqB7oysooBgFzgPGdixJeBnFSX_8H0zbmoszUUW7Wqw_aSKv7aAQ3p2Foha3U7B4B-3lHPetec6wEo1eLvq5XXRbDAZuvIqbG6cvQHS5HDNkiBQIHfED3VwVOnextu0BADL7hYl4bOM50yNquNIoecPbEOC0Tij3JdYdGjHJ_ywDhsGwD08awZLIpPsJ1ppcaxMv3thoMsiInKqX5wLHNmTec13Lyn978; _schn=_27faqp; _scid=d6571009-7472-41bb-9cb9-8510b51a6a95; _fbp=fb.1.1677608908547.2034999500' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: same-site' -H 'TE: trailers' --data-binary $'-----------------------------153785532732722146451504606153\r\nContent-Disposition: form-data; name="file"; filename="20230228_192237.jpg"\r\nContent-Type: image/jpeg\r\n\r\n-----------------------------153785532732722146451504606153--\r\n' - -// curl 'https://api.leboncoin.fr/api/pintad/v1/public/upload/image' \ -// -H 'authority: api.leboncoin.fr' \ - -// -H 'accept: */*' \ -/* --H 'accept-language: en-US,en;q=0.9,fr;q=0.8' \ --H 'api_key: ba0c2dad52b3ec' \ --H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxMTY1NywiaWF0IjoxNjc3NjA0NDU2LCJpZCI6IjQ1MTM1OTUxLTY0ZjQtNDFjZS05NGVjLWJkMzkzZDRjY2U2ZiIsImluc3RhbGxfaWQiOiIwNTA1NzA2YS05NDJhLTQzNjktYTdlYy02MGYxZDYxYWZiNjUiLCJqdGkiOiJlYmQzZDI2My1mMTMzLTQ3MjktYjVjOS1kNTA3ZmYwZjUxNDEiLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6IjcwN2M3M2ZiLTk2NzQtNDZlNC05N2NmLTRkZTk2MzQ3NTYwZiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiI3NGM1ZGY4Yy05MzQ3LTQ0MWQtYWViYi0zZWIzYjYyZTk1MjMiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.dZMGwYAei7ovgsB6REy1XjtTjqbgVj4oXQomz7teIj-z0KEajW1pyLyBp4EweGpyb1McgSu8BOS74da9GEeNZ60x9pOAn9KiS8VNfqMBYiwknURnwI8NJdf9KiB6k__SzXb05uTyDeazLK76MoUIAImT8LwzMrFvdZewmmkqyYqT4o4Bcn0tynDkRLv5dSZ87n4ca0AsOsHt6zgWipwpqsGBom0ysYnOzq-hCkyM1-3SsjR4ohVT--qqR2EIijO2-SGk90kmwDzR9aYwCZzzRAlUTFhpE6-zHO7TquAV9oIQAU2Wmq5HgzhEREjUhJOI0fqXy9xk1dPRzb1A__rDbAm8Nkfxq-mF1JcaRM-nB2Pb1VgDV8j6P2MtPC8TlKyr9dMQFzuTWpvFa8sMYg92f3i1oLwvbgsHu5nweMqrWItDAwja7v35T3IReejBwKGOXXEmsTlJEcq589b2AdtZwH82mcFfwn6QkTPbJVGv7YiSLbyGNCQLbUJ-FhLptq2fLZwJUTEye72u-WzY5yeCxs8ZaIfaQHTduJrVviMlEfam9rnUUU-cUdA7NJx8bg63FqOYhEH-hFHYeo5gSF5EqA97jBwC2KoApADf4t1q5EhUPw7gGR9U7qQuhoRiTLVFV4kEjIWeX1QRntOHkVXfuoWTaTE-X1A6XsBvkcINJho' \ --H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryZ9zBRAquVv1qb78o' \ --H 'cookie: s=red1xa04ffeea4ed8b07c235277adc932a3e4d092859d; log_from=http%3A%2F%2Fwww2.leboncoin.fr%2Fdc%2Frules%3Fca%3D12_s; xtvrn=$266818$; ry_ry-l3b0nco_realytics=eyJpZCI6InJ5Xzg1M0VCQzVGLURCNjgtNDk1NC1COEE0LTM1OTY0RjhDN0U1RSIsImNpZCI6bnVsbCwiZXhwIjoxNjg0MzI0MDMzMDQ1LCJjcyI6bnVsbH0%3D; _pin_unauth=dWlkPU1HVXhPREF4TnpZdE1qTXhZUzAwTXpBd0xXSmpZVFF0TXpoallqRTNNamczTldVeg; _hjSessionUser_2783207=eyJpZCI6ImZjZTk5MmM1LTY0NmItNTViOS05OWY4LWRkYzc2YmE3ODc1YyIsImNyZWF0ZWQiOjE2NTI3ODgwMzQ1MjcsImV4aXN0aW5nIjp0cnVlfQ==; _scid=e7942114-373a-4d77-b41c-1717c1b77529; __Secure-Install=0505706a-942a-4369-a7ec-60f1d61afb65; __Secure-InstanceId=0505706a-942a-4369-a7ec-60f1d61afb65; didomi_token=eyJ1c2VyX2lkIjoiMTcyNmY1NWMtMjgwYS02Njc3LWFmMDAtZjk2YzM1MDM1NDQ3IiwiY3JlYXRlZCI6IjIwMjItMTItMDFUMTk6NTA6MDAuNzMwWiIsInVwZGF0ZWQiOiIyMDIyLTEyLTAxVDE5OjUwOjAwLjczMFoiLCJ2ZW5kb3JzIjp7ImVuYWJsZWQiOlsiZ29vZ2xlIiwiYzpsYmNmcmFuY2UiLCJjOnJldmxpZnRlci1jUnBNbnA1eCIsImM6ZGlkb21pIl19LCJwdXJwb3NlcyI6eyJlbmFibGVkIjpbInBlcnNvbm5hbGlzYXRpb25tYXJrZXRpbmciLCJwcml4IiwibWVzdXJlYXVkaWVuY2UiLCJleHBlcmllbmNldXRpbGlzYXRldXIiXX0sInZlbmRvcnNfbGkiOnsiZW5hYmxlZCI6WyJnb29nbGUiXX0sInZlcnNpb24iOjIsImFjIjoiRExXQkFBRUlBSXdBV1FCLWdHRkFQeUFra0JKWUVBd0lrZ1NrQXR5QnhBRHB3SFZnUU1BaW9CSE9DU2NFdFlLREFVSWdvdEJYT0N3VUZ0NExqQVhMQXdHQmhFREUwR1dvLkRMV0EtQUVJQUl3QV9RRENnSDVBU1NBa3NDQVlFU1FKU0FXNUE0Z0IwNERxd0lHQVJVQWpuQkpPQ1dzRkJnS0VRVVdncm5CWUtDMjhGeGdMbGdZREF3aUJpYURMVUFBQSJ9; euconsent-v2=CPjT1EAPjT1EAAHABBENCsCgAPLAAHLAAAAAIAtB_G_dTyPi-f59YvtwYQ1P4VQnoyACjgaNgwwJiRLBMI0EhmAIKAHqAAACIBAkICJAAQBlCAHAAAAA4IEAASMMAAAAIRAIIgCAAEAAAiJICABZCxAAAQAQgkwAABQAgAICABMgSDAAAAAAFAAAAAgAAAAAAAAAAAAAQAAAAAAAAggCACYatxAA2JY4E0gYRAAARhAEAUAIAKKAIWCAAgJEAAgjAAUYAAAAAoAAAAAAAAgBgAAAAEACEAAAADggEABAAgAAAAgEAgAAAAAQAAAYAAAAAABgAAAAAEABAAABQCAAAIAEABIEAAQAAAEAAAAAAAAAEAgAAAAAAAAAAAAAAACAGKAAwABBJYYABgACCSxAADAAEElg.flgADlgAAAAA; include_in_experiment=true; _gcl_au=1.1.396577628.1672180056; _fbp=fb.1.1674497618789.664585017; __gads=ID=3756ef2df471787f:T=1674498902:S=ALNI_MZYJfxMcpbJdw6KbbvraZ_khGHZnA; __gpi=UID=00000bc9e3cb6789:T=1674498902:RT=1674566983:S=ALNI_MbBn6iqQ8F-8Zpmb1WCKLM9NSFmxg; __gsas=ID=8ce0a4b086087df5:T=1674567077:S=ALNI_Mby9K0wXjz1NzkAZsVzbLFcccNtNw; adview_clickmeter=search__listing__4__8c78a2b2-4b20-497b-ae05-cf1e963b741d; luat=eyJhbGciOiJSUzI1NiIsImtpZCI6IjgyYjFjNmYwLWRiM2EtNTQ2Ny1hYmI2LTJlMzAxNDViZjc3MiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJsYmMtZnJvbnQtd2ViIiwiZGVwcmVjYXRlZF9zdG9yZV9pZCI6NTU3OTE3NDQsImV4cCI6MTY3NzYxMTY1NywiaWF0IjoxNjc3NjA0NDU2LCJpZCI6IjQ1MTM1OTUxLTY0ZjQtNDFjZS05NGVjLWJkMzkzZDRjY2U2ZiIsImluc3RhbGxfaWQiOiIwNTA1NzA2YS05NDJhLTQzNjktYTdlYy02MGYxZDYxYWZiNjUiLCJqdGkiOiJlYmQzZDI2My1mMTMzLTQ3MjktYjVjOS1kNTA3ZmYwZjUxNDEiLCJyZWZ1c2VkX3Njb3BlcyI6bnVsbCwicmVxdWVzdF9pZCI6IjcwN2M3M2ZiLTk2NzQtNDZlNC05N2NmLTRkZTk2MzQ3NTYwZiIsInNjb3BlcyI6WyJsYmMucHJpdmF0ZSIsImxiY2dycC5hdXRoLnR3b2ZhY3Rvci5zbXMubWUuYWN0aXZhdGUiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRpc3BsYXkiLCJvZmZsaW5lIiwibGJjLmF1dGguZW1haWwucGFydC5jaGFuZ2UiLCJsYmMuZXNjcm93YWNjb3VudC5tYWludGVuYW5jZS5yZWFkIiwibGJjZ3JwLmF1dGgudHdvZmFjdG9yLm1lLioiLCJsYmNncnAuYXV0aC5zZXNzaW9uLm1lLmRlbGV0ZSIsImxiY2dycC5hdXRoLnNlc3Npb24ubWUucmVhZCIsImxiYy4qLm1lLioiLCJsYmMuKi4qLm1lLioiLCJiZXRhLmxiYy5hdXRoLnR3b2ZhY3Rvci5tZS4qIl0sInNlc3Npb25faWQiOiI3NGM1ZGY4Yy05MzQ3LTQ0MWQtYWViYi0zZWIzYjYyZTk1MjMiLCJzdWIiOiJsYmM7MTgzMGE2NGEtZjJjYy00Y2Q4LTg3ZjAtYzVkYjdmOTU2N2Q4OzU1NzkxNzQ0In0.dZMGwYAei7ovgsB6REy1XjtTjqbgVj4oXQomz7teIj-z0KEajW1pyLyBp4EweGpyb1McgSu8BOS74da9GEeNZ60x9pOAn9KiS8VNfqMBYiwknURnwI8NJdf9KiB6k__SzXb05uTyDeazLK76MoUIAImT8LwzMrFvdZewmmkqyYqT4o4Bcn0tynDkRLv5dSZ87n4ca0AsOsHt6zgWipwpqsGBom0ysYnOzq-hCkyM1-3SsjR4ohVT--qqR2EIijO2-SGk90kmwDzR9aYwCZzzRAlUTFhpE6-zHO7TquAV9oIQAU2Wmq5HgzhEREjUhJOI0fqXy9xk1dPRzb1A__rDbAm8Nkfxq-mF1JcaRM-nB2Pb1VgDV8j6P2MtPC8TlKyr9dMQFzuTWpvFa8sMYg92f3i1oLwvbgsHu5nweMqrWItDAwja7v35T3IReejBwKGOXXEmsTlJEcq589b2AdtZwH82mcFfwn6QkTPbJVGv7YiSLbyGNCQLbUJ-FhLptq2fLZwJUTEye72u-WzY5yeCxs8ZaIfaQHTduJrVviMlEfam9rnUUU-cUdA7NJx8bg63FqOYhEH-hFHYeo5gSF5EqA97jBwC2KoApADf4t1q5EhUPw7gGR9U7qQuhoRiTLVFV4kEjIWeX1QRntOHkVXfuoWTaTE-X1A6XsBvkcINJho; _hjSession_2783207=eyJpZCI6IjlmOWFiNjllLTk0MTctNDIwOS1iMTU0LWQwODM1OGU2MDVlZSIsImNyZWF0ZWQiOjE2Nzc2MDQ0NTc4NTgsImluU2FtcGxlIjp0cnVlfQ==; _hjAbsoluteSessionInProgress=0; ry_ry-l3b0nco_so_realytics=eyJpZCI6InJ5Xzg1M0VCQzVGLURCNjgtNDk1NC1COEE0LTM1OTY0RjhDN0U1RSIsImNpZCI6bnVsbCwib3JpZ2luIjpmYWxzZSwicmVmIjpudWxsLCJjb250IjpudWxsLCJucyI6ZmFsc2V9; _hjIncludedInSessionSample_2783207=1; cto_bundle=XggTMV9lYzVPa0lSRm5hMGZrMzB1ciUyRkhyamtZUmRmUHFhYkVVVDlMRWZMSUxRQW1PMGdOYyUyQkNwOE5QbFp3Y0U5NmV1aWtyZjg3b2xMZ09tOFVTdWFMenBPR2Y1anNhb0tteFZqUng5NDVBVmdzakRjdVh1cyUyRkdiYWxGVnZXTyUyRkl4dk9Q; datadome=4cwibCI1RYexaCD1wjZqJZ-6hUi16_fPyqPGEtsMSG-r3~8EoWMghXY6ZUZ3L~1GpA3vzRzDw__LqSlDp~FlYdAu_jP3M0N9vV8ZWBSbzQ0~ijJp6tNET4wW0fjfuKGn; utag_main=v_id:01792c6c39a9001726449a99708002069002106100bd0$_sn:149$_ss:0$_st:1677607622614$_pn:3%3Bexp-session$ses_id:1677604455561%3Bexp-session' \ --H 'origin: https://www.leboncoin.fr' \ --H 'referer: https://www.leboncoin.fr/deposer-une-annonce' \ --H 'sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"' \ --H 'sec-ch-ua-mobile: ?0' \ --H 'sec-ch-ua-platform: "Linux"' \ --H 'sec-fetch-dest: empty' \ --H 'sec-fetch-mode: cors' \ --H 'sec-fetch-site: same-site' \ --H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' \ ---data-raw $'------WebKitFormBoundaryZ9zBRAquVv1qb78o\r\nContent-Disposition: form-data; name="file"; filename="20230204_194811.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryZ9zBRAquVv1qb78o--\r\n' \ ---compressed -*/ diff --git a/native/src/publisher.rs b/native/src/publisher.rs index 329cbf3..6008400 100644 --- a/native/src/publisher.rs +++ b/native/src/publisher.rs @@ -1,5 +1,5 @@ -use crate::common::Ad; +use crate::common::{Ad, LbcCredential}; pub trait Publisher { - fn publish(&self, ad: Ad) -> bool; + fn publish(&self, ad: Ad, credential: LbcCredential) -> bool; } From d9cb49b422fae09d47b457f91d2ddb115e9ef603 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 23 Mar 2023 22:55:14 +0100 Subject: [PATCH 39/81] MetadataCollectingWidget: Add deep copy to fix bug where manual blurb is mirror to Babelio and stay to its old value when submiting --- lib/ad_editing.dart | 5 ++++- lib/common.dart | 5 +++++ lib/metadata_collecting.dart | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index d93c739..d2b0598 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -129,7 +129,10 @@ class _AdEditingWidgetState extends State { style: const TextStyle(fontSize: 20), ), ElevatedButton( - onPressed: (ad.title.length < 2 || ad.description.length < 15 || ad.priceCent == null) + onPressed: (ad.title.length < 2 || + ad.description.length < 15 || + ad.description.length > 4000 || + ad.priceCent == null) ? null : () async { print('Try to publish...'); diff --git a/lib/common.dart b/lib/common.dart index a1dd726..be0a498 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -59,3 +59,8 @@ extension IntExt on int { extension DoubleExt on double { double multiply(double other) => this * other; } + +extension BookMetadataExt on BookMetaData { + BookMetaData deepCopy() => + BookMetaData(title: '$title', authors: List.from(authors), blurb: '$blurb', keywords: List.from(keywords)); +} diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 9452a69..8eea2b9 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -37,7 +37,7 @@ class _MetadataCollectingWidgetState extends State { if (provider == ProviderEnum.Babelio) { md.then((value) { if (value != null) { - metadata[isbn]!.manual = value; + metadata[isbn]!.manual = value.deepCopy(); } }); } @@ -117,7 +117,7 @@ class _MetadataCollectingWidgetState extends State { future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( initialValue: data?.blurb, - onChanged: (newText) => setState(() => manual.blurb = newText), + onChanged: (newText) => setState(() => metadata[isbn]!.manual.blurb = newText), maxLines: null, decoration: const InputDecoration( icon: Icon(Icons.description), From 5f9cc9dbb75c64d5de31f08d997df139676ef293 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 24 Mar 2023 00:16:33 +0100 Subject: [PATCH 40/81] MetadataCollectingWidget: add controller and SelectableTextAndUse for blurb only --- lib/metadata_collecting.dart | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 8eea2b9..c64b718 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -6,11 +6,29 @@ import 'main.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 { - const MetadataCollectingWidget({required this.step, required this.onSubmit}); + MetadataCollectingWidget({required this.step, required this.onSubmit}); final MetadataCollectingStep step; final void Function(AdEditingStep newStep) onSubmit; + final blurbTextFieldController = TextEditingController(); + @override State createState() => _MetadataCollectingWidgetState(); } @@ -38,6 +56,7 @@ class _MetadataCollectingWidgetState extends State { md.then((value) { if (value != null) { metadata[isbn]!.manual = value.deepCopy(); + widget.blurbTextFieldController.text = metadata[isbn]!.manual.blurb ?? ''; } }); } @@ -116,7 +135,7 @@ class _MetadataCollectingWidgetState extends State { FutureWidget( future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( - initialValue: data?.blurb, + controller: widget.blurbTextFieldController, onChanged: (newText) => setState(() => metadata[isbn]!.manual.blurb = newText), maxLines: null, decoration: const InputDecoration( @@ -131,7 +150,13 @@ class _MetadataCollectingWidgetState extends State { if (blurb == null) { return noneText; } - return SelectableText(blurb); + return SelectableTextAndUse( + blurb, + onUse: (b) => setState(() { + widget.blurbTextFieldController.text = b; + metadata[isbn]!.manual.blurb = b; + }), + ); })), ]), ], From aaffa8745eb6277644e94e467b671e579f664920 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 24 Mar 2023 09:07:22 +0100 Subject: [PATCH 41/81] Replace title from provider if better --- lib/metadata_collecting.dart | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index c64b718..969689a 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -28,6 +28,7 @@ class MetadataCollectingWidget extends StatefulWidget { final void Function(AdEditingStep newStep) onSubmit; final blurbTextFieldController = TextEditingController(); + final titleTextFieldController = TextEditingController(); @override State createState() => _MetadataCollectingWidgetState(); @@ -42,6 +43,18 @@ class Metadatas { 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; + }); + } + @override void initState() { super.initState(); @@ -52,14 +65,13 @@ class _MetadataCollectingWidgetState extends State { manual: BookMetaData(title: '', authors: [], keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); - if (provider == ProviderEnum.Babelio) { - md.then((value) { - if (value != null) { - metadata[isbn]!.manual = value.deepCopy(); - widget.blurbTextFieldController.text = metadata[isbn]!.manual.blurb ?? ''; - } - }); - } + md.then((value) { + if (value != null) { + replaceIfBetterString(value.title, metadata[isbn]!.manual.title!, () { + _updateManualTitle(isbn, value.title!); + }); + } + }); return MapEntry(provider, md); })))); }); @@ -95,7 +107,7 @@ class _MetadataCollectingWidgetState extends State { FutureWidget( future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( - initialValue: data?.title, + controller: widget.titleTextFieldController, onChanged: (newText) => setState(() => manual.title = newText), decoration: const InputDecoration( icon: Icon(Icons.title), From 40c1130287f3982a809c88e708d25b0ffff64df4 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 24 Mar 2023 09:12:47 +0100 Subject: [PATCH 42/81] Replace blurb from provider if better --- lib/metadata_collecting.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 969689a..73cca4c 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -55,6 +55,13 @@ class _MetadataCollectingWidgetState extends State { }); } + void _updateManualBlurb(String isbn, String newBlurb) { + setState(() { + metadata[isbn]!.manual.blurb = newBlurb; + widget.blurbTextFieldController.text = newBlurb; + }); + } + @override void initState() { super.initState(); @@ -62,7 +69,7 @@ class _MetadataCollectingWidgetState extends State { metadata.putIfAbsent( isbn, () => Metadatas( - manual: BookMetaData(title: '', authors: [], keywords: []), + manual: BookMetaData(title: '', authors: [], blurb: '', keywords: []), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); md.then((value) { @@ -70,6 +77,9 @@ class _MetadataCollectingWidgetState extends State { replaceIfBetterString(value.title, metadata[isbn]!.manual.title!, () { _updateManualTitle(isbn, value.title!); }); + replaceIfBetterString(value.blurb, metadata[isbn]!.manual.blurb!, () { + _updateManualBlurb(isbn, value.blurb!); + }); } }); return MapEntry(provider, md); @@ -164,10 +174,7 @@ class _MetadataCollectingWidgetState extends State { } return SelectableTextAndUse( blurb, - onUse: (b) => setState(() { - widget.blurbTextFieldController.text = b; - metadata[isbn]!.manual.blurb = b; - }), + onUse: (b) => _updateManualBlurb(isbn, b), ); })), ]), From ed5096ae5c85345bf7ffbcf44f37136f23f055f7 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 24 Mar 2023 22:41:44 +0100 Subject: [PATCH 43/81] Store LBC credential in credential.json --- .gitignore | 1 + analysis_options.yaml | 6 +++++- lib/ad_editing.dart | 26 +++++++++++++++++++------- lib/credential.dart | 29 +++++++++++++++++++++++++++++ lib/credential.g.dart | 18 ++++++++++++++++++ pubspec.yaml | 3 +++ 6 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 lib/credential.dart create mode 100644 lib/credential.g.dart diff --git a/.gitignore b/.gitignore index 38d2398..c9e953d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ /build/ personal_info.* +credential.json # Web related diff --git a/analysis_options.yaml b/analysis_options.yaml index 8529660..456dfc6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,4 +12,8 @@ analyzer: enable-experiment: - records - patterns - - sealed-class \ No newline at end of file + - sealed-class + language: + strict-casts: true + strict-inference: true + strict-raw-types: true \ No newline at end of file diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index d2b0598..12b4821 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -3,6 +3,7 @@ import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; import 'common.dart'; +import 'credential.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; class AdEditingWidget extends StatefulWidget { @@ -28,7 +29,7 @@ String _bookFormatTitleAndAuthor(BookMetaData book) { class _AdEditingWidgetState extends State { late Ad ad; - var credential = LbcCredential(lbcToken: '', datadomeCookie: ''); + late Credential credential; @override void initState() { @@ -46,6 +47,9 @@ class _AdEditingWidgetState extends State { } ad = Ad(title: title, description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); + + credential = Credential.loadFromFile(); + print('credential ${credential.lbcToken} ${credential.dataDomeCookie}'); } String _getDescription(Iterable> metadataFromIsbn) { @@ -111,7 +115,7 @@ class _AdEditingWidgetState extends State { ]), ), TextFormField( - initialValue: '', + initialValue: credential.lbcToken, onChanged: (newText) => setState(() => credential.lbcToken = newText), decoration: const InputDecoration( icon: Icon(Icons.key), @@ -120,8 +124,8 @@ class _AdEditingWidgetState extends State { style: const TextStyle(fontSize: 20), ), TextFormField( - initialValue: '', - onChanged: (newText) => setState(() => credential.datadomeCookie = newText), + initialValue: credential.dataDomeCookie, + onChanged: (newText) => setState(() => credential.dataDomeCookie = newText), decoration: const InputDecoration( icon: Icon(Icons.cookie), labelText: 'datadome cookie', @@ -136,11 +140,19 @@ class _AdEditingWidgetState extends State { ? null : () async { print('Try to publish...'); - final res = await api.publishAd(ad: ad, credential: credential); + + final res = await api.publishAd( + ad: ad, + credential: LbcCredential( + lbcToken: credential.lbcToken, datadomeCookie: credential.dataDomeCookie)); if (!context.mounted) return; - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(res ? 'Success' : 'Failure'))); + if (res) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Success'))); + credential.saveToFile(); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Failure'))); + } }, child: const Text('Publish')) ], diff --git a/lib/credential.dart b/lib/credential.dart new file mode 100644 index 0000000..b7790ac --- /dev/null +++ b/lib/credential.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'credential.g.dart'; + +@JsonSerializable() +class Credential { + String lbcToken; + String dataDomeCookie; + + Credential({required this.lbcToken, required this.dataDomeCookie}); + + static final _file = File('credential.json'); + + void saveToFile() { + _file.writeAsStringSync(jsonEncode(toJson())); + } + + factory Credential.loadFromFile() { + final json = _file.readAsStringSync(); + return Credential.fromJson(jsonDecode(json) as Map); + } + + factory Credential.fromJson(Map json) => _$CredentialFromJson(json); + + Map toJson() => _$CredentialToJson(this); +} diff --git a/lib/credential.g.dart b/lib/credential.g.dart new file mode 100644 index 0000000..8a8e092 --- /dev/null +++ b/lib/credential.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'credential.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Credential _$CredentialFromJson(Map json) => Credential( + lbcToken: json['lbcToken'] as String, + dataDomeCookie: json['dataDomeCookie'] as String, + ); + +Map _$CredentialToJson(Credential instance) => + { + 'lbcToken': instance.lbcToken, + 'dataDomeCookie': instance.dataDomeCookie, + }; diff --git a/pubspec.yaml b/pubspec.yaml index 66d5678..1ccfdf5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,12 +23,15 @@ dependencies: super_drag_and_drop: ^0.2.3 collection: ^1.17.1 super_clipboard: ^0.2.3+1 + json_annotation: ^4.8.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 ffigen: ^7.2.7 + build_runner: ^2.4.1 + json_serializable: ^6.6.1 flutter: uses-material-design: true From 5a8583f87ac9d364674e7690b5ca7716c17e6b41 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 25 Mar 2023 15:11:04 +0100 Subject: [PATCH 44/81] Add jwt token validator --- lib/ad_editing.dart | 13 ++++++++++++- lib/main.dart | 8 ++++++++ pubspec.yaml | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 12b4821..4291583 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; +import 'package:jwt_decoder/jwt_decoder.dart'; import 'common.dart'; import 'credential.dart'; @@ -54,7 +55,9 @@ class _AdEditingWidgetState extends State { String _getDescription(Iterable> metadataFromIsbn) { if (metadataFromIsbn.length == 1) { - return 'Résumé:\n' + metadataFromIsbn.single.value.blurb!; + final blurb = metadataFromIsbn.single.value.blurb; + if (blurb == null) return ''; + return 'Résumé:\n' + blurb; } else { final bookTitles = metadataFromIsbn.map((entry) => _bookFormatTitleAndAuthor(entry.value)).join('\n'); final blurbs = metadataFromIsbn @@ -122,6 +125,14 @@ class _AdEditingWidgetState extends State { labelText: 'LBC Bearer token', ), style: const TextStyle(fontSize: 20), + autovalidateMode: AutovalidateMode.always, + validator: (token) { + final remainingDuration = JwtDecoder.getRemainingTime(token!); + if (remainingDuration.isNegative) { + return 'Token expired'; + } + return null; + }, ), TextFormField( initialValue: credential.dataDomeCookie, diff --git a/lib/main.dart b/lib/main.dart index ffa9ced..23bfee3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,6 +41,14 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { BookyStep step = ImageSelectionStep(); + /* AdEditingStep(imgsPaths: [ + '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg' + ], metadata: { + 'myisbn': BookMetaData( + title: 'Mock title', + authors: [Author(firstName: 'Mock firstname', lastName: 'mock lastname')], + keywords: ['mock kw']) + });*/ /* MetadataCollectingStep(imgsPaths: [ '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg', '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194746.jpg', diff --git a/pubspec.yaml b/pubspec.yaml index 1ccfdf5..dd525bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: collection: ^1.17.1 super_clipboard: ^0.2.3+1 json_annotation: ^4.8.0 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: From dca23c101cf2bca3b4124a8ed4ce463309fcf5c5 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 27 Mar 2023 22:37:36 +0200 Subject: [PATCH 45/81] Add bookprices with basic example of Selenium --- native/Cargo.toml | 6 +++- native/src/booksprice.rs | 2 ++ native/src/booksprice/request.rs | 52 ++++++++++++++++++++++++++++++++ native/src/lib.rs | 1 + 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 native/src/booksprice.rs create mode 100644 native/src/booksprice/request.rs diff --git a/native/Cargo.toml b/native/Cargo.toml index 29de5cb..082fd8f 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,7 +11,6 @@ crate-type = ["cdylib", "staticlib"] [dependencies] anyhow = "1" flutter_rust_bridge = "1" -#reqwest = "0.11.14" base64 = "0.21.0" itertools = "0.10.5" regex = "1.7.1" @@ -20,3 +19,8 @@ scraper = "0.14.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" \ No newline at end of file diff --git a/native/src/booksprice.rs b/native/src/booksprice.rs new file mode 100644 index 0000000..c6a30a3 --- /dev/null +++ b/native/src/booksprice.rs @@ -0,0 +1,2 @@ +mod request; + diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs new file mode 100644 index 0000000..a98e15e --- /dev/null +++ b/native/src/booksprice/request.rs @@ -0,0 +1,52 @@ +//! Requires chromedriver running on port 9515: +//! +//! chromedriver --port=9515 +//! +//! Run as follows: +//! +//! cargo run --example tokio_async + +use thirtyfour::prelude::*; +use tokio; + +#[tokio::main] +async fn selenium_fn() -> color_eyre::Result<()> { + // The use of color_eyre gives much nicer error reports, including making + // it much easier to locate where the error occurred. + color_eyre::install()?; + + let caps = DesiredCapabilities::chrome(); + let driver = WebDriver::new("http://localhost:9515", caps).await?; + // Navigate to https://wikipedia.org. + driver.goto("https://wikipedia.org").await?; + let elem_form = driver.find(By::Id("search-form")).await?; + + // Find element from element. + let elem_text = elem_form.find(By::Id("searchInput")).await?; + + // Type in the search terms. + elem_text.send_keys("selenium").await?; + + // Click the search button. + let elem_button = elem_form.find(By::Css("button[type='submit']")).await?; + elem_button.click().await?; + + // Look for header to implicitly wait for the page to load. + driver.find(By::ClassName("firstHeading")).await?; + assert_eq!(driver.title().await?, "Selenium - Wikipedia"); + + // Always explicitly close the browser. There are no async destructors. + driver.quit().await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_selenium() { + selenium_fn(); + } +} diff --git a/native/src/lib.rs b/native/src/lib.rs index c8c5bf4..73cb00d 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -9,3 +9,4 @@ mod image_tools; mod jwt_decoder; mod leboncoin; mod publisher; +mod booksprice; From 2c95d480f2881fd5e7d61bcf96aa75c696b1bb33 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 29 Mar 2023 22:37:35 +0200 Subject: [PATCH 46/81] WIP Selenium offline test work --- native/Cargo.toml | 5 +- native/src/booksprice.rs | 1 + native/src/booksprice/request.rs | 41 +- native/src/booksprice/selenium_common.rs | 209 +++++++++ native/src/lib.rs | 3 + native/tests/test_html/output_bookprice.html | 424 +++++++++++++++++++ native/tests/test_html/sample_page.html | 1 + 7 files changed, 675 insertions(+), 9 deletions(-) create mode 100644 native/src/booksprice/selenium_common.rs create mode 100644 native/tests/test_html/output_bookprice.html create mode 120000 native/tests/test_html/sample_page.html diff --git a/native/Cargo.toml b/native/Cargo.toml index 082fd8f..6a79611 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -22,5 +22,6 @@ 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" \ No newline at end of file +# [dev-dependencies] +color-eyre = "0.6.2" +hyper = { version = "0.14", features = ["server", "tcp"] } diff --git a/native/src/booksprice.rs b/native/src/booksprice.rs index c6a30a3..a5f8510 100644 --- a/native/src/booksprice.rs +++ b/native/src/booksprice.rs @@ -1,2 +1,3 @@ mod request; +mod selenium_common; diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index a98e15e..8c35315 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -1,24 +1,27 @@ //! Requires chromedriver running on port 9515: //! //! chromedriver --port=9515 -//! -//! Run as follows: -//! -//! cargo run --example tokio_async use thirtyfour::prelude::*; use tokio; +// mod selenium_common; + +// use crate::common; +use crate::booksprice::selenium_common::sample_page_url; + #[tokio::main] async fn selenium_fn() -> color_eyre::Result<()> { // The use of color_eyre gives much nicer error reports, including making // it much easier to locate where the error occurred. color_eyre::install()?; + // thirtyfour::resolve!(); + // crate::local_tester!(); + let caps = DesiredCapabilities::chrome(); let driver = WebDriver::new("http://localhost:9515", caps).await?; - // Navigate to https://wikipedia.org. - driver.goto("https://wikipedia.org").await?; + driver.goto("https://www.booksprice.com/comparePrice.do?l=y&searchType=compare&inputData=9782266071529").await?; let elem_form = driver.find(By::Id("search-form")).await?; // Find element from element. @@ -41,12 +44,36 @@ async fn selenium_fn() -> color_eyre::Result<()> { Ok(()) } +async fn parse_booksprices(c: WebDriver, port: u16) -> Result<(), WebDriverError> { + let url = sample_page_url(port); + + c.goto(&url).await?; + println!("{:#?}", c.source().await); + c.find(By::Css("#select1")).await?.click().await?; + + let active = c.active_element().await?; + assert_eq!(active.attr("id").await?, Some(String::from("select1"))); + + c.close_window().await +} + #[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::*; + // #[test] + /* fn test_selenium() { + selenium_fn(); + } */ + #[test] fn test_selenium() { - selenium_fn(); + local_tester!(parse_booksprices, "chrome"); } } diff --git a/native/src/booksprice/selenium_common.rs b/native/src/booksprice/selenium_common.rs new file mode 100644 index 0000000..5a85212 --- /dev/null +++ b/native/src/booksprice/selenium_common.rs @@ -0,0 +1,209 @@ +#![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 sample_page_url(port: u16) -> String { + format!("http://localhost:{}/sample_page.html", port) +} + +pub fn other_page_url(port: u16) -> String { + format!("http://localhost:{}/other_page.html", port) +} + +pub fn drag_to_url(port: u16) -> String { + format!("http://localhost:{}/drag_to.html", port) +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 73cb00d..691c70e 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,3 +1,6 @@ + +// #[macro_use] +// extern crate thirtyfour; mod api; mod babelio; mod bridge_generated; diff --git a/native/tests/test_html/output_bookprice.html b/native/tests/test_html/output_bookprice.html new file mode 100644 index 0000000..55071ca --- /dev/null +++ b/native/tests/test_html/output_bookprice.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/native/tests/test_html/sample_page.html b/native/tests/test_html/sample_page.html new file mode 120000 index 0000000..3c2a2e7 --- /dev/null +++ b/native/tests/test_html/sample_page.html @@ -0,0 +1 @@ +output_bookprice.html \ No newline at end of file From 398cac936b620071f1794a89c4dbf3fcfc296241 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 29 Mar 2023 23:32:03 +0200 Subject: [PATCH 47/81] WIP --- native/src/booksprice/request.rs | 32 +++++++++++------------- native/src/booksprice/selenium_common.rs | 14 +++-------- native/tests/test_html/sample_page.html | 1 - 3 files changed, 17 insertions(+), 30 deletions(-) delete mode 120000 native/tests/test_html/sample_page.html diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index 8c35315..f508d8f 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -8,7 +8,6 @@ use tokio; // mod selenium_common; // use crate::common; -use crate::booksprice::selenium_common::sample_page_url; #[tokio::main] async fn selenium_fn() -> color_eyre::Result<()> { @@ -44,33 +43,30 @@ async fn selenium_fn() -> color_eyre::Result<()> { Ok(()) } -async fn parse_booksprices(c: WebDriver, port: u16) -> Result<(), WebDriverError> { - let url = sample_page_url(port); - - c.goto(&url).await?; - println!("{:#?}", c.source().await); - c.find(By::Css("#select1")).await?.click().await?; - - let active = c.active_element().await?; - assert_eq!(active.attr("id").await?, Some(String::from("select1"))); - - c.close_window().await -} - #[cfg(test)] mod tests { + use crate::booksprice::selenium_common; 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::*; - // #[test] - /* fn test_selenium() { - selenium_fn(); - } */ + async fn parse_booksprices(c: WebDriver, port: u16) -> Result<(), WebDriverError> { + let url = selenium_common::url_from_path(port, "output_bookprice.html"); + + c.goto(&url).await?; + println!("{:#?}", c.source().await); + c.find(By::Css("#select1")).await?.click().await?; + + let active = c.active_element().await?; + assert_eq!(active.attr("id").await?, Some(String::from("select1"))); + + c.close_window().await + } #[test] fn test_selenium() { diff --git a/native/src/booksprice/selenium_common.rs b/native/src/booksprice/selenium_common.rs index 5a85212..e537a40 100644 --- a/native/src/booksprice/selenium_common.rs +++ b/native/src/booksprice/selenium_common.rs @@ -70,7 +70,7 @@ macro_rules! tester { #[macro_export] macro_rules! tester_inner { ($f:ident, $connector:expr) => {{ - use std::sync::{Arc, Mutex}; + // use std::sync::{Arc, Mutex}; use std::thread; let c = $connector; @@ -196,14 +196,6 @@ fn file_not_found() -> Response { .unwrap() } -pub fn sample_page_url(port: u16) -> String { - format!("http://localhost:{}/sample_page.html", port) -} - -pub fn other_page_url(port: u16) -> String { - format!("http://localhost:{}/other_page.html", port) -} - -pub fn drag_to_url(port: u16) -> String { - format!("http://localhost:{}/drag_to.html", port) +pub fn url_from_path(port: u16, path: &str) -> String { + format!("http://localhost:{}/{}", port, path) } diff --git a/native/tests/test_html/sample_page.html b/native/tests/test_html/sample_page.html deleted file mode 120000 index 3c2a2e7..0000000 --- a/native/tests/test_html/sample_page.html +++ /dev/null @@ -1 +0,0 @@ -output_bookprice.html \ No newline at end of file From 3098a29de2f82d4d1cae288e79fcd763ab79d4f8 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 1 Apr 2023 16:19:10 +0200 Subject: [PATCH 48/81] Parse booksPrice works --- native/Cargo.toml | 1 + native/src/booksprice/request.rs | 36 +++++++++++++++---- ...tput_bookprice.html => 9782884747974.html} | 0 3 files changed, 31 insertions(+), 6 deletions(-) rename native/tests/test_html/{output_bookprice.html => 9782884747974.html} (100%) diff --git a/native/Cargo.toml b/native/Cargo.toml index 6a79611..1f31af6 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -25,3 +25,4 @@ tokio = { version = "1.20", features = ["fs", "macros", "rt-multi-thread", "io-u # [dev-dependencies] color-eyre = "0.6.2" hyper = { version = "0.14", features = ["server", "tcp"] } +futures = "0.3.28" diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index f508d8f..d0ebb98 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -45,6 +45,9 @@ async fn selenium_fn() -> color_eyre::Result<()> { #[cfg(test)] mod tests { + use itertools::Itertools; + use tokio::try_join; + use crate::booksprice::selenium_common; use crate::booksprice::selenium_common::handle_test_error; use crate::booksprice::selenium_common::make_capabilities; @@ -56,14 +59,35 @@ mod tests { use super::*; async fn parse_booksprices(c: WebDriver, port: u16) -> Result<(), WebDriverError> { - let url = selenium_common::url_from_path(port, "output_bookprice.html"); + let url = selenium_common::url_from_path(port, "9782884747974.html"); c.goto(&url).await?; - println!("{:#?}", c.source().await); - c.find(By::Css("#select1")).await?.click().await?; - - let active = c.active_element().await?; - assert_eq!(active.attr("id").await?, Some(String::from("select1"))); + // println!("{:#?}", c.source().await); + let entries = c + .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) + .await?; + assert_eq!(entries.len(), 6); + use futures::future::{self, try_join_all}; + + let prices = 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(); + + assert_eq!(prices, vec![16.55, 21.85, 23.75, 27.17, 28.15, 43.20]); c.close_window().await } diff --git a/native/tests/test_html/output_bookprice.html b/native/tests/test_html/9782884747974.html similarity index 100% rename from native/tests/test_html/output_bookprice.html rename to native/tests/test_html/9782884747974.html From ad31bd2ff972581a25d3db544263048eac2eedcf Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 1 Apr 2023 16:25:51 +0200 Subject: [PATCH 49/81] clean --- native/src/booksprice/request.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index d0ebb98..0668849 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -45,9 +45,6 @@ async fn selenium_fn() -> color_eyre::Result<()> { #[cfg(test)] mod tests { - use itertools::Itertools; - use tokio::try_join; - use crate::booksprice::selenium_common; use crate::booksprice::selenium_common::handle_test_error; use crate::booksprice::selenium_common::make_capabilities; @@ -62,14 +59,12 @@ mod tests { let url = selenium_common::url_from_path(port, "9782884747974.html"); c.goto(&url).await?; - // println!("{:#?}", c.source().await); let entries = c .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) .await?; assert_eq!(entries.len(), 6); - use futures::future::{self, try_join_all}; - let prices = try_join_all(entries.iter().map(|e| async { + 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 From cee744c7cbe32e1c4a567d9fd6f5f6f9aba914ec Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 3 Apr 2023 20:41:47 +0200 Subject: [PATCH 50/81] Extract selenium POC into useful function --- native/src/booksprice/request.rs | 113 ++++++++++++++----------------- 1 file changed, 52 insertions(+), 61 deletions(-) diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index 0668849..728a74b 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -5,47 +5,55 @@ use thirtyfour::prelude::*; use tokio; -// mod selenium_common; - -// use crate::common; - #[tokio::main] -async fn selenium_fn() -> color_eyre::Result<()> { - // The use of color_eyre gives much nicer error reports, including making - // it much easier to locate where the error occurred. - color_eyre::install()?; - - // thirtyfour::resolve!(); - // crate::local_tester!(); - +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?; - driver.goto("https://www.booksprice.com/comparePrice.do?l=y&searchType=compare&inputData=9782266071529").await?; - let elem_form = driver.find(By::Id("search-form")).await?; - - // Find element from element. - let elem_text = elem_form.find(By::Id("searchInput")).await?; - - // Type in the search terms. - elem_text.send_keys("selenium").await?; - // Click the search button. - let elem_button = elem_form.find(By::Css("button[type='submit']")).await?; - elem_button.click().await?; - - // Look for header to implicitly wait for the page to load. - driver.find(By::ClassName("firstHeading")).await?; - assert_eq!(driver.title().await?, "Selenium - Wikipedia"); - - // Always explicitly close the browser. There are no async destructors. - driver.quit().await?; + extract_price_from_url( + driver, + &format!( + "https://www.booksprice.com/comparePrice.do?l=y&searchType=compare&inputData={}", + isbn + ), + ) + .await +} - Ok(()) +async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { + c.goto(&url).await?; + let entries = c + .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) + .await?; + assert_eq!(entries.len(), 6); + + 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; use crate::booksprice::selenium_common::handle_test_error; use crate::booksprice::selenium_common::make_capabilities; use crate::booksprice::selenium_common::make_url; @@ -55,40 +63,23 @@ mod tests { use super::*; - async fn parse_booksprices(c: WebDriver, port: u16) -> Result<(), WebDriverError> { - let url = selenium_common::url_from_path(port, "9782884747974.html"); - - c.goto(&url).await?; - let entries = c - .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) - .await?; - assert_eq!(entries.len(), 6); - - 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(); + async fn parse_booksprices_from_9782884747974( + c: WebDriver, + port: u16, + ) -> Result<(), WebDriverError> { + use crate::booksprice::selenium_common; - assert_eq!(prices, vec![16.55, 21.85, 23.75, 27.17, 28.15, 43.20]); + let prices = extract_price_from_url( + c, + &selenium_common::url_from_path(port, "9782884747974.html"), + ).await.unwrap(); - c.close_window().await + 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, "chrome"); + local_tester!(parse_booksprices_from_9782884747974, "chrome"); } } From 40b2e48028633783934457b475594d29c108797f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 3 Apr 2023 21:25:21 +0200 Subject: [PATCH 51/81] Add market price and add BooksPrice provider --- native/src/api.rs | 6 ++++-- native/src/babelio.rs | 2 ++ native/src/babelio/parser.rs | 2 ++ native/src/booksprice.rs | 10 ++++++++++ native/src/booksprice/request.rs | 14 ++++++++------ native/src/common.rs | 2 ++ native/src/google_books.rs | 9 ++++++++- 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/native/src/api.rs b/native/src/api.rs index c692fec..281affd 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,12 +1,13 @@ use crate::cached_client::CachedClient; -use crate::common::{Provider, LbcCredential}; use crate::common::{Ad, BookMetaData}; +use crate::common::{LbcCredential, Provider}; use crate::publisher::Publisher; -use crate::{babelio, google_books, leboncoin}; +use crate::{babelio, booksprice, google_books, leboncoin}; pub enum ProviderEnum { Babelio, GoogleBooks, + BooksPrice, } pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Option { @@ -18,6 +19,7 @@ pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Optio }), } .get_book_metadata_from_isbn(&isbn), + ProviderEnum::BooksPrice => booksprice::BooksPrice {}.get_book_metadata_from_isbn(&isbn), } } diff --git a/native/src/babelio.rs b/native/src/babelio.rs index 8c800e6..5e6115a 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -67,6 +67,7 @@ mod tests { ] .map(|s| s.to_string()) .to_vec(), + market_price: vec![], })); } @@ -87,6 +88,7 @@ Ensemble, les deux enfants devront lutter contre les forces obscures du mal et, ] .map(|s| s.to_string()) .to_vec(), + market_price: vec![], })); } } diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index 0a94177..0132f4b 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -205,6 +205,7 @@ mod tests { ] .map(|s| s.to_string()) .to_vec(), + market_price: vec![], }) ); } @@ -249,6 +250,7 @@ mod tests { ] .map(|s| s.to_string()) .to_vec(), + market_price: vec![], }) ); } diff --git a/native/src/booksprice.rs b/native/src/booksprice.rs index a5f8510..56495a0 100644 --- a/native/src/booksprice.rs +++ b/native/src/booksprice.rs @@ -1,3 +1,13 @@ +use crate::common::{self, BookMetaData}; + 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(BookMetaData{ 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 index 728a74b..1846f28 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -6,9 +6,9 @@ use thirtyfour::prelude::*; use tokio; #[tokio::main] -async fn extract_price_from_isbn( +pub async fn extract_price_from_isbn( isbn: &str, -) -> Result, thirtyfour::prelude::WebDriverError> { +) -> Result, thirtyfour::prelude::WebDriverError> { let caps = DesiredCapabilities::chrome(); let driver = WebDriver::new("http://localhost:9515", caps).await?; @@ -22,7 +22,7 @@ async fn extract_price_from_isbn( .await } -async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { +async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { c.goto(&url).await?; let entries = c .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) @@ -41,7 +41,7 @@ async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, Web 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() + r.get(1).unwrap().as_str().parse::().unwrap() }) })) .await @@ -72,9 +72,11 @@ mod tests { let prices = extract_price_from_url( c, &selenium_common::url_from_path(port, "9782884747974.html"), - ).await.unwrap(); + ) + .await + .unwrap(); - assert_eq!(prices, vec![16.55,21.85,23.75,27.17,28.15,43.20]); + assert_eq!(prices, vec![16.55, 21.85, 23.75, 27.17, 28.15, 43.20]); Ok(()) } diff --git a/native/src/common.rs b/native/src/common.rs index 7b1a52a..1488f3a 100644 --- a/native/src/common.rs +++ b/native/src/common.rs @@ -6,6 +6,8 @@ pub struct BookMetaData { // 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)] diff --git a/native/src/google_books.rs b/native/src/google_books.rs index 67fd6cd..1ebd774 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -49,6 +49,7 @@ fn merge_bmd(bmd1: common::BookMetaData, bmd2: common::BookMetaData) -> common:: authors: bmd1.authors, blurb: longest_string_merger(bmd1.blurb, bmd2.blurb), keywords: merge_vec(bmd1.keywords, bmd2.keywords), + market_price: vec![], } } @@ -90,6 +91,12 @@ mod tests { }), }; let md = g.get_book_metadata_from_isbn("9782266162777"); - assert_eq!(md, Some(BookMetaData { 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![] })) + assert_eq!(md, Some(BookMetaData { + 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![], + })) } } From 82593b0e70b87d50b296f7b9eb27e2eb7a292ca1 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 3 Apr 2023 21:38:39 +0200 Subject: [PATCH 52/81] Run rust codegen --- lib/bridge_definitions.dart | 14 +++++++++----- lib/bridge_generated.dart | 13 +++++++++++-- native/src/bridge_generated.rs | 2 ++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index 50af226..c260598 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -2,18 +2,19 @@ // 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 'dart:async'; import 'dart:convert'; - -import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'dart:async'; import 'package:meta/meta.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; abstract class Native { - Future getMetadataFromProvider({required ProviderEnum provider, required String isbn, 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}); + Future publishAd( + {required Ad ad, required LbcCredential credential, dynamic hint}); FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; } @@ -47,12 +48,14 @@ class BookMetaData { List authors; String? blurb; List keywords; + Float32List marketPrice; BookMetaData({ this.title, required this.authors, this.blurb, required this.keywords, + required this.marketPrice, }); } @@ -69,4 +72,5 @@ class LbcCredential { enum ProviderEnum { Babelio, GoogleBooks, + BooksPrice, } diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index c209bb1..bcc2059 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -88,13 +88,14 @@ class NativeImpl implements Native { BookMetaData _wire2api_book_meta_data(dynamic raw) { final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return BookMetaData( 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]), ); } @@ -106,6 +107,14 @@ class NativeImpl implements Native { return _wire2api_book_meta_data(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(); } diff --git a/native/src/bridge_generated.rs b/native/src/bridge_generated.rs index f99f0c4..4a6d663 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -96,6 +96,7 @@ impl Wire2Api for i32 { match self { 0 => ProviderEnum::Babelio, 1 => ProviderEnum::GoogleBooks, + 2 => ProviderEnum::BooksPrice, _ => unreachable!("Invalid variant for ProviderEnum: {}", self), } } @@ -122,6 +123,7 @@ impl support::IntoDart for BookMetaData { self.authors.into_dart(), self.blurb.into_dart(), self.keywords.into_dart(), + self.market_price.into_dart(), ] .into_dart() } From 3884605aff6e99e5952cf6584e9bccffd0fc6ad3 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 4 Apr 2023 19:56:43 +0200 Subject: [PATCH 53/81] Rename BookMetaData to BookMetaDataFromProvider --- lib/bridge_definitions.dart | 6 +++--- lib/bridge_generated.dart | 20 ++++++++++++-------- lib/common.dart | 9 +++++++-- lib/metadata_collecting.dart | 2 +- native/src/api.rs | 7 +++++-- native/src/babelio.rs | 8 ++++---- native/src/babelio/parser.rs | 10 +++++----- native/src/booksprice.rs | 14 ++++++++++---- native/src/bridge_generated.rs | 6 +++--- native/src/common.rs | 4 ++-- native/src/google_books.rs | 13 ++++++++----- native/src/google_books/parser.rs | 18 +++++++++--------- 12 files changed, 69 insertions(+), 48 deletions(-) diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index c260598..1f2b8df 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -8,7 +8,7 @@ import 'package:meta/meta.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; abstract class Native { - Future getMetadataFromProvider( + Future getMetadataFromProvider( {required ProviderEnum provider, required String isbn, dynamic hint}); FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; @@ -43,14 +43,14 @@ class Author { }); } -class BookMetaData { +class BookMetaDataFromProvider { String? title; List authors; String? blurb; List keywords; Float32List marketPrice; - BookMetaData({ + const BookMetaDataFromProvider({ this.title, required this.authors, this.blurb, diff --git a/lib/bridge_generated.dart b/lib/bridge_generated.dart index bcc2059..c53f603 100644 --- a/lib/bridge_generated.dart +++ b/lib/bridge_generated.dart @@ -24,14 +24,14 @@ class NativeImpl implements Native { factory NativeImpl.wasm(FutureOr module) => NativeImpl(module as ExternalLibrary); NativeImpl.raw(this._platform); - Future getMetadataFromProvider( + 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_get_metadata_from_provider(port_, arg0, arg1), - parseSuccessData: _wire2api_opt_box_autoadd_book_meta_data, + parseSuccessData: _wire2api_opt_box_autoadd_book_meta_data_from_provider, constMeta: kGetMetadataFromProviderConstMeta, argValues: [provider, isbn], hint: hint, @@ -86,11 +86,11 @@ class NativeImpl implements Native { ); } - BookMetaData _wire2api_book_meta_data(dynamic raw) { + 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 BookMetaData( + return BookMetaDataFromProvider( title: _wire2api_opt_String(arr[0]), authors: _wire2api_list_author(arr[1]), blurb: _wire2api_opt_String(arr[2]), @@ -103,8 +103,9 @@ class NativeImpl implements Native { return raw as bool; } - BookMetaData _wire2api_box_autoadd_book_meta_data(dynamic raw) { - return _wire2api_book_meta_data(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) { @@ -123,8 +124,11 @@ class NativeImpl implements Native { return raw == null ? null : _wire2api_String(raw); } - BookMetaData? _wire2api_opt_box_autoadd_book_meta_data(dynamic raw) { - return raw == null ? null : _wire2api_box_autoadd_book_meta_data(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) { diff --git a/lib/common.dart b/lib/common.dart index be0a498..f67ab09 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -61,6 +62,10 @@ extension DoubleExt on double { } extension BookMetadataExt on BookMetaData { - BookMetaData deepCopy() => - BookMetaData(title: '$title', authors: List.from(authors), blurb: '$blurb', keywords: List.from(keywords)); + BookMetaData deepCopy() => BookMetaData( + title: '$title', + authors: List.from(authors), + blurb: '$blurb', + keywords: List.from(keywords), + marketPrice: Float32List.fromList(marketPrice)); } diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 73cca4c..00a7f80 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -69,7 +69,7 @@ class _MetadataCollectingWidgetState extends State { metadata.putIfAbsent( isbn, () => Metadatas( - manual: BookMetaData(title: '', authors: [], blurb: '', keywords: []), + manual: BookMetaData(title: '', authors: [], blurb: '', keywords: [], price), mdFromProviders: Map.fromEntries(ProviderEnum.values.map((provider) { final md = api.getMetadataFromProvider(provider: provider, isbn: isbn); md.then((value) { diff --git a/native/src/api.rs b/native/src/api.rs index 281affd..dfdb8da 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -1,5 +1,5 @@ use crate::cached_client::CachedClient; -use crate::common::{Ad, BookMetaData}; +use crate::common::{Ad, BookMetaDataFromProvider}; use crate::common::{LbcCredential, Provider}; use crate::publisher::Publisher; use crate::{babelio, booksprice, google_books, leboncoin}; @@ -10,7 +10,10 @@ pub enum ProviderEnum { BooksPrice, } -pub fn get_metadata_from_provider(provider: ProviderEnum, isbn: String) -> Option { +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 { diff --git a/native/src/babelio.rs b/native/src/babelio.rs index 5e6115a..fe7782b 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -5,7 +5,7 @@ mod request; pub struct Babelio; impl common::Provider for Babelio { - fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + 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, @@ -30,7 +30,7 @@ impl common::Provider for Babelio { #[cfg(test)] mod tests { - use crate::common::{Author, BookMetaData, Provider}; + use crate::common::{Author, BookMetaDataFromProvider, Provider}; use super::*; @@ -38,7 +38,7 @@ mod tests { fn get_metadata_from_normal_book() { let isbn = "9782266071529"; let md = Babelio {}.get_book_metadata_from_isbn(isbn); - assert_eq!(md, Some(BookMetaData { + 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\n".to_string()), @@ -75,7 +75,7 @@ mod tests { 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(BookMetaData { + 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. diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index 0132f4b..620c731 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -1,4 +1,4 @@ -use crate::common::{html_select, BookMetaData}; +use crate::common::{html_select, BookMetaDataFromProvider}; use itertools::Itertools; #[derive(PartialEq, Debug)] @@ -96,7 +96,7 @@ fn extract_author(author_scope: scraper::ElementRef) -> crate::common::Author { } } -pub fn extract_title_author_keywords(html: &str) -> Option { +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\"]"); @@ -146,7 +146,7 @@ pub fn extract_title_author_keywords(html: &str) -> Option { ) }) .collect(); - Some(BookMetaData { + Some(BookMetaDataFromProvider { title: Some(title), authors, keywords, @@ -174,7 +174,7 @@ mod tests { let title_author_keywords = extract_title_author_keywords(&html); assert_eq!( title_author_keywords, - Some(BookMetaData { + Some(BookMetaDataFromProvider { title: Some("Le nom de la bête".to_string()), authors: vec![crate::common::Author { first_name: "Daniel".to_string(), @@ -219,7 +219,7 @@ mod tests { let title_author_keywords = extract_title_author_keywords(&html); assert_eq!( title_author_keywords, - Some(BookMetaData { + Some(BookMetaDataFromProvider { title: Some("Bardo-Thödol : Le livre tibétain des morts".to_string()), authors: vec![crate::common::Author { first_name: "".to_string(), diff --git a/native/src/booksprice.rs b/native/src/booksprice.rs index 56495a0..e8be244 100644 --- a/native/src/booksprice.rs +++ b/native/src/booksprice.rs @@ -1,4 +1,4 @@ -use crate::common::{self, BookMetaData}; +use crate::common::{self, BookMetaDataFromProvider}; mod request; mod selenium_common; @@ -6,8 +6,14 @@ 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(BookMetaData{ title: None, authors: vec![], blurb: None, keywords: vec![], market_price: prices.unwrap() }) + 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/bridge_generated.rs b/native/src/bridge_generated.rs index 4a6d663..719b492 100644 --- a/native/src/bridge_generated.rs +++ b/native/src/bridge_generated.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use crate::common::Ad; use crate::common::Author; -use crate::common::BookMetaData; +use crate::common::BookMetaDataFromProvider; use crate::common::LbcCredential; // Section: wire functions @@ -116,7 +116,7 @@ impl support::IntoDart for Author { } impl support::IntoDartExceptPrimitive for Author {} -impl support::IntoDart for BookMetaData { +impl support::IntoDart for BookMetaDataFromProvider { fn into_dart(self) -> support::DartAbi { vec![ self.title.into_dart(), @@ -128,7 +128,7 @@ impl support::IntoDart for BookMetaData { .into_dart() } } -impl support::IntoDartExceptPrimitive for BookMetaData {} +impl support::IntoDartExceptPrimitive for BookMetaDataFromProvider {} // Section: executor diff --git a/native/src/common.rs b/native/src/common.rs index 1488f3a..28ad14e 100644 --- a/native/src/common.rs +++ b/native/src/common.rs @@ -1,5 +1,5 @@ #[derive(Default, Debug, PartialEq)] -pub struct BookMetaData { +pub struct BookMetaDataFromProvider { pub title: Option, pub authors: Vec, // A book blurb is a short promotional description. @@ -21,7 +21,7 @@ pub fn html_select(sel: &str) -> scraper::Selector { } pub trait Provider { - fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option; + fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option; } pub struct LbcCredential { diff --git a/native/src/google_books.rs b/native/src/google_books.rs index 1ebd774..08b49ac 100644 --- a/native/src/google_books.rs +++ b/native/src/google_books.rs @@ -40,8 +40,11 @@ fn merge_vec( .collect_vec() } -fn merge_bmd(bmd1: common::BookMetaData, bmd2: common::BookMetaData) -> common::BookMetaData { - common::BookMetaData { +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 @@ -54,7 +57,7 @@ fn merge_bmd(bmd1: common::BookMetaData, bmd2: common::BookMetaData) -> common:: } impl common::Provider for GoogleBooks { - fn get_book_metadata_from_isbn(&self, isbn: &str) -> Option { + 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); @@ -78,7 +81,7 @@ impl common::Provider for GoogleBooks { #[cfg(test)] mod tests { use crate::cached_client::MockClient; - use crate::common::BookMetaData; + use crate::common::BookMetaDataFromProvider; use crate::common::Provider; use super::*; @@ -91,7 +94,7 @@ mod tests { }), }; let md = g.get_book_metadata_from_isbn("9782266162777"); - assert_eq!(md, Some(BookMetaData { + 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()), diff --git a/native/src/google_books/parser.rs b/native/src/google_books/parser.rs index 3e8ee75..f27a2aa 100644 --- a/native/src/google_books/parser.rs +++ b/native/src/google_books/parser.rs @@ -1,12 +1,12 @@ use itertools::Itertools; -use crate::common::{self, BookMetaData}; +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::BookMetaData { +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; @@ -24,21 +24,21 @@ pub fn extract_metadata_from_isbn_response(html: &str) -> common::BookMetaData { .description .clone() .map(|d| d.to_string()); - BookMetaData { + BookMetaDataFromProvider { authors, blurb, ..Default::default() } }); - a.unwrap_or(BookMetaData { + a.unwrap_or(BookMetaDataFromProvider { ..Default::default() }) } -pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaData { +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::BookMetaData { + common::BookMetaDataFromProvider { title: Some(first_book.title.to_string()), authors: first_book .authors @@ -56,7 +56,7 @@ pub fn extract_metadata_from_self_link_response(html: &str) -> common::BookMetaD #[cfg(test)] mod tests { - use crate::common::BookMetaData; + use crate::common::BookMetaDataFromProvider; use super::*; @@ -78,7 +78,7 @@ mod tests { 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, BookMetaData{ + 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()), @@ -106,7 +106,7 @@ mod tests { let metadata = extract_metadata_from_self_link_response(&html); assert_eq!( metadata, - BookMetaData { + BookMetaDataFromProvider { title: Some("L'essence du Tao".to_string()), authors: vec![common::Author { first_name: "".to_string(), From 54790b41ecf31da3267c8f79017a0f1fe28f5572 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 4 Apr 2023 21:01:59 +0200 Subject: [PATCH 54/81] Adaot flutter --- lib/ad_editing.dart | 17 ++++++++++------- lib/bridge_definitions.dart | 10 +++++----- lib/common.dart | 20 ++++++++++++++++++-- lib/main.dart | 12 ++++++------ lib/metadata_collecting.dart | 6 +++--- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 4291583..cc33a8d 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -16,16 +16,16 @@ class AdEditingWidget extends StatefulWidget { State createState() => _AdEditingWidgetState(); } -String vecFmt(List vec) { +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(BookMetaData book) { - final authors = book.authors.map((a) => '${a.firstName} ${a.lastName}').toList(); - return '"${book.title}" ${vecFmt(authors)}'; +String _bookFormatTitleAndAuthor(String title, Iterable authors) { + return '"$title" ${vecFmt(authors.map((a) => '${a.firstName} ${a.lastName}'))}'; } class _AdEditingWidgetState extends State { @@ -53,15 +53,18 @@ class _AdEditingWidgetState extends State { print('credential ${credential.lbcToken} ${credential.dataDomeCookie}'); } - String _getDescription(Iterable> metadataFromIsbn) { + 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)).join('\n'); + final bookTitles = metadataFromIsbn + .map((entry) => _bookFormatTitleAndAuthor(entry.value.title!, entry.value.authors)) + .join('\n'); final blurbs = metadataFromIsbn - .map((entry) => _bookFormatTitleAndAuthor(entry.value) + ':\n' + entry.value.blurb!) + .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; diff --git a/lib/bridge_definitions.dart b/lib/bridge_definitions.dart index 1f2b8df..bf214fa 100644 --- a/lib/bridge_definitions.dart +++ b/lib/bridge_definitions.dart @@ -2,10 +2,11 @@ // 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 '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 getMetadataFromProvider( @@ -13,8 +14,7 @@ abstract class Native { FlutterRustBridgeTaskConstMeta get kGetMetadataFromProviderConstMeta; - Future publishAd( - {required Ad ad, required LbcCredential credential, dynamic hint}); + Future publishAd({required Ad ad, required LbcCredential credential, dynamic hint}); FlutterRustBridgeTaskConstMeta get kPublishAdConstMeta; } @@ -50,7 +50,7 @@ class BookMetaDataFromProvider { List keywords; Float32List marketPrice; - const BookMetaDataFromProvider({ + BookMetaDataFromProvider({ this.title, required this.authors, this.blurb, diff --git a/lib/common.dart b/lib/common.dart index f67ab09..c9e504f 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -61,6 +60,23 @@ 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', @@ -68,4 +84,4 @@ extension BookMetadataExt on BookMetaData { blurb: '$blurb', keywords: List.from(keywords), marketPrice: Float32List.fromList(marketPrice)); -} +}*/ diff --git a/lib/main.dart b/lib/main.dart index 23bfee3..df589b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/common.dart'; import 'ad_editing.dart'; import 'drag_and_drop.dart' as drag_and_drop; -import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'isbn_decoding.dart'; import 'metadata_collecting.dart'; @@ -27,7 +27,7 @@ class MetadataCollectingStep implements BookyStep { class AdEditingStep implements BookyStep { List imgsPaths = []; - Map metadata = {}; + Map metadata = {}; AdEditingStep({required this.imgsPaths, required this.metadata}); } @@ -40,8 +40,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = ImageSelectionStep(); - /* AdEditingStep(imgsPaths: [ + BookyStep step = //ImageSelectionStep(); + /* AdEditingStep(imgsPaths: [ '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg' ], metadata: { 'myisbn': BookMetaData( @@ -49,7 +49,7 @@ class _MyAppState extends State { authors: [Author(firstName: 'Mock firstname', lastName: 'mock lastname')], keywords: ['mock kw']) });*/ - /* MetadataCollectingStep(imgsPaths: [ + 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', @@ -57,7 +57,7 @@ class _MyAppState extends State { ], isbns: { '9782253029854', '9782277223634', - });*/ + }); @override Widget build(BuildContext context) { return MaterialApp( diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 00a7f80..e7b1b86 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -35,8 +35,8 @@ class MetadataCollectingWidget extends StatefulWidget { } class Metadatas { - final Map> mdFromProviders; - BookMetaData manual; + final Map> mdFromProviders; + BookMetaDataManual manual; Metadatas({required this.mdFromProviders, required this.manual}); } @@ -69,7 +69,7 @@ class _MetadataCollectingWidgetState extends State { metadata.putIfAbsent( isbn, () => Metadatas( - manual: BookMetaData(title: '', authors: [], blurb: '', keywords: [], price), + 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) { From 624eca73e2562b17e753db875e0b96ae05185604 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 4 Apr 2023 21:49:27 +0200 Subject: [PATCH 55/81] BooksPrice: remove assert, add wait_until --- lib/metadata_collecting.dart | 1 + native/src/booksprice/request.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index e7b1b86..d5865bb 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -112,6 +112,7 @@ class _MetadataCollectingWidgetState extends State { Text('Manual'), Text('Babelio'), Text('GoogleBooks'), + Text('BooksPrice'), ]), TableRow(children: [ FutureWidget( diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index 1846f28..62cbee3 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -2,6 +2,8 @@ //! //! chromedriver --port=9515 +use std::time::Duration; + use thirtyfour::prelude::*; use tokio; @@ -24,10 +26,13 @@ pub async fn extract_price_from_isbn( async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { c.goto(&url).await?; + + c.query(By::XPath("//*[@id='chart']")) + .wait(Duration::from_secs(10), Duration::from_secs(1)); + let entries = c .find_all(By::XPath("//*[@id='chart']/tbody/tr[position()>1]")) .await?; - assert_eq!(entries.len(), 6); let prices = futures::future::try_join_all(entries.iter().map(|e| async { let price_text = e From 47190316bfa206b580117304101f96ec466ced86 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 10 Apr 2023 16:57:47 +0200 Subject: [PATCH 56/81] Price works rust: wait for chart to appear. flutter: show price and use it in ad_editing --- lib/ad_editing.dart | 15 ++- lib/main.dart | 10 +- lib/metadata_collecting.dart | 175 ++++++++++++++++++------------- native/src/booksprice/request.rs | 7 +- 4 files changed, 122 insertions(+), 85 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index cc33a8d..2016f59 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; @@ -47,7 +48,9 @@ class _AdEditingWidgetState extends State { description += '\n\nMots-clés:\n' + keywords; } - ad = Ad(title: title, description: description, priceCent: 1000, imgsPath: widget.step.imgsPaths); + final totalPrice = metadataFromIsbn.map((e) => e.value.priceCent ?? 0).sum; + + ad = Ad(title: title, description: description, priceCent: totalPrice, imgsPath: widget.step.imgsPaths); credential = Credential.loadFromFile(); print('credential ${credential.lbcToken} ${credential.dataDomeCookie}'); @@ -130,9 +133,13 @@ class _AdEditingWidgetState extends State { style: const TextStyle(fontSize: 20), autovalidateMode: AutovalidateMode.always, validator: (token) { - final remainingDuration = JwtDecoder.getRemainingTime(token!); - if (remainingDuration.isNegative) { - return 'Token expired'; + try { + final remainingDuration = JwtDecoder.getRemainingTime(token!); + if (remainingDuration.isNegative) { + return 'Token expired'; + } + } on FormatException catch (e) { + return 'Not a JWT token'; } return null; }, diff --git a/lib/main.dart b/lib/main.dart index df589b5..f1ff9e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,8 +40,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = //ImageSelectionStep(); - /* AdEditingStep(imgsPaths: [ + BookyStep step = ImageSelectionStep(); + /* AdEditingStep(imgsPaths: [ '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg' ], metadata: { 'myisbn': BookMetaData( @@ -49,15 +49,15 @@ class _MyAppState extends State { authors: [Author(firstName: 'Mock firstname', lastName: 'mock lastname')], keywords: ['mock kw']) });*/ - MetadataCollectingStep(imgsPaths: [ + /* 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', - }); + // '9782277223634', + });*/ @override Widget build(BuildContext context) { return MaterialApp( diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index d5865bb..7f9aae2 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_rust_bridge_template/common.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; @@ -29,6 +30,7 @@ class MetadataCollectingWidget extends StatefulWidget { final blurbTextFieldController = TextEditingController(); final titleTextFieldController = TextEditingController(); + final priceTextFieldController = TextEditingController(); @override State createState() => _MetadataCollectingWidgetState(); @@ -98,89 +100,114 @@ class _MetadataCollectingWidgetState extends State { 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: Row( + child: Column( children: [ - SelectableText('ISBN: $isbn'), - Expanded( - child: Table( - children: [ - const TableRow(children: [ - Text('Manual'), - Text('Babelio'), - Text('GoogleBooks'), - Text('BooksPrice'), - ]), - 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( + 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( - initialValue: data?.authors.toText(), - onChanged: (newText) => setState(() => manual.authors = newText - .split('\n') - .map((line) => Author(firstName: '', lastName: line)) - .toList()), - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Authors', - ), + 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( + initialValue: data?.authors.toText(), + 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), - ); - })), - ]), - ], - ), + ), + ...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)}', + ); + })), + ]), + ], ), ], ), diff --git a/native/src/booksprice/request.rs b/native/src/booksprice/request.rs index 62cbee3..ee1a12d 100644 --- a/native/src/booksprice/request.rs +++ b/native/src/booksprice/request.rs @@ -27,8 +27,11 @@ pub async fn extract_price_from_isbn( async fn extract_price_from_url(c: WebDriver, url: &str) -> Result, WebDriverError> { c.goto(&url).await?; - c.query(By::XPath("//*[@id='chart']")) - .wait(Duration::from_secs(10), Duration::from_secs(1)); + 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]")) From 6a8a8b07f635b1f3920271ed557261dcf4041abc Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 12 Apr 2023 22:36:10 +0200 Subject: [PATCH 57/81] Convert html encoded text to String --- native/Cargo.toml | 1 + native/src/babelio.rs | 11 +++++------ native/src/babelio/parser.rs | 14 ++++++++++++-- .../test/get_book_blurb_see_more_179245.html | 4 ++++ 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 native/src/babelio/test/get_book_blurb_see_more_179245.html diff --git a/native/Cargo.toml b/native/Cargo.toml index 1f31af6..c38db3c 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -26,3 +26,4 @@ tokio = { version = "1.20", features = ["fs", "macros", "rt-multi-thread", "io-u color-eyre = "0.6.2" hyper = { version = "0.14", features = ["server", "tcp"] } futures = "0.3.28" +html2text = "0.5.1" diff --git a/native/src/babelio.rs b/native/src/babelio.rs index fe7782b..4f40ce4 100644 --- a/native/src/babelio.rs +++ b/native/src/babelio.rs @@ -21,7 +21,7 @@ impl common::Provider for Babelio { request::get_book_blurb_see_more(&cached_client, &id_obj) } }; - res.blurb = parser::parse_blurb(&raw_blurb); + res.blurb = Some(parser::parse_blurb(&raw_blurb)); } Some(res) @@ -41,7 +41,7 @@ mod tests { 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\n".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", @@ -78,10 +78,9 @@ mod tests { 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()), + 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", diff --git a/native/src/babelio/parser.rs b/native/src/babelio/parser.rs index 620c731..4544d81 100644 --- a/native/src/babelio/parser.rs +++ b/native/src/babelio/parser.rs @@ -154,8 +154,9 @@ pub fn extract_title_author_keywords(html: &str) -> Option Option { - Some(raw_blurb.trim().replace("
", "\n")) +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)] @@ -168,6 +169,15 @@ mod tests { 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(); 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 From f7c39e151073dcf4be285b6e6c047d5d8fd04df8 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 12 Apr 2023 22:49:09 +0200 Subject: [PATCH 58/81] ISBNDeconding: use Wrap --- lib/isbn_decoding.dart | 62 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 78ef669..6adc1f5 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -41,35 +41,41 @@ class _ISBNDecodingWidgetState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Row( + body: Column( children: [ - ...widget.step.imgsPaths - .map((imgPath) => Column( - children: [ - ImageWidget(imgPath), - FutureBuilder( - future: isbns[imgPath]!, - builder: (context, snap) { - if (snap.hasData == false) { - return const CircularProgressIndicator(); - } - return Column(children: snap.data!.map((isbn) => Text(isbn)).toList()); - }) - ], - )) - .toList(), - const Spacer(), - FutureBuilder( - future: Future.wait(isbns.values), - builder: (context, snap) { - return ElevatedButton( - onPressed: () { - final isbnSet = snap.data!.expand((e) => e).toSet(); - print('isbnSet = $isbnSet'); - widget.onSubmit(MetadataCollectingStep(imgsPaths: widget.step.imgsPaths, isbns: isbnSet)); - }, - child: const Text('Validate ISBNs')); - }) + Wrap( + children: [ + ...widget.step.imgsPaths + .map((imgPath) => Column( + children: [ + ImageWidget(imgPath), + FutureBuilder( + future: isbns[imgPath]!, + 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), + builder: (context, snap) { + return ElevatedButton( + onPressed: () { + final isbnSet = snap.data!.expand((e) => e).toSet(); + print('isbnSet = $isbnSet'); + widget.onSubmit(MetadataCollectingStep(imgsPaths: widget.step.imgsPaths, isbns: isbnSet)); + }, + child: const Text('Validate ISBNs')); + }), + ) ], ), ); From c3869d029f966b7cb4484bcf318740b12127e468 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 21:32:46 +0200 Subject: [PATCH 59/81] Select bundle from phone --- lib/ad_editing.dart | 12 +++- lib/bundle.dart | 20 +++++++ lib/bundle_selection.dart | 64 ++++++++++++++++++++ lib/common.dart | 109 ++++++++++------------------------- lib/common.g.dart | 24 ++++++++ lib/helpers.dart | 87 ++++++++++++++++++++++++++++ lib/isbn_decoding.dart | 12 ++-- lib/main.dart | 29 +++++----- lib/metadata_collecting.dart | 4 +- pubspec.yaml | 1 + 10 files changed, 256 insertions(+), 106 deletions(-) create mode 100644 lib/bundle.dart create mode 100644 lib/bundle_selection.dart create mode 100644 lib/common.g.dart create mode 100644 lib/helpers.dart diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 2016f59..a982246 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -1,12 +1,14 @@ +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; import 'package:jwt_decoder/jwt_decoder.dart'; -import 'common.dart'; import 'credential.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import 'helpers.dart'; class AdEditingWidget extends StatefulWidget { const AdEditingWidget({required this.step, required this.onSubmit}); @@ -50,7 +52,11 @@ class _AdEditingWidgetState extends State { final totalPrice = metadataFromIsbn.map((e) => e.value.priceCent ?? 0).sum; - ad = Ad(title: title, description: description, priceCent: totalPrice, imgsPath: widget.step.imgsPaths); + ad = Ad( + title: title, + description: description, + priceCent: totalPrice, + imgsPath: widget.step.bundle.images.map((e) => e.path).toList()); credential = Credential.loadFromFile(); print('credential ${credential.lbcToken} ${credential.dataDomeCookie}'); @@ -120,7 +126,7 @@ class _AdEditingWidgetState extends State { color: Colors.grey, ), const SizedBox(width: 16), - ...ad.imgsPath.map((imgPath) => ImageWidget(imgPath)).toList(), + ...ad.imgsPath.map((img) => ImageWidget(File(img))).toList(), ]), ), TextFormField( diff --git a/lib/bundle.dart b/lib/bundle.dart new file mode 100644 index 0000000..0c954e7 --- /dev/null +++ b/lib/bundle.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; +import 'dart:io'; + +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'); + } + + Metadata get metadata { + final metadataFile = File(path.join(directory.path, 'metadata.json')); + return Metadata.fromJson(jsonDecode(metadataFile.readAsStringSync()) as Map); + } +} diff --git a/lib/bundle_selection.dart b/lib/bundle_selection.dart new file mode 100644 index 0000000..c8bd3cf --- /dev/null +++ b/lib/bundle_selection.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/main.dart'; +import 'package:path/path.dart' as path; + +import 'bundle.dart'; + +class BundleSelection extends StatelessWidget { + const BundleSelection({required this.onSubmit}); + + final void Function(ISBNDecodingStep newStep) onSubmit; + + @override + Widget build(BuildContext context) { + final bundleDirs = + Directory('/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/booky/') + .listSync() + .whereType(); + + return Scaffold( + appBar: AppBar(title: const Text('Bundle Section')), + body: Wrap( + children: bundleDirs + .map((d) => Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + child: BundleWidget(d), + onTap: () => onSubmit(ISBNDecodingStep(bundle: Bundle(d))), + ), + )) + .toList(), + ), + ); + } +} + +class BundleWidget extends StatelessWidget { + const BundleWidget(this.directory); + + final Directory directory; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: const BoxDecoration(color: Colors.blue), + child: Wrap( + children: directory + .listSync() + .whereType() + .where((f) => path.extension(f.path) == '.jpg') + .map((f) => Padding( + padding: const EdgeInsets.all(8.0), + child: Image.file(f, height: 150), + )) + .toList(), + )), + Text(path.basename(directory.path)) + ], + ); + } +} diff --git a/lib/common.dart b/lib/common.dart index c9e504f..cb56f1b 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -1,87 +1,36 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'bridge_definitions.dart'; - -class ImageWidget extends StatelessWidget { - const ImageWidget(this.imgPath); - final String imgPath; - - @override - Widget build(BuildContext context) { - return Image.file( - File(imgPath), - height: 200, - 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('???'); +// TODO: merge with same file in camera_app + +import 'package:json_annotation/json_annotation.dart'; + +part 'common.g.dart'; + +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'; } } } -extension AuthorsExt on List { - String toText() => map((a) => '${a.firstName} ${a.lastName}').join('\n'); -} +@JsonSerializable() +class Metadata { + Metadata({this.weightGrams, this.itemState}); + int? weightGrams; + ItemState? itemState; -extension IntExt on int { - int divide(int other) => this ~/ other; -} + factory Metadata.fromJson(Map json) => _$MetadataFromJson(json); -extension DoubleExt on double { - double multiply(double other) => this * other; + Map toJson() => _$MetadataToJson(this); } - -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/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/helpers.dart b/lib/helpers.dart new file mode 100644 index 0000000..42809f7 --- /dev/null +++ b/lib/helpers.dart @@ -0,0 +1,87 @@ +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, + height: 200, + 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 AuthorsExt on List { + String toText() => map((a) => '${a.firstName} ${a.lastName}').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/isbn_decoding.dart b/lib/isbn_decoding.dart index 6adc1f5..c4efacf 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; -import 'common.dart'; +import 'helpers.dart'; class ISBNDecodingWidget extends StatefulWidget { const ISBNDecodingWidget({required this.step, required this.onSubmit}); @@ -15,14 +15,14 @@ class ISBNDecodingWidget extends StatefulWidget { } class _ISBNDecodingWidgetState extends State { + // TODO: Don't use Map because operator[] accept Object as parameter instead of o Key type Map>> isbns = {}; @override void initState() { - // TODO: implement initState super.initState(); - print('initState'); - widget.step.imgsPaths.forEach((imgPath) { + 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', @@ -45,7 +45,7 @@ class _ISBNDecodingWidgetState extends State { children: [ Wrap( children: [ - ...widget.step.imgsPaths + ...widget.step.bundle.images .map((imgPath) => Column( children: [ ImageWidget(imgPath), @@ -71,7 +71,7 @@ class _ISBNDecodingWidgetState extends State { onPressed: () { final isbnSet = snap.data!.expand((e) => e).toSet(); print('isbnSet = $isbnSet'); - widget.onSubmit(MetadataCollectingStep(imgsPaths: widget.step.imgsPaths, isbns: isbnSet)); + widget.onSubmit(MetadataCollectingStep(bundle: widget.step.bundle, isbns: isbnSet)); }, child: const Text('Validate ISBNs')); }), diff --git a/lib/main.dart b/lib/main.dart index f1ff9e1..60ed4e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_template/common.dart'; +import 'package:flutter_rust_bridge_template/helpers.dart'; import 'ad_editing.dart'; -import 'drag_and_drop.dart' as drag_and_drop; +import 'bundle.dart'; +import 'bundle_selection.dart'; import 'isbn_decoding.dart'; import 'metadata_collecting.dart'; @@ -12,24 +13,25 @@ void main() { sealed class BookyStep {} -class ImageSelectionStep implements BookyStep {} +class BundleSelectionStep implements BookyStep {} class ISBNDecodingStep implements BookyStep { - List imgsPaths = []; - ISBNDecodingStep({required this.imgsPaths}); + Bundle bundle; + ISBNDecodingStep({required this.bundle}); } class MetadataCollectingStep implements BookyStep { - List imgsPaths = []; + Bundle bundle; Set isbns = {}; - MetadataCollectingStep({required this.imgsPaths, required this.isbns}); + MetadataCollectingStep({required this.bundle, required this.isbns}); } class AdEditingStep implements BookyStep { - List imgsPaths = []; + Bundle bundle; + Map metadata = {}; - AdEditingStep({required this.imgsPaths, required this.metadata}); + AdEditingStep({required this.bundle, required this.metadata}); } class MyApp extends StatefulWidget { @@ -40,7 +42,7 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = ImageSelectionStep(); + BookyStep step = BundleSelectionStep(); /* AdEditingStep(imgsPaths: [ '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg' ], metadata: { @@ -64,11 +66,8 @@ class _MyAppState extends State { title: 'BookAdPublisher', theme: ThemeData(primarySwatch: Colors.blue), home: switch (step) { - ImageSelectionStep() => drag_and_drop.SelectImages(onSelect: (List paths) { - setState(() { - step = ISBNDecodingStep(imgsPaths: paths); - }); - }), + BundleSelectionStep() => + BundleSelection(onSubmit: (ISBNDecodingStep newStep) => setState(() => step = newStep)), ISBNDecodingStep() => ISBNDecodingWidget( step: step as ISBNDecodingStep, onSubmit: (MetadataCollectingStep newStep) => setState(() => step = newStep)), diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index 7f9aae2..ae29840 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_rust_bridge_template/common.dart'; +import 'package:flutter_rust_bridge_template/helpers.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'main.dart'; @@ -219,7 +219,7 @@ class _MetadataCollectingWidgetState extends State { child: ElevatedButton( onPressed: () { widget.onSubmit(AdEditingStep( - imgsPaths: widget.step.imgsPaths, + bundle: widget.step.bundle, metadata: metadata.map((key, value) => MapEntry(key, value.manual)))); }, child: const Text('Validate Metadatas')), diff --git a/pubspec.yaml b/pubspec.yaml index dd525bb..b6937f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: super_clipboard: ^0.2.3+1 json_annotation: ^4.8.0 jwt_decoder: ^2.0.1 + path: ^1.8.3 dev_dependencies: flutter_test: From 5975106514ed87935401c8aa9a74a15098001cd4 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 22:27:45 +0200 Subject: [PATCH 60/81] Add drag and drop images --- lib/ad_editing.dart | 4 ++- lib/draggable_files_widget.dart | 51 +++++++++++++++++++++++++++++++++ lib/main.dart | 24 ++++++++++------ pubspec.yaml | 9 +++--- 4 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 lib/draggable_files_widget.dart diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index a982246..6ab87d6 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -7,6 +7,7 @@ import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_inf import 'package:jwt_decoder/jwt_decoder.dart'; import 'credential.dart'; +import 'draggable_files_widget.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'helpers.dart'; @@ -122,10 +123,11 @@ class _AdEditingWidgetState extends State { padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row(children: [ const Icon( - Icons.image, + Icons.collections, color: Colors.grey, ), const SizedBox(width: 16), + DraggableFilesWidget(uris: ad.imgsPath.map((path) => Uri.file(path))), ...ad.imgsPath.map((img) => ImageWidget(File(img))).toList(), ]), ), diff --git a/lib/draggable_files_widget.dart b/lib/draggable_files_widget.dart new file mode 100644 index 0000000..9a35d1c --- /dev/null +++ b/lib/draggable_files_widget.dart @@ -0,0 +1,51 @@ +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}); + + final Iterable uris; + + @override + Widget build(BuildContext context) => FallbackSnapshotWidget( + child: Builder( + builder: (context) => BaseDraggableWidget( + hitTestBehavior: HitTestBehavior.deferToChild, + child: const Text('Drag and drop images'), + 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/main.dart b/lib/main.dart index 60ed4e4..a0fd790 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/helpers.dart'; import 'ad_editing.dart'; +import 'bridge_definitions.dart'; import 'bundle.dart'; import 'bundle_selection.dart'; import 'isbn_decoding.dart'; @@ -42,15 +45,18 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = BundleSelectionStep(); - /* AdEditingStep(imgsPaths: [ - '/home/julien/Perso/LeBonCoin/chain_automatisation/test_images/20230204_194742.jpg' - ], metadata: { - 'myisbn': BookMetaData( - title: 'Mock title', - authors: [Author(firstName: 'Mock firstname', lastName: 'mock lastname')], - keywords: ['mock kw']) - });*/ + 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')], + keywords: ['mock kw'], + 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', diff --git a/pubspec.yaml b/pubspec.yaml index b6937f2..fe09f42 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,15 +17,16 @@ environment: dependencies: flutter: sdk: flutter + collection: ^1.17.1 ffi: ^2.0.1 flutter_rust_bridge: ^1.45.0 - meta: ^1.8.0 - super_drag_and_drop: ^0.2.3 - collection: ^1.17.1 - super_clipboard: ^0.2.3+1 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 dev_dependencies: flutter_test: From f32ebfcfd5ec2c79c4dd3f67b6629ed102dec158 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 22:31:51 +0200 Subject: [PATCH 61/81] Remove LBC tokenm cookie and publish button --- lib/ad_editing.dart | 54 --------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 6ab87d6..370591d 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; -import 'package:jwt_decoder/jwt_decoder.dart'; import 'credential.dart'; import 'draggable_files_widget.dart'; @@ -131,59 +130,6 @@ class _AdEditingWidgetState extends State { ...ad.imgsPath.map((img) => ImageWidget(File(img))).toList(), ]), ), - TextFormField( - initialValue: credential.lbcToken, - onChanged: (newText) => setState(() => credential.lbcToken = newText), - decoration: const InputDecoration( - icon: Icon(Icons.key), - labelText: 'LBC Bearer token', - ), - style: const TextStyle(fontSize: 20), - autovalidateMode: AutovalidateMode.always, - validator: (token) { - try { - final remainingDuration = JwtDecoder.getRemainingTime(token!); - if (remainingDuration.isNegative) { - return 'Token expired'; - } - } on FormatException catch (e) { - return 'Not a JWT token'; - } - return null; - }, - ), - TextFormField( - initialValue: credential.dataDomeCookie, - onChanged: (newText) => setState(() => credential.dataDomeCookie = newText), - decoration: const InputDecoration( - icon: Icon(Icons.cookie), - labelText: 'datadome cookie', - ), - style: const TextStyle(fontSize: 20), - ), - ElevatedButton( - onPressed: (ad.title.length < 2 || - ad.description.length < 15 || - ad.description.length > 4000 || - ad.priceCent == null) - ? null - : () async { - print('Try to publish...'); - - final res = await api.publishAd( - ad: ad, - credential: LbcCredential( - lbcToken: credential.lbcToken, datadomeCookie: credential.dataDomeCookie)); - - if (!context.mounted) return; - if (res) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Success'))); - credential.saveToFile(); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Failure'))); - } - }, - child: const Text('Publish')) ], ), ), From b8340b2a9d2b55a90b933c3c6897c5f875d7af71 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 22:50:40 +0200 Subject: [PATCH 62/81] Add state and weight in AdEditing --- lib/ad_editing.dart | 17 +++++++++++++++++ lib/main.dart | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 370591d..5c473c1 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -82,6 +82,7 @@ class _AdEditingWidgetState extends State { @override Widget build(BuildContext context) { + final metadata = widget.step.bundle.metadata; return Scaffold( appBar: AppBar(title: const Text('Ad editing')), body: Padding( @@ -98,6 +99,14 @@ class _AdEditingWidgetState extends State { ), style: const TextStyle(fontSize: 30), ), + TextFormField( + initialValue: metadata.itemState?.loc, + decoration: const InputDecoration( + icon: Icon(Icons.diamond), + labelText: 'State', + ), + style: const TextStyle(fontSize: 20), + ), TextFormField( initialValue: ad.description, maxLines: null, @@ -118,6 +127,14 @@ class _AdEditingWidgetState extends State { ), 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: [ diff --git a/lib/main.dart b/lib/main.dart index a0fd790..c5527ce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,7 +53,8 @@ class _MyAppState extends State { 'myisbn': BookMetaDataManual( title: 'Mock title', authors: [const Author(firstName: 'Mock firstname', lastName: 'mock lastname')], - keywords: ['mock kw'], + blurb: 'This is a mock blurb', + keywords: ['kw1', 'kw2', 'kw3'], priceCent: 1234) }, ); From ec4cc4e9c5054d1632d6762baeadd3b18694c4af Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 23:38:52 +0200 Subject: [PATCH 63/81] Add copy button for text field --- lib/ad_editing.dart | 19 ++++++++++--------- lib/copiable_text_field.dart | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 lib/copiable_text_field.dart diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 5c473c1..8009aaf 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; +import 'copiable_text_field.dart'; import 'credential.dart'; import 'draggable_files_widget.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; @@ -90,15 +91,15 @@ class _AdEditingWidgetState extends State { child: SingleChildScrollView( child: Column( children: [ - TextFormField( - initialValue: ad.title, + 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( @@ -107,8 +108,8 @@ class _AdEditingWidgetState extends State { ), style: const TextStyle(fontSize: 20), ), - TextFormField( - initialValue: ad.description, + CopiableTextField(TextFormField( + controller: TextEditingController(text: ad.description), maxLines: null, scrollPhysics: const NeverScrollableScrollPhysics(), onChanged: (newText) => setState(() => ad.description = newText), @@ -116,9 +117,9 @@ class _AdEditingWidgetState extends State { icon: Icon(Icons.text_snippet), labelText: 'Ad description', ), - ), - TextFormField( - initialValue: ad.priceCent /*?*/ .divide(100).toString(), + )), + CopiableTextField(TextFormField( + controller: TextEditingController(text: ad.priceCent.divide(100).toString()), onChanged: (newText) => setState(() => ad.priceCent = double.tryParse(newText)! /*?*/ .multiply(100).round()), decoration: const InputDecoration( @@ -126,7 +127,7 @@ class _AdEditingWidgetState extends State { labelText: 'Price', ), style: const TextStyle(fontSize: 20), - ), + )), TextFormField( initialValue: metadata.weightGrams?.toString(), decoration: const InputDecoration( 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), + ], + ); +} From 1cf3257d6a180c7e148474d0a084cd6e43c5039a Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 18 Apr 2023 23:40:48 +0200 Subject: [PATCH 64/81] Remove unused credentials --- lib/ad_editing.dart | 5 ----- lib/credential.dart | 29 ----------------------------- lib/credential.g.dart | 18 ------------------ 3 files changed, 52 deletions(-) delete mode 100644 lib/credential.dart delete mode 100644 lib/credential.g.dart diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 8009aaf..f5853b0 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -6,7 +6,6 @@ import 'package:flutter_rust_bridge_template/main.dart'; import 'package:flutter_rust_bridge_template/personal_info.dart' as personal_info; import 'copiable_text_field.dart'; -import 'credential.dart'; import 'draggable_files_widget.dart'; import 'ffi.dart' if (dart.library.html) 'ffi_web.dart'; import 'helpers.dart'; @@ -34,7 +33,6 @@ String _bookFormatTitleAndAuthor(String title, Iterable authors) { class _AdEditingWidgetState extends State { late Ad ad; - late Credential credential; @override void initState() { @@ -58,9 +56,6 @@ class _AdEditingWidgetState extends State { description: description, priceCent: totalPrice, imgsPath: widget.step.bundle.images.map((e) => e.path).toList()); - - credential = Credential.loadFromFile(); - print('credential ${credential.lbcToken} ${credential.dataDomeCookie}'); } String _getDescription(Iterable> metadataFromIsbn) { diff --git a/lib/credential.dart b/lib/credential.dart deleted file mode 100644 index b7790ac..0000000 --- a/lib/credential.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:json_annotation/json_annotation.dart'; - -part 'credential.g.dart'; - -@JsonSerializable() -class Credential { - String lbcToken; - String dataDomeCookie; - - Credential({required this.lbcToken, required this.dataDomeCookie}); - - static final _file = File('credential.json'); - - void saveToFile() { - _file.writeAsStringSync(jsonEncode(toJson())); - } - - factory Credential.loadFromFile() { - final json = _file.readAsStringSync(); - return Credential.fromJson(jsonDecode(json) as Map); - } - - factory Credential.fromJson(Map json) => _$CredentialFromJson(json); - - Map toJson() => _$CredentialToJson(this); -} diff --git a/lib/credential.g.dart b/lib/credential.g.dart deleted file mode 100644 index 8a8e092..0000000 --- a/lib/credential.g.dart +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'credential.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Credential _$CredentialFromJson(Map json) => Credential( - lbcToken: json['lbcToken'] as String, - dataDomeCookie: json['dataDomeCookie'] as String, - ); - -Map _$CredentialToJson(Credential instance) => - { - 'lbcToken': instance.lbcToken, - 'dataDomeCookie': instance.dataDomeCookie, - }; From a6bd4ab848b723ab2042c0c511879402cc169090 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 19 Apr 2023 00:30:27 +0200 Subject: [PATCH 65/81] Fix Authors not taken into account in AdEditing step. Better hanle Author.toText() with empty first name. Use KtMap --- lib/ad_editing.dart | 8 ++++++-- lib/helpers.dart | 6 +++++- lib/isbn_decoding.dart | 8 ++++---- lib/main.dart | 9 +++------ lib/metadata_collecting.dart | 18 ++++++++++++++++-- pubspec.yaml | 1 + 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index f5853b0..60abf67 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -28,7 +28,7 @@ String vecFmt(Iterable it) { } String _bookFormatTitleAndAuthor(String title, Iterable authors) { - return '"$title" ${vecFmt(authors.map((a) => '${a.firstName} ${a.lastName}'))}'; + return '"$title" ${vecFmt(authors.map((a) => a.toText()))}'; } class _AdEditingWidgetState extends State { @@ -39,7 +39,11 @@ class _AdEditingWidgetState extends State { super.initState(); final metadataFromIsbn = widget.step.metadata.entries; - final title = metadataFromIsbn.length == 1 ? (metadataFromIsbn.first.value.title ?? '') : ''; + 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; diff --git a/lib/helpers.dart b/lib/helpers.dart index 42809f7..e8e20c1 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -48,8 +48,12 @@ class AsyncSnapshotWidget extends StatelessWidget { } } +extension AuthorExt on Author { + String toText() => [firstName, lastName].where((s) => s.isNotEmpty).join(' '); +} + extension AuthorsExt on List { - String toText() => map((a) => '${a.firstName} ${a.lastName}').join('\n'); + String toText() => map((a) => a.toText()).join('\n'); } extension IntExt on int { diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index c4efacf..3a5daee 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; +import 'package:kt_dart/collection.dart'; import 'helpers.dart'; @@ -15,8 +16,7 @@ class ISBNDecodingWidget extends StatefulWidget { } class _ISBNDecodingWidgetState extends State { - // TODO: Don't use Map because operator[] accept Object as parameter instead of o Key type - Map>> isbns = {}; + KtMutableMap>> isbns = KtMutableMap.empty(); @override void initState() { @@ -50,7 +50,7 @@ class _ISBNDecodingWidgetState extends State { children: [ ImageWidget(imgPath), FutureBuilder( - future: isbns[imgPath]!, + future: isbns[imgPath.path]!, builder: (context, snap) { if (snap.hasData == false) { return const CircularProgressIndicator(); @@ -65,7 +65,7 @@ class _ISBNDecodingWidgetState extends State { Padding( padding: const EdgeInsets.all(8.0), child: FutureBuilder( - future: Future.wait(isbns.values), + future: Future.wait(isbns.values.iter), builder: (context, snap) { return ElevatedButton( onPressed: () { diff --git a/lib/main.dart b/lib/main.dart index c5527ce..293609e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,7 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/helpers.dart'; import 'ad_editing.dart'; -import 'bridge_definitions.dart'; import 'bundle.dart'; import 'bundle_selection.dart'; import 'isbn_decoding.dart'; @@ -45,8 +42,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - BookyStep step = //BundleSelectionStep(); - AdEditingStep( + BookyStep step = BundleSelectionStep(); + /*AdEditingStep( bundle: Bundle( Directory('/home/julien/Perso/LeBonCoin/chain_automatisation/open_cv_test/test_images/booky_example/normal')), metadata: { @@ -57,7 +54,7 @@ class _MyAppState extends State { 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', diff --git a/lib/metadata_collecting.dart b/lib/metadata_collecting.dart index ae29840..e1620e1 100644 --- a/lib/metadata_collecting.dart +++ b/lib/metadata_collecting.dart @@ -28,8 +28,9 @@ class MetadataCollectingWidget extends StatefulWidget { final MetadataCollectingStep step; final void Function(AdEditingStep newStep) onSubmit; - final blurbTextFieldController = TextEditingController(); final titleTextFieldController = TextEditingController(); + final authorsTextFieldController = TextEditingController(); + final blurbTextFieldController = TextEditingController(); final priceTextFieldController = TextEditingController(); @override @@ -57,6 +58,13 @@ class _MetadataCollectingWidgetState extends State { }); } + 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; @@ -79,6 +87,12 @@ class _MetadataCollectingWidgetState extends State { 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!); }); @@ -136,7 +150,7 @@ class _MetadataCollectingWidgetState extends State { FutureWidget( future: metadata[isbn]!.mdFromProviders.entries.first.value, builder: (data) => TextFormField( - initialValue: data?.authors.toText(), + controller: widget.authorsTextFieldController, onChanged: (newText) => setState(() => manual.authors = newText.split('\n').map((line) => Author(firstName: '', lastName: line)).toList()), decoration: const InputDecoration( diff --git a/pubspec.yaml b/pubspec.yaml index fe09f42..d68f81f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: 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 dev_dependencies: flutter_test: From cf4be4a006c0b001863ae091314d36645b78a204 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 19 Apr 2023 21:29:13 +0200 Subject: [PATCH 66/81] BundleSelection: sort images by date, antialiase --- lib/bundle_selection.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/bundle_selection.dart b/lib/bundle_selection.dart index c8bd3cf..0e9ca2e 100644 --- a/lib/bundle_selection.dart +++ b/lib/bundle_selection.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.dart'; import 'package:path/path.dart' as path; @@ -51,9 +52,14 @@ class BundleWidget extends StatelessWidget { .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: Image.file(f, height: 150), + child: Image.file( + f, + height: 150, + filterQuality: FilterQuality.medium, + ), )) .toList(), )), From 35010bed88eef019a6a32378e0439d1cc8d3bb79 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 19 Apr 2023 22:42:12 +0200 Subject: [PATCH 67/81] AdEditing: images are draggable, add "Mark as Publish" button --- lib/ad_editing.dart | 25 +++++++++++++++++++++++-- lib/bundle.dart | 7 ++++++- lib/bundle_selection.dart | 7 ++----- lib/draggable_files_widget.dart | 5 +++-- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index 60abf67..e5bd61d 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge_template/main.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'; @@ -143,10 +144,30 @@ class _AdEditingWidgetState extends State { color: Colors.grey, ), const SizedBox(width: 16), - DraggableFilesWidget(uris: ad.imgsPath.map((path) => Uri.file(path))), - ...ad.imgsPath.map((img) => ImageWidget(File(img))).toList(), + 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'), + )); + }, + child: const Text('Mark as published')) ], ), ), diff --git a/lib/bundle.dart b/lib/bundle.dart index 0c954e7..02fff96 100644 --- a/lib/bundle.dart +++ b/lib/bundle.dart @@ -1,6 +1,7 @@ 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; @@ -10,7 +11,11 @@ class Bundle { final Directory directory; Iterable get images { - return directory.listSync().whereType().where((file) => path.extension(file.path) == '.jpg'); + return directory + .listSync() + .whereType() + .where((file) => path.extension(file.path) == '.jpg') + .sorted((f1, f2) => f1.lastModifiedSync().compareTo(f2.lastModifiedSync())); } Metadata get metadata { diff --git a/lib/bundle_selection.dart b/lib/bundle_selection.dart index 0e9ca2e..00b6f2f 100644 --- a/lib/bundle_selection.dart +++ b/lib/bundle_selection.dart @@ -6,6 +6,7 @@ import 'package:flutter_rust_bridge_template/main.dart'; import 'package:path/path.dart' as path; import 'bundle.dart'; +import 'helpers.dart'; class BundleSelection extends StatelessWidget { const BundleSelection({required this.onSubmit}); @@ -55,11 +56,7 @@ class BundleWidget extends StatelessWidget { .sorted((f1, f2) => f1.lastModifiedSync().compareTo(f2.lastModifiedSync())) .map((f) => Padding( padding: const EdgeInsets.all(8.0), - child: Image.file( - f, - height: 150, - filterQuality: FilterQuality.medium, - ), + child: ImageWidget(f), )) .toList(), )), diff --git a/lib/draggable_files_widget.dart b/lib/draggable_files_widget.dart index 9a35d1c..8cb6c77 100644 --- a/lib/draggable_files_widget.dart +++ b/lib/draggable_files_widget.dart @@ -5,16 +5,17 @@ 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}); + 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: const Text('Drag and drop images'), + child: child, dragConfiguration: (location, session) async { Future getSnapshot(Offset location) async { final snapshotter = Snapshotter.of(context)!; From 87513e4fb5aab507673ccecc1b9864cf16917088 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 19 Apr 2023 22:55:25 +0200 Subject: [PATCH 68/81] Come back to BundleSelection after publish --- lib/ad_editing.dart | 3 ++- lib/main.dart | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ad_editing.dart b/lib/ad_editing.dart index e5bd61d..ffc81d8 100644 --- a/lib/ad_editing.dart +++ b/lib/ad_editing.dart @@ -14,7 +14,7 @@ import 'helpers.dart'; class AdEditingWidget extends StatefulWidget { const AdEditingWidget({required this.step, required this.onSubmit}); final AdEditingStep step; - final void Function(bool newStep) onSubmit; + final void Function() onSubmit; @override State createState() => _AdEditingWidgetState(); @@ -166,6 +166,7 @@ class _AdEditingWidgetState extends State { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Moved'), )); + widget.onSubmit(); }, child: const Text('Mark as published')) ], diff --git a/lib/main.dart b/lib/main.dart index 293609e..2aef8c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -78,9 +78,8 @@ class _MyAppState extends State { MetadataCollectingStep() => MetadataCollectingWidget( step: step as MetadataCollectingStep, onSubmit: (AdEditingStep newStep) => setState(() => step = newStep)), - AdEditingStep() => AdEditingWidget( - step: step as AdEditingStep, - onSubmit: (bool publishSuccess) => print('onSubmit with bool = $publishSuccess')), + AdEditingStep() => + AdEditingWidget(step: step as AdEditingStep, onSubmit: () => setState(() => step = BundleSelectionStep())), BookyStep() => throw UnimplementedError('Not possible') }); } From 9861383599af684f113882118c5628ea775259f9 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 19 Apr 2023 23:29:16 +0200 Subject: [PATCH 69/81] Fix ISBN decoding split on new line --- lib/isbn_decoding.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/isbn_decoding.dart b/lib/isbn_decoding.dart index 3a5daee..158002e 100644 --- a/lib/isbn_decoding.dart +++ b/lib/isbn_decoding.dart @@ -33,7 +33,7 @@ class _ISBNDecodingWidgetState extends State { throw Exception('decoder status is ${decoderProcess.exitCode}'); } final s = decoderProcess.stdout as String; - return s.split(' ').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); + return s.split('\n').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); }); }); } From e2ca44804233be70968dee3453816dbe75f214ec Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 27 Apr 2023 21:22:29 +0200 Subject: [PATCH 70/81] Move 4 steps into enrichment/ --- lib/{ => enrichment}/ad_editing.dart | 10 +-- lib/{ => enrichment}/bundle_selection.dart | 6 +- lib/enrichment/enrichment.dart | 82 ++++++++++++++++++ lib/{ => enrichment}/isbn_decoding.dart | 4 +- lib/{ => enrichment}/metadata_collecting.dart | 4 +- lib/main.dart | 83 +------------------ 6 files changed, 96 insertions(+), 93 deletions(-) rename lib/{ => enrichment}/ad_editing.dart (96%) rename lib/{ => enrichment}/bundle_selection.dart (94%) create mode 100644 lib/enrichment/enrichment.dart rename lib/{ => enrichment}/isbn_decoding.dart (97%) rename lib/{ => enrichment}/metadata_collecting.dart (99%) diff --git a/lib/ad_editing.dart b/lib/enrichment/ad_editing.dart similarity index 96% rename from lib/ad_editing.dart rename to lib/enrichment/ad_editing.dart index ffc81d8..ed3875d 100644 --- a/lib/ad_editing.dart +++ b/lib/enrichment/ad_editing.dart @@ -2,14 +2,14 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_template/main.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 '../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}); diff --git a/lib/bundle_selection.dart b/lib/enrichment/bundle_selection.dart similarity index 94% rename from lib/bundle_selection.dart rename to lib/enrichment/bundle_selection.dart index 00b6f2f..87bc253 100644 --- a/lib/bundle_selection.dart +++ b/lib/enrichment/bundle_selection.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_template/main.dart'; import 'package:path/path.dart' as path; -import 'bundle.dart'; -import 'helpers.dart'; +import '../bundle.dart'; +import '../helpers.dart'; +import 'enrichment.dart'; class BundleSelection extends StatelessWidget { const BundleSelection({required this.onSubmit}); diff --git a/lib/enrichment/enrichment.dart b/lib/enrichment/enrichment.dart new file mode 100644 index 0000000..3e49741 --- /dev/null +++ b/lib/enrichment/enrichment.dart @@ -0,0 +1,82 @@ +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)), + 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/isbn_decoding.dart b/lib/enrichment/isbn_decoding.dart similarity index 97% rename from lib/isbn_decoding.dart rename to lib/enrichment/isbn_decoding.dart index 158002e..6d52b04 100644 --- a/lib/isbn_decoding.dart +++ b/lib/enrichment/isbn_decoding.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_template/main.dart'; import 'package:kt_dart/collection.dart'; -import 'helpers.dart'; +import '../helpers.dart'; +import 'enrichment.dart'; class ISBNDecodingWidget extends StatefulWidget { const ISBNDecodingWidget({required this.step, required this.onSubmit}); diff --git a/lib/metadata_collecting.dart b/lib/enrichment/metadata_collecting.dart similarity index 99% rename from lib/metadata_collecting.dart rename to lib/enrichment/metadata_collecting.dart index e1620e1..f1182b7 100644 --- a/lib/metadata_collecting.dart +++ b/lib/enrichment/metadata_collecting.dart @@ -2,8 +2,8 @@ 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 'main.dart'; +import '../ffi.dart' if (dart.library.html) 'ffi_web.dart'; +import 'enrichment.dart'; const noneText = Text('None', style: TextStyle(fontStyle: FontStyle.italic)); diff --git a/lib/main.dart b/lib/main.dart index 2aef8c9..b29c5a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,86 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_template/helpers.dart'; -import 'ad_editing.dart'; -import 'bundle.dart'; -import 'bundle_selection.dart'; -import 'isbn_decoding.dart'; -import 'metadata_collecting.dart'; +import 'enrichment/enrichment.dart'; void main() { - runApp(const MyApp()); -} - -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 MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState 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)), - 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') - }); - } + runApp(const EnrichmentApp()); } From 08a0e307ff842edbf5cdfcc973e525741c1caad0 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 28 Apr 2023 00:02:24 +0200 Subject: [PATCH 71/81] Merge Camera_app. Compile on Android --- android/app/build.gradle | 4 +- android/gradle.properties | 2 +- lib/camera/camera.dart | 464 +++++++++++++++++++++++++++++++ lib/camera/draggable_widget.dart | 44 +++ lib/common.dart | 4 +- lib/main.dart | 31 ++- native/Cargo.toml | 2 +- pubspec.yaml | 4 + 8 files changed, 549 insertions(+), 6 deletions(-) create mode 100644 lib/camera/camera.dart create mode 100644 lib/camera/draggable_widget.dart 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/gradle.properties b/android/gradle.properties index 5c76181..ddbbffb 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -ANDROID_NDK=/home/julien/Android/Sdk/ndk-bundle/ +ANDROID_NDK=/home/julien/Android/Sdk/ndk/ diff --git a/lib/camera/camera.dart b/lib/camera/camera.dart new file mode 100644 index 0000000..9fd6f04 --- /dev/null +++ b/lib/camera/camera.dart @@ -0,0 +1,464 @@ +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); + + _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 index cb56f1b..1e0f1c0 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -1,9 +1,11 @@ -// TODO: merge with same file in camera_app +import 'dart:io'; import 'package:json_annotation/json_annotation.dart'; part 'common.g.dart'; +final bookyDir = Directory('/storage/emulated/0/DCIM/booky/'); + enum ItemState { brandNew, veryGood, diff --git a/lib/main.dart b/lib/main.dart index b29c5a8..52c6d9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,36 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge_template/camera/camera.dart'; import 'enrichment/enrichment.dart'; void main() { - runApp(const EnrichmentApp()); + runApp(const BookyApp()); +} + +enum BookyAppActivity { + camera, + enrichment, +} + +class BookyApp extends StatefulWidget { + const BookyApp(); + + @override + State createState() => _BookyAppState(); +} + +class _BookyAppState extends State { + BookyAppActivity activity = Platform.isAndroid ? BookyAppActivity.camera : BookyAppActivity.enrichment; + + @override + Widget build(BuildContext context) { + switch (activity) { + case BookyAppActivity.camera: + return const CameraApp(); + case BookyAppActivity.enrichment: + return const EnrichmentApp(); + } + } } diff --git a/native/Cargo.toml b/native/Cargo.toml index c38db3c..bb2b159 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -14,7 +14,7 @@ flutter_rust_bridge = "1" base64 = "0.21.0" itertools = "0.10.5" regex = "1.7.1" -reqwest = { version = "0.11.14", features = ["blocking", "json", "multipart"] } +reqwest = { version = "0.11.14", default-features = false, features = ["blocking", "json", "rustls-tls", "multipart"] } scraper = "0.14.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.91" diff --git a/pubspec.yaml b/pubspec.yaml index d68f81f..dd9fdbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,10 @@ dependencies: 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: From 6751dcfa2dfad330e7593ee9047a62fd92ef1922 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 28 Apr 2023 00:09:51 +0200 Subject: [PATCH 72/81] Camera: initialize _camera list --- lib/camera/camera.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/camera/camera.dart b/lib/camera/camera.dart index 9fd6f04..c5cccaa 100644 --- a/lib/camera/camera.dart +++ b/lib/camera/camera.dart @@ -59,7 +59,15 @@ class _CameraExampleHomeState extends State with WidgetsBindi _generateNewFolderPath(); WidgetsBinding.instance.addObserver(this); - _onNewCameraSelected(_cameras.first); + Future(() async { + try { + // WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + _onNewCameraSelected(_cameras.first); + }); } @override From 707373a46c6d759678948f84ae854a97c274123f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 28 Apr 2023 23:45:03 +0200 Subject: [PATCH 73/81] Add storage permission to write metadata.json --- android/app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 @@ - + From f3429498901ded8165a9b77185cfc2f2824feea0 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 28 Apr 2023 23:47:34 +0200 Subject: [PATCH 74/81] Better explorer, use Card --- lib/common.dart | 4 +- lib/enrichment/bundle_selection.dart | 58 ++++++++++++++++------------ lib/helpers.dart | 2 +- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/lib/common.dart b/lib/common.dart index 1e0f1c0..4ef9a58 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -4,7 +4,9 @@ import 'package:json_annotation/json_annotation.dart'; part 'common.g.dart'; -final bookyDir = Directory('/storage/emulated/0/DCIM/booky/'); +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, diff --git a/lib/enrichment/bundle_selection.dart b/lib/enrichment/bundle_selection.dart index 87bc253..3b9559f 100644 --- a/lib/enrichment/bundle_selection.dart +++ b/lib/enrichment/bundle_selection.dart @@ -5,6 +5,7 @@ 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'; @@ -15,17 +16,16 @@ class BundleSelection extends StatelessWidget { @override Widget build(BuildContext context) { - final bundleDirs = - Directory('/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RFCRA1CG6KT/Internal storage/DCIM/booky/') - .listSync() - .whereType(); + final bundleDirs = common.bookyDir.listSync().whereType().sorted((d1, d2) => d1.path.compareTo(d2.path)); return Scaffold( appBar: AppBar(title: const Text('Bundle Section')), - body: Wrap( + body: GridView.extent( + maxCrossAxisExtent: 500, + childAspectRatio: 2, children: bundleDirs .map((d) => Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(2.0), child: GestureDetector( child: BundleWidget(d), onTap: () => onSubmit(ISBNDecodingStep(bundle: Bundle(d))), @@ -44,24 +44,34 @@ class BundleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - Container( - decoration: const BoxDecoration(color: Colors.blue), - child: Wrap( - 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(), - )), - Text(path.basename(directory.path)) - ], + 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: () {}, + ), + ], + ), + ), + ], + ), ); } } diff --git a/lib/helpers.dart b/lib/helpers.dart index e8e20c1..ecd8114 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -12,7 +12,7 @@ class ImageWidget extends StatelessWidget { Widget build(BuildContext context) { return Image.file( image, - height: 200, + fit: BoxFit.fitHeight, isAntiAlias: true, filterQuality: FilterQuality.medium, ); From c2a009a3964e1b86a8200c462d5ea0e28ad2f490 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 28 Apr 2023 23:57:02 +0200 Subject: [PATCH 75/81] Better ISBNDecoding ui --- lib/enrichment/isbn_decoding.dart | 37 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/enrichment/isbn_decoding.dart b/lib/enrichment/isbn_decoding.dart index 6d52b04..1a8c4fc 100644 --- a/lib/enrichment/isbn_decoding.dart +++ b/lib/enrichment/isbn_decoding.dart @@ -44,23 +44,26 @@ class _ISBNDecodingWidgetState extends State { body: Column( children: [ Wrap( - children: [ - ...widget.step.bundle.images - .map((imgPath) => Column( - children: [ - 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(), - ], + 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), From 7fd9a64739843be69b91e1e17bde24200b53d140 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 29 Apr 2023 00:11:54 +0200 Subject: [PATCH 76/81] BundleSelection: delete button move to booky_deleted --- lib/enrichment/bundle_selection.dart | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/enrichment/bundle_selection.dart b/lib/enrichment/bundle_selection.dart index 3b9559f..7b573ff 100644 --- a/lib/enrichment/bundle_selection.dart +++ b/lib/enrichment/bundle_selection.dart @@ -9,11 +9,16 @@ import '../common.dart' as common; import '../helpers.dart'; import 'enrichment.dart'; -class BundleSelection extends StatelessWidget { +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) { final bundleDirs = common.bookyDir.listSync().whereType().sorted((d1, d2) => d1.path.compareTo(d2.path)); @@ -27,8 +32,10 @@ class BundleSelection extends StatelessWidget { .map((d) => Padding( padding: const EdgeInsets.all(2.0), child: GestureDetector( - child: BundleWidget(d), - onTap: () => onSubmit(ISBNDecodingStep(bundle: Bundle(d))), + child: BundleWidget(d, onDelete: () { + setState(() {}); + }), + onTap: () => widget.onSubmit(ISBNDecodingStep(bundle: Bundle(d))), ), )) .toList(), @@ -38,9 +45,10 @@ class BundleSelection extends StatelessWidget { } class BundleWidget extends StatelessWidget { - const BundleWidget(this.directory); + const BundleWidget(this.directory, {required this.onDelete}); final Directory directory; + final void Function() onDelete; @override Widget build(BuildContext context) { @@ -65,7 +73,15 @@ class BundleWidget extends StatelessWidget { const Expanded(child: SizedBox.expand()), IconButton( icon: const Icon(Icons.delete), - onPressed: () {}, + 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(); + }, ), ], ), From bb1fd7fa93ad225428e80d1d18134436cde2ec1f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 29 Apr 2023 00:43:50 +0200 Subject: [PATCH 77/81] Bundle Selection: Better message if no device connected --- lib/enrichment/bundle_selection.dart | 34 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/enrichment/bundle_selection.dart b/lib/enrichment/bundle_selection.dart index 7b573ff..86910a3 100644 --- a/lib/enrichment/bundle_selection.dart +++ b/lib/enrichment/bundle_selection.dart @@ -21,11 +21,15 @@ class BundleSelection extends StatefulWidget { class _BundleSelectionState extends State { @override Widget build(BuildContext context) { - final bundleDirs = common.bookyDir.listSync().whereType().sorted((d1, d2) => d1.path.compareTo(d2.path)); + 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 Scaffold( - appBar: AppBar(title: const Text('Bundle Section')), - body: GridView.extent( + return GridView.extent( maxCrossAxisExtent: 500, childAspectRatio: 2, children: bundleDirs @@ -39,8 +43,26 @@ class _BundleSelectionState extends State { ), )) .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(() {}); + }, + ), + ], + ), + ); + } } } From 7defe21120abdac6d5a85ac057d1984930ff8187 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 29 Apr 2023 01:04:52 +0200 Subject: [PATCH 78/81] ISBNDecding: Add AppBar and back button --- lib/enrichment/enrichment.dart | 3 ++- lib/enrichment/isbn_decoding.dart | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/enrichment/enrichment.dart b/lib/enrichment/enrichment.dart index 3e49741..a98f98b 100644 --- a/lib/enrichment/enrichment.dart +++ b/lib/enrichment/enrichment.dart @@ -70,7 +70,8 @@ class _EnrichmentAppState extends State { BundleSelection(onSubmit: (ISBNDecodingStep newStep) => setState(() => step = newStep)), ISBNDecodingStep() => ISBNDecodingWidget( step: step as ISBNDecodingStep, - onSubmit: (MetadataCollectingStep newStep) => setState(() => step = newStep)), + onSubmit: (MetadataCollectingStep newStep) => setState(() => step = newStep), + onBack: () => setState(() => step = BundleSelectionStep())), MetadataCollectingStep() => MetadataCollectingWidget( step: step as MetadataCollectingStep, onSubmit: (AdEditingStep newStep) => setState(() => step = newStep)), diff --git a/lib/enrichment/isbn_decoding.dart b/lib/enrichment/isbn_decoding.dart index 1a8c4fc..8365e70 100644 --- a/lib/enrichment/isbn_decoding.dart +++ b/lib/enrichment/isbn_decoding.dart @@ -7,9 +7,10 @@ import '../helpers.dart'; import 'enrichment.dart'; class ISBNDecodingWidget extends StatefulWidget { - const ISBNDecodingWidget({required this.step, required this.onSubmit}); + 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(); @@ -41,6 +42,12 @@ class _ISBNDecodingWidgetState extends State { @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( From a992c39b4db782a48def71b7443408683b378a2c Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 30 Apr 2023 18:15:00 +0200 Subject: [PATCH 79/81] Add README --- README.md | 136 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 50 deletions(-) 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 From b1501df7ada7c825eab922d27e960a096624280f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 30 Apr 2023 18:23:53 +0200 Subject: [PATCH 80/81] Add TODO --- TODO.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 TODO.md 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) From f2dce6ff8579a55370a0d0534f090cdfee2ade07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Apr 2023 16:25:49 +0000 Subject: [PATCH 81/81] Update scraper requirement from 0.14.0 to 0.16.0 in /native Updates the requirements on [scraper](https://github.com/causal-agent/scraper) to permit the latest version. - [Release notes](https://github.com/causal-agent/scraper/releases) - [Commits](https://github.com/causal-agent/scraper/compare/v0.14.0...v0.16.0) --- updated-dependencies: - dependency-name: scraper dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- native/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Cargo.toml b/native/Cargo.toml index bb2b159..af08ea5 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -15,7 +15,7 @@ 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.14.0" +scraper = "0.16.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.91" mockito = "1.0.0"