Merge branch 'master' of github.com:AnonymusRaccoon/Aeris into multilng-about-json

This commit is contained in:
Clément Le Bihan
2022-03-04 16:32:57 +01:00
48 changed files with 1303 additions and 509 deletions

View File

@@ -4,6 +4,8 @@ POSTGRES_PORT=
POSTGRES_DB= POSTGRES_DB=
POSTGRES_HOST= POSTGRES_HOST=
WORKER_API_KEY= WORKER_API_KEY=
HOSTNAME=aeris.com
WORKER_API_URL= WORKER_API_URL=
WORKER_URL= WORKER_URL=
DISCORD_CLIENT_ID= DISCORD_CLIENT_ID=
@@ -18,3 +20,4 @@ SPOTIFY_CLIENT_ID=
SPOTIFY_SECRET= SPOTIFY_SECRET=
ANILIST_SECRET= ANILIST_SECRET=
ANILIST_CLIENT_ID= ANILIST_CLIENT_ID=
BACK_URL=

View File

@@ -20,7 +20,6 @@ jobs:
- name: Run Docker - name: Run Docker
run: docker run -v $PWD:/dist aeris_mobile_build run: docker run -v $PWD:/dist aeris_mobile_build
- name: Upload build artifact - name: Upload build artifact
if: github.ref == 'refs/head/master'
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: aeris_apk name: aeris_apk

View File

@@ -3,7 +3,7 @@
"actions": [], "actions": [],
"reactions": [ "reactions": [
{ {
"name": "UpdateAbout", "name": "Anilist_UpdateAbout",
"description": { "description": {
"en": "Update the about you section.", "en": "Update the about you section.",
"fr": "Mets à jour votre section \"À propos de moi\"." "fr": "Mets à jour votre section \"À propos de moi\"."
@@ -25,7 +25,7 @@
"returns": [] "returns": []
}, },
{ {
"name": "ToggleFavourite", "name": "Anilist_ToggleFavourite",
"description": { "description": {
"en": "Add or remove an anime from your favorite.", "en": "Add or remove an anime from your favorite.",
"fr": "Ajoute ou retire un animé de vos favoris." "fr": "Ajoute ou retire un animé de vos favoris."

View File

@@ -69,6 +69,7 @@
], ],
"reactions": [ "reactions": [
{ {
<<<<<<< HEAD
"name": "PlayTrack", "name": "PlayTrack",
"description": { "description": {
"en": "Play a track", "en": "Play a track",
@@ -78,6 +79,10 @@
"en": "Play a track", "en": "Play a track",
"fr": "Joue une musique" "fr": "Joue une musique"
}, },
=======
"name": "Spotify_PlayTrack",
"description": "Play a track",
>>>>>>> 26566154676c8e279c3615522129e61b851c75a9
"params": [ "params": [
{ {
"name": "artist", "name": "artist",
@@ -134,6 +139,7 @@
"returns": [] "returns": []
}, },
{ {
<<<<<<< HEAD
"name": "AddTrackToLibrary", "name": "AddTrackToLibrary",
"description": { "description": {
"en": "Add a track to library", "en": "Add a track to library",
@@ -143,6 +149,10 @@
"en": "Add a song to library", "en": "Add a song to library",
"fr": "Ajoute une musique à votre librarie" "fr": "Ajoute une musique à votre librarie"
}, },
=======
"name": "Spotify_AddTrackToLibrary",
"description": "Add a track to library",
>>>>>>> 26566154676c8e279c3615522129e61b851c75a9
"params": [ "params": [
{ {
"name": "artist", "name": "artist",

View File

@@ -40,34 +40,34 @@ urlHandler :: Service -> Maybe String -> AppM NoContent
urlHandler _ Nothing = throwError err400 urlHandler _ Nothing = throwError err400
urlHandler Anilist (Just r) = do urlHandler Anilist (Just r) = do
clientId <- liftIO $ envAsString "ANILIST_CLIENT_ID" "" clientId <- liftIO $ envAsString "ANILIST_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://anilist.co/api/v2/oauth/authorize?client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://anilist.co/api/v2/oauth/authorize?client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Discord (Just r) = do urlHandler Discord (Just r) = do
clientId <- liftIO $ envAsString "DISCORD_CLIENT_ID" "" clientId <- liftIO $ envAsString "DISCORD_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://discord.com/api/oauth2/authorize?response_type=code&scope=identify&client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://discord.com/api/oauth2/authorize?response_type=code&scope=identify%20guilds%20messages.read%20activities.write%20webhook.incoming&client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Google (Just r) = do urlHandler Google (Just r) = do
clientId <- liftIO $ envAsString "GOOGLE_CLIENT_ID" "" clientId <- liftIO $ envAsString "GOOGLE_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/youtube.force-ssl&access_type=offline&prompt=consent&include_granted_scopes=true&response_type=code&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/youtube.force-ssl&access_type=offline&prompt=consent&include_granted_scopes=true&response_type=code&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Twitter (Just r) = do urlHandler Twitter (Just r) = do
clientId <- liftIO $ envAsString "TWITTER_CLIENT_ID" "" clientId <- liftIO $ envAsString "TWITTER_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://twitter.com/i/oauth2/authorize?response_type=code&scope=like.write like.read follows.read follows.write offline.access tweet.read tweet.write&code_challenge=challenge&code_challenge_method=plain&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://twitter.com/i/oauth2/authorize?response_type=code&scope=like.write like.read follows.read follows.write offline.access tweet.read tweet.write&code_challenge=challenge&code_challenge_method=plain&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Spotify (Just r) = do urlHandler Spotify (Just r) = do
clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" "" clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Github (Just r) = do urlHandler Github (Just r) = do
clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" "" clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" "" backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders = throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://github.com/login/oauth/authorize?response_type=code&scope=repo&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "&state=" ++ r)] } [("Location", B8.pack $ "https://github.com/login/oauth/authorize?response_type=code&scope=repo&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
servicesHandler :: AuthRes -> AppM [String] servicesHandler :: AuthRes -> AppM [String]
servicesHandler (Authenticated (User uid name slug)) = do servicesHandler (Authenticated (User uid name slug)) = do

View File

@@ -19,6 +19,7 @@ services:
depends_on: depends_on:
- "db" - "db"
environment: environment:
- BACK_URL=${BACK_URL}
- POSTGRES_USER=${POSTGRES_USER} - POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_HOST=${POSTGRES_HOST} - POSTGRES_HOST=${POSTGRES_HOST}
@@ -59,7 +60,6 @@ services:
- ANILIST_SECRET=${ANILIST_SECRET} - ANILIST_SECRET=${ANILIST_SECRET}
- WORKER_API_URL=${WORKER_API_URL} - WORKER_API_URL=${WORKER_API_URL}
- WORKER_API_KEY=${WORKER_API_KEY} - WORKER_API_KEY=${WORKER_API_KEY}
volumes: volumes:
apk: apk:
cache: cache:

View File

@@ -42,6 +42,7 @@ services:
- POSTGRES_PORT=${POSTGRES_PORT} - POSTGRES_PORT=${POSTGRES_PORT}
- WORKER_API_KEY=${WORKER_API_KEY} - WORKER_API_KEY=${WORKER_API_KEY}
- WORKER_URL=${WORKER_URL} - WORKER_URL=${WORKER_URL}
- BACK_URL=${BACK_URL}
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID} - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- DISCORD_SECRET=${DISCORD_SECRET} - DISCORD_SECRET=${DISCORD_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}

View File

@@ -21,5 +21,5 @@ RUN flutter gen-l10n
RUN flutter pub run flutter_launcher_icons:main RUN flutter pub run flutter_launcher_icons:main
# Generate native splashscreen # Generate native splashscreen
RUN flutter pub run flutter_native_splash:create RUN flutter pub run flutter_native_splash:create
RUN flutter build apk lib/src/main.dart RUN flutter build apk lib/main.dart
CMD cp ./build/app/outputs/flutter-apk/app-release.apk /dist/aeris_android.apk CMD cp ./build/app/outputs/flutter-apk/app-release.apk /dist/aeris_android.apk

View File

@@ -1,7 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aeris.mobile"> package="com.aeris.mobile">
<!-- Flutter needs it to communicate with the running application <uses-permission android:name="android.permission.INTERNET" />
to allow setting breakpoints, to provide hot reload, etc. <application
--> android:label="Aeris"
<uses-permission android:name="android.permission.INTERNET"/> android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="arthichaud.me" />
<data android:scheme="https" android:host="arthichaud.me"/>
<data android:scheme="aeris" android:host="arthichaud.me"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest> </manifest>

View File

@@ -25,6 +25,16 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="arthichaud.me" />
<data android:scheme="https" android:host="arthichaud.me"/>
<data android:scheme="aeris" android:host="arthichaud.me"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@@ -1,10 +1,14 @@
PODS: PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
- Flutter - Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- sqflite (0.0.2): - sqflite (0.0.2):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
@@ -13,7 +17,9 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -24,8 +30,12 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
path_provider_ios: path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios: url_launcher_ios:
@@ -33,8 +43,10 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de

View File

@@ -18,6 +18,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0B9CFEF16F77B4F2467EF56A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 0B9CFEF16F77B4F2467EF56A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
0C6C3D7227D0D7C100B12C20 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
@@ -99,6 +100,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C6C3D7227D0D7C100B12C20 /* RunnerDebug.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -217,7 +219,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
}; };
755B6DB6A94E09EAD68F291E /* [CP] Embed Pods Frameworks */ = { 755B6DB6A94E09EAD68F291E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@@ -248,7 +250,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@@ -476,6 +478,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = HJ45QP4WWR; DEVELOPMENT_TEAM = HJ45QP4WWR;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;

View File

@@ -1,17 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon.jpg", "filename" : "BrandingImage.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "icon-1.jpg", "filename" : "BrandingImage@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "icon-2.jpg", "filename" : "BrandingImage@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

View File

@@ -47,5 +47,20 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>arthichaud.me</string>
<key>CFBundleURLSchemes</key>
<array>
<string>aeris</string>
</array>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -45,5 +45,20 @@
<false/> <false/>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<false/> <false/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>arthichaud.me</string>
<key>CFBundleURLSchemes</key>
<array>
<string>aeris</string>
</array>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
</dict>
</plist>

View File

@@ -36,5 +36,12 @@
"pipelineFormMisingAction": "You must select at least a trigger and a reaction", "pipelineFormMisingAction": "You must select at least a trigger and a reaction",
"logoutWarningMessage": "You are about to logout, are you sure?", "logoutWarningMessage": "You are about to logout, are you sure?",
"warning": "Warning", "warning": "Warning",
"cancel": "Cancel" "cancel": "Cancel",
"errorOnSignup": "An error occured while signing you up, please try again",
"loading": "Loading...",
"invalidUrl": "Invalid URL",
"tryToConnect": "Try to connect",
"routeToApi": "Route to API",
"setupAPIRoute": "Setup API Route",
"paramInheritTip": "To inherit parameters from previous actions, type '{' in the text field and tap on the choosen parameter"
} }

View File

@@ -6,8 +6,8 @@
"today": "Aujourd'hui", "today": "Aujourd'hui",
"nameOfThePipeline": "Nom de la pipeline", "nameOfThePipeline": "Nom de la pipeline",
"addReaction": "Ajouter une Reaction", "addReaction": "Ajouter une Reaction",
"addTrigger": "Ajouter un Déclancheur", "addTrigger": "Ajouter un déclencheur",
"setupTrigger": "Gérer une Déclancheur", "setupTrigger": "Gérer un déclencheur",
"setupReaction": "Gérer une Réaction", "setupReaction": "Gérer une Réaction",
"action": "Action", "action": "Action",
"reactions": "Réactions", "reactions": "Réactions",
@@ -33,8 +33,15 @@
"dangerZone": "Zone dangereuse", "dangerZone": "Zone dangereuse",
"createNewPipeline": "Créer une nouvelle pipeline", "createNewPipeline": "Créer une nouvelle pipeline",
"save": "Enregistrer", "save": "Enregistrer",
"pipelineFormMisingAction": "Vous devez selectionner au moins un déclancheur et une réaction", "pipelineFormMisingAction": "Vous devez selectionner au moins un déclencheur et une réaction",
"logoutWarningMessage": "Êtes-vous sûr(e) de voulour vous déconnecter d'Aeris?", "logoutWarningMessage": "Êtes-vous sûr(e) de voulour vous déconnecter d'Aeris?",
"warning": "Attention", "warning": "Attention",
"cancel": "Annuler" "cancel": "Annuler",
"errorOnSignup": "Une erreur est survenue, veuillez réessayer",
"loading": "Chargement...",
"invalidUrl": "URL invalide",
"tryToConnect": "Tester la connection",
"routeToApi": "Route de l'API",
"setupAPIRoute": "Choisir la route de l'API",
"paramInheritTip": "Afin d'hériter de variables venant d'actions précedentes, entrez '{' dans un champ et choisissez la valeur"
} }

View File

@@ -1,8 +1,10 @@
import 'package:aeris/src/aeris_api.dart'; import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/providers/action_catalogue_provider.dart';
import 'package:aeris/src/views/authorization_page.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:form_builder_validators/localization/l10n.dart'; import 'package:form_builder_validators/localization/l10n.dart';
import 'package:aeris/src/providers/pipelines_provider.dart'; import 'package:aeris/src/providers/pipelines_provider.dart';
import 'package:aeris/src/providers/user_services_provider.dart'; import 'package:aeris/src/providers/services_provider.dart';
import 'package:aeris/src/views/startup_page.dart'; import 'package:aeris/src/views/startup_page.dart';
import 'package:aeris/src/views/login_page.dart'; import 'package:aeris/src/views/login_page.dart';
import 'package:aeris/src/views/home_page.dart'; import 'package:aeris/src/views/home_page.dart';
@@ -11,13 +13,19 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() { void main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
GetIt.I.registerSingleton<SharedPreferences>(prefs);
AerisAPI interface = AerisAPI(); AerisAPI interface = AerisAPI();
GetIt.I.registerSingleton<AerisAPI>(interface); GetIt.I.registerSingleton<AerisAPI>(interface);
await interface.restoreConnection();
runApp(MultiProvider(providers: [ runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: (_) => PipelineProvider()), ChangeNotifierProvider(create: (_) => PipelineProvider()),
ChangeNotifierProvider(create: (_) => UserServiceProvider()) ChangeNotifierProvider(create: (_) => ServiceProvider()),
ChangeNotifierProvider(create: (_) => ActionCatalogueProvider(), lazy: false)
], child: const Aeris())); ], child: const Aeris()));
} }
@@ -48,22 +56,23 @@ class Aeris extends StatelessWidget {
'/login': () => const LoginPage(), '/login': () => const LoginPage(),
'/home': () => const HomePage(), '/home': () => const HomePage(),
}; };
return PageRouteBuilder( return PageRouteBuilder(
opaque: false, opaque: false,
settings: settings, settings: settings,
pageBuilder: (_, __, ___) => routes[settings.name].call(), pageBuilder: (_, __, ___) {
if (settings.name!.startsWith('/authorization')) {
return const AuthorizationPage();
}
return routes[settings.name].call();
},
transitionDuration: const Duration(milliseconds: 350), transitionDuration: const Duration(milliseconds: 350),
transitionsBuilder: (context, animation, secondaryAnimation, child) => transitionsBuilder: (context, animation, secondaryAnimation,
SlideTransition( child) =>
child: child, SlideTransition(
position: animation.drive( child: child,
Tween( position: animation.drive(Tween(
begin: const Offset(1.0, 0.0), begin: const Offset(1.0, 0.0), end: Offset.zero))));
end: Offset.zero
)
)
)
);
}); });
} }
} }

