mirror of
https://github.com/zoriya/Aeris.git
synced 2025-12-06 06:36:12 +00:00
Merge branch 'master' of github.com:AnonymusRaccoon/Aeris into multilng-about-json
This commit is contained in:
@@ -4,6 +4,8 @@ POSTGRES_PORT=
|
||||
POSTGRES_DB=
|
||||
POSTGRES_HOST=
|
||||
WORKER_API_KEY=
|
||||
|
||||
HOSTNAME=aeris.com
|
||||
WORKER_API_URL=
|
||||
WORKER_URL=
|
||||
DISCORD_CLIENT_ID=
|
||||
@@ -18,3 +20,4 @@ SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_SECRET=
|
||||
ANILIST_SECRET=
|
||||
ANILIST_CLIENT_ID=
|
||||
BACK_URL=
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
- name: Run Docker
|
||||
run: docker run -v $PWD:/dist aeris_mobile_build
|
||||
- name: Upload build artifact
|
||||
if: github.ref == 'refs/head/master'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: aeris_apk
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"actions": [],
|
||||
"reactions": [
|
||||
{
|
||||
"name": "UpdateAbout",
|
||||
"name": "Anilist_UpdateAbout",
|
||||
"description": {
|
||||
"en": "Update the about you section.",
|
||||
"fr": "Mets à jour votre section \"À propos de moi\"."
|
||||
@@ -25,7 +25,7 @@
|
||||
"returns": []
|
||||
},
|
||||
{
|
||||
"name": "ToggleFavourite",
|
||||
"name": "Anilist_ToggleFavourite",
|
||||
"description": {
|
||||
"en": "Add or remove an anime from your favorite.",
|
||||
"fr": "Ajoute ou retire un animé de vos favoris."
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
],
|
||||
"reactions": [
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"name": "PlayTrack",
|
||||
"description": {
|
||||
"en": "Play a track",
|
||||
@@ -78,6 +79,10 @@
|
||||
"en": "Play a track",
|
||||
"fr": "Joue une musique"
|
||||
},
|
||||
=======
|
||||
"name": "Spotify_PlayTrack",
|
||||
"description": "Play a track",
|
||||
>>>>>>> 26566154676c8e279c3615522129e61b851c75a9
|
||||
"params": [
|
||||
{
|
||||
"name": "artist",
|
||||
@@ -134,6 +139,7 @@
|
||||
"returns": []
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"name": "AddTrackToLibrary",
|
||||
"description": {
|
||||
"en": "Add a track to library",
|
||||
@@ -143,6 +149,10 @@
|
||||
"en": "Add a song to library",
|
||||
"fr": "Ajoute une musique à votre librarie"
|
||||
},
|
||||
=======
|
||||
"name": "Spotify_AddTrackToLibrary",
|
||||
"description": "Add a track to library",
|
||||
>>>>>>> 26566154676c8e279c3615522129e61b851c75a9
|
||||
"params": [
|
||||
{
|
||||
"name": "artist",
|
||||
|
||||
@@ -40,34 +40,34 @@ urlHandler :: Service -> Maybe String -> AppM NoContent
|
||||
urlHandler _ Nothing = throwError err400
|
||||
urlHandler Anilist (Just r) = do
|
||||
clientId <- liftIO $ envAsString "ANILIST_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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
|
||||
clientId <- liftIO $ envAsString "DISCORD_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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
|
||||
clientId <- liftIO $ envAsString "GOOGLE_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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
|
||||
clientId <- liftIO $ envAsString "TWITTER_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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
|
||||
clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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
|
||||
clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_REDIRECT_URL" ""
|
||||
backRedirect <- liftIO $ envAsString "BACK_URL" ""
|
||||
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 (Authenticated (User uid name slug)) = do
|
||||
|
||||
@@ -19,6 +19,7 @@ services:
|
||||
depends_on:
|
||||
- "db"
|
||||
environment:
|
||||
- BACK_URL=${BACK_URL}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_HOST=${POSTGRES_HOST}
|
||||
@@ -59,7 +60,6 @@ services:
|
||||
- ANILIST_SECRET=${ANILIST_SECRET}
|
||||
- WORKER_API_URL=${WORKER_API_URL}
|
||||
- WORKER_API_KEY=${WORKER_API_KEY}
|
||||
|
||||
volumes:
|
||||
apk:
|
||||
cache:
|
||||
|
||||
@@ -42,6 +42,7 @@ services:
|
||||
- POSTGRES_PORT=${POSTGRES_PORT}
|
||||
- WORKER_API_KEY=${WORKER_API_KEY}
|
||||
- WORKER_URL=${WORKER_URL}
|
||||
- BACK_URL=${BACK_URL}
|
||||
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- DISCORD_SECRET=${DISCORD_SECRET}
|
||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
||||
|
||||
@@ -21,5 +21,5 @@ RUN flutter gen-l10n
|
||||
RUN flutter pub run flutter_launcher_icons:main
|
||||
# Generate native splashscreen
|
||||
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
|
||||
|
||||
@@ -1,7 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.aeris.mobile">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application
|
||||
android:label="Aeris"
|
||||
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>
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
<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 -->
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
@@ -13,7 +17,9 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/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`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
@@ -24,8 +30,12 @@ SPEC REPOS:
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
url_launcher_ios:
|
||||
@@ -33,8 +43,10 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
/* 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -99,6 +100,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6C3D7227D0D7C100B12C20 /* RunnerDebug.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@@ -217,7 +219,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
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 */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -248,7 +250,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
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 */
|
||||
|
||||
@@ -476,6 +478,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = HJ45QP4WWR;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.jpg",
|
||||
"filename" : "BrandingImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1.jpg",
|
||||
"filename" : "BrandingImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-2.jpg",
|
||||
"filename" : "BrandingImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,20 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
||||
@@ -45,5 +45,20 @@
|
||||
<false/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<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>
|
||||
</plist>
|
||||
8
mobile/ios/Runner/RunnerDebug.entitlements
Normal file
8
mobile/ios/Runner/RunnerDebug.entitlements
Normal 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>
|
||||
@@ -36,5 +36,12 @@
|
||||
"pipelineFormMisingAction": "You must select at least a trigger and a reaction",
|
||||
"logoutWarningMessage": "You are about to logout, are you sure?",
|
||||
"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"
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
"today": "Aujourd'hui",
|
||||
"nameOfThePipeline": "Nom de la pipeline",
|
||||
"addReaction": "Ajouter une Reaction",
|
||||
"addTrigger": "Ajouter un Déclancheur",
|
||||
"setupTrigger": "Gérer une Déclancheur",
|
||||
"addTrigger": "Ajouter un déclencheur",
|
||||
"setupTrigger": "Gérer un déclencheur",
|
||||
"setupReaction": "Gérer une Réaction",
|
||||
"action": "Action",
|
||||
"reactions": "Réactions",
|
||||
@@ -33,8 +33,15 @@
|
||||
"dangerZone": "Zone dangereuse",
|
||||
"createNewPipeline": "Créer une nouvelle pipeline",
|
||||
"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?",
|
||||
"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"
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
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:form_builder_validators/localization/l10n.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/login_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:flutter_gen/gen_l10n/app_localizations.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();
|
||||
GetIt.I.registerSingleton<AerisAPI>(interface);
|
||||
await interface.restoreConnection();
|
||||
runApp(MultiProvider(providers: [
|
||||
ChangeNotifierProvider(create: (_) => PipelineProvider()),
|
||||
ChangeNotifierProvider(create: (_) => UserServiceProvider())
|
||||
ChangeNotifierProvider(create: (_) => ServiceProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ActionCatalogueProvider(), lazy: false)
|
||||
], child: const Aeris()));
|
||||
}
|
||||
|
||||
@@ -48,22 +56,23 @@ class Aeris extends StatelessWidget {
|
||||
'/login': () => const LoginPage(),
|
||||
'/home': () => const HomePage(),
|
||||
};
|
||||
|
||||
return PageRouteBuilder(
|
||||
opaque: false,
|
||||
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),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
transitionsBuilder: (context, animation, secondaryAnimation,
|
||||
child) =>
|
||||
SlideTransition(
|
||||
child: child,
|
||||
position: animation.drive(
|
||||
Tween(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
position: animation.drive(Tween(
|
||||
begin: const Offset(1.0, 0.0), end: Offset.zero))));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,249 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
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/pipeline.dart';
|
||||
import 'package:aeris/src/models/reaction.dart';
|
||||
import 'package:aeris/src/models/service.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
|
||||
class AerisAPI {
|
||||
///TODO set status based on stored credentials
|
||||
bool connected = true;
|
||||
late List<Pipeline> fakeAPI;
|
||||
/// Get Connection state
|
||||
bool _connected = false;
|
||||
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() {
|
||||
var trigger1 = Trigger(
|
||||
service: const Service.spotify(),
|
||||
name: "Play song",
|
||||
last: DateTime.now());
|
||||
var trigger3 = Trigger(
|
||||
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
|
||||
];
|
||||
var scheme = "http";
|
||||
if (Platform.isIOS) {
|
||||
scheme = "aeris";
|
||||
}
|
||||
deepLinkRoute = "$scheme://arthichaud.me";
|
||||
}
|
||||
|
||||
/// Adds new pipeline to API
|
||||
Future<void> createPipeline(Pipeline newPipeline) async {
|
||||
///TODO Send Pipeline to API
|
||||
fakeAPI.add(newPipeline);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
/// Name of the file that contains the JWT used for Aeris' API requestd
|
||||
static const String jwtFile = 'aeris_jwt.txt';
|
||||
|
||||
///ROUTES
|
||||
/// Registers new user in the database and connects it. Returns false if register failed
|
||||
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
|
||||
Future<void> removePipeline(Pipeline pipeline) async {
|
||||
///TODO Send delete request to API
|
||||
fakeAPI.remove(pipeline);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return;
|
||||
Future<bool> removePipeline(Pipeline pipeline) async {
|
||||
var res = await _requestAPI(
|
||||
'/workflow/${pipeline.id}', AerisAPIRequestType.delete, null);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
Future<void> editPipeline(Pipeline updatedPipeline) async {
|
||||
///TODO Send update request to API
|
||||
for (var pipeline in fakeAPI) {
|
||||
if (pipeline.id == updatedPipeline.id) {
|
||||
///TODO Call Api
|
||||
break;
|
||||
}
|
||||
String getServiceAuthURL(Service service) {
|
||||
final serviceName = service == const Service.youtube()
|
||||
? "google"
|
||||
: service.name.toLowerCase();
|
||||
return "$baseRoute/auth/$serviceName/url?redirect_uri=$deepLinkRoute/authorization/$serviceName";
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return;
|
||||
/// Send PUT request to update Pipeline, returns false if failed
|
||||
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
|
||||
Future<List<Pipeline>> getPipelines() async {
|
||||
/// TODO Fetch the API
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return fakeAPI;
|
||||
var res = await _requestAPI('/workflows', AerisAPIRequestType.get, null);
|
||||
if (res.ok == false) return [];
|
||||
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
|
||||
Future<void> disconnectService(Service service) async {
|
||||
///TODO disconnect service from user
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return;
|
||||
Future<bool> disconnectService(Service service) async {
|
||||
var res = await _requestAPI('/auth/${service.name.toLowerCase()}',
|
||||
AerisAPIRequestType.delete, null);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
Future<List<ActionTemplate>> getActionsFor(
|
||||
Service service, Action action) async {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
if (action is Trigger) {
|
||||
///TODO get triggers
|
||||
} else if (action is Reaction) {
|
||||
///TODO get reactions
|
||||
/// Connects the user from the service
|
||||
Future<bool> connectService(Service service, String code) async {
|
||||
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) {
|
||||
return catalogue!.triggerTemplates[service]!;
|
||||
}
|
||||
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'})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:aeris/src/models/action_parameter.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:aeris/src/models/service.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
///Base class for reactions and trigger
|
||||
abstract class Action {
|
||||
@@ -10,10 +12,27 @@ abstract class Action {
|
||||
String name;
|
||||
|
||||
///Action's parameters
|
||||
Map<String, Object> parameters;
|
||||
List<ActionParameter> parameters;
|
||||
|
||||
/// Description of the action (used in catalogue)
|
||||
String? description;
|
||||
|
||||
Action(
|
||||
{Key? key,
|
||||
required this.service,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
25
mobile/lib/src/models/action_parameter.dart
Normal file
25
mobile/lib/src/models/action_parameter.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
import 'package:aeris/src/models/action.dart';
|
||||
import 'package:aeris/src/models/action_parameter.dart';
|
||||
import 'package:aeris/src/models/service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Template for actions, for forms
|
||||
class ActionTemplate extends Action {
|
||||
|
||||
///List of values returned by the action
|
||||
final List<ActionParameter> returnedValues;
|
||||
|
||||
ActionTemplate(
|
||||
{Key? key,
|
||||
required Service service,
|
||||
required String name,
|
||||
Map<String, Object> parameters = const {}})
|
||||
: super(service: service, name: name, parameters: parameters);
|
||||
required String description,
|
||||
this.returnedValues = const [],
|
||||
List<ActionParameter> parameters = const []})
|
||||
: super(service: service, name: name, parameters: parameters, description: description);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:aeris/src/models/trigger.dart';
|
||||
/// Object representation of a pipeline
|
||||
class Pipeline {
|
||||
///Unique identifier
|
||||
final int id;
|
||||
int id;
|
||||
|
||||
/// Name of the pipeline, defined by the user
|
||||
final String name;
|
||||
@@ -16,7 +16,6 @@ class Pipeline {
|
||||
/// Is the pipeline enabled
|
||||
bool enabled;
|
||||
|
||||
|
||||
///The pipeline's reactions
|
||||
final List<Reaction> reactions;
|
||||
|
||||
@@ -29,4 +28,34 @@ class Pipeline {
|
||||
required this.enabled,
|
||||
required this.trigger,
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ignore_for_file: hash_and_equals
|
||||
|
||||
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:aeris/src/models/service.dart';
|
||||
|
||||
@@ -10,20 +11,33 @@ class Reaction extends aeris_action.Action {
|
||||
{Key? key,
|
||||
required Service service,
|
||||
required String name,
|
||||
Map<String, Object> parameters = const {}})
|
||||
List<ActionParameter> parameters = const []})
|
||||
: super(service: service, name: name, parameters: parameters);
|
||||
|
||||
/// Template trigger, used as an 'empty' trigger
|
||||
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
|
||||
bool operator ==(Object other) {
|
||||
Reaction otherReaction = other as Reaction;
|
||||
return service.name == otherReaction.service.name &&
|
||||
name == otherReaction.name &&
|
||||
parameters.values.toString() ==
|
||||
otherReaction.parameters.values.toString() &&
|
||||
parameters.keys.toString() == otherReaction.parameters.keys.toString();
|
||||
parameters.map((e) => e.name).toString() == other.parameters.map((e) => e.name).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Class for a service (Youtube, Gmail, ...)
|
||||
import 'package:aeris/src/aeris_api.dart';
|
||||
import 'package:cached_network_image/cached_network_image.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)
|
||||
class Service {
|
||||
@@ -22,16 +26,19 @@ class Service {
|
||||
height: logoSize,
|
||||
));
|
||||
|
||||
/// Get full url for OAuth2
|
||||
String get authUrl => GetIt.I<AerisAPI>().getServiceAuthURL(this);
|
||||
|
||||
const Service.spotify()
|
||||
: name = "Spotify",
|
||||
url = "https://www.spotify.com",
|
||||
logoUrl =
|
||||
"https://www.presse-citron.net/app/uploads/2020/06/spotify-une-.jpg";
|
||||
const Service.gmail()
|
||||
: name = "Gmail",
|
||||
url = "https://mail.google.com/",
|
||||
const Service.anilist()
|
||||
: name = "Anilist",
|
||||
url = "https://anilist.co",
|
||||
logoUrl =
|
||||
"https://play-lh.googleusercontent.com/KSuaRLiI_FlDP8cM4MzJ23ml3og5Hxb9AapaGTMZ2GgR103mvJ3AAnoOFz1yheeQBBI";
|
||||
"https://anilist.co/img/icons/android-chrome-512x512.png";
|
||||
const Service.discord()
|
||||
: name = "Discord",
|
||||
url = "https://discord.com/app",
|
||||
@@ -43,7 +50,7 @@ class Service {
|
||||
logoUrl =
|
||||
"https://f.hellowork.com/blogdumoderateur/2019/11/twitter-logo-1200x1200.jpg";
|
||||
const Service.github()
|
||||
: name = "GitHub",
|
||||
: name = "Github",
|
||||
url = "https://github.com/",
|
||||
logoUrl = "https://avatars.githubusercontent.com/u/9919?s=280&v=4";
|
||||
const Service.youtube()
|
||||
@@ -57,13 +64,23 @@ class Service {
|
||||
logoUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Cle.png/1024px-Cle.png";
|
||||
|
||||
/// Returns a list of all the available services
|
||||
static all() => const [
|
||||
static List<Service> all() => const [
|
||||
Service.discord(),
|
||||
Service.github(),
|
||||
Service.gmail(),
|
||||
Service.anilist(),
|
||||
Service.youtube(),
|
||||
Service.twitter(),
|
||||
Service.spotify(),
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:aeris/src/models/action_parameter.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/action.dart' as aeris_action;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
@@ -14,11 +15,27 @@ class Trigger extends aeris_action.Action {
|
||||
{Key? key,
|
||||
required Service service,
|
||||
required String name,
|
||||
Map<String, Object> parameters = const {},
|
||||
List<ActionParameter> parameters = const [],
|
||||
this.last})
|
||||
: 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() {
|
||||
var context = AppLocalizations.of(Aeris.materialKey.currentContext!);
|
||||
@@ -32,7 +49,7 @@ class Trigger extends aeris_action.Action {
|
||||
|
||||
/// Template trigger, used as an 'empty' trigger
|
||||
Trigger.template({Key? key, this.last})
|
||||
: super(service: const Service.twitter(), name: '', parameters: {});
|
||||
: super(service: Service.all()[0], name: '', parameters: []);
|
||||
|
||||
@override
|
||||
// ignore: avoid_renaming_method_parameters
|
||||
@@ -41,7 +58,7 @@ class Trigger extends aeris_action.Action {
|
||||
return service.name == other.service.name &&
|
||||
name == other.name &&
|
||||
last == other.last &&
|
||||
parameters.values.toString() == other.parameters.values.toString() &&
|
||||
parameters.keys.toString() == other.parameters.keys.toString();
|
||||
parameters.map((e) => e.name).toString() ==
|
||||
other.parameters.map((e) => e.name).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
73
mobile/lib/src/providers/action_catalogue_provider.dart
Normal file
73
mobile/lib/src/providers/action_catalogue_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ class PipelineProvider extends ChangeNotifier {
|
||||
late PipelineCollection _pipelineCollection;
|
||||
|
||||
/// Tells if the provers has loaded data at least once
|
||||
bool initialized = false;
|
||||
bool _initialized = false;
|
||||
bool get initialized => _initialized;
|
||||
|
||||
PipelineProvider() {
|
||||
_pipelineCollection = PipelineCollection(
|
||||
@@ -23,17 +24,17 @@ class PipelineProvider extends ChangeNotifier {
|
||||
/// Fetches the pipelines from API and put them in the collection
|
||||
Future<void> fetchPipelines() {
|
||||
return GetIt.I<AerisAPI>().getPipelines().then((pipelines) {
|
||||
_initialized = true;
|
||||
_pipelineCollection.pipelines = pipelines;
|
||||
sortPipelines();
|
||||
initialized = true;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a pipeline in the Provider
|
||||
addPipeline(Pipeline newPipeline) {
|
||||
initialized = true;
|
||||
addPipeline(Pipeline newPipeline) async {
|
||||
await GetIt.I<AerisAPI>().createPipeline(newPipeline);
|
||||
_pipelineCollection.pipelines.add(newPipeline);
|
||||
GetIt.I<AerisAPI>().createPipeline(newPipeline);
|
||||
sortPipelines();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
41
mobile/lib/src/providers/services_provider.dart
Normal file
41
mobile/lib/src/providers/services_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
28
mobile/lib/src/views/authorization_page.dart
Normal file
28
mobile/lib/src/views/authorization_page.dart
Normal 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],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -79,15 +79,19 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
|
||||
onTap: () {
|
||||
showAerisCardPage(
|
||||
context,
|
||||
(_) =>
|
||||
SetupActionPage(action: trigger))
|
||||
(_) => SetupActionPage(
|
||||
action: trigger,
|
||||
parentReactions: reactions
|
||||
))
|
||||
.then((_) => setState(() {}));
|
||||
})
|
||||
: ActionCard(
|
||||
leading: trigger.service.getLogo(logoSize: 50),
|
||||
title: trigger.name,
|
||||
title: trigger.displayName(),
|
||||
trailing: ActionCardPopupMenu(
|
||||
deletable: false,
|
||||
parentReactions: reactions,
|
||||
parentTrigger: trigger,
|
||||
action: trigger,
|
||||
then: () => setState(() {})),
|
||||
),
|
||||
@@ -104,8 +108,10 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
|
||||
itemBuilder: (reaction) => ActionCard(
|
||||
key: ValueKey(reactions.indexOf(reaction)),
|
||||
leading: reaction.service.getLogo(logoSize: 50),
|
||||
title: reaction.name,
|
||||
title: reaction.displayName(),
|
||||
trailing: ActionCardPopupMenu(
|
||||
parentTrigger: trigger == Trigger.template() ? null : trigger,
|
||||
parentReactions: reactions,
|
||||
deletable: reactions.length > 1,
|
||||
action: reaction,
|
||||
then: () => setState(() {}),
|
||||
@@ -128,7 +134,10 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
|
||||
showAerisCardPage(
|
||||
context,
|
||||
(_) => SetupActionPage(
|
||||
action: newreact))
|
||||
action: newreact,
|
||||
parentReactions: reactions,
|
||||
parentTrigger: trigger == Trigger.template() ? null : trigger,
|
||||
))
|
||||
.then((_) => setState(() {
|
||||
if (newreact != Reaction.template()) {
|
||||
reactions.add(newreact);
|
||||
|
||||
@@ -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/service_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/home_page_sort_menu.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:provider/provider.dart';
|
||||
import 'package:skeleton_loader/skeleton_loader.dart';
|
||||
@@ -28,23 +30,25 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
Widget serviceActionButtons = IconButton(
|
||||
icon: const Icon(Icons.electrical_services),
|
||||
onPressed: () => showAerisCardPage(context, (context) => const ServicePage())
|
||||
);
|
||||
onPressed: () =>
|
||||
showAerisCardPage(context, (context) => const ServicePage()));
|
||||
Widget logoutActionButton = IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => WarningDialog(
|
||||
message: AppLocalizations.of(context).logoutWarningMessage,
|
||||
onAccept: () => Navigator.of(context).popAndPushNamed('/'), //TODO logout
|
||||
warnedAction: AppLocalizations.of(context).logout
|
||||
)
|
||||
),
|
||||
onAccept: () {
|
||||
GetIt.I<AerisAPI>().stopConnection();
|
||||
Navigator.of(context).popAndPushNamed('/');
|
||||
},
|
||||
warnedAction: AppLocalizations.of(context).logout)),
|
||||
);
|
||||
return Consumer<PipelineProvider>(
|
||||
builder: (context, provider, _) => AerisPage(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => showAerisCardPage(context, (_) => const CreatePipelinePage()),
|
||||
onPressed: () => showAerisCardPage(
|
||||
context, (_) => const CreatePipelinePage()),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
elevation: 10,
|
||||
child: const Icon(Icons.add),
|
||||
@@ -57,27 +61,36 @@ class _HomePageState extends State<HomePage> {
|
||||
logoutActionButton
|
||||
],
|
||||
body: provider.initialized == false
|
||||
? ListView(physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 20, top: 20, left: 10, right: 10),
|
||||
children: [SkeletonLoader(
|
||||
builder: ClickableCard(onTap:(){}, body: const SizedBox(height: 80)),
|
||||
? ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 20, top: 20, left: 10, right: 10),
|
||||
children: [
|
||||
SkeletonLoader(
|
||||
builder: ClickableCard(
|
||||
onTap: () {},
|
||||
body: const SizedBox(height: 80)),
|
||||
items: 10,
|
||||
highlightColor: Theme.of(context).colorScheme.secondary
|
||||
)])
|
||||
highlightColor:
|
||||
Theme.of(context).colorScheme.secondary)
|
||||
])
|
||||
: LiquidPullToRefresh(
|
||||
borderWidth: 2,
|
||||
animSpeedFactor: 3,
|
||||
color: Colors.transparent,
|
||||
showChildOpacityTransition: false,
|
||||
onRefresh: () => provider.fetchPipelines()
|
||||
onRefresh: () => provider
|
||||
.fetchPipelines()
|
||||
.then((_) => setState(() {})), // refresh callback
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 20, top: 20, left: 10, right: 10),
|
||||
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),
|
||||
PipelineCard(
|
||||
pipeline: provider.getPipelineAt(index),
|
||||
),
|
||||
)),
|
||||
));
|
||||
|
||||
@@ -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:flutter_login/flutter_login.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
const users = {
|
||||
'dribbble@gmail.com': '12345',
|
||||
'hunter@gmail.com': 'hunter',
|
||||
};
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
/// Login Page Widget
|
||||
class LoginPage extends StatelessWidget {
|
||||
@@ -17,39 +14,24 @@ class LoginPage extends StatelessWidget {
|
||||
Duration get loginDuration => const Duration(milliseconds: 2500);
|
||||
|
||||
/// Called when user clicks on [FlutterLogin] widget 'login' button
|
||||
Future<String?> _authUser(LoginData data) {
|
||||
debugPrint('Name: ${data.name}, Password: ${data.password}');
|
||||
return Future.delayed(loginDuration).then((_) {
|
||||
if (!users.containsKey(data.name)) {
|
||||
return AppLocalizations.of(Aeris.materialKey.currentContext!)
|
||||
.usernameOrPasswordIncorrect;
|
||||
}
|
||||
if (users[data.name] != data.password) {
|
||||
Future<String?> _authUser(LoginData data) async {
|
||||
bool connected =
|
||||
await GetIt.I<AerisAPI>().createConnection(data.name, data.password);
|
||||
if (!connected) {
|
||||
return AppLocalizations.of(Aeris.materialKey.currentContext!)
|
||||
.usernameOrPasswordIncorrect;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens signup page of [FlutterLogin] widget
|
||||
Future<String?> _signupUser(SignupData data) {
|
||||
debugPrint('Signup Name: ${data.name}, Password: ${data.password}');
|
||||
return Future.delayed(loginDuration).then((_) {
|
||||
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;
|
||||
Future<String?> _signupUser(SignupData data) async {
|
||||
bool connected =
|
||||
await GetIt.I<AerisAPI>().signUpUser(data.name!, data.password!);
|
||||
if (connected == false) {
|
||||
return AppLocalizations.of(Aeris.materialKey.currentContext!).errorOnSignup;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -59,16 +41,21 @@ class LoginPage extends StatelessWidget {
|
||||
body: FlutterLogin(
|
||||
disableCustomPageTransformer: true,
|
||||
logo: const AssetImage("assets/logo.png"),
|
||||
onRecoverPassword: _recoverPassword,
|
||||
hideForgotPasswordButton: true,
|
||||
onRecoverPassword: (_) => null,
|
||||
theme: LoginTheme(
|
||||
pageColorLight: Colors.transparent,
|
||||
pageColorDark: Colors.transparent,
|
||||
primaryColor: Theme.of(context).colorScheme.primary),
|
||||
onLogin: _authUser,
|
||||
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: () {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Navigator.of(context).popAndPushNamed("/home");
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,12 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
|
||||
onTap: () {
|
||||
Reaction newreaction = Reaction.template();
|
||||
showAerisCardPage(
|
||||
context, (_) => SetupActionPage(action: newreaction))
|
||||
context, (_) => SetupActionPage(
|
||||
action: newreaction,
|
||||
parentTrigger: pipeline.trigger,
|
||||
parentReactions: pipeline.reactions,
|
||||
)
|
||||
)
|
||||
.then((r) {
|
||||
if (newreaction != Reaction.template()) {
|
||||
setState(() {
|
||||
@@ -135,9 +140,11 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
ActionCard(
|
||||
leading: pipeline.trigger.service.getLogo(logoSize: 50),
|
||||
title: pipeline.trigger.name,
|
||||
title: pipeline.trigger.displayName(),
|
||||
trailing: ActionCardPopupMenu(
|
||||
deletable: false,
|
||||
parentTrigger: pipeline.trigger,
|
||||
parentReactions: pipeline.reactions,
|
||||
action: pipeline.trigger,
|
||||
then: () {
|
||||
setState(() {});
|
||||
@@ -152,8 +159,10 @@ class _PipelineDetailPageState extends State<PipelineDetailPage> {
|
||||
itemBuilder: (reaction) => ActionCard(
|
||||
key: ValueKey(pipeline.reactions.indexOf(reaction)),
|
||||
leading: reaction.service.getLogo(logoSize: 50),
|
||||
title: reaction.name,
|
||||
title: reaction.displayName(),
|
||||
trailing: ActionCardPopupMenu(
|
||||
parentTrigger: pipeline.trigger,
|
||||
parentReactions: pipeline.reactions,
|
||||
deletable: pipeline.reactions.length > 1,
|
||||
action: reaction,
|
||||
then: () {
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
import 'package:aeris/src/aeris_api.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/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/aeris_card_page.dart';
|
||||
import 'package:aeris/src/widgets/warning_dialog.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
///Page listing connected & available services
|
||||
class ServicePage extends StatelessWidget {
|
||||
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) {
|
||||
UserServiceProvider uServicesProvider =
|
||||
Provider.of<UserServiceProvider>(context);
|
||||
|
||||
if (services.isEmpty) return [];
|
||||
return [
|
||||
Text(
|
||||
"$groupName:",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (var service in uServicesProvider.userServices)
|
||||
for (var service in services)
|
||||
ActionCard(
|
||||
leading: service.serviceProvider.getLogo(logoSize: 50),
|
||||
title: service.serviceProvider.name,
|
||||
leading: service.getLogo(logoSize: 50),
|
||||
title: service.name,
|
||||
trailing: IconButton(
|
||||
splashColor: trailingIcon.color!.withAlpha(100),
|
||||
splashRadius: 20,
|
||||
icon: trailingIcon,
|
||||
onPressed: () => onTap(service.serviceProvider),
|
||||
onPressed: () => onTap(service),
|
||||
)),
|
||||
const SizedBox(height: 30),
|
||||
];
|
||||
@@ -43,34 +38,22 @@ class ServicePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Service> services = const [
|
||||
Service.discord(),
|
||||
Service.gmail(),
|
||||
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(
|
||||
return Consumer<ServiceProvider>(
|
||||
builder: (context, serviceProvider, _) => Consumer<PipelineProvider>(
|
||||
builder: (context, pipelineProvider, _) => AerisCardPage(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...[
|
||||
Align(
|
||||
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)
|
||||
],
|
||||
...getServiceGroup(
|
||||
serviceProvider.connectedServices,
|
||||
AppLocalizations.of(context).connected,
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
(Service service) => showDialog(
|
||||
@@ -78,34 +61,22 @@ class ServicePage extends StatelessWidget {
|
||||
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)),
|
||||
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) =>
|
||||
print("Connected") /* TODO open page to connect service*/,
|
||||
const Icon(Icons.connect_without_contact, color: Colors.green),
|
||||
(Service service) {
|
||||
launch(Uri.parse(service.authUrl).toString(), forceSafariVC: false);
|
||||
},
|
||||
context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:aeris/src/models/action_parameter.dart';
|
||||
import 'package:aeris/src/models/action_template.dart';
|
||||
import 'package:aeris/src/aeris_api.dart';
|
||||
import 'package:aeris/src/models/reaction.dart';
|
||||
import 'package:aeris/src/models/trigger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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
|
||||
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
|
||||
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
|
||||
State<SetupActionPage> createState() => _SetupActionPageState();
|
||||
@@ -30,9 +37,7 @@ class _SetupActionPageState extends State<SetupActionPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
serviceState = widget.action.service;
|
||||
GetIt.I<AerisAPI>().getActionsFor(serviceState!, widget.action).then((actions) => setState(() {
|
||||
availableActions = actions;
|
||||
}));
|
||||
availableActions = GetIt.I<AerisAPI>().getActionsFor(serviceState!, widget.action);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -43,12 +48,9 @@ class _SetupActionPageState extends State<SetupActionPage> {
|
||||
elevation: 8,
|
||||
underline: Container(),
|
||||
onChanged: (service) {
|
||||
GetIt.I<AerisAPI>().getActionsFor(service!, widget.action).then((actions) => setState(() {
|
||||
availableActions = actions;
|
||||
}));
|
||||
setState(() {
|
||||
serviceState = service;
|
||||
availableActions = [];
|
||||
availableActions = GetIt.I<AerisAPI>().getActionsFor(service!, widget.action);
|
||||
});
|
||||
},
|
||||
items: Service.all().map<DropdownMenuItem<Service>>((Service service) {
|
||||
@@ -66,6 +68,9 @@ class _SetupActionPageState extends State<SetupActionPage> {
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
var cardShape = const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)));
|
||||
|
||||
return AerisCardPage(
|
||||
body: Padding(
|
||||
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)
|
||||
SkeletonLoader(
|
||||
builder: const Card(child: SizedBox(height: 40), elevation: 5),
|
||||
items: 10,
|
||||
builder: Card(shape: cardShape, child: const SizedBox(height: 40), elevation: 5),
|
||||
items: 15,
|
||||
highlightColor: Theme.of(context).colorScheme.secondary
|
||||
)
|
||||
else
|
||||
...[for (aeris.Action availableAction in availableActions!)
|
||||
...[for (ActionTemplate availableAction in availableActions!)
|
||||
Card(
|
||||
elevation: 5,
|
||||
child: ExpandablePanel(
|
||||
shape: cardShape,
|
||||
child: ExpandableNotifier(
|
||||
child: ScrollOnExpand(child: ExpandablePanel(
|
||||
header: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 30, top: 20, bottom: 20),
|
||||
child: Text(availableAction.name,
|
||||
child: Text(availableAction.displayName(),
|
||||
style: const TextStyle(fontSize: 15))),
|
||||
collapsed: Container(),
|
||||
expanded: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
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,
|
||||
parametersNames:
|
||||
availableAction.parameters.keys.toList(),
|
||||
initValues: widget.action.name == availableAction.name
|
||||
&& availableAction.service.name == widget.action.service.name
|
||||
? widget.action.parameters : const {},
|
||||
parameters: availableAction.parameters.map((param) {
|
||||
if (widget.action.service.name == serviceState!.name && widget.action.name == availableAction.name) {
|
||||
var previousParams = widget.action.parameters.where((element) => element.name == param.name);
|
||||
if (previousParams.isNotEmpty) {
|
||||
param.value = previousParams.first.value;
|
||||
}
|
||||
}
|
||||
return param;
|
||||
}).toList(),
|
||||
onValidate: (parameters) {
|
||||
widget.action.service = serviceState!;
|
||||
widget.action.parameters = parameters;
|
||||
widget.action.parameters = ActionParameter.fromJSON(parameters);
|
||||
widget.action.name = availableAction.name;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
)),
|
||||
),
|
||||
))),
|
||||
const SizedBox(height: 10)
|
||||
]
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
@@ -15,11 +16,30 @@ class StartupPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _StartupPageState extends State<StartupPage> {
|
||||
bool connected = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
GetIt.I<AerisAPI>().getAbout().then((value) {
|
||||
setState(() {
|
||||
connected = value.isNotEmpty;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isConnected = GetIt.I<AerisAPI>().connected;
|
||||
bool isConnected = GetIt.I<AerisAPI>().isConnected;
|
||||
return AerisPage(
|
||||
displayAppbar: false,
|
||||
floatingActionButton: SetupAPIRouteButton(
|
||||
connected: connected,
|
||||
onSetup: () => GetIt.I<AerisAPI>().getAbout().then((value) {
|
||||
setState(() {
|
||||
connected = value.isNotEmpty;
|
||||
});
|
||||
})
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -48,13 +68,13 @@ class _StartupPageState extends State<StartupPage> {
|
||||
textStyle: const TextStyle(fontSize: 20),
|
||||
primary: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed: () {
|
||||
onPressed: connected ? () {
|
||||
if (isConnected) {
|
||||
Navigator.of(context).popAndPushNamed('/home');
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
}
|
||||
},
|
||||
} : null,
|
||||
child: Tooltip(
|
||||
message: 'Connexion',
|
||||
child: Text(AppLocalizations.of(context).connect)
|
||||
|
||||
@@ -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:flutter/material.dart';
|
||||
import 'package:aeris/src/views/setup_action_page.dart';
|
||||
@@ -11,6 +13,8 @@ class ActionCardPopupMenu extends StatelessWidget {
|
||||
ActionCardPopupMenu({
|
||||
Key? key,
|
||||
required this.action,
|
||||
this.parentTrigger,
|
||||
required this.parentReactions,
|
||||
required this.then,
|
||||
required this.deletable,
|
||||
this.onDelete,
|
||||
@@ -22,6 +26,10 @@ class ActionCardPopupMenu extends StatelessWidget {
|
||||
|
||||
/// Selected 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
|
||||
final void Function() then;
|
||||
@@ -46,15 +54,18 @@ class ActionCardPopupMenu extends StatelessWidget {
|
||||
icon: Icons.settings,
|
||||
title: AppLocalizations.of(context).modify,
|
||||
value: () => showAerisCardPage(
|
||||
context, (_) => SetupActionPage(action: action)).then((_) => then())
|
||||
),
|
||||
context, (_) => SetupActionPage(
|
||||
action: action,
|
||||
parentTrigger: parentTrigger,
|
||||
parentReactions: parentReactions,
|
||||
))
|
||||
.then((_) => then())),
|
||||
AerisPopupMenuItem(
|
||||
context: context,
|
||||
icon: Icons.delete,
|
||||
title: AppLocalizations.of(context).delete,
|
||||
value: onDelete,
|
||||
enabled: deletable
|
||||
),
|
||||
enabled: deletable),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.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
|
||||
class ActionForm extends StatefulWidget {
|
||||
/// Name of the action
|
||||
final String name;
|
||||
/// Names of the parameters
|
||||
final List<String> parametersNames;
|
||||
/// Initial values of the fields
|
||||
final Map<String, Object> initValues;
|
||||
/// List of parameters, 'values' are used as default values
|
||||
final List<ActionParameter> parameters;
|
||||
/// What the action does
|
||||
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
|
||||
final void Function(Map<String, String>) onValidate;
|
||||
@@ -18,9 +45,13 @@ class ActionForm extends StatefulWidget {
|
||||
const ActionForm(
|
||||
{Key? key,
|
||||
required this.name,
|
||||
required this.parametersNames,
|
||||
required this.description,
|
||||
required this.parameters,
|
||||
required this.onValidate,
|
||||
this.initValues = const {}})
|
||||
required this.candidate,
|
||||
this.triggerCandidate,
|
||||
required this.reactionsCandidates,
|
||||
})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -28,32 +59,91 @@ class ActionForm extends StatefulWidget {
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilder(
|
||||
return Consumer<ActionCatalogueProvider>(
|
||||
builder: (__, catalogue, _) => Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...widget.parametersNames.map((name) => FormBuilderTextField(
|
||||
initialValue: (widget.initValues.containsKey(name)) ? widget.initValues[name] as String : null,
|
||||
name: name,
|
||||
Text(widget.description, textAlign: TextAlign.left, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||
...widget.parameters.map((param) {
|
||||
final textEditingController = TextEditingController(text: values[param.name] ?? param.value?.toString());
|
||||
return TypeAheadFormField<Suggestion>(
|
||||
key: Key(param.description),
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
autofocus: true,
|
||||
controller: textEditingController,
|
||||
enableSuggestions: widget.candidate is Reaction,
|
||||
decoration: InputDecoration(
|
||||
labelText: name,
|
||||
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),
|
||||
]),
|
||||
keyboardType: (widget.initValues.containsKey(name)) && widget.initValues[name] is int ? TextInputType.number : null,
|
||||
)),
|
||||
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(
|
||||
child: Text(AppLocalizations.of(context).save),
|
||||
onPressed: () {
|
||||
_formKey.currentState!.save();
|
||||
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> {
|
||||
]
|
||||
]
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ class HomePageSortMenu extends StatelessWidget {
|
||||
value: !split),
|
||||
],
|
||||
onSelected: (sortingMethod) {
|
||||
/// TODO: not clean
|
||||
if (sortingMethod is bool) {
|
||||
collectionProvider.splitDisabled = sortingMethod;
|
||||
} else {
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:reorderables/reorderables.dart';
|
||||
|
||||
class ReorderableReactionCardsList extends StatefulWidget {
|
||||
ReorderableReactionCardsList(
|
||||
const ReorderableReactionCardsList(
|
||||
{Key? key,
|
||||
required this.onReorder,
|
||||
required this.reactionList,
|
||||
required this.itemBuilder})
|
||||
: super(key: key);
|
||||
|
||||
List<Reaction> reactionList;
|
||||
final List<Reaction> reactionList;
|
||||
|
||||
// Callback when a list has been reordered
|
||||
final void Function() onReorder;
|
||||
|
||||
118
mobile/lib/src/widgets/setup_api_route.dart
Normal file
118
mobile/lib/src/widgets/setup_api_route.dart
Normal 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,
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -233,6 +254,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -260,7 +288,7 @@ packages:
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
@@ -308,6 +336,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -358,7 +393,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
@@ -434,6 +469,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -476,6 +518,62 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -483,6 +581,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -558,6 +663,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -53,6 +53,14 @@ dependencies:
|
||||
skeleton_loader: ^2.0.0+4
|
||||
drag_and_drop_lists: ^0.3.2+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:
|
||||
flutter_launcher_icons: "^0.9.2"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:aeris/src/main.dart';
|
||||
import 'package:aeris/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
|
||||
Reference in New Issue
Block a user