View File

@@ -1,127 +1,249 @@
// ignore_for_file: unused_import
import 'dart:async'; import 'dart:async';
import 'package:aeris/src/models/action.dart'; import 'dart:convert';
import 'dart:io';
import 'package:aeris/main.dart';
import 'package:aeris/src/models/action.dart' as aeris;
import 'package:aeris/src/models/action_parameter.dart';
import 'package:aeris/src/models/action_template.dart'; import 'package:aeris/src/models/action_template.dart';
import 'package:aeris/src/models/pipeline.dart'; import 'package:aeris/src/models/pipeline.dart';
import 'package:aeris/src/models/reaction.dart'; import 'package:aeris/src/models/reaction.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
import 'package:aeris/src/models/trigger.dart'; import 'package:aeris/src/models/trigger.dart';
import 'package:aeris/src/providers/action_catalogue_provider.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
extension IsOk on http.Response {
bool get ok => (statusCode ~/ 100) == 2;
}
/// Requests types supported by Aeris API
enum AerisAPIRequestType { get, post, put, delete }
/// Call to interact with Aeris' Back end /// Call to interact with Aeris' Back end
class AerisAPI { class AerisAPI {
///TODO set status based on stored credentials /// Get Connection state
bool connected = true; bool _connected = false;
late List<Pipeline> fakeAPI; bool get isConnected => _connected;
/// JWT token used to request API
late String _jwt;
late final String deepLinkRoute;
String _baseRoute =
GetIt.I<SharedPreferences>().getString('api') ?? "http://localhost:8080";
String get baseRoute => _baseRoute;
set baseRoute(value) => _baseRoute = value;
AerisAPI() { AerisAPI() {
var trigger1 = Trigger( var scheme = "http";
service: const Service.spotify(), if (Platform.isIOS) {
name: "Play song", scheme = "aeris";
last: DateTime.now()); }
var trigger3 = Trigger( deepLinkRoute = "$scheme://arthichaud.me";
service: const Service.discord(),
name: "Send a message",
last: DateTime.now());
var trigger2 = Trigger(
service: const Service.spotify(),
name: "Play song",
last: DateTime.parse("2022-01-01"));
var reaction = Reaction(
service: const Service.twitter(), parameters: {}, name: "Post a tweet");
var reaction2 = Reaction(
service: const Service.gmail(), parameters: {}, name: "Do smth");
var reaction1 = Reaction(
service: const Service.youtube(), parameters: {}, name: "Do smth youtube");
var pipeline1 = Pipeline(
id: 10,
name: "My Action",
triggerCount: 1,
enabled: true,
trigger: trigger1,
reactions: [reaction]);
var pipeline2 = Pipeline(
id: 10,
name: "My very long action Action",
triggerCount: 10,
enabled: true,
trigger: trigger2,
reactions: [reaction, reaction1, reaction2]);
var pipeline3 = Pipeline(
id: 10,
name: "Disabled",
triggerCount: 3,
enabled: false,
trigger: trigger3,
reactions: [reaction]);
fakeAPI = [
pipeline3,
pipeline2,
pipeline1,
pipeline3,
pipeline2,
pipeline1,
pipeline3,
pipeline2,
pipeline1
];
} }
/// Adds new pipeline to API /// Name of the file that contains the JWT used for Aeris' API requestd
Future<void> createPipeline(Pipeline newPipeline) async { static const String jwtFile = 'aeris_jwt.txt';
///TODO Send Pipeline to API
fakeAPI.add(newPipeline); ///ROUTES
await Future.delayed(const Duration(seconds: 2)); /// Registers new user in the database and connects it. Returns false if register failed
return; Future<bool> signUpUser(String username, String password) async {
http.Response response =
await _requestAPI('/auth/signup', AerisAPIRequestType.post, {
'username': username,
'password': password,
});
if (!response.ok) {
return false;
}
return createConnection(username, password);
}
/// On success, sets API as connected to given user. Returns false if connection false
Future<bool> createConnection(String username, String password) async {
http.Response response =
await _requestAPI('/auth/login', AerisAPIRequestType.post, {
'username': username,
'password': password,
});
if (!response.ok) {
return false;
}
try {
final String jwt = jsonDecode(response.body)['jwt'];
await GetIt.I<SharedPreferences>().setString('jwt', jwt);
_connected = true;
_jwt = jwt;
} catch (e) {
return false;
}
return true;
}
/// Create an API connection using previously created credentials
Future<void> restoreConnection() async {
try {
final cred = GetIt.I<SharedPreferences>().getString('jwt');
if (cred == "" || cred == null) {
throw Exception("Empty creds");
}
_jwt = cred;
_connected = true;
} catch (e) {
return;
}
}
/// Delete JWT file and disconnect from API
Future<void> stopConnection() async {
await GetIt.I<SharedPreferences>().remove('jwt');
_connected = false;
}
///Get /about.json
Future<Map<String, dynamic>> getAbout() async {
var res = await _requestAPI('/about.json', AerisAPIRequestType.get, null);
if (!res.ok) return {};
return jsonDecode(res.body);
}
/// Adds new pipeline to API, returns false if post failed
Future<bool> createPipeline(Pipeline newPipeline) async {
var res = await _requestAPI(
'/workflow', AerisAPIRequestType.post, newPipeline.toJSON());
newPipeline.id = Pipeline.fromJSON(jsonDecode(res.body)).id;
return res.ok;
} }
/// Removes pipeline from API /// Removes pipeline from API
Future<void> removePipeline(Pipeline pipeline) async { Future<bool> removePipeline(Pipeline pipeline) async {
///TODO Send delete request to API var res = await _requestAPI(
fakeAPI.remove(pipeline); '/workflow/${pipeline.id}', AerisAPIRequestType.delete, null);
await Future.delayed(const Duration(seconds: 2)); return res.ok;
return;
} }
Future<void> editPipeline(Pipeline updatedPipeline) async { String getServiceAuthURL(Service service) {
///TODO Send update request to API final serviceName = service == const Service.youtube()
for (var pipeline in fakeAPI) { ? "google"
if (pipeline.id == updatedPipeline.id) { : service.name.toLowerCase();
///TODO Call Api return "$baseRoute/auth/$serviceName/url?redirect_uri=$deepLinkRoute/authorization/$serviceName";
break; }
}
}
await Future.delayed(const Duration(seconds: 2)); /// Send PUT request to update Pipeline, returns false if failed
return; Future<bool> editPipeline(Pipeline updatedPipeline) async {
var res = await _requestAPI('/workflow/${updatedPipeline.id}',
AerisAPIRequestType.put, updatedPipeline.toJSON());
return res.ok;
} }
/// Fetches the Pipelines from the API /// Fetches the Pipelines from the API
Future<List<Pipeline>> getPipelines() async { Future<List<Pipeline>> getPipelines() async {
/// TODO Fetch the API var res = await _requestAPI('/workflows', AerisAPIRequestType.get, null);
await Future.delayed(const Duration(seconds: 2)); if (res.ok == false) return [];
return fakeAPI; final List body = jsonDecode(res.body);
return body.map((e) => Pipeline.fromJSON(Map.from(e))).toList();
}
/// Fetch the services the user is authenticated to
Future<List<Service>> getConnectedService() async {
var res =
await _requestAPI('/auth/services', AerisAPIRequestType.get, null);
if (!res.ok) return [];
return (jsonDecode(res.body) as List)
.map((e) => Service.factory(e.toString()))
.toList();
} }
/// Disconnects the user from the service /// Disconnects the user from the service
Future<void> disconnectService(Service service) async { Future<bool> disconnectService(Service service) async {
///TODO disconnect service from user var res = await _requestAPI('/auth/${service.name.toLowerCase()}',
await Future.delayed(const Duration(seconds: 2)); AerisAPIRequestType.delete, null);
return; return res.ok;
} }
Future<List<ActionTemplate>> getActionsFor( /// Connects the user from the service
Service service, Action action) async { Future<bool> connectService(Service service, String code) async {
await Future.delayed(const Duration(seconds: 3)); var res = await _requestAPI(
'/auth/${service.name.toLowerCase()}?code=$code&redirect_uri=$deepLinkRoute/authorization/${service.name.toLowerCase()}',
AerisAPIRequestType.get,
null);
return res.ok;
}
List<ActionTemplate> getActionsFor(Service service, aeris.Action action) {
final catalogue =
Aeris.materialKey.currentContext?.read<ActionCatalogueProvider>();
if (action is Trigger) { if (action is Trigger) {
///TODO get triggers return catalogue!.triggerTemplates[service]!;
} else if (action is Reaction) { }
///TODO get reactions return catalogue!.reactionTemplates[service]!;
}
/// Encodes Uri for request
Uri _encoreUri(String route) {
return Uri.parse('$_baseRoute$route');
}
/// Calls API using a HTTP request type, a route and body
Future<http.Response> _requestAPI(
String route, AerisAPIRequestType requestType, Object? body) async {
final Map<String, String> header = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
if (_connected) {
header.addAll({'Authorization': 'Bearer $_jwt'});
}
const duration = Duration(seconds: 3);
try {
switch (requestType) {
case AerisAPIRequestType.delete:
return await http
.delete(_encoreUri(route),
body: jsonEncode(body), headers: header)
.timeout(
duration,
onTimeout: () {
return http.Response('Error', 408);
},
);
case AerisAPIRequestType.get:
return await http.get(_encoreUri(route), headers: header).timeout(
duration,
onTimeout: () {
return http.Response('Error', 408);
},
);
case AerisAPIRequestType.post:
return await http
.post(_encoreUri(route), body: jsonEncode(body), headers: header)
.timeout(
duration,
onTimeout: () {
return http.Response('Error', 408);
},
);
case AerisAPIRequestType.put:
return await http
.put(_encoreUri(route), body: jsonEncode(body), headers: header)
.timeout(
duration,
onTimeout: () {
return http.Response('Error', 408);
},
);
}
} catch (e) {
return http.Response('{}', 400);
} }
return [
for (int i = 0; i <= 10; i++)
ActionTemplate(
service: service,
name: "action$i",
parameters: {'key1': 'value1', 'key2': 'value2'})
];
} }
} }

View File

@@ -1,5 +1,7 @@
import 'package:aeris/src/models/action_parameter.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
import 'package:recase/recase.dart';
///Base class for reactions and trigger ///Base class for reactions and trigger
abstract class Action { abstract class Action {
@@ -10,10 +12,27 @@ abstract class Action {
String name; String name;
///Action's parameters ///Action's parameters
Map<String, Object> parameters; List<ActionParameter> parameters;
/// Description of the action (used in catalogue)
String? description;
Action( Action(
{Key? key, {Key? key,
required this.service, required this.service,
required this.name, required this.name,
this.parameters = const {}}); this.description,
this.parameters = const []});
static Service parseServiceInName(String rType) {
var snake = rType.split('_');
var service = snake.removeAt(0);
return Service.factory(service);
}
String displayName() {
var words = name.split('_');
words.removeAt(0);
return ReCase(words.join()).titleCase;
}
} }

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
/// Object representation of an action's parameter
class ActionParameter {
/// Name of the action parameter
final String name;
/// Description of theparameter
final String description;
/// Value of the pamrameter
Object? value;
ActionParameter(
{Key? key, required this.name, this.description = "", this.value});
MapEntry<String, dynamic> toJson() => MapEntry(name, value);
static List<ActionParameter> fromJSON(Map<String, dynamic> params) {
List<ActionParameter> actionParameters = [];
params.forEach((key, value) =>
actionParameters.add(ActionParameter(name: key, value: value)));
return actionParameters;
}
}

View File

@@ -1,13 +1,20 @@
import 'package:aeris/src/models/action.dart'; import 'package:aeris/src/models/action.dart';
import 'package:aeris/src/models/action_parameter.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// Template for actions, for forms /// Template for actions, for forms
class ActionTemplate extends Action { class ActionTemplate extends Action {
///List of values returned by the action
final List<ActionParameter> returnedValues;
ActionTemplate( ActionTemplate(
{Key? key, {Key? key,
required Service service, required Service service,
required String name, required String name,
Map<String, Object> parameters = const {}}) required String description,
: super(service: service, name: name, parameters: parameters); this.returnedValues = const [],
List<ActionParameter> parameters = const []})
: super(service: service, name: name, parameters: parameters, description: description);
} }

View File

@@ -5,7 +5,7 @@ import 'package:aeris/src/models/trigger.dart';
/// Object representation of a pipeline /// Object representation of a pipeline
class Pipeline { class Pipeline {
///Unique identifier ///Unique identifier
final int id; int id;
/// Name of the pipeline, defined by the user /// Name of the pipeline, defined by the user
final String name; final String name;
@@ -16,7 +16,6 @@ class Pipeline {
/// Is the pipeline enabled /// Is the pipeline enabled
bool enabled; bool enabled;
///The pipeline's reactions ///The pipeline's reactions
final List<Reaction> reactions; final List<Reaction> reactions;
@@ -29,4 +28,34 @@ class Pipeline {
required this.enabled, required this.enabled,
required this.trigger, required this.trigger,
required this.reactions}); required this.reactions});
/// Unserialize Pipeline from JSON
static Pipeline fromJSON(Map<String, dynamic> data) {
var action = data['action'] as Map<String, dynamic>;
var reactions = data['reactions'] as List<dynamic>;
return Pipeline(
name: action['name'] as String,
enabled: action['enabled'] as bool,
id: action['id'] as int,
triggerCount: action['triggerCount'] as int,
trigger: Trigger.fromJSON(action),
reactions: reactions
.map<Reaction>((e) => Reaction.fromJSON(e))
.toList());
}
/// Serialize Pipeline into JSON
Object toJSON() => {
"action": {
"id": id,
"name": name,
"pType": trigger.name,
"pParams": { for (var e in trigger.parameters) e.name : e.value }, ///Serialize
"enabled": enabled,
"lastTrigger": trigger.last?.toIso8601String(),
"triggerCount": triggerCount
},
'reactions': reactions.map((e) => e.toJSON()).toList()
};
} }

View File

@@ -1,6 +1,7 @@
// ignore_for_file: hash_and_equals // ignore_for_file: hash_and_equals
import 'package:aeris/src/models/action.dart' as aeris_action; import 'package:aeris/src/models/action.dart' as aeris_action;
import 'package:aeris/src/models/action_parameter.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
@@ -10,20 +11,33 @@ class Reaction extends aeris_action.Action {
{Key? key, {Key? key,
required Service service, required Service service,
required String name, required String name,
Map<String, Object> parameters = const {}}) List<ActionParameter> parameters = const []})
: super(service: service, name: name, parameters: parameters); : super(service: service, name: name, parameters: parameters);
/// Template trigger, used as an 'empty' trigger /// Template trigger, used as an 'empty' trigger
Reaction.template() Reaction.template()
: super(service: Service.all()[0], name: '', parameters: {}); : super(service: Service.all()[0], name: '', parameters: []);
static Reaction fromJSON(Object reaction) {
var reactionJSON = reaction as Map<String, dynamic>;
var service = aeris_action.Action.parseServiceInName(reactionJSON['rType'] as String);
return Reaction(
service: service,
name: reactionJSON['rType'] as String,
parameters: ActionParameter.fromJSON((reactionJSON['rParams'] as Map<String, dynamic>)));
}
/// Serialize Reaction to JSON
Object toJSON() => {
"rType": name,
"rParams": { for (var e in parameters) e.name : e.value }
};
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
Reaction otherReaction = other as Reaction; Reaction otherReaction = other as Reaction;
return service.name == otherReaction.service.name && return service.name == otherReaction.service.name &&
name == otherReaction.name && name == otherReaction.name &&
parameters.values.toString() == parameters.map((e) => e.name).toString() == other.parameters.map((e) => e.name).toString();
otherReaction.parameters.values.toString() &&
parameters.keys.toString() == otherReaction.parameters.keys.toString();
} }
} }

View File

@@ -1,6 +1,10 @@
// Class for a service (Youtube, Gmail, ...) // Class for a service (Youtube, Gmail, ...)
import 'package:aeris/src/aeris_api.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:core';
import 'package:get_it/get_it.dart';
/// Data class used to store data about a service (logo, url, name) /// Data class used to store data about a service (logo, url, name)
class Service { class Service {
@@ -22,16 +26,19 @@ class Service {
height: logoSize, height: logoSize,
)); ));
/// Get full url for OAuth2
String get authUrl => GetIt.I<AerisAPI>().getServiceAuthURL(this);
const Service.spotify() const Service.spotify()
: name = "Spotify", : name = "Spotify",
url = "https://www.spotify.com", url = "https://www.spotify.com",
logoUrl = logoUrl =
"https://www.presse-citron.net/app/uploads/2020/06/spotify-une-.jpg"; "https://www.presse-citron.net/app/uploads/2020/06/spotify-une-.jpg";
const Service.gmail() const Service.anilist()
: name = "Gmail", : name = "Anilist",
url = "https://mail.google.com/", url = "https://anilist.co",
logoUrl = logoUrl =
"https://play-lh.googleusercontent.com/KSuaRLiI_FlDP8cM4MzJ23ml3og5Hxb9AapaGTMZ2GgR103mvJ3AAnoOFz1yheeQBBI"; "https://anilist.co/img/icons/android-chrome-512x512.png";
const Service.discord() const Service.discord()
: name = "Discord", : name = "Discord",
url = "https://discord.com/app", url = "https://discord.com/app",
@@ -43,7 +50,7 @@ class Service {
logoUrl = logoUrl =
"https://f.hellowork.com/blogdumoderateur/2019/11/twitter-logo-1200x1200.jpg"; "https://f.hellowork.com/blogdumoderateur/2019/11/twitter-logo-1200x1200.jpg";
const Service.github() const Service.github()
: name = "GitHub", : name = "Github",
url = "https://github.com/", url = "https://github.com/",
logoUrl = "https://avatars.githubusercontent.com/u/9919?s=280&v=4"; logoUrl = "https://avatars.githubusercontent.com/u/9919?s=280&v=4";
const Service.youtube() const Service.youtube()
@@ -57,13 +64,23 @@ class Service {
logoUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Cle.png/1024px-Cle.png"; logoUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Cle.png/1024px-Cle.png";
/// Returns a list of all the available services /// Returns a list of all the available services
static all() => const [ static List<Service> all() => const [
Service.discord(), Service.discord(),
Service.github(), Service.github(),
Service.gmail(), Service.anilist(),
Service.youtube(), Service.youtube(),
Service.twitter(), Service.twitter(),
Service.spotify(), Service.spotify(),
Service.utils(), Service.utils(),
]; ];
/// Construct a service based on a lowercase string, the name of the service
static Service factory(String name) {
if (name.toLowerCase() == "git") return const Service.github();
if (name.toLowerCase() == "ani") return const Service.anilist();
for (Service service in Service.all()) {
if (service.name.toLowerCase() == name.toLowerCase()) return service;
}
throw Exception("Unknown service");
}
} }

View File

@@ -1,7 +1,8 @@
// ignore_for_file: hash_and_equals // ignore_for_file: hash_and_equals
import 'package:aeris/src/models/action_parameter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:aeris/src/main.dart'; import 'package:aeris/main.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
import 'package:aeris/src/models/action.dart' as aeris_action; import 'package:aeris/src/models/action.dart' as aeris_action;
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -14,11 +15,27 @@ class Trigger extends aeris_action.Action {
{Key? key, {Key? key,
required Service service, required Service service,
required String name, required String name,
Map<String, Object> parameters = const {}, List<ActionParameter> parameters = const [],
this.last}) this.last})
: super(service: service, name: name, parameters: parameters); : super(service: service, name: name, parameters: parameters);
///TODO Constructor from DB 'Type' field /// Unserialize
static Trigger fromJSON(Object action) {
var triggerJSON = action as Map<String, dynamic>;
Service service =
aeris_action.Action.parseServiceInName(triggerJSON['pType'] as String);
var lastTriggerField = action['lastTrigger'];
DateTime? last = lastTriggerField == null
? null
: DateTime.parse(lastTriggerField as String);
return Trigger(
service: service,
name: triggerJSON['pType'] as String,
last: last,
parameters: ActionParameter.fromJSON((triggerJSON['pParams'] as Map<String, dynamic>))
);
}
String lastToString() { String lastToString() {
var context = AppLocalizations.of(Aeris.materialKey.currentContext!); var context = AppLocalizations.of(Aeris.materialKey.currentContext!);
@@ -32,7 +49,7 @@ class Trigger extends aeris_action.Action {
/// Template trigger, used as an 'empty' trigger /// Template trigger, used as an 'empty' trigger
Trigger.template({Key? key, this.last}) Trigger.template({Key? key, this.last})
: super(service: const Service.twitter(), name: '', parameters: {}); : super(service: Service.all()[0], name: '', parameters: []);
@override @override
// ignore: avoid_renaming_method_parameters // ignore: avoid_renaming_method_parameters
@@ -41,7 +58,7 @@ class Trigger extends aeris_action.Action {
return service.name == other.service.name && return service.name == other.service.name &&
name == other.name && name == other.name &&
last == other.last && last == other.last &&
parameters.values.toString() == other.parameters.values.toString() && parameters.map((e) => e.name).toString() ==
parameters.keys.toString() == other.parameters.keys.toString(); other.parameters.map((e) => e.name).toString();
} }
} }

View File

@@ -1,28 +0,0 @@
// Class for a service related to a user (Youtube, Gmail, ...)
import 'package:aeris/src/models/service.dart';
import 'package:flutter/cupertino.dart';
class UserService {
/// Service name related to the user
final Service serviceProvider;
/// Id of an user for this service
final String serviceAccountId;
/// Account Username
final String accountUsername;
/// Account Slug for this Service
final String accountSlug;
/// Account External Token for this Service
final String userExternalToken;
UserService(
{Key? key,
required this.serviceAccountId,
required this.accountUsername,
required this.accountSlug,
required this.userExternalToken,
required this.serviceProvider});
}

View File

@@ -0,0 +1,73 @@
import 'package:aeris/src/models/action_parameter.dart';
import 'package:aeris/src/models/action_template.dart';
import 'package:aeris/src/models/service.dart';
import 'package:flutter/cupertino.dart';
import 'package:aeris/src/aeris_api.dart';
import 'package:get_it/get_it.dart';
/// Provider class for Action listed in /about.json
class ActionCatalogueProvider extends ChangeNotifier {
/// Tells if the provers has loaded data at least once
final Map<Service, List<ActionTemplate>> _triggerTemplates = {};
final Map<Service, List<ActionTemplate>> _reactionTemplates = {};
Map<Service, List<ActionTemplate>> get triggerTemplates => _triggerTemplates;
Map<Service, List<ActionTemplate>> get reactionTemplates =>
_reactionTemplates;
String removeServiceFromAName(String aName) {
var words = aName.split('_');
words.removeAt(0);
return words.join();
}
void reloadCatalogue() {
_triggerTemplates.clear();
_reactionTemplates.clear();
Service.all().forEach((element) {
_triggerTemplates.putIfAbsent(element, () => []);
_reactionTemplates.putIfAbsent(element, () => []);
});
GetIt.I<AerisAPI>().getAbout().then((about) {
if (about.isEmpty || about == null) return;
final services = (about['server'] as Map<String, dynamic>)['services'] as List<dynamic>;
for (var serviceContent in services) {
Service service = Service.factory(serviceContent['name']);
for (var action in (serviceContent['actions'] as List)) {
_triggerTemplates[service]!.add(
ActionTemplate(
name: action['name'],
service: service,
description: action['description'],
parameters: (action['params'] as List).map(
(e) => ActionParameter(name: e['name'], description: e['description'])
).toList(),
returnedValues: (action['returns'] as List).map(
(e) => ActionParameter(name: e['name'], description: e['description'])
).toList(),
)
);
}
for (var reaction in serviceContent['reactions']) {
_reactionTemplates[service]!.add(
ActionTemplate(
name: reaction['name'],
service: service,
description: reaction['description'],
parameters: (reaction['params'] as List).map(
(e) => ActionParameter(name: e['name'], description: e['description'])
).toList(),
returnedValues: (reaction['returns'] as List).map(
(e) => ActionParameter(name: e['name'], description: e['description'])
).toList(),
)
);
}
}
notifyListeners();
});
}
ActionCatalogueProvider() {
reloadCatalogue();
}
}

View File

@@ -10,7 +10,8 @@ class PipelineProvider extends ChangeNotifier {
late PipelineCollection _pipelineCollection; late PipelineCollection _pipelineCollection;
/// Tells if the provers has loaded data at least once /// Tells if the provers has loaded data at least once
bool initialized = false; bool _initialized = false;
bool get initialized => _initialized;
PipelineProvider() { PipelineProvider() {
_pipelineCollection = PipelineCollection( _pipelineCollection = PipelineCollection(
@@ -23,17 +24,17 @@ class PipelineProvider extends ChangeNotifier {
/// Fetches the pipelines from API and put them in the collection /// Fetches the pipelines from API and put them in the collection
Future<void> fetchPipelines() { Future<void> fetchPipelines() {
return GetIt.I<AerisAPI>().getPipelines().then((pipelines) { return GetIt.I<AerisAPI>().getPipelines().then((pipelines) {
_initialized = true;
_pipelineCollection.pipelines = pipelines; _pipelineCollection.pipelines = pipelines;
sortPipelines(); sortPipelines();
initialized = true; notifyListeners();
}); });
} }
/// Adds a pipeline in the Provider /// Adds a pipeline in the Provider
addPipeline(Pipeline newPipeline) { addPipeline(Pipeline newPipeline) async {
initialized = true; await GetIt.I<AerisAPI>().createPipeline(newPipeline);
_pipelineCollection.pipelines.add(newPipeline); _pipelineCollection.pipelines.add(newPipeline);
GetIt.I<AerisAPI>().createPipeline(newPipeline);
sortPipelines(); sortPipelines();
notifyListeners(); notifyListeners();
} }

View File

@@ -0,0 +1,41 @@
import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/models/service.dart';
import 'package:flutter/cupertino.dart';
import 'package:get_it/get_it.dart';
/// Provider used to store every Service the User is authenticated to
class ServiceProvider extends ChangeNotifier {
/// List of [Service] related to the user
List<Service> _connectedServices = [];
List<Service> get connectedServices => _connectedServices;
/// Get the services the user is not connected to
List<Service> get availableServices => Service.all()
.where((element) => !_connectedServices.contains(element))
.toList();
ServiceProvider() {
refreshServices();
}
/// Adds a service into the Provider
addService(Service service, String code) async {
_connectedServices.add(service);
GetIt.I<AerisAPI>()
.connectService(service, code)
.then((value) => notifyListeners());
}
/// Refresh services from API
refreshServices() async {
_connectedServices = await GetIt.I<AerisAPI>().getConnectedService();
notifyListeners();
}
/// Removes a service from the Provider, and calls API
removeService(Service service) async {
_connectedServices.remove(service);
notifyListeners();
await GetIt.I<AerisAPI>().disconnectService(service);
}
}

View File

@@ -1,80 +0,0 @@
import 'package:aeris/src/models/user_service.dart';
import 'package:aeris/src/models/service.dart';
import 'package:flutter/cupertino.dart';
/// Provider used to store every Service related to the User
class UserServiceProvider extends ChangeNotifier {
/// List of [Service] related to the user
List<UserService> userServices = [];
/// Adds a service into the Provider
addServiceForUser(UserService newService) {
userServices.add(newService);
notifyListeners();
}
/// Creates a new service related to the user
createUserService(Service serviceToSet,
{String accountId = "",
String accUsername = "",
String accountSlug = "",
String externalToken = ""}) {
UserService newService = UserService(
serviceAccountId: accountId,
accountUsername: accUsername,
accountSlug: accountSlug,
userExternalToken: externalToken,
serviceProvider: serviceToSet);
userServices.add(newService);
// notifyListeners(); /// TODO Get the notifyListeners method back.
}
/// Sets a list of service into the Provider
setServiceForUser(List<UserService> newServices) {
userServices = [];
userServices = newServices;
notifyListeners();
}
/// Modifies a service given as argument
modifyService(UserService toModify, String serviceAccountId,
String accountUsername, String accountSlug, String userExternalToken) {
for (int i = 0; i < userServices.length; i++) {
if (userServices[i].serviceProvider.name ==
toModify.serviceProvider.name &&
userServices[i].serviceAccountId == toModify.serviceAccountId &&
userServices[i].userExternalToken == toModify.userExternalToken) {
UserService newService = UserService(
serviceProvider: userServices[i].serviceProvider,
serviceAccountId: serviceAccountId,
accountUsername: accountUsername,
accountSlug: accountSlug,
userExternalToken: userExternalToken);
userServices[i] = newService;
notifyListeners();
return true;
}
}
return false;
}
/// Removes a service from the Provider
removeService(UserService toRemove) {
for (UserService uService in userServices) {
if (uService.serviceProvider.name == toRemove.serviceProvider.name &&
uService.serviceAccountId == toRemove.serviceAccountId &&
uService.userExternalToken == toRemove.userExternalToken) {
userServices.remove(uService);
notifyListeners();
return true;
}
}
return false;
}
/// Clears Provider from data
clearProvider() {
userServices.clear();
notifyListeners();
}
}

View File

@@ -0,0 +1,28 @@
import 'package:aeris/src/models/service.dart';
import 'package:aeris/src/providers/services_provider.dart';
import 'package:flutter/material.dart';
import 'package:loading_indicator/loading_indicator.dart';
import 'package:provider/provider.dart';
class AuthorizationPage extends StatelessWidget {
const AuthorizationPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final route = ModalRoute.of(context)!.settings.name!;
final code = Uri.parse(route).queryParameters['code']!;
final serviceName = Uri.parse(route).pathSegments.last;
final service = Service.factory(serviceName);
Provider.of<ServiceProvider>(context, listen: false).addService(service, code).then((_) {
Provider.of<ServiceProvider>(context, listen: false).notifyListeners();
Navigator.pop(context);
});
return Container(
alignment: Alignment.center,
child: LoadingIndicator(
indicatorType: Indicator.ballClipRotateMultiple,
colors: [Theme.of(context).colorScheme.secondary],
));
}
}

View File

@@ -79,15 +79,19 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
onTap: () { onTap: () {
showAerisCardPage( showAerisCardPage(
context, context,
(_) => (_) => SetupActionPage(
SetupActionPage(action: trigger)) action: trigger,
parentReactions: reactions
))
.then((_) => setState(() {})); .then((_) => setState(() {}));
}) })
: ActionCard( : ActionCard(
leading: trigger.service.getLogo(logoSize: 50), leading: trigger.service.getLogo(logoSize: 50),
title: trigger.name, title: trigger.displayName(),
trailing: ActionCardPopupMenu( trailing: ActionCardPopupMenu(
deletable: false, deletable: false,
parentReactions: reactions,
parentTrigger: trigger,
action: trigger, action: trigger,
then: () => setState(() {})), then: () => setState(() {})),
), ),
@@ -104,8 +108,10 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
itemBuilder: (reaction) => ActionCard( itemBuilder: (reaction) => ActionCard(
key: ValueKey(reactions.indexOf(reaction)), key: ValueKey(reactions.indexOf(reaction)),
leading: reaction.service.getLogo(logoSize: 50), leading: reaction.service.getLogo(logoSize: 50),
title: reaction.name, title: reaction.displayName(),
trailing: ActionCardPopupMenu( trailing: ActionCardPopupMenu(
parentTrigger: trigger == Trigger.template() ? null : trigger,
parentReactions: reactions,
deletable: reactions.length > 1, deletable: reactions.length > 1,
action: reaction, action: reaction,
then: () => setState(() {}), then: () => setState(() {}),
@@ -128,7 +134,10 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
showAerisCardPage( showAerisCardPage(
context, context,
(_) => SetupActionPage( (_) => SetupActionPage(
action: newreact)) action: newreact,
parentReactions: reactions,
parentTrigger: trigger == Trigger.template() ? null : trigger,
))
.then((_) => setState(() { .then((_) => setState(() {
if (newreact != Reaction.template()) { if (newreact != Reaction.template()) {
reactions.add(newreact); reactions.add(newreact);

View File

@@ -1,3 +1,4 @@
import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/views/create_pipeline_page.dart'; import 'package:aeris/src/views/create_pipeline_page.dart';
import 'package:aeris/src/views/service_page.dart'; import 'package:aeris/src/views/service_page.dart';
import 'package:aeris/src/widgets/aeris_card_page.dart'; import 'package:aeris/src/widgets/aeris_card_page.dart';
@@ -8,6 +9,7 @@ import 'package:aeris/src/widgets/aeris_page.dart';
import 'package:aeris/src/widgets/clickable_card.dart'; import 'package:aeris/src/widgets/clickable_card.dart';
import 'package:aeris/src/widgets/home_page_sort_menu.dart'; import 'package:aeris/src/widgets/home_page_sort_menu.dart';
import 'package:aeris/src/widgets/pipeline_card.dart'; import 'package:aeris/src/widgets/pipeline_card.dart';
import 'package:get_it/get_it.dart';
import 'package:liquid_pull_to_refresh/liquid_pull_to_refresh.dart'; import 'package:liquid_pull_to_refresh/liquid_pull_to_refresh.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:skeleton_loader/skeleton_loader.dart'; import 'package:skeleton_loader/skeleton_loader.dart';
@@ -28,58 +30,69 @@ class _HomePageState extends State<HomePage> {
Widget serviceActionButtons = IconButton( Widget serviceActionButtons = IconButton(
icon: const Icon(Icons.electrical_services), icon: const Icon(Icons.electrical_services),
onPressed: () => showAerisCardPage(context, (context) => const ServicePage()) onPressed: () =>
); showAerisCardPage(context, (context) => const ServicePage()));
Widget logoutActionButton = IconButton( Widget logoutActionButton = IconButton(
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
onPressed: () => showDialog<String>( onPressed: () => showDialog<String>(
context: context, context: context,
builder: (BuildContext context) => WarningDialog( builder: (BuildContext context) => WarningDialog(
message: AppLocalizations.of(context).logoutWarningMessage, message: AppLocalizations.of(context).logoutWarningMessage,
onAccept: () => Navigator.of(context).popAndPushNamed('/'), //TODO logout onAccept: () {
warnedAction: AppLocalizations.of(context).logout GetIt.I<AerisAPI>().stopConnection();
) Navigator.of(context).popAndPushNamed('/');
), },
warnedAction: AppLocalizations.of(context).logout)),
); );
return Consumer<PipelineProvider>( return Consumer<PipelineProvider>(
builder: (context, provider, _) => AerisPage( builder: (context, provider, _) => AerisPage(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => showAerisCardPage(context, (_) => const CreatePipelinePage()), onPressed: () => showAerisCardPage(
backgroundColor: Theme.of(context).colorScheme.secondary, context, (_) => const CreatePipelinePage()),
elevation: 10, backgroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.add), elevation: 10,
), child: const Icon(Icons.add),
actions: [ ),
HomePageSortMenu( actions: [
collectionProvider: provider, HomePageSortMenu(
), collectionProvider: provider,
serviceActionButtons, ),
logoutActionButton serviceActionButtons,
], logoutActionButton
body: provider.initialized == false ],
? ListView(physics: const BouncingScrollPhysics(), body: provider.initialized == false
padding: const EdgeInsets.only(bottom: 20, top: 20, left: 10, right: 10), ? ListView(
children: [SkeletonLoader( physics: const BouncingScrollPhysics(),
builder: ClickableCard(onTap:(){}, body: const SizedBox(height: 80)), padding: const EdgeInsets.only(
items: 10, bottom: 20, top: 20, left: 10, right: 10),
highlightColor: Theme.of(context).colorScheme.secondary children: [
)]) SkeletonLoader(
: LiquidPullToRefresh( builder: ClickableCard(
borderWidth: 2, onTap: () {},
animSpeedFactor: 3, body: const SizedBox(height: 80)),
color: Colors.transparent, items: 10,
showChildOpacityTransition: false, highlightColor:
onRefresh: () => provider.fetchPipelines() Theme.of(context).colorScheme.secondary)
.then((_) => setState(() {})), // refresh callback ])
child: ListView.builder( : LiquidPullToRefresh(
physics: const BouncingScrollPhysics(), borderWidth: 2,
padding: const EdgeInsets.only(bottom: 20, top: 20, left: 10, right: 10), animSpeedFactor: 3,
controller: listController, color: Colors.transparent,
itemCount: provider.pipelineCount, showChildOpacityTransition: false,
itemBuilder: (BuildContext context, int index) => onRefresh: () => provider
PipelineCard(pipeline: provider.getPipelineAt(index), .fetchPipelines()
), .then((_) => setState(() {})), // refresh callback
)), child: ListView.builder(
)); physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(
bottom: 20, top: 20, left: 10, right: 10),
controller: listController,
itemCount: provider.pipelineCount,
itemBuilder: (BuildContext context, int index) =>
PipelineCard(
pipeline: provider.getPipelineAt(index),
),
)),
));
} }
} }

View File

@@ -1,13 +1,10 @@
import 'package:aeris/src/main.dart'; import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/main.dart';
import 'package:aeris/src/widgets/aeris_page.dart'; import 'package:aeris/src/widgets/aeris_page.dart';
import 'package:flutter_login/flutter_login.dart'; import 'package:flutter_login/flutter_login.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
const users = {
'dribbble@gmail.com': '12345',
'hunter@gmail.com': 'hunter',
};
/// Login Page Widget /// Login Page Widget
class LoginPage extends StatelessWidget { class LoginPage extends StatelessWidget {
@@ -17,39 +14,24 @@ class LoginPage extends StatelessWidget {
Duration get loginDuration => const Duration(milliseconds: 2500); Duration get loginDuration => const Duration(milliseconds: 2500);
/// Called when user clicks on [FlutterLogin] widget 'login' button /// Called when user clicks on [FlutterLogin] widget 'login' button
Future<String?> _authUser(LoginData data) { Future<String?> _authUser(LoginData data) async {
debugPrint('Name: ${data.name}, Password: ${data.password}'); bool connected =
return Future.delayed(loginDuration).then((_) { await GetIt.I<AerisAPI>().createConnection(data.name, data.password);
if (!users.containsKey(data.name)) { if (!connected) {
return AppLocalizations.of(Aeris.materialKey.currentContext!) return AppLocalizations.of(Aeris.materialKey.currentContext!)
.usernameOrPasswordIncorrect; .usernameOrPasswordIncorrect;
} }
if (users[data.name] != data.password) { return null;
return AppLocalizations.of(Aeris.materialKey.currentContext!)
.usernameOrPasswordIncorrect;
}
return null;
});
} }
/// Opens signup page of [FlutterLogin] widget /// Opens signup page of [FlutterLogin] widget
Future<String?> _signupUser(SignupData data) { Future<String?> _signupUser(SignupData data) async {
debugPrint('Signup Name: ${data.name}, Password: ${data.password}'); bool connected =
return Future.delayed(loginDuration).then((_) { await GetIt.I<AerisAPI>().signUpUser(data.name!, data.password!);
return null; if (connected == false) {
}); return AppLocalizations.of(Aeris.materialKey.currentContext!).errorOnSignup;
} }
return null;
/// Opens user password recovery page
Future<String?> _recoverPassword(String name) {
debugPrint('Name: $name');
return Future.delayed(loginDuration).then((_) {
if (!users.containsKey(name)) {
return AppLocalizations.of(Aeris.materialKey.currentContext!)
.userDoesNotExist;
}
return null;
});
} }
@override @override
@@ -59,16 +41,21 @@ class LoginPage extends StatelessWidget {
body: FlutterLogin( body: FlutterLogin(
disableCustomPageTransformer: true, disableCustomPageTransformer: true,
logo: const AssetImage("assets/logo.png"), logo: const AssetImage("assets/logo.png"),
onRecoverPassword: _recoverPassword, hideForgotPasswordButton: true,
onRecoverPassword: (_) => null,
theme: LoginTheme( theme: LoginTheme(
pageColorLight: Colors.transparent, pageColorLight: Colors.transparent,
pageColorDark: Colors.transparent, pageColorDark: Colors.transparent,
primaryColor: Theme.of(context).colorScheme.primary), primaryColor: Theme.of(context).colorScheme.primary),
onLogin: _authUser, onLogin: _authUser,
onSignup: _signupUser, onSignup: _signupUser,
userType: LoginUserType.name,
userValidator: (input) {
if (input == null || input.trim().length < 4) return "Must be at least 4 chars long";
return null;
},
onSubmitAnimationCompleted: () { onSubmitAnimationCompleted: () {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
Navigator.of(context).popAndPushNamed("/home");
})); }));
} }
} }

View File

@@ -95,7 +95,12 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
onTap: () { onTap: () {
Reaction newreaction = Reaction.template(); Reaction newreaction = Reaction.template();
showAerisCardPage( showAerisCardPage(
context, (_) => SetupActionPage(action: newreaction)) context, (_) => SetupActionPage(
action: newreaction,
parentTrigger: pipeline.trigger,
parentReactions: pipeline.reactions,
)
)
.then((r) { .then((r) {
if (newreaction != Reaction.template()) { if (newreaction != Reaction.template()) {
setState(() { setState(() {
@@ -135,9 +140,11 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
style: const TextStyle(fontWeight: FontWeight.w500)), style: const TextStyle(fontWeight: FontWeight.w500)),
ActionCard( ActionCard(
leading: pipeline.trigger.service.getLogo(logoSize: 50), leading: pipeline.trigger.service.getLogo(logoSize: 50),
title: pipeline.trigger.name, title: pipeline.trigger.displayName(),
trailing: ActionCardPopupMenu( trailing: ActionCardPopupMenu(
deletable: false, deletable: false,
parentTrigger: pipeline.trigger,
parentReactions: pipeline.reactions,
action: pipeline.trigger, action: pipeline.trigger,
then: () { then: () {
setState(() {}); setState(() {});
@@ -152,8 +159,10 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
itemBuilder: (reaction) => ActionCard( itemBuilder: (reaction) => ActionCard(
key: ValueKey(pipeline.reactions.indexOf(reaction)), key: ValueKey(pipeline.reactions.indexOf(reaction)),
leading: reaction.service.getLogo(logoSize: 50), leading: reaction.service.getLogo(logoSize: 50),
title: reaction.name, title: reaction.displayName(),
trailing: ActionCardPopupMenu( trailing: ActionCardPopupMenu(
parentTrigger: pipeline.trigger,
parentReactions: pipeline.reactions,
deletable: pipeline.reactions.length > 1, deletable: pipeline.reactions.length > 1,
action: reaction, action: reaction,
then: () { then: () {

View File

@@ -1,41 +1,36 @@
import 'package:aeris/src/aeris_api.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:aeris/src/models/pipeline.dart';
import 'package:aeris/src/models/reaction.dart';
import 'package:aeris/src/models/service.dart'; import 'package:aeris/src/models/service.dart';
import 'package:aeris/src/providers/pipelines_provider.dart'; import 'package:aeris/src/providers/pipelines_provider.dart';
import 'package:aeris/src/providers/user_services_provider.dart'; import 'package:aeris/src/providers/services_provider.dart';
import 'package:aeris/src/widgets/action_card.dart'; import 'package:aeris/src/widgets/action_card.dart';
import 'package:aeris/src/widgets/aeris_card_page.dart'; import 'package:aeris/src/widgets/aeris_card_page.dart';
import 'package:aeris/src/widgets/warning_dialog.dart'; import 'package:aeris/src/widgets/warning_dialog.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
///Page listing connected & available services ///Page listing connected & available services
class ServicePage extends StatelessWidget { class ServicePage extends StatelessWidget {
const ServicePage({Key? key}) : super(key: key); const ServicePage({Key? key}) : super(key: key);
List<Widget> getServiceGroup(String groupName, Icon trailingIcon, List<Widget> getServiceGroup(List<Service> services, String groupName, Icon trailingIcon,
void Function(Service) onTap, BuildContext context) { void Function(Service) onTap, BuildContext context) {
UserServiceProvider uServicesProvider = if (services.isEmpty) return [];
Provider.of<UserServiceProvider>(context);
return [ return [
Text( Text(
"$groupName:", "$groupName:",
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
for (var service in uServicesProvider.userServices) for (var service in services)
ActionCard( ActionCard(
leading: service.serviceProvider.getLogo(logoSize: 50), leading: service.getLogo(logoSize: 50),
title: service.serviceProvider.name, title: service.name,
trailing: IconButton( trailing: IconButton(
splashColor: trailingIcon.color!.withAlpha(100), splashColor: trailingIcon.color!.withAlpha(100),
splashRadius: 20, splashRadius: 20,
icon: trailingIcon, icon: trailingIcon,
onPressed: () => onTap(service.serviceProvider), onPressed: () => onTap(service),
)), )),
const SizedBox(height: 30), const SizedBox(height: 30),
]; ];
@@ -43,69 +38,45 @@ class ServicePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Service> services = const [ return Consumer<ServiceProvider>(
Service.discord(), builder: (context, serviceProvider, _) => Consumer<PipelineProvider>(
Service.gmail(), builder: (context, pipelineProvider, _) => AerisCardPage(
Service.github(),
Service.youtube(),
Service.twitter(),
Service.spotify()
];
UserServiceProvider uServiceProvider =
Provider.of<UserServiceProvider>(context, listen: false);
uServiceProvider.clearProvider();
for (var service in services) {
uServiceProvider.createUserService(service);
}
return Consumer<PipelineProvider>(
builder: (context, provider, _) => AerisCardPage(
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...[ ...[
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text(AppLocalizations.of(context).services, style: const TextStyle(fontSize: 25)), child: Text(AppLocalizations.of(context).services,
), style: const TextStyle(fontSize: 25)),
const SizedBox(height: 60) ),
], const SizedBox(height: 60)
...getServiceGroup(
AppLocalizations.of(context).connected,
const Icon(Icons.delete, color: Colors.red),
(Service service) => showDialog(
context: context,
builder: (BuildContext context) => WarningDialog(
message: AppLocalizations.of(context)
.disconnectServiceWarningMessage,
onAccept: () => {
provider.removePipelinesWhere((Pipeline pipeline) {
if (pipeline.trigger.service == service) {
return true;
}
if (pipeline.reactions
.where((Reaction react) =>
react.service == service)
.isNotEmpty) {
return true;
}
return false;
}),
GetIt.I<AerisAPI>().disconnectService(service)
},
warnedAction:
AppLocalizations.of(context).disconnect)),
context),
...getServiceGroup(
AppLocalizations.of(context).available,
const Icon(Icons.connect_without_contact,
color: Colors.green),
(Service service) =>
print("Connected") /* TODO open page to connect service*/,
context),
], ],
), ...getServiceGroup(
serviceProvider.connectedServices,
AppLocalizations.of(context).connected,
const Icon(Icons.delete, color: Colors.red),
(Service service) => showDialog(
context: context,
builder: (BuildContext context) => WarningDialog(
message: AppLocalizations.of(context)
.disconnectServiceWarningMessage,
onAccept: () => serviceProvider
.removeService(service)
.then((_) => pipelineProvider.fetchPipelines()),
warnedAction: AppLocalizations.of(context).disconnect)),
context),
...getServiceGroup(
serviceProvider.availableServices,
AppLocalizations.of(context).available,
const Icon(Icons.connect_without_contact, color: Colors.green),
(Service service) {
launch(Uri.parse(service.authUrl).toString(), forceSafariVC: false);
},
context),
],
), ),
); ),
));
} }
} }

View File

@@ -1,5 +1,7 @@
import 'package:aeris/src/models/action_parameter.dart';
import 'package:aeris/src/models/action_template.dart'; import 'package:aeris/src/models/action_template.dart';
import 'package:aeris/src/aeris_api.dart'; import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/models/reaction.dart';
import 'package:aeris/src/models/trigger.dart'; import 'package:aeris/src/models/trigger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:aeris/src/models/action.dart' as aeris; import 'package:aeris/src/models/action.dart' as aeris;
@@ -13,10 +15,15 @@ import 'package:skeleton_loader/skeleton_loader.dart';
///Page to setup an action ///Page to setup an action
class SetupActionPage extends StatefulWidget { class SetupActionPage extends StatefulWidget {
const SetupActionPage({Key? key, required this.action}) : super(key: key); const SetupActionPage({Key? key, required this.action, required this.parentReactions, this.parentTrigger}) : super(key: key);
/// Action to setup /// Action to setup
final aeris.Action action; final aeris.Action action;
/// Trigger of Parent of the action to setup
final Trigger? parentTrigger;
/// reactions of Parent of the action to setup
final List<Reaction> parentReactions;
@override @override
State<SetupActionPage> createState() => _SetupActionPageState(); State<SetupActionPage> createState() => _SetupActionPageState();
@@ -30,9 +37,7 @@ class _SetupActionPageState extends State<SetupActionPage> {
void initState() { void initState() {
super.initState(); super.initState();
serviceState = widget.action.service; serviceState = widget.action.service;
GetIt.I<AerisAPI>().getActionsFor(serviceState!, widget.action).then((actions) => setState(() { availableActions = GetIt.I<AerisAPI>().getActionsFor(serviceState!, widget.action);
availableActions = actions;
}));
} }
@override @override
@@ -43,12 +48,9 @@ class _SetupActionPageState extends State<SetupActionPage> {
elevation: 8, elevation: 8,
underline: Container(), underline: Container(),
onChanged: (service) { onChanged: (service) {
GetIt.I<AerisAPI>().getActionsFor(service!, widget.action).then((actions) => setState(() {
availableActions = actions;
}));
setState(() { setState(() {
serviceState = service; serviceState = service;
availableActions = []; availableActions = GetIt.I<AerisAPI>().getActionsFor(service!, widget.action);
}); });
}, },
items: Service.all().map<DropdownMenuItem<Service>>((Service service) { items: Service.all().map<DropdownMenuItem<Service>>((Service service) {
@@ -66,6 +68,9 @@ class _SetupActionPageState extends State<SetupActionPage> {
}).toList(), }).toList(),
); );
var cardShape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)));
return AerisCardPage( return AerisCardPage(
body: Padding( body: Padding(
padding: const EdgeInsets.only(bottom: 20, left: 10, right: 10), padding: const EdgeInsets.only(bottom: 20, left: 10, right: 10),
@@ -98,41 +103,54 @@ class _SetupActionPageState extends State<SetupActionPage> {
), ),
], ],
), ),
const SizedBox(height: 30), const SizedBox(height: 20),
Text(AppLocalizations.of(context).paramInheritTip),
const SizedBox(height: 20),
if (availableActions == null) if (availableActions == null)
SkeletonLoader( SkeletonLoader(
builder: const Card(child: SizedBox(height: 40), elevation: 5), builder: Card(shape: cardShape, child: const SizedBox(height: 40), elevation: 5),
items: 10, items: 15,
highlightColor: Theme.of(context).colorScheme.secondary highlightColor: Theme.of(context).colorScheme.secondary
) )
else else
...[for (aeris.Action availableAction in availableActions!) ...[for (ActionTemplate availableAction in availableActions!)
Card( Card(
elevation: 5, elevation: 5,
child: ExpandablePanel( shape: cardShape,
child: ExpandableNotifier(
child: ScrollOnExpand(child: ExpandablePanel(
header: Padding( header: Padding(
padding: padding:
const EdgeInsets.only(left: 30, top: 20, bottom: 20), const EdgeInsets.only(left: 30, top: 20, bottom: 20),
child: Text(availableAction.name, child: Text(availableAction.displayName(),
style: const TextStyle(fontSize: 15))), style: const TextStyle(fontSize: 15))),
collapsed: Container(), collapsed: Container(),
expanded: Padding( expanded: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: ActionForm( child: ActionForm(
reactionsCandidates: widget.parentReactions,
triggerCandidate: widget.parentTrigger,
candidate: widget.action,
key: Key("${availableAction.name}${availableAction.description}${availableAction.service}"),
description: availableAction.description!,
name: availableAction.name, name: availableAction.name,
parametersNames: parameters: availableAction.parameters.map((param) {
availableAction.parameters.keys.toList(), if (widget.action.service.name == serviceState!.name && widget.action.name == availableAction.name) {
initValues: widget.action.name == availableAction.name var previousParams = widget.action.parameters.where((element) => element.name == param.name);
&& availableAction.service.name == widget.action.service.name if (previousParams.isNotEmpty) {
? widget.action.parameters : const {}, param.value = previousParams.first.value;
}
}
return param;
}).toList(),
onValidate: (parameters) { onValidate: (parameters) {
widget.action.service = serviceState!; widget.action.service = serviceState!;
widget.action.parameters = parameters; widget.action.parameters = ActionParameter.fromJSON(parameters);
widget.action.name = availableAction.name; widget.action.name = availableAction.name;
Navigator.of(context).pop(); Navigator.of(context).pop();
}), }),
)), )),
), ))),
const SizedBox(height: 10) const SizedBox(height: 10)
] ]
], ],

View File

@@ -1,4 +1,5 @@
import 'package:aeris/src/aeris_api.dart'; import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/widgets/setup_api_route.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -15,11 +16,30 @@ class StartupPage extends StatefulWidget {
} }
class _StartupPageState extends State<StartupPage> { class _StartupPageState extends State<StartupPage> {
bool connected = false;
@override
void initState() {
super.initState();
GetIt.I<AerisAPI>().getAbout().then((value) {
setState(() {
connected = value.isNotEmpty;
});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isConnected = GetIt.I<AerisAPI>().connected; bool isConnected = GetIt.I<AerisAPI>().isConnected;
return AerisPage( return AerisPage(
displayAppbar: false, displayAppbar: false,
floatingActionButton: SetupAPIRouteButton(
connected: connected,
onSetup: () => GetIt.I<AerisAPI>().getAbout().then((value) {
setState(() {
connected = value.isNotEmpty;
});
})
),
body: Column( body: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -48,13 +68,13 @@ class _StartupPageState extends State<StartupPage> {
textStyle: const TextStyle(fontSize: 20), textStyle: const TextStyle(fontSize: 20),
primary: Theme.of(context).colorScheme.secondary, primary: Theme.of(context).colorScheme.secondary,
), ),
onPressed: () { onPressed: connected ? () {
if (isConnected) { if (isConnected) {
Navigator.of(context).popAndPushNamed('/home'); Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
} else { } else {
Navigator.of(context).pushNamed('/login'); Navigator.of(context).pushNamed('/login');
} }
}, } : null,
child: Tooltip( child: Tooltip(
message: 'Connexion', message: 'Connexion',
child: Text(AppLocalizations.of(context).connect) child: Text(AppLocalizations.of(context).connect)

View File

@@ -1,3 +1,5 @@
import 'package:aeris/src/models/reaction.dart';
import 'package:aeris/src/models/trigger.dart';
import 'package:aeris/src/widgets/aeris_card_page.dart'; import 'package:aeris/src/widgets/aeris_card_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:aeris/src/views/setup_action_page.dart'; import 'package:aeris/src/views/setup_action_page.dart';
@@ -11,6 +13,8 @@ class ActionCardPopupMenu extends StatelessWidget {
ActionCardPopupMenu({ ActionCardPopupMenu({
Key? key, Key? key,
required this.action, required this.action,
this.parentTrigger,
required this.parentReactions,
required this.then, required this.then,
required this.deletable, required this.deletable,
this.onDelete, this.onDelete,
@@ -22,6 +26,10 @@ class ActionCardPopupMenu extends StatelessWidget {
/// Selected Action /// Selected Action
final aeris.Action action; final aeris.Action action;
/// Trigger of the Parent of the action
final Trigger? parentTrigger;
/// Trigger of the Parent of the action
final List<Reaction> parentReactions;
/// Function to trigger once the Edit menu is closed /// Function to trigger once the Edit menu is closed
final void Function() then; final void Function() then;
@@ -46,15 +54,18 @@ class ActionCardPopupMenu extends StatelessWidget {
icon: Icons.settings, icon: Icons.settings,
title: AppLocalizations.of(context).modify, title: AppLocalizations.of(context).modify,
value: () => showAerisCardPage( value: () => showAerisCardPage(
context, (_) => SetupActionPage(action: action)).then((_) => then()) context, (_) => SetupActionPage(
), action: action,
parentTrigger: parentTrigger,
parentReactions: parentReactions,
))
.then((_) => then())),
AerisPopupMenuItem( AerisPopupMenuItem(
context: context, context: context,
icon: Icons.delete, icon: Icons.delete,
title: AppLocalizations.of(context).delete, title: AppLocalizations.of(context).delete,
value: onDelete, value: onDelete,
enabled: deletable enabled: deletable),
),
]); ]);
} }
} }

View File

@@ -1,16 +1,43 @@
import 'package:aeris/src/models/action_parameter.dart';
import 'package:aeris/src/models/action_template.dart';
import 'package:aeris/src/models/reaction.dart';
import 'package:aeris/src/models/action.dart' as aeris;
import 'package:aeris/src/models/trigger.dart';
import 'package:aeris/src/providers/action_catalogue_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:recase/recase.dart';
import 'package:tuple/tuple.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
class Suggestion extends Tuple3<int, ActionParameter, ActionTemplate> {
Suggestion(int item1, ActionParameter item2, ActionTemplate item3) : super(item1, item2, item3);
// Overriding show method
@override
String toString() {
return "${item2.name}@$item1";
}
}
/// Form for an action /// Form for an action
class ActionForm extends StatefulWidget { class ActionForm extends StatefulWidget {
/// Name of the action /// Name of the action
final String name; final String name;
/// Names of the parameters /// List of parameters, 'values' are used as default values
final List<String> parametersNames; final List<ActionParameter> parameters;
/// Initial values of the fields /// What the action does
final Map<String, Object> initValues; final String description;
/// The Action that will be eventually filled by the form
final aeris.Action candidate;
/// The trigger candidate in the parent form page
final Trigger? triggerCandidate;
/// The trigger candidate in the parent form page
final List<Reaction> reactionsCandidates;
/// On validate callback /// On validate callback
final void Function(Map<String, String>) onValidate; final void Function(Map<String, String>) onValidate;
@@ -18,9 +45,13 @@ class ActionForm extends StatefulWidget {
const ActionForm( const ActionForm(
{Key? key, {Key? key,
required this.name, required this.name,
required this.parametersNames, required this.description,
required this.parameters,
required this.onValidate, required this.onValidate,
this.initValues = const {}}) required this.candidate,
this.triggerCandidate,
required this.reactionsCandidates,
})
: super(key: key); : super(key: key);
@override @override
@@ -28,32 +59,91 @@ class ActionForm extends StatefulWidget {
} }
class _ActionFormState extends State<ActionForm> { class _ActionFormState extends State<ActionForm> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormState>();
List<Suggestion> getSuggestions(String pattern, ActionCatalogueProvider catalogue) {
List<Suggestion> suggestions = [];
if (pattern.endsWith("{") == false) return suggestions;
if (widget.candidate is Trigger) return suggestions;
if (widget.triggerCandidate != null) {
Trigger trigger = widget.triggerCandidate!;
var triggerTemplate = catalogue.triggerTemplates[trigger.service]!.firstWhere(
(element) => element.name == trigger.name
);
for (var parameter in triggerTemplate.returnedValues) {
suggestions.add(Suggestion(0, parameter, triggerTemplate));
}
}
int index = 1;
int indexOfCandidate = widget.reactionsCandidates.indexOf(widget.candidate as Reaction);
for (var reactionCandidate in widget.reactionsCandidates) {
if (index == indexOfCandidate + 1) break;
var reactionTemplate = catalogue.reactionTemplates[reactionCandidate.service]!.firstWhere(
(element) => element.name == reactionCandidate.name
);
for (var parameter in reactionTemplate.returnedValues) {
suggestions.add(Suggestion(index, parameter, reactionTemplate));
}
index++;
}
return suggestions;
}
Map<String, String> values = {};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormBuilder( return Consumer<ActionCatalogueProvider>(
builder: (__, catalogue, _) => Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
...widget.parametersNames.map((name) => FormBuilderTextField( Text(widget.description, textAlign: TextAlign.left, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
initialValue: (widget.initValues.containsKey(name)) ? widget.initValues[name] as String : null, ...widget.parameters.map((param) {
name: name, final textEditingController = TextEditingController(text: values[param.name] ?? param.value?.toString());
decoration: InputDecoration( return TypeAheadFormField<Suggestion>(
labelText: name, key: Key(param.description),
), textFieldConfiguration: TextFieldConfiguration(
validator: FormBuilderValidators.compose([ autofocus: true,
FormBuilderValidators.required(context), controller: textEditingController,
]), enableSuggestions: widget.candidate is Reaction,
keyboardType: (widget.initValues.containsKey(name)) && widget.initValues[name] is int ? TextInputType.number : null, decoration: InputDecoration(
)), labelText: ReCase(param.name).titleCase,
helperText: param.description
),
),
onSaved: (value) {
values[param.name] = value!;
},
suggestionsBoxDecoration: const SuggestionsBoxDecoration(
elevation: 6,
borderRadius: BorderRadius.all(Radius.circular(10))
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
]),
hideOnEmpty: true,
suggestionsCallback: (suggestion) => getSuggestions(suggestion, catalogue),
onSuggestionSelected: (suggestion) {
textEditingController.text += suggestion.toString();
textEditingController.text += "}";
values[param.name] = textEditingController.text;
},
itemBuilder: (context, suggestion) => ListTile(
isThreeLine: true,
dense: true,
leading: suggestion.item3.service.getLogo(logoSize: 30),
title: Text(suggestion.item2.name),
subtitle: Text("${suggestion.item2.description}, from '${suggestion.item3.displayName()}'")
));}),
...[ ...[
ElevatedButton( ElevatedButton(
child: Text(AppLocalizations.of(context).save), child: Text(AppLocalizations.of(context).save),
onPressed: () { onPressed: () {
_formKey.currentState!.save();
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
widget.onValidate(_formKey.currentState!.value.map((key, value) => MapEntry(key, value))); _formKey.currentState!.save();
widget.onValidate(values);
} }
}, },
), ),
@@ -61,6 +151,6 @@ class _ActionFormState extends State<ActionForm> {
] ]
] ]
) )
); ));
} }
} }

View File

@@ -50,7 +50,6 @@ class HomePageSortMenu extends StatelessWidget {
value: !split), value: !split),
], ],
onSelected: (sortingMethod) { onSelected: (sortingMethod) {
/// TODO: not clean
if (sortingMethod is bool) { if (sortingMethod is bool) {
collectionProvider.splitDisabled = sortingMethod; collectionProvider.splitDisabled = sortingMethod;
} else { } else {

View File

@@ -3,14 +3,14 @@ import 'package:flutter/widgets.dart';
import 'package:reorderables/reorderables.dart'; import 'package:reorderables/reorderables.dart';
class ReorderableReactionCardsList extends StatefulWidget { class ReorderableReactionCardsList extends StatefulWidget {
ReorderableReactionCardsList( const ReorderableReactionCardsList(
{Key? key, {Key? key,
required this.onReorder, required this.onReorder,
required this.reactionList, required this.reactionList,
required this.itemBuilder}) required this.itemBuilder})
: super(key: key); : super(key: key);
List<Reaction> reactionList; final List<Reaction> reactionList;
// Callback when a list has been reordered // Callback when a list has been reordered
final void Function() onReorder; final void Function() onReorder;

View File

@@ -0,0 +1,118 @@
import 'package:aeris/src/aeris_api.dart';
import 'package:aeris/src/providers/action_catalogue_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Floating Action button to access the setup API route modal
class SetupAPIRouteButton extends StatefulWidget {
///Can the app access the api with the current baseRoute?
bool connected;
void Function() onSetup;
SetupAPIRouteButton(
{Key? key, required this.connected, required this.onSetup})
: super(key: key);
@override
State<SetupAPIRouteButton> createState() => _SetupAPIRouteButtonState();
}
class _SetupAPIRouteButtonState extends State<SetupAPIRouteButton> {
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () => showDialog(
context: context, builder: (_) => const SetupAPIRouteModal())
.then((_) => widget.onSetup()),
backgroundColor: Theme.of(context).colorScheme.secondary,
elevation: 10,
child: Icon(widget.connected == true
? Icons.wifi
: Icons.signal_cellular_connected_no_internet_0_bar_sharp),
);
}
}
/// Modal to setup route to connect to api
class SetupAPIRouteModal extends StatefulWidget {
const SetupAPIRouteModal({Key? key}) : super(key: key);
@override
State<SetupAPIRouteModal> createState() => _SetupAPIRouteModalState();
}
class _SetupAPIRouteModalState extends State<SetupAPIRouteModal> {
bool? connected;
final _formKey = GlobalKey<FormBuilderState>();
@override
void initState() {
super.initState();
GetIt.I<AerisAPI>().getAbout().then((value) {
setState(() {
connected = value.isNotEmpty;
});
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).setupAPIRoute),
content: FormBuilder(
key: _formKey,
child: FormBuilderTextField(
initialValue: GetIt.I<AerisAPI>().baseRoute,
name: "route",
validator: FormBuilderValidators.required(context),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).routeToApi,
helperText: "Ex: http://host:port")),
),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
ElevatedButton(
child: Text(AppLocalizations.of(context).tryToConnect),
onPressed: () {
_formKey.currentState!.save();
if (_formKey.currentState!.validate()) {
var route = _formKey.currentState!.value['route'];
if (Uri.tryParse(route) == null) {
setState(() => connected = false);
} else {
final oldRoute = GetIt.I<AerisAPI>().baseRoute;
GetIt.I<AerisAPI>().baseRoute = route;
setState(() {
connected = null;
});
GetIt.I<AerisAPI>().getAbout().then((value) {
setState(() {
connected = value.isNotEmpty;
});
}, onError: (_) => GetIt.I<AerisAPI>().baseRoute = oldRoute);
}
}
},
),
ElevatedButton(
child: Text(connected == null
? AppLocalizations.of(context).loading
: connected == true
? AppLocalizations.of(context).save
: AppLocalizations.of(context).invalidUrl),
onPressed: connected == true
? () {
GetIt.I<SharedPreferences>()
.setString('api', GetIt.I<AerisAPI>().baseRoute);
Provider.of<ActionCatalogueProvider>(context, listen: false).reloadCatalogue();
Navigator.of(context).pop();
}
: null,
)
]);
}
}

View File

@@ -181,6 +181,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.1.0" version: "7.1.0"
flutter_keyboard_visibility:
dependency: transitive
description:
name: flutter_keyboard_visibility
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.0"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_web:
dependency: transitive
description:
name: flutter_keyboard_visibility_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -233,6 +254,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_typeahead:
dependency: "direct main"
description:
name: flutter_typeahead
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.4"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -260,7 +288,7 @@ packages:
source: hosted source: hosted
version: "7.2.0" version: "7.2.0"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@@ -308,6 +336,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
loading_indicator:
dependency: "direct main"
description:
name: loading_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -358,7 +393,7 @@ packages:
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@@ -434,6 +469,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
positioned_tap_detector_2:
dependency: "direct main"
description:
name: positioned_tap_detector_2
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
process: process:
dependency: transitive dependency: transitive
description: description:
@@ -476,6 +518,62 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.27.3" version: "0.27.3"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
shared_preferences_ios:
dependency: transitive
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
shimmer: shimmer:
dependency: transitive dependency: transitive
description: description:
@@ -483,6 +581,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
simple_autocomplete_formfield:
dependency: "direct main"
description:
name: simple_autocomplete_formfield
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
skeleton_loader: skeleton_loader:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -558,6 +663,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.8" version: "0.4.8"
textfield_state:
dependency: transitive
description:
name: textfield_state
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
tuple:
dependency: "direct main"
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@@ -53,6 +53,14 @@ dependencies:
skeleton_loader: ^2.0.0+4 skeleton_loader: ^2.0.0+4
drag_and_drop_lists: ^0.3.2+2 drag_and_drop_lists: ^0.3.2+2
reorderables: ^0.4.2 reorderables: ^0.4.2
path_provider: ^2.0.9
http: ^0.13.4
tuple: ^2.0.0
loading_indicator: ^3.0.3
shared_preferences: ^2.0.13
flutter_typeahead: ^3.2.4
simple_autocomplete_formfield: ^0.3.0
positioned_tap_detector_2: ^1.0.4
dev_dependencies: dev_dependencies:
flutter_launcher_icons: "^0.9.2" flutter_launcher_icons: "^0.9.2"

View File

@@ -7,7 +7,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:aeris/src/main.dart'; import 'package:aeris/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {