├── magic_flutter ├── app │ ├── android │ │ ├── gradle.properties │ │ ├── .settings │ │ │ └── org.eclipse.buildship.core.prefs │ │ ├── app │ │ │ ├── .settings │ │ │ │ └── org.eclipse.buildship.core.prefs │ │ │ ├── src │ │ │ │ └── main │ │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── splash.png │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── java │ │ │ │ │ └── apiexplorer │ │ │ │ │ │ └── com │ │ │ │ │ │ └── app │ │ │ │ │ │ └── MainActivity.java │ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── proguard-rules.pro │ │ │ ├── .classpath │ │ │ ├── .project │ │ │ ├── google-services.json │ │ │ └── build.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .project │ │ ├── settings.gradle │ │ └── build.gradle │ ├── mountains.jpg │ ├── ios │ │ ├── Flutter │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── AppFrameworkInfo.plist │ │ ├── Runner │ │ │ ├── AppDelegate.h │ │ │ ├── Assets.xcassets │ │ │ │ ├── LaunchImage.imageset │ │ │ │ │ ├── LaunchImage.png │ │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ │ ├── README.md │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ │ └── Contents.json │ │ │ ├── main.m │ │ │ ├── AppDelegate.m │ │ │ ├── Info.plist │ │ │ └── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ ├── Runner.xcodeproj │ │ │ ├── project.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ │ ├── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ │ └── Runner.xcscheme │ │ │ └── project.pbxproj │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── Podfile │ ├── .metadata │ ├── lib │ │ ├── ui │ │ │ ├── image_page.dart │ │ │ ├── search_page.dart │ │ │ ├── app_data_loader.dart │ │ │ ├── about_api_page.dart │ │ │ ├── config.dart │ │ │ ├── card_details.dart │ │ │ ├── card_details_edit.dart │ │ │ └── card_listing.dart │ │ ├── models │ │ │ ├── model_card.dart │ │ │ └── end_point.dart │ │ ├── main.dart │ │ └── app_data.dart │ ├── test │ │ └── widget_test.dart │ ├── .gitignore │ └── pubspec.yaml ├── .idea │ └── codeStyles │ │ └── Project.xml └── magic_flutter.iml ├── screenshots ├── diapo-all.png ├── magic-list.png ├── shop-ios.png ├── composition.png ├── detail-hero.PNG ├── magic-detail.png ├── movies-grid.PNG ├── shop-android.png ├── movies-detail.PNG ├── places-detail.png ├── planets-detail.PNG └── planets-detail-compare.PNG ├── .idea ├── vcs.xml ├── modules.xml ├── magic.iml └── workspace.xml ├── aboutPlanetsAPI.md ├── .gitignore ├── data ├── categories.json ├── brand-new-proposal.json ├── medium-stories.txt └── firebase-remoteconfig.json └── README.md /magic_flutter/app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /screenshots/diapo-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/diapo-all.png -------------------------------------------------------------------------------- /screenshots/magic-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/magic-list.png -------------------------------------------------------------------------------- /screenshots/shop-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/shop-ios.png -------------------------------------------------------------------------------- /screenshots/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/composition.png -------------------------------------------------------------------------------- /screenshots/detail-hero.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/detail-hero.PNG -------------------------------------------------------------------------------- /screenshots/magic-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/magic-detail.png -------------------------------------------------------------------------------- /screenshots/movies-grid.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/movies-grid.PNG -------------------------------------------------------------------------------- /screenshots/shop-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/shop-android.png -------------------------------------------------------------------------------- /magic_flutter/app/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/mountains.jpg -------------------------------------------------------------------------------- /screenshots/movies-detail.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/movies-detail.PNG -------------------------------------------------------------------------------- /screenshots/places-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/places-detail.png -------------------------------------------------------------------------------- /screenshots/planets-detail.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/planets-detail.PNG -------------------------------------------------------------------------------- /magic_flutter/app/android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /screenshots/planets-detail-compare.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/screenshots/planets-detail-compare.PNG -------------------------------------------------------------------------------- /magic_flutter/app/android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-hdpi/splash.png -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /aboutPlanetsAPI.md: -------------------------------------------------------------------------------- 1 | # About Planets API 2 | 3 | Planet info taken from https://dry-plains-91502.herokuapp.com/planets 4 | Planet images by Erick Lara (https://www.behance.net/gallery/62133591/PLANETS) -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotoxl/flutter_magic/HEAD/magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /magic_flutter/app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | #Flutter Wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } 8 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /magic_flutter/app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8b81449d4bb02a8b4c4357dbd486e435cd0ac1ee 8 | channel: master 9 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/java/apiexplorer/com/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package apiexplorer.com.app; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | #073349 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | .pub/ 7 | build/ 8 | # If you're building an application, you may want to check-in your pubspec.lock 9 | pubspec.lock 10 | 11 | # Directory created by dartdoc 12 | # If you don't generate documentation locally you can remove this line. 13 | doc/api/ 14 | magic_flutter/app/android/google-services.json 15 | magic_flutter/app/ios/Runner/GoogleService-Info.plist 16 | -------------------------------------------------------------------------------- /magic_flutter/app/android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/magic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /magic_flutter/app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | @import Firebase; 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application 8 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 9 | [GeneratedPluginRegistrant registerWithRegistry:self]; 10 | // Override point for customization after application launch. 11 | 12 | [FIRApp configure]; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /magic_flutter/app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | maven { url "https://maven.google.com" } // Gradle < 4.0 6 | } 7 | 8 | dependencies { 9 | classpath 'com.google.gms:google-services:4.0.1' // new 10 | classpath 'com.google.gms:google-services:4.0.1' 11 | classpath "com.android.tools.build:gradle:3.1.2" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /data/categories.json: -------------------------------------------------------------------------------- 1 | https://developer.apple.com/app-store/categories/ 2 | 3 | { 4 | "version_code":"201810022251", 5 | "tags":[ 6 | {"name":"Art"}, 7 | {"name":"Science"}, 8 | {"name":"Local"}, 9 | 10 | {"name":"Technology"}, 11 | 12 | {"name":"Games"}, 13 | {"name":"Business"}, 14 | {"name":"Education"}, 15 | {"name":"Lifestyle"}, 16 | {"name":"Entertainment", "eg":"books, music, movies, tv series"}, 17 | {"name":"Utilities"}, 18 | {"name":"Travel"}, 19 | {"name":"Health"}, 20 | 21 | {"name":"Food & drink"}, 22 | {"name":"Productivity"}, 23 | 24 | {"name":"Finance"}, 25 | {"name":"Photo & video"}, 26 | {"name":"Reference"}, 27 | {"name":"Sports"}, 28 | {"name":"Social"}, 29 | {"name":"News"}, 30 | {"name":"Medical"}, 31 | {"name":"Shopping"}, 32 | 33 | {"name":"Kids", "eg":"content suitable for children aged under 11"} 34 | ] 35 | } 36 | 37 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/image_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_view/photo_view.dart'; 3 | import 'package:app/app_data.dart'; 4 | 5 | 6 | class ImagePage extends StatelessWidget { 7 | final String url; 8 | 9 | const ImagePage({Key key, this.url}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return new Scaffold( 14 | appBar: AppBar(title:new Text(this.url) ), 15 | body:imageView(context) 16 | ); 17 | } 18 | 19 | imageView(BuildContext context) { 20 | double screenHeight=MediaQuery.of(context).size.height - 120.0; 21 | double screenWidth=MediaQuery.of(context).size.width;//- 120.0; 22 | 23 | appData.logEvent('images_show', {'ep':appData.getCurrEndPoint().endpointTitle, 'image_src':url}); 24 | 25 | return new Container( 26 | child: new PhotoView( 27 | size:Size.fromHeight(screenWidth), 28 | imageProvider: NetworkImage(url), 29 | minScale: PhotoViewComputedScale.contained * 0.3, 30 | maxScale: 3.0, 31 | ) 32 | ); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /magic_flutter/app/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import 'package:app/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(new MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /magic_flutter/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /magic_flutter/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /magic_flutter/magic_flutter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # New article in Medium 2 | 3 | https://medium.com/@rotoxl/api-explorer-a6b8c9aa97bb 4 | 5 | # Now in beta@Google Play 6 | 7 | ![https://play.google.com/store/apps/details?id=apiexplorer.com.app](screenshots/shop-android.png) 8 | 9 | ![](screenshots/diapo-all.png) 10 | 11 | 12 | # Magic API & Flutter 13 | 14 | * data is fetched from an API endpoint 15 | * data is shown in listing+detail 16 | * 4 flavours for listing (list, gridWithName, gridWithoutName, match) 17 | * 4 flavours for detail (hero, data, compare, match) 18 | 19 | ![Structure](screenshots/composition.png) 20 | 21 | # Next steps 22 | * sprint 1 23 | * onboarding screen 24 | 25 | * sprint 2 26 | * list page -> set query parameter 27 | * cupertino style? 28 | * detail window -> related items widget 29 | * change API endpoint window 30 | * "add new endpoint" logic & form 31 | * detailconfig window 32 | * new chips, stats widgets, new fields are still not configurable 33 | 34 | # Recently added 35 | * analytics 36 | * basic scaffolding 37 | * add more and more 38 | * startup 39 | * choose last used EndPoint 40 | * remote config with firebase 41 | * "about this API" dialog 42 | * app icon & splash 43 | * list page 44 | * empty states 45 | * list page -> search (in results) 46 | * toggle view list/grid 47 | * more styles: gridWithText, gridWithoutText 48 | * config/change API screen 49 | * firebase remote config support 50 | * sorted by endpoint (so that same-domain endpoints are grouped together) 51 | * "change endpoint" to choose from a list of bundled & user API endpoints (recently used first) 52 | * show color 53 | * added google books API 54 | * added (static) Planets API in 3 flavours 55 | * added (static) Places API 56 | * detail window 57 | * more templates: heroStyle + compareStyle + match 58 | * poster photos are wiser now, if the image is a png it doesn't show a border 59 | * open main image 60 | * images widget 61 | * rebuild to be closer to [this](https://d33wubrfki0l68.cloudfront.net/4ac7d7e147f5505b66e74ce6698193a58f796776/67682/images/from-wireframes-to-flutter-movie-details-page/movie_details_ui_result.png) 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | 65 | pod 'Firebase/Core' -------------------------------------------------------------------------------- /data/brand-new-proposal.json: -------------------------------------------------------------------------------- 1 | { 2 | "_theaudiodbapi_key":"195003", 3 | 4 | "endpoint":{ 5 | "title": "_albums", 6 | "url": "https://www.theaudiodb.com/api/v1/json/195003/album.php?i={i}", 7 | "__url": "https://s3-eu-west-1.amazonaws.com/api-explorer-app/top20_artist/album{i}.json", 8 | "parameters": [{ "name":"i", "value": 111247, "label":"Artist ID" }], 9 | "imagesProxy":"https://images.weserv.nl/?url={url}", 10 | "theme": {"base": "dark"}, 11 | "categories": ["Entertainment"], 12 | "dependencies": "Billboard: Top 20 artists" 13 | }, 14 | 15 | "about": { 16 | "web": "https://www.theaudiodb.com", 17 | "doc": "https://www.theaudiodb.com/api_guide.php", 18 | "info": "TheAudioDB is a community run database of Music Artwork, Metadata, Charts, Events, Music Videos and File Hashes, with free access via our front-end Website or back-end JSON API. The Data and Artwork available here, is only possible thanks to the hard work of our editors.", 19 | "logo": "https://www.theaudiodb.com/images/logo_new_4.png" 20 | }, 21 | 22 | "listing": { 23 | "type":"gridWithoutName" 24 | }, 25 | "detail": { 26 | "type":"detail", 27 | "widgetsOrder": [ 28 | { 29 | "type": "header", 30 | "left": [ 31 | { 32 | "type": "image" 33 | } 34 | ], 35 | "right": [{ "id": "name" }] 36 | }, 37 | { 38 | "type": "separator" 39 | }, 40 | { 41 | "id": "description" 42 | } 43 | ], 44 | "id": "idAlbum", 45 | "name": { 46 | "field": "strAlbum" 47 | }, 48 | "description": { 49 | "field": "strDescriptionEN", 50 | "label": "Bio" 51 | }, 52 | "images": { 53 | "type": "images", 54 | "fields": [ 55 | { 56 | "field": "strAlbumThumb", 57 | "type": "poster", 58 | "docu": "type is one of [thumbnail, poster, hero, null]" 59 | }, 60 | { 61 | "field": "strAlbumThumbBack", 62 | "type": "image", 63 | "docu": "type is one of [thumbnail, poster, hero, null]" 64 | }, 65 | { 66 | "field": "strAlbumCDart", 67 | "type": "image", 68 | "docu": "type is one of [thumbnail, poster, hero, null]" 69 | } 70 | ] 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "366854329978", 4 | "firebase_url": "https://api-explorer-app.firebaseio.com", 5 | "project_id": "api-explorer-app", 6 | "storage_bucket": "api-explorer-app.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:366854329978:android:f549eda0036bb022", 12 | "android_client_info": { 13 | "package_name": "apiexplorer.com.app" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "366854329978-nbg1kmjr3k7eebl0tvhga3i13dgjm01t.apps.googleusercontent.com", 19 | "client_type": 1, 20 | "android_info": { 21 | "package_name": "apiexplorer.com.app", 22 | "certificate_hash": "60c3bcd02f6c8619bf15a9b3f04d89bf4afcdc9d" 23 | } 24 | }, 25 | { 26 | "client_id": "366854329978-t246eq5jvekftj762nusjq2kh7mvqmnc.apps.googleusercontent.com", 27 | "client_type": 3 28 | } 29 | ], 30 | "api_key": [ 31 | { 32 | "current_key": "AIzaSyDcbUMF0zFu85g_E456gTYvAvxRxJDCo_E" 33 | } 34 | ], 35 | "services": { 36 | "analytics_service": { 37 | "status": 1 38 | }, 39 | "appinvite_service": { 40 | "status": 2, 41 | "other_platform_oauth_client": [ 42 | { 43 | "client_id": "366854329978-t246eq5jvekftj762nusjq2kh7mvqmnc.apps.googleusercontent.com", 44 | "client_type": 3 45 | } 46 | ] 47 | }, 48 | "ads_service": { 49 | "status": 2 50 | } 51 | } 52 | }, 53 | { 54 | "client_info": { 55 | "mobilesdk_app_id": "1:366854329978:android:3415cc42abe4ef05", 56 | "android_client_info": { 57 | "package_name": "es.apiexplorer.app" 58 | } 59 | }, 60 | "oauth_client": [ 61 | { 62 | "client_id": "366854329978-t246eq5jvekftj762nusjq2kh7mvqmnc.apps.googleusercontent.com", 63 | "client_type": 3 64 | } 65 | ], 66 | "api_key": [ 67 | { 68 | "current_key": "AIzaSyDcbUMF0zFu85g_E456gTYvAvxRxJDCo_E" 69 | } 70 | ], 71 | "services": { 72 | "analytics_service": { 73 | "status": 1 74 | }, 75 | "appinvite_service": { 76 | "status": 1, 77 | "other_platform_oauth_client": [] 78 | }, 79 | "ads_service": { 80 | "status": 2 81 | } 82 | } 83 | } 84 | ], 85 | "configuration_version": "1" 86 | } -------------------------------------------------------------------------------- /magic_flutter/app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: API Explorer 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+8 11 | 12 | environment: 13 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | 19 | cupertino_icons: ^0.1.2 20 | 21 | http: 0.12.0 22 | shared_preferences: ^0.4.2 23 | photo_view: ^0.0.8 24 | url_launcher: ^3.0.3 25 | firebase_analytics: ^1.0.3 26 | material_search: ^0.2.8 27 | firebase_remote_config: ^0.0.5 28 | cached_network_image: ^0.5.0 29 | flutter_cache_manager: ^0.2.0 30 | 31 | 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | 38 | # For information on the generic Dart part of this file, see the 39 | # following page: https://www.dartlang.org/tools/pub/pubspec 40 | 41 | # The following section is specific to Flutter. 42 | flutter: 43 | 44 | # The following line ensures that the Material Icons font is 45 | # included with your application, so that you can use the icons in 46 | # the material Icons class. 47 | uses-material-design: true 48 | 49 | # To add assets to your application, add an assets section, like this: 50 | # assets: 51 | # - images/a_dot_burr.jpeg 52 | # - images/a_dot_ham.jpeg 53 | 54 | # An image asset can refer to one or more resolution-specific "variants", see 55 | # https://flutter.io/assets-and-images/#resolution-aware. 56 | 57 | # For details regarding adding assets from package dependencies, see 58 | # https://flutter.io/assets-and-images/#from-packages 59 | 60 | # To add custom fonts to your application, add a fonts section here, 61 | # in this "flutter" section. Each entry in this list should have a 62 | # "family" key with the font family name, and a "fonts" key with a 63 | # list giving the asset and other descriptors for the font. For 64 | # example: 65 | # fonts: 66 | # - family: Schyler 67 | # fonts: 68 | # - asset: fonts/Schyler-Regular.ttf 69 | # - asset: fonts/Schyler-Italic.ttf 70 | # style: italic 71 | # - family: Trajan Pro 72 | # fonts: 73 | # - asset: fonts/TrajanPro.ttf 74 | # - asset: fonts/TrajanPro_Bold.ttf 75 | # weight: 700 76 | # 77 | # For details regarding fonts from package dependencies, 78 | # see https://flutter.io/custom-fonts/#from-packages 79 | -------------------------------------------------------------------------------- /magic_flutter/app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | def keystorePropertiesFile = rootProject.file("key.properties") 28 | def keystoreProperties = new Properties() 29 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 30 | 31 | android { 32 | compileSdkVersion 27 33 | 34 | lintOptions { 35 | disable 'InvalidPackage' 36 | } 37 | 38 | defaultConfig { 39 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 40 | applicationId "apiexplorer.com.app" 41 | minSdkVersion 16 42 | targetSdkVersion 27 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 46 | } 47 | 48 | signingConfigs { 49 | release { 50 | keyAlias keystoreProperties['keyAlias'] 51 | keyPassword keystoreProperties['keyPassword'] 52 | storeFile file(keystoreProperties['storeFile']) 53 | storePassword keystoreProperties['storePassword'] 54 | } 55 | } 56 | buildTypes { 57 | debug{ 58 | debuggable true 59 | } 60 | release { 61 | signingConfig signingConfigs.release 62 | 63 | // minifyEnabled true 64 | // useProguard true 65 | 66 | // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 67 | } 68 | } 69 | } 70 | 71 | flutter { 72 | source '../..' 73 | } 74 | 75 | dependencies { 76 | testImplementation 'junit:junit:4.12' 77 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 78 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 79 | 80 | implementation 'com.google.firebase:firebase-core:16.0.3' 81 | implementation 'com.google.firebase:firebase-analytics:16.0.3' 82 | 83 | } 84 | apply plugin: 'com.google.gms.google-services' 85 | com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true 86 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/models/model_card.dart'; 2 | import 'package:app/ui/card_details.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:app/app_data.dart'; 5 | import 'package:app/models/end_point.dart'; 6 | import 'package:material_search/material_search.dart'; 7 | 8 | 9 | class SearchPage extends StatefulWidget { 10 | @override 11 | _SearchPageState createState() => new _SearchPageState(); 12 | } 13 | 14 | class _SearchPageState extends State { 15 | GlobalKey _scaffoldKey; 16 | 17 | EndPoint ep; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | this.ep=appData.getCurrEndPoint(); 22 | 23 | String _selected; 24 | 25 | return new Scaffold( 26 | resizeToAvoidBottomPadding: false, 27 | body:new Container( 28 | child: new MaterialSearch( 29 | placeholder: 'Search', //placeholder of the search bar text input 30 | 31 | getResults:(String criteria)=>this.getResults(criteria), 32 | //sort: (String value, String criteria, String ) {return 0;}, 33 | onSelect: (dynamic selected)=>this.onSelect(selected), 34 | onSubmit: (String value)=>this.onSubmit(value), 35 | 36 | ), 37 | ), 38 | ); 39 | } 40 | 41 | void onSelect(dynamic selected) { 42 | String selectedID=selected.toString(); 43 | var card=ep.cards.firstWhere((card)=>card.get(ep.id)==selectedID); 44 | 45 | appData.logEvent('search_selectOne', {'ep':appData.getCurrEndPoint().endpointTitle}); 46 | 47 | Navigator.push(context, new MaterialPageRoute( 48 | builder: (BuildContext context){ 49 | return new DetailPage(card); 50 | } 51 | )); 52 | } 53 | 54 | void onSubmit(String value) { 55 | print ('onSubmit'); 56 | } 57 | 58 | _doSearch(String criteria) { 59 | var criteriaRE=new RegExp(r'' + criteria.toLowerCase().trim() + ''); 60 | 61 | List _cards=ep.cards; 62 | var foundCards=_cards.where((ModelCard card){ 63 | return card.json.values.toList().toString().toLowerCase().contains(criteriaRE); 64 | }); 65 | 66 | appData.logEvent('search_search', {'ep':appData.getCurrEndPoint().endpointTitle, 'what':criteria, 'found':foundCards.length}); 67 | return foundCards.toList(); 68 | } 69 | 70 | Future>> getResults(String criteria) async{ 71 | if (criteria=='') 72 | return new List>(); 73 | 74 | List resultsList = _doSearch(criteria); 75 | List> ret=new List>(); 76 | 77 | for (var i=0; i( 81 | value: card.get(ep.id), //The value must be of type 82 | text: card.get(ep.firstName()), //String that will be show in the list 83 | icon: Icons.panorama_fish_eye, 84 | ) 85 | ); 86 | } 87 | 88 | return ret; 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /data/medium-stories.txt: -------------------------------------------------------------------------------- 1 | Hi there! this is a story about a new app, API Explorer, built on top of Flutter and Firebase. You can grab the code from https://github.com/rotoxl/flutter_magic. 2 | 3 | = Overview = 4 | API Explorer's target is to provide a tool somewhere between low and highlevel to visualize the data in an API. 5 | 6 | Think about a data API that provides a few URLs to get a sample of raw data that interests you. 7 | Eg: 8 | * https://www.theaudiodb.com, "TheAudioDB is a community run database of Music Artwork, Metadata, Charts, Events, Music Videos and File Hashes, with free access via our front-end Website or back-end JSON API". 9 | * Info about an artist -> https://www.theaudiodb.com/api/v1/json/1/search.php?s=the%20beatles 10 | * Albums by an artist -> https://www.theaudiodb.com/api/v1/json/1/searchalbum.php?s=the%20beatles) 11 | 12 | Well, API Explorer allows anyone to go from raw data to this: 13 | 14 | + flecha + captura de un detalle de APIExplorer 15 | 16 | The same idea is applicable to more APIs of different kind of information 17 | 18 | 19 | 20 | Can you see the pattern? All the samples follow the same design! 21 | 22 | 23 | 24 | 25 | Wait: but what if data doesn't follow this structure? 26 | Well, the template is flexible, did you notice that Mars' screenshot doesn't have a description? what about the tags? 27 | Also, there are a few templates more, each one suitable for a different use case, and it's even possible to link several templates to one data API. 28 | 29 | 30 | 31 | 32 | Enter the template model 33 | Right now API Explorers provides 4 basic templates for the listing and 4 templates for the detail window. Every template is just a starting point, they all contain several customizable features 34 | * detail window: suitable for... pretty much everything. Contains a hero section, a header section with poster, title, stats and chips, description, key/value listing, images widget, related widget... 35 | * compare: suitable for item compare screens 36 | * hero: large, background image, with a few texts 37 | * match: suitable for competitions, like a football league. 38 | 39 | 40 | Listing model 41 | There are also, several models to list the items in the data API: 42 | * list 43 | * image grid (without name) 44 | * image grid with name 45 | * match 46 | 47 | 48 | What's next 49 | In future releases I plan to allow everyone to add new data APIs. You will also be able to test your own APIs. I also want to add new templates for new use cases. Time will tell. 50 | 51 | 52 | 53 | -- 54 | Note: API Explorer is not affiliated with, or in any way associated with any of the APIs described here 55 | 56 | 57 | 58 | 59 | 60 | =============================================== 61 | Under the hood 62 | It all starts with a public data API: 63 | https://s3-eu-west-1.amazonaws.com/api-explorer-app/planets/planets.json 64 | 65 | API Explorer reads & parses it, obtaining a set of items 66 | =============================================== 67 | The series will contain the following articles 68 | * Overview 69 | At first API Explorer only needs a data API EndPoint 70 | * Lists and grids 71 | * Templates ('detail', 'compare', 'hero', 'match') 72 | * Advanced templating (hint:it's all json) 73 | * My experience building it with Flutter -------------------------------------------------------------------------------- /magic_flutter/app/lib/models/model_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/widgets.dart'; 2 | 3 | class ModelCard{ 4 | String id; 5 | Map json; 6 | //final Map rulings, legalities; 7 | 8 | final cacheExpresiones={}; 9 | get(String expression){ 10 | if (expression==null) 11 | return null; 12 | 13 | if (expression.indexOf("{")>-1){//es una expresión "{name}" 14 | var camposEncontrados=[]; 15 | if (!cacheExpresiones.containsKey(expression)){ 16 | var temp=expression.split("}"); 17 | for (var i=0; i-1){ 19 | var pos=temp[i].indexOf('{')+1; 20 | camposEncontrados.add(temp[i].substring(pos)); 21 | } 22 | } 23 | cacheExpresiones[expression]=camposEncontrados; 24 | } 25 | 26 | String ret=expression; 27 | camposEncontrados=cacheExpresiones[expression]; 28 | for (var i=0; i-1){// campo||alternativa 34 | var temp=field.split('||'); 35 | newvalue=json[temp[0]]; 36 | 37 | if (newvalue==null || newvalue=='') 38 | newvalue=json[temp[1]]; 39 | 40 | if (newvalue==null || newvalue=='') 41 | newvalue=null; 42 | } 43 | else if (field.indexOf('|')>-1){ // campo|transformación 44 | var temp=field.split('|'); 45 | field=temp[0]; 46 | mod=temp[1]; 47 | 48 | newvalue=json[field]; 49 | } else if (field.indexOf('/')>-1){ // xPath 50 | var temp=field.split('/'); 51 | 52 | try{ 53 | newvalue=json; 54 | for (var j=0; j'){ 65 | //warn expresión no completa 66 | // newvalue=newvalue.toString(); 67 | } 68 | 69 | } catch(e){ 70 | return null; 71 | } 72 | } else{ 73 | newvalue=json[field]; 74 | } 75 | 76 | if (newvalue==null) 77 | newvalue=""; 78 | 79 | if (mod==null){ 80 | //pass 81 | } else if (mod=='lower'){ 82 | newvalue=newvalue.toLowerCase(); 83 | } 84 | 85 | ret=ret.replaceAll("{"+camposEncontrados[i]+"}", newvalue.toString()) ; 86 | } 87 | 88 | return ret.trim(); 89 | 90 | } else { 91 | var newvalue; 92 | if (json.containsKey(expression)) 93 | newvalue=json[expression]; 94 | else 95 | newvalue=expression;//es un literal, un valor fijo: {valor} 96 | 97 | if (newvalue==null) 98 | return null; 99 | if (newvalue.runtimeType.toString()=='List' && newvalue.length==1) 100 | newvalue=newvalue[0]; 101 | 102 | return newvalue; 103 | } 104 | } 105 | ModelCard({this.id}); 106 | 107 | factory ModelCard.fromJson(Map json) { 108 | var c=ModelCard(id: json['id'].toString()); 109 | 110 | c.json=json; 111 | return c; 112 | } 113 | 114 | static getImgPlaceholder() { 115 | return 'https://imgplaceholder.com/420x320/cccccc/757575/glyphicon-picture'; 116 | } 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/app_data_loader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:io'; 3 | 4 | import 'package:app/ui/card_listing.dart'; 5 | import 'package:app/ui/config.dart'; 6 | import 'package:firebase_analytics/firebase_analytics.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:app/models/end_point.dart'; 9 | 10 | import 'package:app/app_data.dart'; 11 | 12 | class AppDataXLoader extends StatefulWidget{ 13 | final void Function(AppData p) _themeUpdater; 14 | FirebaseAnalytics _analytics; 15 | 16 | AppDataXLoader(void Function(AppData p) this._themeUpdater, FirebaseAnalytics this._analytics); 17 | 18 | @override 19 | State createState() => new _AppDataXLoaderState(this._analytics, _themeUpdater); 20 | } 21 | 22 | class _AppDataXLoaderState extends State { 23 | bool _loaded=false; 24 | FirebaseAnalytics _analytics; 25 | final void Function(AppData p) _themeUpdater; 26 | 27 | DateTime whatever; 28 | 29 | _AppDataXLoaderState(this._analytics, this._themeUpdater); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | if (appData!=null) 34 | appData.analytics=this._analytics; 35 | 36 | return new Scaffold( 37 | appBar: _buildAppBar(context), 38 | body: _buildBody(context), 39 | ); 40 | } 41 | void _navigateNext(BuildContext context, AsyncSnapshot> snapshot) { 42 | var current=appData.getFixedEndPoint(); 43 | if (current==null){ 44 | Navigator.pushReplacement(context, new MaterialPageRoute( 45 | builder: (BuildContext context) { 46 | return new ConfigPage(_themeUpdater); 47 | }, 48 | )); 49 | } 50 | else 51 | Navigator.pushReplacement(context, new MaterialPageRoute( 52 | builder: (BuildContext context){ 53 | return new CardListing(_themeUpdater); 54 | } 55 | ) 56 | ); 57 | } 58 | 59 | _buildAppBar(BuildContext context){ 60 | return AppBar( 61 | toolbarOpacity: .1, 62 | ); 63 | } 64 | _buildBody(BuildContext context) { 65 | return FutureBuilder>( 66 | future: appData.loadEndPoints(), 67 | builder: (context, snapshot) { 68 | print ('snapshot, one more time'); 69 | if (snapshot.connectionState==ConnectionState.none) 70 | return Center(child:Text('No internet connection. Tap to try again')); 71 | else if (snapshot.hasData && this._loaded==false) { 72 | this._loaded=true; 73 | print('loaded!'); 74 | 75 | new Future.delayed(const Duration(milliseconds: 10), ()=>_navigateNext(context, snapshot)); 76 | return Center(child: CircularProgressIndicator()); 77 | } else if (snapshot.hasError) { 78 | return 79 | GestureDetector( 80 | onTap: (){ 81 | setState(() { 82 | whatever=DateTime.now(); 83 | }); 84 | }, 85 | child: Center(child:Text('Error starting app. Tap to try again')) 86 | );//TODO sacar un Admonition 87 | } 88 | // By default, show a loading spinner 89 | return Center(child: CircularProgressIndicator()); 90 | }); 91 | } 92 | 93 | Future internetConnection() async { 94 | try { 95 | final result = await InternetAddress.lookup('google.com'); 96 | if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { 97 | return Center(child:Text('Error starting app')); 98 | } 99 | } on SocketException catch (_) { 100 | return Center(child:Text('No internet connection')); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/about_api_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/models/end_point.dart'; 4 | import 'package:cached_network_image/cached_network_image.dart'; 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | 9 | 10 | enum Type{sep, link, endPoint} 11 | 12 | class AboutAPIPage { 13 | EPAbout about; 14 | 15 | TextTheme textTheme; 16 | TextStyle linkStyle, aboutStyle; 17 | 18 | AboutAPIPage(this.about); 19 | 20 | Future _openURL(String url) async { 21 | if (await canLaunch(url)) { 22 | await launch(url, forceSafariVC: false, forceWebView: false); 23 | } 24 | } 25 | 26 | customDialogShow(BuildContext context){ 27 | this.textTheme= Theme.of(context).textTheme; 28 | this.linkStyle=this.textTheme.button.copyWith(color: Colors.black); 29 | this.aboutStyle=this.textTheme.subhead.copyWith(color: Colors.black54); 30 | 31 | Widget dom=_getDom(context); 32 | 33 | var fnCallBack=(response){ 34 | print('You selected: $response'); 35 | }; 36 | _doShowDemoDialog(context:context, child:dom, fnCallBack:fnCallBack); 37 | } 38 | 39 | Widget _getDom(BuildContext context){ 40 | // final ThemeData theme = Theme.of(context); 41 | 42 | var txt=Text(this.about.info, softWrap:true, style:this.aboutStyle, overflow: TextOverflow.ellipsis, maxLines: 3,); 43 | 44 | var col1; 45 | if (this.about.logo!=null){ 46 | col1=CachedNetworkImage(imageUrl:this.about.logo, placeholder: new CircularProgressIndicator(), fit: BoxFit.cover, width: 100.0, height: 100.0); 47 | } 48 | else{ 49 | col1=Container(constraints: BoxConstraints(minWidth:100.0, minHeight: 100.0), 50 | child: new Icon(Icons.info_outline, color:Colors.black45, size: 90.0) ); 51 | } 52 | 53 | var col2=new Container( 54 | height: 100.0, 55 | width: 180.0, 56 | child: Padding( 57 | padding:EdgeInsets.symmetric(horizontal:8.0), 58 | child:Center( 59 | child: txt 60 | ) 61 | ), 62 | ); 63 | 64 | var fl1=Flex( 65 | direction:Axis.horizontal , 66 | children: [ 67 | new Row(children: [ 68 | col1, col2 69 | ]), 70 | ], 71 | ); 72 | 73 | Widget dom = new SimpleDialog( 74 | titlePadding: const EdgeInsets.all(0.0), 75 | contentPadding: const EdgeInsets.all(0.0), 76 | children: [ 77 | new Container( 78 | color:Colors.white, 79 | child:Column( 80 | children: [ 81 | fl1, 82 | ], 83 | ) 84 | ), 85 | new Container( 86 | color:Colors.white, 87 | padding:EdgeInsets.symmetric(horizontal: 0.0, vertical:4.0,), 88 | // color:Colors.red, 89 | child:Column( 90 | children: [ 91 | _link(this.about.web, this.about.web), 92 | _link(this.about.doc, 'Visit docs') 93 | ], 94 | ) 95 | ), 96 | 97 | ]); 98 | return dom; 99 | } 100 | 101 | _link(String url, text) { 102 | var sd=new SimpleDialogOption( 103 | onPressed:()=>_openURL(url), 104 | child:Row( 105 | mainAxisSize:MainAxisSize.max, 106 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 107 | children: [ 108 | new Text(text, style: this.linkStyle, overflow: TextOverflow.ellipsis, ), 109 | new Align(alignment:Alignment.bottomRight, child: Icon(Icons.chevron_right, color:Colors.black45)) 110 | ] 111 | ) 112 | ); 113 | return sd; 114 | } 115 | _doShowDemoDialog({ BuildContext context, Widget child, Null Function(String response) fnCallBack }) { 116 | showDialog( 117 | context: context, 118 | builder: (BuildContext context) => child, 119 | ).then((T value) { // The value passed to Navigator.pop() or null. 120 | fnCallBack(value.toString()); 121 | }); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/models/end_point.dart'; 2 | import 'package:app/ui/app_data_loader.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | import 'package:app/app_data.dart'; 7 | import 'package:app/ui/card_listing.dart'; 8 | import 'package:app/ui/config.dart'; 9 | 10 | import 'package:firebase_analytics/observer.dart'; 11 | import 'package:firebase_analytics/firebase_analytics.dart'; 12 | 13 | 14 | void main() => runApp(new MyApp()); 15 | 16 | class MyApp extends StatefulWidget { 17 | @override 18 | _MyAppState createState() => new _MyAppState(); 19 | } 20 | 21 | class _MyAppState extends State{ 22 | AppData appData; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | FirebaseAnalytics analytics = new FirebaseAnalytics(); 27 | 28 | this.appData=appData; 29 | 30 | var routes={ 31 | '/':(BuildContext context){ 32 | return new AppDataXLoader(themeUpdater, analytics); 33 | }, 34 | '/cardlisting':(BuildContext context){ 35 | return new CardListing(themeUpdater); 36 | }, 37 | '/config': (BuildContext context) => new ConfigPage(themeUpdater), 38 | }; 39 | 40 | return new MaterialApp( 41 | debugShowCheckedModeBanner:false, 42 | title: 'API Explorer', 43 | theme: theme, 44 | navigatorObservers: [ 45 | new FirebaseAnalyticsObserver(analytics: analytics), 46 | ], 47 | routes: routes, 48 | onGenerateRoute: _getRoute, 49 | ); 50 | } 51 | 52 | @override 53 | void initState() { 54 | super.initState(); 55 | } 56 | 57 | void themeUpdater(AppData newappData) { 58 | setState(() { 59 | this.appData=newappData; 60 | 61 | var ep=this.appData!=null?this.appData.getCurrEndPoint():null; 62 | if (ep!=null) 63 | appData.themeApplied=ep.epTheme; 64 | }); 65 | } 66 | ThemeData get theme{ 67 | EndPoint ep; 68 | if (this.appData!=null && this.appData.getCurrEndPoint()!=null) 69 | ep=this.appData.getCurrEndPoint(); 70 | 71 | if (ep!=null && ep.epTheme!=null){ 72 | return ep.epTheme.theme; 73 | } 74 | else { 75 | return 76 | new ThemeData( 77 | brightness: Brightness.light, 78 | primarySwatch: ep==null?Colors.indigo: ep.epTheme.color, 79 | platform: Theme.of(context).platform, 80 | ); 81 | } 82 | } 83 | 84 | Route _getRoute(RouteSettings settings) { 85 | // Routes, by convention, are split on slashes, like filesystem paths. 86 | final List path = settings.name.split('/'); 87 | // We only support paths that start with a slash, so bail if 88 | // the first component is not emfpty: 89 | if (path[0] != '') 90 | return null; 91 | // The other paths we support are in the routes table. 92 | return null; 93 | } 94 | } 95 | 96 | 97 | //////////////////////// 98 | 99 | /* 100 | Future editAttrList_dialog() async { 101 | void onSubmit(String result) { 102 | print(result); 103 | } 104 | var dialog=Dialog_chooseOne(onSubmit:onSubmit, options:ModelCard.members); 105 | 106 | var response=await showDialog( 107 | context: context, 108 | builder: (BuildContext context) => dialog 109 | ); 110 | 111 | print (response!=null? response[0]: 'No hay respuesta'); 112 | } 113 | */ 114 | typedef void _DialogChooseOneCallback(String result); 115 | class DialogChooseOne extends StatefulWidget{ 116 | final _DialogChooseOneCallback onSubmit; 117 | final List options; 118 | 119 | DialogChooseOne({this.onSubmit, this.options}); 120 | 121 | @override 122 | _DialogChooseOneState createState() => new _DialogChooseOneState(this.options); 123 | } 124 | class _DialogChooseOneState extends State { 125 | final List options; 126 | Map chosenValues = {'name': true, 'id': true,}; 127 | 128 | _DialogChooseOneState(this.options); 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | var w=List(); 133 | for (var i=0; i(); 142 | retPop.add(key); 143 | 144 | Navigator.pop(context, retPop); 145 | }, 146 | child:ListTile( 147 | title:Text(key) 148 | ), 149 | ), 150 | ); 151 | } 152 | 153 | return new SimpleDialog( 154 | title: new Text("Choose field"), 155 | children: w, 156 | ); 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/card_listing.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:app/app_data.dart'; 4 | import 'package:app/models/end_point.dart'; 5 | 6 | class ConfigPage extends StatefulWidget { 7 | void Function(AppData p) _themeUpdater; 8 | 9 | ConfigPage(void Function(AppData p) this._themeUpdater); 10 | 11 | @override 12 | _ConfigPageState createState() => new _ConfigPageState(); 13 | } 14 | 15 | class _ConfigPageState extends State { 16 | GlobalKey _scaffoldKey; 17 | 18 | TextTheme textTheme; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | this._scaffoldKey = new GlobalKey(); 23 | this.textTheme=Theme.of(context).textTheme; 24 | 25 | return new Scaffold( 26 | key: this._scaffoldKey, 27 | appBar: _buildAppBar(context), 28 | body: _buildList(context), 29 | ); 30 | } 31 | 32 | _buildAppBar(BuildContext context){ 33 | return AppBar( 34 | title: new Text('Choose endpoint'), 35 | actions: [ 36 | // IconButton(icon: const Icon(Icons.save),onPressed: () {appData.save();},), 37 | // IconButton(icon: const Icon(Icons.folder_open),onPressed: () {print(appData.loadEndPoints());},), 38 | // IconButton(icon: const Icon(Icons.add), onPressed: () {this.endPointAdd();},), 39 | // IconButton(icon: const Icon(Icons.cloud_download), onPressed: () {this.endPointsRefresh();},), 40 | ] 41 | ); 42 | } 43 | _buildList(BuildContext context) { 44 | var _endpoints=appData.endPoints().values.toList(); 45 | _endpoints.sort((EndPoint a, EndPoint b) => a.endpointUrl.compareTo(b.endpointUrl) ); 46 | 47 | if (AppData.productionMode){ 48 | for (var i=_endpoints.length-1; i>=0; i--){ 49 | var e=_endpoints[i]; 50 | if (e.endpointTitle.startsWith('_')){ 51 | _endpoints.removeAt(i); 52 | } 53 | } 54 | } 55 | 56 | var itemCount=(_endpoints.length*2)-1; 57 | return ListView.builder( 58 | padding: const EdgeInsets.all(0.0), 59 | itemCount:itemCount, 60 | itemBuilder: (context, index) { 61 | if (index==itemCount-1) 62 | return SizedBox(height:28.0); 63 | else if (index.isOdd) 64 | return Divider(); 65 | else 66 | return _buildRow(_endpoints[index ~/ 2], context); 67 | }); 68 | } 69 | Widget _buildChip(String value) { 70 | return Padding( 71 | padding: const EdgeInsets.only(right: 2.0), 72 | child: Chip( 73 | labelPadding: EdgeInsets.symmetric(horizontal:0.0, vertical:0.0, ) , 74 | labelStyle: this.textTheme.caption, 75 | label: Text(value), 76 | backgroundColor: Colors.black12, 77 | ), 78 | ); 79 | } 80 | _buildRow(EndPoint ep, BuildContext context){ 81 | var l=[]; 82 | for (var i=0; i[ 89 | SizedBox(width: 20.0,), 90 | Expanded( 91 | child: Column( 92 | crossAxisAlignment: CrossAxisAlignment.start, 93 | children: [ 94 | chips, 95 | Text(ep.endpointTitle, style:this.textTheme.subhead), 96 | ], 97 | ), 98 | ), 99 | Container( 100 | width:26.0, 101 | child:Icon(Icons.brightness_1, color: ep.epTheme.color,), 102 | ), 103 | SizedBox(width: 16.0,), 104 | ] 105 | ); 106 | 107 | return new GestureDetector( 108 | behavior: HitTestBehavior.opaque, 109 | child:row, 110 | onTap: () { 111 | var curr_ep=appData.getFixedEndPoint(); 112 | 113 | appData.setCurrEndPoint(ep.endpointTitle); 114 | 115 | appData.logEvent('endpoint_select', {'title': ep.endpointTitle,}); 116 | 117 | if (curr_ep==null){ 118 | Navigator.pushReplacement(context, new MaterialPageRoute( 119 | builder: (BuildContext context){ 120 | return new CardListing(widget._themeUpdater); 121 | } 122 | )); 123 | } 124 | else 125 | Navigator.pop(context); 126 | }, 127 | ); 128 | } 129 | 130 | void endPointAdd() async{ 131 | appData.logEvent('endpoint_add', {'err': 'not-implemented',}); 132 | this._showSnackbar('Not implemented yet'); 133 | } 134 | void _showSnackbar(String text){ 135 | final snackBar = SnackBar(content: Text(text), duration: Duration(seconds: 3)); 136 | this._scaffoldKey.currentState.showSnackBar(snackBar); 137 | } 138 | 139 | void endPointsRefresh() async{ 140 | appData.loadEndPoints(); //this only makes sense while debuggin' remoteConfig 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/card_details.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/app_data.dart'; 4 | import 'package:cached_network_image/cached_network_image.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | import 'package:app/models/end_point.dart'; 8 | import 'package:app/models/model_card.dart'; 9 | 10 | import 'package:app/ui/widgets.dart'; 11 | 12 | class DetailPage extends StatefulWidget { 13 | final ModelCard _card; 14 | ModelCard _cardToCompare; 15 | EndPoint ep; 16 | 17 | DetailPage(this._card, {Key key, EndPoint this.ep}){ 18 | if (this.ep==null) 19 | this.ep=appData.getCurrEndPoint(); 20 | } 21 | 22 | DetailPage.compare(this._card, this._cardToCompare, {Key key}) : super(key: key); 23 | 24 | @override 25 | DetailPageState createState() => new DetailPageState(this._card, this._cardToCompare, this.ep); 26 | } 27 | 28 | class DetailPageState extends State { 29 | final ModelCard _card; 30 | final ModelCard _cardToCompare; 31 | 32 | final HERO_HEIGHT = 156.0; 33 | final MARGIN_H=16.0; 34 | 35 | ThemeData theme; 36 | TextTheme textTheme; 37 | TextStyle styleOverline, styleBody; 38 | 39 | Map> relatedCards=Map>(); 40 | EndPoint ep; 41 | 42 | DetailPageState(this._card, this._cardToCompare, this.ep); 43 | 44 | Future editAttributes() async { 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | this.theme= Theme.of(context); 50 | this.textTheme= Theme.of(context).textTheme; 51 | this.styleOverline=this.textTheme.button.copyWith(fontSize: 10.0); 52 | this.styleBody=this.textTheme.body1; 53 | 54 | appData.logEvent('detail_show', { 55 | 'ep':ep.endpointTitle, 56 | 'typeOfDetail':ep.typeOfDetail.toString(), 57 | 'card':this._card.id, 58 | 'cardToCompare':this._cardToCompare!=null?this._cardToCompare.id:null 59 | } 60 | ); 61 | 62 | print ('----------------------'); 63 | print (this._card.json); 64 | 65 | ListallWidgets; 66 | if (ep.typeOfDetail==TypeOfDetail.details){ 67 | allWidgets=detailPage(context); 68 | } else if (ep.typeOfDetail==TypeOfDetail.match){ 69 | allWidgets=matchPage(); 70 | } else if (ep.typeOfDetail==TypeOfDetail.productCompare){ 71 | allWidgets=comparePage(); 72 | } else if (ep.typeOfDetail==TypeOfDetail.hero){ 73 | allWidgets=heroPage(); 74 | } 75 | 76 | if (ep.typeOfDetail==TypeOfDetail.hero){ 77 | return Scaffold( 78 | body: Column(children:allWidgets) 79 | ); 80 | } 81 | else { 82 | return Scaffold( 83 | body: CustomScrollView( 84 | slivers: [ 85 | appBar(), 86 | SliverList( 87 | delegate:new SliverChildListDelegate(allWidgets), 88 | ), 89 | ], 90 | ), 91 | ); 92 | } 93 | } 94 | 95 | List detailPage(BuildContext context){ 96 | Listret=new List(); 97 | 98 | for (var i=0; i matchPage(){ 111 | Listret=new List(); 112 | 113 | for (var i=0; i comparePage(){ 126 | Listret=new List(); 127 | 128 | for (var i=0; i heroPage(){ 142 | var size=MediaQuery.of(context).size; 143 | 144 | Widget image; 145 | List text=new List(); 146 | String src; 147 | 148 | for (var i=0; i[ 171 | new Positioned( 172 | bottom: 10.0, left: MARGIN_H, right: MARGIN_H, 173 | child: Column(children:text) 174 | ), 175 | ],) 176 | ) 177 | ); 178 | 179 | return [ 180 | GestureDetector(child: ret, onTap: () => Navigator.pop(context, 'Nope!'),) 181 | ]; 182 | } 183 | 184 | Widget appBar() { 185 | var expanded_height; var domImage; 186 | 187 | if (ep.typeOfDetail==TypeOfDetail.hero){ 188 | } 189 | else { 190 | var h=ep.getWidgetByType(EPWidgetType.hero); 191 | if (h!=null){ 192 | expanded_height=HERO_HEIGHT; 193 | 194 | var src=_card.get(ep.firstImageOfType(ImageType.hero).field ); 195 | if (src==null || src=='') src=ModelCard.getImgPlaceholder(); 196 | 197 | domImage=Image.network( ep.proxiedImage(src), fit: BoxFit.cover, height: expanded_height, color: Colors.white.withOpacity(0.15),colorBlendMode: BlendMode.lighten); 198 | } else { 199 | expanded_height=16.0; 200 | domImage=Container(); 201 | } 202 | 203 | return SliverAppBar( 204 | pinned: false, 205 | leading: IconButton( 206 | icon: Icon(Icons.close, color:Theme.of(context).textTheme.title.color), 207 | color:Theme.of(context).accentColor, 208 | onPressed: () { 209 | Navigator.pop(context, 'Nope!'); 210 | }), 211 | expandedHeight: expanded_height, 212 | elevation: 1.0, 213 | floating: false, 214 | // floating: true, snap: true, 215 | flexibleSpace: new FlexibleSpaceBar(background: domImage), 216 | ); 217 | } 218 | 219 | 220 | } 221 | 222 | EndPoint getEndPoint(){ 223 | return appData.getCurrEndPoint(); 224 | } 225 | 226 | void _showSnackbar(String text, BuildContext xcontext) { 227 | final snackBar = 228 | SnackBar(content: Text(text), duration: Duration(seconds: 3)); 229 | Scaffold.of(xcontext).showSnackBar(snackBar); 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/card_details_edit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:app/models/end_point.dart'; 4 | 5 | class EditDetailsPage extends StatefulWidget { 6 | final EndPoint endPoint; 7 | EditDetailsPage(this.endPoint, {Key key}) : super(key: key); 8 | 9 | @override 10 | EditDetailsPageState createState() => 11 | new EditDetailsPageState(endPoint); 12 | } 13 | class EditDetailsPageState extends State { 14 | EndPoint endPoint; 15 | 16 | String _fieldID, _fieldName, _fieldImage; 17 | String _fieldSecondary; 18 | 19 | final TextEditingController _fieldNameController = new TextEditingController(); 20 | // final TextEditingController _fieldMainController = new TextEditingController(); 21 | final TextEditingController _fieldSecondaryController = new TextEditingController(); 22 | 23 | final GlobalKey _formKey = new GlobalKey(); 24 | 25 | EditDetailsPageState(this.endPoint); 26 | 27 | @override 28 | void initState() { 29 | // _fieldName = this.endPoint.firstName(); 30 | // 31 | // _fieldID = this.endPoint.id; 32 | // _fieldImage = this.endPoint.firstImage(); 33 | // 34 | // _fieldSecondary= this.endPoint.fields.join(", "); 35 | // 36 | // _fieldNameController.text = _fieldName; 37 | // 38 | // _fieldSecondaryController.text = _fieldSecondary; 39 | // 40 | // return super.initState(); 41 | } 42 | 43 | void _handleSubmitted() { 44 | // final FormState form = _formKey.currentState; 45 | // if (!form.validate()) { 46 | // } else { 47 | // form.save(); 48 | // 49 | // this.endPoint.id=_fieldID; 50 | // this.endPoint.images=[_fieldImage]; 51 | // this.endPoint.name=_fieldName; 52 | // 53 | //// var mainTemp=_fieldMain.split(",").map( (s) => s.trim() ); 54 | // var secTemp=_fieldSecondary.split(",").map( (s) => s.trim() ); 55 | // 56 | //// this.endPoint.mainFields=mainTemp.toList(); 57 | // this.endPoint.fields=secTemp.toList(); 58 | // 59 | // appData.save().then((result) { 60 | // print("Saving done: ${result}."); 61 | // }); 62 | // 63 | // Navigator.pop(context); 64 | // } 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | final ThemeData theme = Theme.of(context); 70 | 71 | var listItems = /*endPoint.allFields()*/['**TOO', '**TODO'].map((String value) { 72 | return new DropdownMenuItem( 73 | value: value, 74 | child: new Text(value), 75 | ); 76 | }).toList(); 77 | 78 | return new Scaffold( 79 | appBar:new AppBar( 80 | title: new Text('Edit details page'), 81 | actions: [ 82 | new FlatButton( 83 | child: new Text('SAVE'), 84 | onPressed: () { 85 | _handleSubmitted(); 86 | }) 87 | ] 88 | ), 89 | body: new Form( 90 | key: _formKey, 91 | // autovalidate: _autovalidate, 92 | // onWillPop: _warnUserAboutInvalidData, 93 | child: new ListView( 94 | padding: const EdgeInsets.all(16.0), 95 | children: [ 96 | new Container( 97 | padding: const EdgeInsets.symmetric(vertical: 8.0), 98 | alignment: Alignment.bottomLeft, 99 | child: new Row( 100 | children: [ 101 | new Column( 102 | crossAxisAlignment: CrossAxisAlignment.start, 103 | children: [ 104 | new Text( 105 | 'ID Field', 106 | style: theme.textTheme.caption, 107 | textAlign: TextAlign.start, 108 | ), 109 | new DropdownButton( 110 | items: listItems, 111 | value: _fieldID, 112 | onChanged: (String value) { 113 | setState(() { 114 | _fieldID = value; 115 | }); 116 | }), 117 | ], 118 | ), 119 | new Expanded(child: 120 | new Column( 121 | crossAxisAlignment: CrossAxisAlignment.end, 122 | children: [ 123 | new Column( 124 | crossAxisAlignment: CrossAxisAlignment.start, 125 | children: [ 126 | new Text( 127 | 'Image Field', 128 | style: theme.textTheme.caption, 129 | textAlign: TextAlign.start, 130 | ), 131 | new DropdownButton( 132 | items: listItems, 133 | value: _fieldImage, 134 | onChanged: (String value) { 135 | setState(() { 136 | _fieldImage = value; 137 | }); 138 | }), 139 | ], 140 | ) 141 | ], 142 | ) 143 | ) 144 | ], 145 | )), 146 | // new Container( 147 | // padding: const EdgeInsets.symmetric(vertical: 8.0), 148 | // alignment: Alignment.bottomLeft, 149 | // child: new TextField( 150 | // decoration: const InputDecoration(labelText: 'Name field', hintText: 'field1',), 151 | // autocorrect: false, 152 | // controller: _fieldNameController, 153 | // onChanged: (String value) { 154 | // setState(() { 155 | // _fieldName = value; 156 | // }); 157 | // })), new Container( 158 | // padding: const EdgeInsets.symmetric(vertical: 8.0), 159 | // alignment: Alignment.bottomLeft, 160 | // child: new TextField( 161 | // decoration: const InputDecoration(labelText: 'Main section fields (CSV)', hintText: 'field1, field2',), 162 | // autocorrect: false, 163 | // controller: _fieldMainController, 164 | // onChanged: (String value) { 165 | // setState(() { 166 | // _fieldMain = value; 167 | // }); 168 | // })), 169 | new Container( 170 | padding: const EdgeInsets.symmetric(vertical: 8.0), 171 | alignment: Alignment.bottomLeft, 172 | child: new TextField( 173 | decoration: const InputDecoration(labelText: 'Secondary section fields (CSV)', hintText: 'field1, field2',), 174 | autocorrect: false, 175 | controller: _fieldSecondaryController, 176 | onChanged: (String value) { 177 | setState(() { 178 | _fieldSecondary = value; 179 | }); 180 | })), 181 | ].map((Widget child) { 182 | return new Container( 183 | padding: const EdgeInsets.symmetric(vertical: 8.0), 184 | height: 96.0, 185 | child: child); 186 | }).toList())), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/app_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | import 'dart:convert'; 4 | import 'package:firebase_analytics/firebase_analytics.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | import 'package:app/models/end_point.dart'; 9 | import 'package:app/ui/widgets.dart'; 10 | import 'package:firebase_remote_config/firebase_remote_config.dart'; 11 | 12 | enum MColors{ 13 | red, 14 | pink, 15 | purple, 16 | deepPurple, 17 | indigo, 18 | blue, 19 | lightBlue, 20 | cyan, 21 | teal, 22 | green, 23 | lightGreen, 24 | lime, 25 | yellow, 26 | amber, 27 | orange, 28 | deepOrange, 29 | brown, 30 | // The grey swatch is intentionally omitted because when picking a color 31 | // randomly from this list to colorize an application, picking grey suddenly 32 | // makes the app look disabled. 33 | blueGrey, 34 | } 35 | 36 | class AppData{ 37 | static bool productionMode=true; 38 | HashMap _endPoints=HashMap(); 39 | String _fixedEndPoint; 40 | 41 | EPTheme themeApplied; 42 | 43 | FirebaseAnalytics analytics; 44 | 45 | Future logEvent(name, Map parameters) async{ 46 | // await appData.analytics.logEvent(name: name, /*parameters:parameters*/); 47 | } 48 | Future logScreen(name) async{ 49 | await appData.analytics.setCurrentScreen(screenName: name); 50 | } 51 | 52 | 53 | EndPoint getEndPoint(String id) { 54 | if (_endPoints.length>0) 55 | return _endPoints[id]; 56 | else { 57 | loadEndPoints(); 58 | return _endPoints[id]; 59 | } 60 | } 61 | String getFixedEndPoint(){ 62 | return _fixedEndPoint; 63 | } 64 | EndPoint getCurrEndPoint(){ 65 | if (_fixedEndPoint==null){ 66 | loadEndPoints(); 67 | } 68 | 69 | if (_fixedEndPoint!=null && !this._endPoints.containsKey(_fixedEndPoint)) 70 | _fixedEndPoint=this._endPoints.keys.toList()[0]; 71 | 72 | return getEndPoint(_fixedEndPoint); 73 | } 74 | void setCurrEndPoint(String id){ 75 | _fixedEndPoint=id; 76 | saveLastUsed(); 77 | } 78 | 79 | ThemeData darkTheme(){ 80 | return ThemeData.dark().copyWith( 81 | accentColor: Colors.grey[800], 82 | buttonColor: Colors.grey[800], 83 | primaryColor:Colors.grey[800], 84 | ); 85 | } 86 | 87 | 88 | _loadPlanetsAPI(){ 89 | /*https://api.myjson.com/bins/133s70 --> https://s3-eu-west-1.amazonaws.com/api-explorer-app/planets/planets.json 90 | { 91 | "id": 1, 92 | "name": "Mercury", 93 | "mass": "0.33", 94 | "diameter": 4879, 95 | "density": 5427, 96 | "gravity": "3.7", 97 | "rotation_period": "1407.6", 98 | "length_of_day": "4222.6", 99 | "distance_from_sun": "57.9", 100 | "orbital_period": "88.0", 101 | "orbital_velocity": "47.4", 102 | "mean_temperature": 167, 103 | "number_of_moons": 0, 104 | "created_at": "2017-11-12T22:46:36.587Z", 105 | "updated_at": "2017-11-12T22:46:36.587Z", 106 | "img": "https://s3-eu-west-1.amazonaws.com/api-explorer-app/planets/mercury.jpg" 107 | }*/ 108 | 109 | ///common stuff 110 | var epTheme=EPTheme(); 111 | epTheme.theme=darkTheme().copyWith(accentColor: Colors.white); epTheme.color=Colors.black; 112 | 113 | var about=EPAbout(); 114 | about.web='https://github.com/rotoxl/flutter_magic/blob/master/aboutPlanetsAPI.md'; 115 | about.info='Planets in our Solar System'; 116 | about.logo='https://s3-eu-west-1.amazonaws.com/api-explorer-app/planets/planets.jpg'; 117 | 118 | var catList=['Science', 'Reference']; 119 | 120 | EPSeparatorWidget separator=EPSeparatorWidget(); 121 | EPWidget wname=EPNameWidget(); 122 | var en=EPLabelText(); 123 | en.field="name"; 124 | wname.fields=[en]; 125 | 126 | var fields=['number_of_moons', 'mass', 'diameter', 'density', 'gravity', 'mean_temperature', 'rotation_period']; 127 | var labels=['Moons', 'Mass (x10e24 kg)', 'Diameter', 'Density', 'Gravity (m/s2)', 'Mean temperature', 'Rotation period (h)']; 128 | EPWidget wfields=EPFieldsWidget(); 129 | for (var i=0; i> loadEndPoints() async { 216 | print ('loadEndPoints... bundled'); 217 | 218 | //bundled APIs (from https://github.com/toddmotto/public-apis) 219 | _loadPlanetsAPI(); 220 | 221 | print ('loadEndPoints... firebase.remoteconfig'); 222 | final RemoteConfig remoteConfig = await RemoteConfig.instance; 223 | await remoteConfig.fetch(expiration: const Duration(minutes: 0)); 224 | await remoteConfig.activateFetched(); 225 | String r=remoteConfig.getString('init_endpoints'); 226 | try{ 227 | if (r!=null) parseFirebaseRemoteConfig(r); 228 | } catch (e){ 229 | appData.logEvent("err_on_remote_config", {"version_code":remoteConfig.getString('version_code'), "err":e.toString()}); 230 | } 231 | 232 | print ('loadEndPoints... localstorage'); //bundled APIs are overwritten by these 233 | SharedPreferences prefs = await SharedPreferences.getInstance(); 234 | var temp=prefs.getString("user_endpoints"); 235 | 236 | if (temp!=null){ 237 | // var jsondata=json.decode(temp); 238 | // Listjsonep=jsondata['endPoints']; 239 | // for (var i=0; i toJson() => { 256 | 'endPoints': _endPoints.values.toList(), 257 | }; 258 | 259 | Future save() async{ 260 | SharedPreferences prefs = await SharedPreferences.getInstance(); 261 | var json=jsonEncode(this.toJson()); 262 | print ('about to save: $json'); 263 | prefs.setString("user_endpoints", json ); 264 | 265 | print ('save: done $json'); 266 | return true; 267 | } 268 | Future saveLastUsed() async{ 269 | SharedPreferences prefs = await SharedPreferences.getInstance(); 270 | prefs.setString("last_endpoint", this.getCurrEndPoint().endpointTitle); 271 | return true; 272 | } 273 | 274 | void parseFirebaseRemoteConfig(String r) { 275 | var endpoints=json.decode(r)['default_endpoints']; 276 | if (endpoints==null) return; 277 | 278 | for (var i=0; i'+literal); 306 | print (json); 307 | } 308 | } 309 | 310 | } 311 | AppData appData=new AppData(); -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 15 | 16 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 67 | 68 | 69 | 81 | 82 | 88 | 89 | 90 | 91 | 109 | 115 | 116 | 124 | 125 | 126 | 127 | 141 | 142 | 143 | 144 | 145 | 146 | 148 | 149 | 150 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 1534399265944 159 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/models/end_point.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:app/ui/widgets.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'package:app/models/model_card.dart'; 7 | import 'package:app/app_data.dart'; 8 | import 'dart:async'; 9 | import 'dart:convert'; 10 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 11 | 12 | enum TypeOfListing{list, gridWithoutName, gridWithName, match} 13 | enum TypeOfDetail{ 14 | details, 15 | match, 16 | productCompare, 17 | hero //https://www.uplabs.com/posts/lonely-planet-hp-destination-selector 18 | } 19 | 20 | class EndPoint{ 21 | static bool debug_showLog=false; 22 | 23 | TypeOfListing typeOfListing=TypeOfListing.gridWithName; 24 | TypeOfDetail typeOfDetail=TypeOfDetail.details; 25 | 26 | String endpointTitle; 27 | String endpointUrl; 28 | String endpointPath; 29 | 30 | List> endpointParameters; 31 | Map headers; 32 | String imagesProxy; //eg: https://images.weserv.nl/?url={url} 33 | String dependencies;//related endPoint for cross navigation 34 | 35 | List categories=new List(); 36 | 37 | EPTheme epTheme; 38 | EPAbout about; 39 | 40 | String type="User"; 41 | 42 | String id; 43 | EPTextWidget section; //just to group items 44 | EPNameWidget names; 45 | EPImagesWidget images; 46 | 47 | List widgets=new List(); 48 | List widgetsOrder=new List(); 49 | 50 | EndPoint({this.endpointTitle, this.endpointUrl}); 51 | 52 | dynamic getWidgetByID(String id){ 53 | for (var i=0;i0){ 58 | for (var j=0; j allFields(){ 86 | // //TODO dynamically load fields from 1st query 87 | // 88 | // List members= ["name", "manaCost", "type", "rarity", "set", "setName", "text", "artist", "number", "power", "toughness", "layout", "imageUrl", "originalText", "originalType", "id", 89 | // "cmc","multiverseid", 90 | // "colors", "colorIdentity", "types", "subtypes", "printings"]; 91 | // 92 | // return members; 93 | // } 94 | 95 | EPImage firstImageForListing() { 96 | return firstImageOfType(ImageType.thumbnail) ?? firstImageOfType(ImageType.image); 97 | } 98 | EPImage secondImageForListing() { 99 | return secondImageOfType(ImageType.thumbnail) ?? secondImageOfType(ImageType.image); 100 | } 101 | 102 | EPImage firstImageOfType(ImageType type){ 103 | if (this.images==null) return null; 104 | return this.images.getFirstImageOfType(type); 105 | } 106 | EPImage secondImageOfType(ImageType type){ 107 | if (this.images==null) return null; 108 | return this.images.getSecondImageOfType(type); 109 | } 110 | 111 | String firstName(){ 112 | if (this.names==null) 113 | return null; 114 | return this.names.firstName(); 115 | } 116 | secondName(){ 117 | return this.names.secondName(); 118 | } 119 | 120 | List _cards=new List(); 121 | List get cards{ 122 | return _cards; 123 | } 124 | set cards(Listnewcards){ 125 | this._cards=newcards; 126 | } 127 | clearCards(){ 128 | _cards.clear(); 129 | } 130 | 131 | factory EndPoint.fromJson(Map json) { 132 | var c=EndPoint(); 133 | 134 | AppData.debugPrintSection(EndPoint.debug_showLog, 'endpointTitle', json['endpointTitle']); 135 | c.endpointTitle=json['endpointTitle']; 136 | 137 | c.endpointUrl=json['endpointUrl']; 138 | AppData.debugPrint(debug_showLog, 'headers', json['headers']); 139 | if (json['headers']!=null) 140 | c.headers=Map.from(json['headers']); 141 | 142 | AppData.debugPrint(debug_showLog, 'endpointParameters', json['endpointParameters']); 143 | if (json['endpointParameters']!=null) 144 | c.endpointParameters=List>.from(json['endpointParameters']); 145 | 146 | if (json['imagesProxy']!=null) 147 | c.imagesProxy=json['imagesProxy']; 148 | 149 | if (json['endpointPath']!=null) 150 | c.endpointPath=json['endpointPath']; 151 | 152 | 153 | AppData.debugPrint(debug_showLog, 'dependencies', json['dependencies']); 154 | if (json['dependencies']!=null) 155 | c.dependencies=json['dependencies']; 156 | 157 | AppData.debugPrint(debug_showLog, 'theme', json['theme']); 158 | if (json['theme']!=null) 159 | c.epTheme=EPTheme.fromJson( json['theme'] ); 160 | else { 161 | c.epTheme=EPTheme.randomTheme(); 162 | } 163 | c.about=EPAbout.fromJson( json['about'] ); 164 | 165 | c.id=json['id']; 166 | 167 | var nonWidgetKeys=['endpointTitle', 'endpointUrl', 'endpointPath', 'endpointParameters', 'imagesProxy', 'dependencies', 'theme','categories','type', 'typeOfListing', 'typeOfDetail', 'about','widgetsOrder','id',]; 168 | 169 | EPWidget w; 170 | for (var i=0; i.from(json['categories']); 219 | 220 | return c; 221 | } 222 | 223 | static void swallowWidgetsOrder(EndPoint c, List json) { 224 | if (json!=null){ 225 | var list=List>.from(json); 226 | for (var i=0; i> fetchData({Map parameters=null}) async { 288 | 289 | var tCall=new DateTime.now(); 290 | 291 | AppData.debugPrint(debug_showLog, this.endpointTitle, null); 292 | String url=this.endpointUrl; 293 | 294 | if (parameters==null && endpointParameters!=null){ 295 | //just use defaults 296 | parameters=Map(); 297 | this.endpointParameters.forEach((Map parMap){ 298 | String key=parMap['name']; 299 | String v=parMap['value'].toString(); 300 | 301 | parameters[key]=v; 302 | }); 303 | } 304 | 305 | AppData.debugPrint(debug_showLog, "parameters", parameters); 306 | 307 | if (parameters!=null){ 308 | parameters.forEach((String k, String v){ 309 | url=url.replaceAll( '{'+k+'}', v); 310 | }); 311 | } 312 | 313 | AppData.debugPrint (debug_showLog, 'Querying (${this.endpointTitle})', url); 314 | 315 | CacheManager.showDebugLogs = EndPoint.debug_showLog; 316 | var cacheManager = await CacheManager.getInstance(); 317 | CacheManager.maxAgeCacheObject = new Duration(hours: 1); 318 | 319 | final file = await cacheManager.getFile(url, headers:this.headers); 320 | var response_body=file.readAsStringSync(); 321 | 322 | var tResponse=new DateTime.now(); 323 | // If the call to the server was successful, parse the JSON 324 | var jsonData=json.decode(response_body); 325 | var jsonCards; 326 | 327 | 328 | if (this.endpointPath!=null){ 329 | Listtemp=this.endpointPath.split('/'); 330 | 331 | jsonCards=jsonData; 332 | for (int i=0; i' || xtype.endsWith('List') ){ 342 | jsonCards=jsonData; 343 | } else { 344 | 345 | var keys=jsonData.keys.toList(); 346 | String finalkey; 347 | 348 | for (var i=0; i' || type.endsWith('List')){ 353 | finalkey=key; 354 | break; 355 | } 356 | } catch (e){ 357 | } 358 | } 359 | jsonCards=jsonData[finalkey]; 360 | } 361 | } 362 | 363 | var cards=List(); 364 | for (var i=0; i toJson() => { 394 | // 'title': endpointTitle, 395 | // 'url': endpointUrl, 396 | // 397 | // 'id': id, 398 | // 'name': name, 399 | // 'text': text, 400 | // 401 | // 'images': images, 402 | // 'stats': stats, 403 | // 'tags': tags, 404 | // 'fields': fields, 405 | // 406 | // //'color': color, 407 | // 'type': type, 408 | // }; 409 | // List> _values; 410 | // dynamic values(int rowNum, String field_id){ 411 | // if (rowNum<0 || rowNum>=_values.length) 412 | // return null; 413 | // 414 | // return _values[rowNum][field_id]; 415 | // } 416 | } 417 | class EPTheme{ 418 | ThemeData theme; 419 | Color color; 420 | 421 | EPTheme(); 422 | 423 | factory EPTheme.fromJson(Map json) { 424 | var c=new EPTheme(); 425 | 426 | if (json['color']!=null){ 427 | var newcolor=json['color']; 428 | Color finalcolor; 429 | if (newcolor==null){ 430 | finalcolor=randomColor(); 431 | } else if (newcolor=='red'){ 432 | finalcolor=Colors.red; 433 | } else if (newcolor=='green'){ 434 | finalcolor=Colors.green; 435 | } else if (newcolor=='orange'){ 436 | finalcolor=Colors.orange; 437 | } else if (newcolor=='grey'){ 438 | finalcolor=Colors.grey; 439 | } else if (newcolor=='yellow'){ 440 | finalcolor=Colors.yellow; 441 | } else if (newcolor=='indigo'){ 442 | finalcolor=Colors.indigo; 443 | } else if (newcolor=='pink'){ 444 | finalcolor=Colors.pink; 445 | } else if (newcolor=='brown'){ 446 | finalcolor=Colors.brown; 447 | } else { 448 | finalcolor=Colors.deepPurple; 449 | } 450 | c.color=finalcolor; 451 | } else { 452 | c.color=Colors.grey[800]; 453 | } 454 | 455 | if (json['base']=='dark'){ 456 | c.theme=ThemeData.dark().copyWith(accentColor: c.color, buttonColor: c.color, primaryColor: c.color); 457 | } 458 | else { 459 | c.theme=ThemeData.light().copyWith(accentColor: c.color, buttonColor: c.color, primaryColor: c.color); 460 | } 461 | 462 | return c; 463 | } 464 | 465 | static Color randomColor(){ 466 | List colorList=[Colors.black, Colors.red, Colors.yellow, Colors.orange, Colors.green, Colors.blue, Colors.indigo, Colors.pink]; 467 | var colorIndex=appData.endPoints().size; 468 | 469 | if (colorIndex>colorList.length-1) 470 | colorIndex=colorIndex % colorList.length; 471 | 472 | Color cc=colorList[colorIndex]; 473 | return cc; 474 | } 475 | static EPTheme randomTheme() { 476 | var c=EPTheme(); 477 | 478 | var cc=randomColor(); 479 | c.theme=ThemeData.light().copyWith(accentColor:cc, primaryColor:cc); 480 | 481 | return c; 482 | } 483 | 484 | } 485 | class EPAbout{ 486 | String web, doc, info, logo; 487 | 488 | EPAbout(); 489 | 490 | factory EPAbout.fromJson(Map json) { 491 | var c=new EPAbout(); 492 | 493 | c.web=json['web']; 494 | c.doc=json['doc']; 495 | c.info=json['info']; 496 | c.logo=json['logo']; 497 | 498 | return c; 499 | } 500 | 501 | } 502 | 503 | -------------------------------------------------------------------------------- /magic_flutter/app/lib/ui/card_listing.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:app/app_data.dart'; 4 | import 'package:app/ui/search_page.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:app/models/end_point.dart'; 7 | import 'package:app/models/model_card.dart'; 8 | import 'package:app/ui/about_api_page.dart'; 9 | import 'package:app/ui/card_details.dart'; 10 | import 'package:cached_network_image/cached_network_image.dart'; 11 | 12 | 13 | enum Mode { list, grid, source } 14 | enum MenuItems {search, /*toggleGridList,*/ about, viewSource} 15 | 16 | class CardListing extends StatefulWidget { 17 | double offset = 0.0; 18 | 19 | void Function(AppData p) _themeUpdater; 20 | 21 | CardListing(void Function(AppData p) this._themeUpdater); 22 | 23 | double getOffsetMethod() { 24 | return offset; 25 | } 26 | void setOffsetMethod(double val) { 27 | offset=val; 28 | } 29 | 30 | @override 31 | _CardListingState createState() => new _CardListingState(Mode.grid); 32 | } 33 | 34 | class _CardListingState extends State { 35 | 36 | GlobalKey _scaffoldKey; 37 | 38 | EndPoint ep; 39 | Mode mode; 40 | bool modeChangedAtRunTime = false; 41 | 42 | ScrollController _scrollController; 43 | 44 | List selectedCards=new List(); 45 | 46 | _CardListingState(this.mode); 47 | 48 | EndPoint getEndPoint(){ 49 | return appData.getCurrEndPoint(); 50 | } 51 | List epCards(){ 52 | if (getEndPoint().cards!=null && getEndPoint().cards.length>0){ 53 | return getEndPoint().cards; 54 | } else { 55 | // 56 | var cardsToRecycle=null; 57 | //If same url has already being downloaded in another, related, endpoint --> lets recycle them 58 | appData.endPoints().forEach((String key, EndPoint value){ 59 | if (value.endpointUrl==ep.endpointUrl){ 60 | if (value.cards!=null && value.cards.length>0){ 61 | print ('recycled!'); 62 | cardsToRecycle=value.cards; 63 | } 64 | } 65 | }); 66 | if (cardsToRecycle!=null) 67 | return cardsToRecycle; 68 | else 69 | return []; 70 | } 71 | } 72 | 73 | @override 74 | void initState() { 75 | super.initState(); 76 | _scrollController = new ScrollController( 77 | initialScrollOffset: widget.getOffsetMethod(), 78 | keepScrollOffset: true, 79 | ); 80 | } 81 | @override 82 | void dispose() { 83 | _scrollController.dispose(); 84 | super.dispose(); 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | this._scaffoldKey = new GlobalKey(); 90 | ep = getEndPoint(); 91 | 92 | var sc=new Scaffold( 93 | resizeToAvoidBottomPadding:false, 94 | key: this._scaffoldKey, 95 | appBar: _buildAppBar(context), 96 | body: _buildBody(context), 97 | ); 98 | 99 | return new WillPopScope( 100 | onWillPop: () => _exitApp(context), 101 | child:sc 102 | ); 103 | } 104 | Future _exitApp(BuildContext context) { 105 | return showDialog( 106 | context: context, 107 | child: new AlertDialog( 108 | title: new Text('Do you want to exit the app?'), 109 | actions: [ 110 | new FlatButton( 111 | textTheme: ep.epTheme.theme.buttonTheme.textTheme, 112 | onPressed: () => Navigator.of(context).pop(false), 113 | child: new Text('No'), 114 | ), 115 | new FlatButton( 116 | textTheme: ep.epTheme.theme.buttonTheme.textTheme, 117 | onPressed: () => Navigator.of(context).pop(true), 118 | child: new Text('Yes'), 119 | ), 120 | ], 121 | ), 122 | ) ?? false; 123 | } 124 | 125 | _buildAppBar(BuildContext context) { 126 | return new AppBar( 127 | title: new Text(ep.endpointTitle), 128 | 129 | actions: [ 130 | new IconButton(icon: const Icon(Icons.cloud), onPressed:_navigateConfig,), 131 | new PopupMenuButton( 132 | onSelected: (String value) { _handleMenuTap(context, value); }, 133 | 134 | itemBuilder: (BuildContext context) => >[ 135 | 136 | new PopupMenuItem(value: "search", child: const Text('Search in results'),), 137 | // new PopupMenuItem(value: MenuItems.toggleGridList, child: const Text('Toggle list/grid mode'),), 138 | // new PopupMenuDivider(), 139 | // new PopupMenuItem(value: MenuItems.viewSource, child: const Text('View source'),), 140 | 141 | new PopupMenuDivider(), 142 | new PopupMenuItem(value: "about", child: const Text('About this API'),), 143 | 144 | ], 145 | ), 146 | ]); 147 | } 148 | _handleSearch(){ 149 | if (epCards().length>0){ 150 | Navigator.push(context, new MaterialPageRoute( 151 | builder: (BuildContext context){return new SearchPage();} 152 | )); 153 | } 154 | } 155 | _navigateConfig(){ 156 | Navigator.pushNamed(context, '/config'); 157 | } 158 | _handleMenuTap(BuildContext context, String value) { 159 | switch (value) { 160 | // case MenuItems.toggleGridList: 161 | // appData.logEvent('listing_toggleGridList', {'ep':ep.endpointTitle}); 162 | // setState(() { 163 | // if (this.mode == Mode.list) 164 | // this.mode = Mode.grid; 165 | // else 166 | // this.mode = Mode.list; 167 | // 168 | // modeChangedAtRunTime = true; 169 | // }); 170 | // break; 171 | 172 | case "about": 173 | new AboutAPIPage(this.ep.about).customDialogShow(context); 174 | break; 175 | 176 | case "search": 177 | _handleSearch(); 178 | break; 179 | 180 | case "viewSource": 181 | setState(() { 182 | if (this.mode == Mode.source){ 183 | if (ep.typeOfListing==TypeOfListing.list) 184 | this.mode=Mode.list; 185 | else 186 | this.mode=Mode.grid; 187 | } 188 | else 189 | this.mode = Mode.source; 190 | modeChangedAtRunTime = true; 191 | }); 192 | break; 193 | } 194 | } 195 | 196 | _buildBody(BuildContext context) { 197 | appData.logEvent('listing_show', {'ep':ep.endpointTitle, 'typeOfListing':ep.typeOfListing.toString()} ); 198 | 199 | var _cards=epCards(); 200 | 201 | var setThemeIn10ms=(){ 202 | if (this.ep.epTheme!=null && appData.themeApplied==this.ep.epTheme) 203 | return; 204 | 205 | print ('Screen repaint in 10ms'); 206 | new Future.delayed(const Duration(milliseconds: 10), (){ 207 | if (widget._themeUpdater!=null) 208 | widget._themeUpdater(appData); //forzamos tema 209 | }); 210 | }; 211 | 212 | if (_cards.length > 0) { 213 | setThemeIn10ms(); 214 | 215 | return _buildGridOrList(context); 216 | } else { 217 | var ep=getEndPoint(); 218 | 219 | return FutureBuilder>( 220 | future: ep.fetchData(), 221 | builder: (context, snapshot) { 222 | if (snapshot.hasData) { 223 | setThemeIn10ms(); 224 | return Center(child: CircularProgressIndicator()); 225 | } else if (snapshot.hasError) { 226 | return _emptyState(snapshot.error); 227 | } 228 | 229 | // By default, show a loading spinner 230 | return Center(child: CircularProgressIndicator()); 231 | }); 232 | } 233 | } 234 | 235 | _buildGridOrList(BuildContext context) { 236 | if (this.mode == Mode.list) 237 | return _buildList(context, _buildRow); 238 | else if (this.mode==Mode.source) 239 | return _buildList(context, _buildSourceRow); 240 | else{ 241 | if (this.ep.typeOfListing==TypeOfListing.match) 242 | return _buildList(context, _buildRow); 243 | else if (this.ep.typeOfListing==TypeOfListing.list) 244 | return _buildList(context, _buildRow); 245 | else 246 | return _buildGrid(context); 247 | } 248 | } 249 | 250 | _buildList(BuildContext context, Function(ModelCard card, int index, BuildContext context) fnBuildRow) { 251 | var _cards=epCards(); 252 | 253 | _scrollController = new ScrollController( 254 | initialScrollOffset: widget.getOffsetMethod(), 255 | keepScrollOffset: true, 256 | ); 257 | 258 | var lvw=ListView.builder( 259 | padding: const EdgeInsets.all(16.0), 260 | itemCount: (_cards.length * 2) - 1, 261 | controller: _scrollController, 262 | itemBuilder: (context, index) { 263 | if (index.isOdd) 264 | return Divider(height:2.0); 265 | else { 266 | var nextIndex=index ~/ 2; 267 | print ('building listitem $nextIndex'); 268 | return fnBuildRow(_cards[nextIndex], nextIndex, context); 269 | } 270 | } 271 | ); 272 | 273 | return new NotificationListener( 274 | child:lvw, 275 | onNotification: (notification) { 276 | if (notification is ScrollNotification) { 277 | widget.setOffsetMethod(notification.metrics.pixels); 278 | } 279 | }, 280 | ); 281 | } 282 | _imgPlaceholder(){ 283 | return new Center(child:Container(height:30.0, width:30.0, child:CircularProgressIndicator())); 284 | } 285 | Widget _imgErrorPlaceholder({double width, double height}){ 286 | return new Container(color:Colors.black26, width: width??80.0, height:height??80.0,); 287 | } 288 | _buildRow(ModelCard card, int index, BuildContext context) { 289 | var tt=Theme.of(context).textTheme; 290 | var title=tt.title; 291 | var body=tt.body1; 292 | 293 | var primaryColor=Theme.of(context).primaryColor; 294 | 295 | if (this.ep.typeOfListing == TypeOfListing.match) { //TypeOfListing.match 296 | var f1 = card.get(this.ep.firstImageForListing().field); 297 | var s1 = (f1 != null ? f1 : ModelCard.getImgPlaceholder()); 298 | 299 | var f2 = card.get(this.ep.secondImageForListing().field); 300 | var s2 = (f2 != null ? f2 : ModelCard.getImgPlaceholder()); 301 | 302 | var txt1 = card.get(this.ep.firstName()); 303 | if (txt1 == null) {txt1 = "Not found: ${this.ep.firstName()}";} 304 | 305 | var txt2 = card.get(this.ep.secondName()); 306 | if (txt2 == null) {txt2 = "Not found: ${this.ep.firstName()}";} 307 | 308 | var team1=new Expanded(child:new Row(children: [ 309 | Text(txt1, style: body), 310 | Material(borderRadius: BorderRadius.circular(4.0), elevation: 5.0, child: 311 | CachedNetworkImage(imageUrl:this.ep.proxiedImage(s1), placeholder:_imgPlaceholder(), errorWidget:_imgErrorPlaceholder(width:40.0, height:30.0), height: 30.0, width: 40.0, fit: BoxFit.cover) 312 | ) 313 | ], mainAxisAlignment: MainAxisAlignment.spaceBetween,)) ; 314 | var team2=new Expanded(child:new Row(children: [ 315 | Material(borderRadius: BorderRadius.circular(4.0), elevation: 5.0, child: 316 | CachedNetworkImage(imageUrl:this.ep.proxiedImage(s2), placeholder: _imgPlaceholder(), errorWidget:_imgErrorPlaceholder(width:40.0, height:30.0), height: 30.0, width: 40.0, fit: BoxFit.cover) 317 | ), 318 | Text(txt2, style: body) 319 | ], mainAxisAlignment: MainAxisAlignment.spaceBetween,)); 320 | 321 | Widget sectionLabel; 322 | if (this.ep.section!=null){ 323 | var ff=this.ep.section.field; 324 | var newSection=card.get(ff); 325 | var lastSection=index==0?null: epCards()[index-1].get(ff); 326 | 327 | if (newSection!=lastSection){ 328 | sectionLabel=new Text(newSection, style:tt.caption, textAlign: TextAlign.start,); 329 | } 330 | } 331 | 332 | var gd=GestureDetector( 333 | behavior: HitTestBehavior.opaque, 334 | child: new Container( 335 | child: Row(children: [team1, SizedBox(width: 16.0, height: 60.0,), team2], mainAxisAlignment: MainAxisAlignment.spaceBetween,), 336 | ), onTap: () { 337 | print('tap'); 338 | _navigateDetailPage(card, context); 339 | } 340 | ); 341 | 342 | if (sectionLabel!=null){ 343 | return new Column(children: [ 344 | new Container(height:32.0, child:sectionLabel, margin:EdgeInsets.only(top:20.0)), 345 | gd 346 | ], crossAxisAlignment: CrossAxisAlignment.start,); 347 | } else { 348 | return gd; 349 | } 350 | } 351 | else if (this.ep.typeOfListing == TypeOfListing.list) { 352 | var txt = card.get(this.ep.firstName()); 353 | if (txt == null) {txt = "Not found: ${this.ep.firstName()}";} 354 | 355 | return ListTile( 356 | contentPadding: EdgeInsets.only(left:0.0, right:0.0), 357 | title: Text(txt, style: title,), 358 | trailing: Icon(Icons.panorama_fish_eye, color: primaryColor,), 359 | onTap: () {_navigateDetailPage(card, context);}, 360 | ); 361 | } 362 | } 363 | _buildSourceRow(ModelCard card, int index, BuildContext context) { 364 | var txt=card.get(this.ep.firstName()); 365 | if (txt==null){ 366 | txt="Not found: ${this.ep.firstName()}"; 367 | } 368 | 369 | JsonEncoder encoder = new JsonEncoder.withIndent(' '); 370 | 371 | var j=json.encode(card.json); 372 | var pprint=( encoder.convert( json.decode(j) ) ); 373 | 374 | return ListTile( 375 | contentPadding: EdgeInsets.only(left:0.0, right:0.0), 376 | title: Text(txt, style: Theme.of(context).textTheme.title,), 377 | subtitle: Text(pprint), 378 | trailing: Icon(Icons.code, color: Theme.of(context).primaryColor,), 379 | onTap:() { 380 | _navigateDetailPage(card, context); 381 | }, 382 | ); 383 | } 384 | 385 | _buildGrid(BuildContext context) { 386 | var _cards=epCards(); 387 | 388 | double verticalMargin=0.0, horizontalMargin=0.0; 389 | if (this.ep.typeOfListing==TypeOfListing.gridWithoutName){ 390 | verticalMargin=10.0; 391 | horizontalMargin=3.0; 392 | } 393 | 394 | _scrollController = new ScrollController( 395 | initialScrollOffset: widget.getOffsetMethod(), 396 | keepScrollOffset: true, 397 | ); 398 | 399 | return new OrientationBuilder( 400 | builder: (context, orientation) { 401 | return new NotificationListener(child:new GridView.count( 402 | controller: _scrollController, 403 | padding: EdgeInsets.symmetric(horizontal: horizontalMargin, vertical: verticalMargin), 404 | mainAxisSpacing: verticalMargin, 405 | childAspectRatio: 1.1, 406 | crossAxisCount: (orientation == Orientation.portrait ? 2 : 3), 407 | children: List.generate(_cards.length, (index) { 408 | return _buildGridItem(_cards[index], index, context); 409 | }) 410 | ), 411 | onNotification: (notification) { 412 | if (notification is ScrollNotification) { 413 | widget.setOffsetMethod(notification.metrics.pixels); 414 | } 415 | } 416 | ); 417 | } 418 | ); 419 | } 420 | _buildGridItem(ModelCard card, int index, BuildContext context) { 421 | var fi=card.get( this.ep.firstImageForListing().field ); 422 | 423 | var src=this.ep.proxiedImage( (fi!=null?fi:ModelCard.getImgPlaceholder()) ); 424 | 425 | var textStyle=Theme.of(context).textTheme.body1.copyWith(color: Colors.white); 426 | 427 | var txt=card.get(this.ep.firstName()); 428 | if (txt==null){ 429 | txt="Not found: ${this.ep.firstName()}"; 430 | } 431 | 432 | Widget domcard, domImg, domTxt; 433 | 434 | var isSelected=this.selectedCards.contains(card); 435 | 436 | if (this.ep.typeOfListing==TypeOfListing.gridWithName) { 437 | domImg = CachedNetworkImage(imageUrl:this.ep.proxiedImage(src), fit: BoxFit.cover); 438 | 439 | domTxt = Container( 440 | height: 30.0, 441 | padding: EdgeInsets.only(top: 5.0), 442 | decoration: new BoxDecoration(color: Colors.black54), 443 | child: new Text(txt, style: textStyle, 444 | textAlign: TextAlign.center, 445 | overflow: TextOverflow.ellipsis,), 446 | ); 447 | 448 | domcard=new Container( 449 | color:isSelected?Theme.of(context).selectedRowColor:null, 450 | child:new GridTile( 451 | child: domImg, 452 | footer:domTxt 453 | ) 454 | ); 455 | 456 | } else if (this.ep.typeOfListing==TypeOfListing.gridWithoutName) { 457 | 458 | domImg = CachedNetworkImage(imageUrl:this.ep.proxiedImage(src), placeholder: _imgPlaceholder(), errorWidget:_imgErrorPlaceholder(), fit: BoxFit.fitHeight, ); 459 | 460 | domcard=new Container( 461 | color:isSelected?Theme.of(context).accentColor:null, 462 | child:new GridTile(child: domImg,), 463 | ); 464 | } 465 | 466 | return new GestureDetector( 467 | child:domcard, 468 | onTap: () { 469 | if (this.ep.typeOfDetail==TypeOfDetail.productCompare) 470 | _toggleSelection(card, context); 471 | else 472 | _navigateDetailPage(card, context); 473 | }, 474 | ); 475 | 476 | } 477 | _toggleSelection(ModelCard card, BuildContext context){ 478 | var newSelectedCards=this.selectedCards.sublist(0); 479 | 480 | if (newSelectedCards.contains(card)){ 481 | newSelectedCards.remove(card); 482 | } else{ 483 | newSelectedCards.add(card); 484 | } 485 | 486 | if (newSelectedCards.length>=2){ 487 | this.selectedCards.clear(); 488 | _navigateDetailPage(newSelectedCards[0], context, cardToCompare: card,); 489 | } else { 490 | setState(() {this.selectedCards=newSelectedCards;}); 491 | } 492 | } 493 | _navigateDetailPage(ModelCard card, BuildContext context, {ModelCard cardToCompare}) async { 494 | // Navigator.pushNamed(context, '/detail');//no se puede usar pushNamed porque no recibe ningún otro parámetro 495 | 496 | Navigator.push(context, new MaterialPageRoute( 497 | builder: (BuildContext context){ 498 | if (ep.typeOfDetail==TypeOfDetail.productCompare){ 499 | 500 | return new DetailPage.compare(card, cardToCompare); 501 | 502 | } else { 503 | return new DetailPage(card); 504 | 505 | } 506 | } 507 | )); 508 | } 509 | 510 | _emptyState(err){ 511 | var tt=Theme.of(context).textTheme; 512 | 513 | var title='Unable to connect'; 514 | var subtitle="Maybe it's not you but we can't find the resource. Tap to try again."; 515 | 516 | appData.logEvent('listing_error', {'ep':appData.getCurrEndPoint().endpointTitle, 'err':err.toString()}); 517 | 518 | // if (err.toString().contains('SocketException')){ 519 | return new GestureDetector( 520 | onTap: (){ 521 | setState((){/*retry connection*/}); 522 | }, 523 | child:new Column( 524 | children: [ 525 | SizedBox(height: 100.0), 526 | new Icon(Icons.cloud_off, color:Colors.black45, size: 90.0), 527 | 528 | Text(title, style:tt.title), 529 | SizedBox(height: 10.0), 530 | 531 | new Container(padding:EdgeInsets.symmetric(horizontal: 16.0), 532 | child:Text(subtitle, style:tt.subhead, textAlign: TextAlign.center,) 533 | ) 534 | ],) 535 | ); 536 | // } 537 | } 538 | void _showSnackbar(String text) { 539 | final snackBar = 540 | SnackBar(content: Text(text), duration: Duration(seconds: 3)); 541 | if (this._scaffoldKey != null && this._scaffoldKey.currentState != null) 542 | this._scaffoldKey.currentState.showSnackBar(snackBar); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /magic_flutter/app/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 14 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 18 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 19 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 20 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 21 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 22 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 23 | BFE1089033CABBA3E267C8B3 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BE3129320515ED8BD1253ED7 /* libPods-Runner.a */; }; 24 | FC558B092158FAA700DF6DCD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FC558B082158FAA700DF6DCD /* GoogleService-Info.plist */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXCopyFilesBuildPhase section */ 28 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 29 | isa = PBXCopyFilesBuildPhase; 30 | buildActionMask = 2147483647; 31 | dstPath = ""; 32 | dstSubfolderSpec = 10; 33 | files = ( 34 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 35 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 36 | ); 37 | name = "Embed Frameworks"; 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXCopyFilesBuildPhase section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 44 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 45 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 46 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 47 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 48 | 4856BE9E960E1B06DC9B8ECF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 49 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 50 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 51 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 52 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 53 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 54 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 55 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 57 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 58 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 60 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 9AFC6E825743569D9B5C5C06 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 62 | BE3129320515ED8BD1253ED7 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | FC558B082158FAA700DF6DCD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 72 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 73 | BFE1089033CABBA3E267C8B3 /* libPods-Runner.a in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 02198F328076213DBECCB6B4 /* Pods */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 9AFC6E825743569D9B5C5C06 /* Pods-Runner.debug.xcconfig */, 84 | 4856BE9E960E1B06DC9B8ECF /* Pods-Runner.release.xcconfig */, 85 | ); 86 | path = Pods; 87 | sourceTree = ""; 88 | }; 89 | 5B69F4D403C1445D709CD19B /* Frameworks */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | BE3129320515ED8BD1253ED7 /* libPods-Runner.a */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | 9740EEB11CF90186004384FC /* Flutter */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 101 | 3B80C3931E831B6300D905FE /* App.framework */, 102 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 103 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 104 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 105 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 106 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 107 | ); 108 | name = Flutter; 109 | sourceTree = ""; 110 | }; 111 | 97C146E51CF9000F007C117D = { 112 | isa = PBXGroup; 113 | children = ( 114 | 9740EEB11CF90186004384FC /* Flutter */, 115 | 97C146F01CF9000F007C117D /* Runner */, 116 | 97C146EF1CF9000F007C117D /* Products */, 117 | 02198F328076213DBECCB6B4 /* Pods */, 118 | 5B69F4D403C1445D709CD19B /* Frameworks */, 119 | ); 120 | sourceTree = ""; 121 | }; 122 | 97C146EF1CF9000F007C117D /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 97C146EE1CF9000F007C117D /* Runner.app */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 97C146F01CF9000F007C117D /* Runner */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 134 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 135 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 136 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 137 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 138 | 97C147021CF9000F007C117D /* Info.plist */, 139 | 97C146F11CF9000F007C117D /* Supporting Files */, 140 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 141 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 142 | ); 143 | path = Runner; 144 | sourceTree = ""; 145 | }; 146 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | FC558B082158FAA700DF6DCD /* GoogleService-Info.plist */, 150 | 97C146F21CF9000F007C117D /* main.m */, 151 | ); 152 | name = "Supporting Files"; 153 | sourceTree = ""; 154 | }; 155 | /* End PBXGroup section */ 156 | 157 | /* Begin PBXNativeTarget section */ 158 | 97C146ED1CF9000F007C117D /* Runner */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 161 | buildPhases = ( 162 | D5FF5F4BCADBC83CAD7FAC21 /* [CP] Check Pods Manifest.lock */, 163 | 9740EEB61CF901F6004384FC /* Run Script */, 164 | 97C146EA1CF9000F007C117D /* Sources */, 165 | 97C146EB1CF9000F007C117D /* Frameworks */, 166 | 97C146EC1CF9000F007C117D /* Resources */, 167 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 168 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 169 | 58D932D17432603ED3F68D94 /* [CP] Embed Pods Frameworks */, 170 | ); 171 | buildRules = ( 172 | ); 173 | dependencies = ( 174 | ); 175 | name = Runner; 176 | productName = Runner; 177 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | /* End PBXNativeTarget section */ 181 | 182 | /* Begin PBXProject section */ 183 | 97C146E61CF9000F007C117D /* Project object */ = { 184 | isa = PBXProject; 185 | attributes = { 186 | LastUpgradeCheck = 0910; 187 | ORGANIZATIONNAME = "The Chromium Authors"; 188 | TargetAttributes = { 189 | 97C146ED1CF9000F007C117D = { 190 | CreatedOnToolsVersion = 7.3.1; 191 | DevelopmentTeam = 3K986X45WA; 192 | }; 193 | }; 194 | }; 195 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 196 | compatibilityVersion = "Xcode 3.2"; 197 | developmentRegion = English; 198 | hasScannedForEncodings = 0; 199 | knownRegions = ( 200 | en, 201 | Base, 202 | ); 203 | mainGroup = 97C146E51CF9000F007C117D; 204 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 205 | projectDirPath = ""; 206 | projectRoot = ""; 207 | targets = ( 208 | 97C146ED1CF9000F007C117D /* Runner */, 209 | ); 210 | }; 211 | /* End PBXProject section */ 212 | 213 | /* Begin PBXResourcesBuildPhase section */ 214 | 97C146EC1CF9000F007C117D /* Resources */ = { 215 | isa = PBXResourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 219 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 220 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 221 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 222 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 223 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 224 | FC558B092158FAA700DF6DCD /* GoogleService-Info.plist in Resources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXResourcesBuildPhase section */ 229 | 230 | /* Begin PBXShellScriptBuildPhase section */ 231 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 232 | isa = PBXShellScriptBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | inputPaths = ( 237 | ); 238 | name = "Thin Binary"; 239 | outputPaths = ( 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | shellPath = /bin/sh; 243 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 244 | }; 245 | 58D932D17432603ED3F68D94 /* [CP] Embed Pods Frameworks */ = { 246 | isa = PBXShellScriptBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | ); 250 | inputPaths = ( 251 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", 252 | "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", 253 | ); 254 | name = "[CP] Embed Pods Frameworks"; 255 | outputPaths = ( 256 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | shellPath = /bin/sh; 260 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 261 | showEnvVarsInLog = 0; 262 | }; 263 | 9740EEB61CF901F6004384FC /* Run Script */ = { 264 | isa = PBXShellScriptBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | ); 268 | inputPaths = ( 269 | ); 270 | name = "Run Script"; 271 | outputPaths = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | shellPath = /bin/sh; 275 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 276 | }; 277 | D5FF5F4BCADBC83CAD7FAC21 /* [CP] Check Pods Manifest.lock */ = { 278 | isa = PBXShellScriptBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | ); 282 | inputPaths = ( 283 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 284 | "${PODS_ROOT}/Manifest.lock", 285 | ); 286 | name = "[CP] Check Pods Manifest.lock"; 287 | outputPaths = ( 288 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | shellPath = /bin/sh; 292 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 293 | showEnvVarsInLog = 0; 294 | }; 295 | /* End PBXShellScriptBuildPhase section */ 296 | 297 | /* Begin PBXSourcesBuildPhase section */ 298 | 97C146EA1CF9000F007C117D /* Sources */ = { 299 | isa = PBXSourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 303 | 97C146F31CF9000F007C117D /* main.m in Sources */, 304 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | /* End PBXSourcesBuildPhase section */ 309 | 310 | /* Begin PBXVariantGroup section */ 311 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | 97C146FB1CF9000F007C117D /* Base */, 315 | ); 316 | name = Main.storyboard; 317 | sourceTree = ""; 318 | }; 319 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 320 | isa = PBXVariantGroup; 321 | children = ( 322 | 97C147001CF9000F007C117D /* Base */, 323 | ); 324 | name = LaunchScreen.storyboard; 325 | sourceTree = ""; 326 | }; 327 | /* End PBXVariantGroup section */ 328 | 329 | /* Begin XCBuildConfiguration section */ 330 | 97C147031CF9000F007C117D /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | CLANG_ANALYZER_NONNULL = YES; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 352 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 353 | CLANG_WARN_STRICT_PROTOTYPES = YES; 354 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 355 | CLANG_WARN_UNREACHABLE_CODE = YES; 356 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 357 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 358 | COPY_PHASE_STRIP = NO; 359 | DEBUG_INFORMATION_FORMAT = dwarf; 360 | ENABLE_STRICT_OBJC_MSGSEND = YES; 361 | ENABLE_TESTABILITY = YES; 362 | GCC_C_LANGUAGE_STANDARD = gnu99; 363 | GCC_DYNAMIC_NO_PIC = NO; 364 | GCC_NO_COMMON_BLOCKS = YES; 365 | GCC_OPTIMIZATION_LEVEL = 0; 366 | GCC_PREPROCESSOR_DEFINITIONS = ( 367 | "DEBUG=1", 368 | "$(inherited)", 369 | ); 370 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 371 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 372 | GCC_WARN_UNDECLARED_SELECTOR = YES; 373 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 374 | GCC_WARN_UNUSED_FUNCTION = YES; 375 | GCC_WARN_UNUSED_VARIABLE = YES; 376 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 377 | MTL_ENABLE_DEBUG_INFO = YES; 378 | ONLY_ACTIVE_ARCH = YES; 379 | SDKROOT = iphoneos; 380 | TARGETED_DEVICE_FAMILY = "1,2"; 381 | }; 382 | name = Debug; 383 | }; 384 | 97C147041CF9000F007C117D /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 387 | buildSettings = { 388 | ALWAYS_SEARCH_USER_PATHS = NO; 389 | CLANG_ANALYZER_NONNULL = YES; 390 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 391 | CLANG_CXX_LIBRARY = "libc++"; 392 | CLANG_ENABLE_MODULES = YES; 393 | CLANG_ENABLE_OBJC_ARC = YES; 394 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 395 | CLANG_WARN_BOOL_CONVERSION = YES; 396 | CLANG_WARN_COMMA = YES; 397 | CLANG_WARN_CONSTANT_CONVERSION = YES; 398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 399 | CLANG_WARN_EMPTY_BODY = YES; 400 | CLANG_WARN_ENUM_CONVERSION = YES; 401 | CLANG_WARN_INFINITE_RECURSION = YES; 402 | CLANG_WARN_INT_CONVERSION = YES; 403 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNREACHABLE_CODE = YES; 410 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 411 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 412 | COPY_PHASE_STRIP = NO; 413 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 414 | ENABLE_NS_ASSERTIONS = NO; 415 | ENABLE_STRICT_OBJC_MSGSEND = YES; 416 | GCC_C_LANGUAGE_STANDARD = gnu99; 417 | GCC_NO_COMMON_BLOCKS = YES; 418 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 419 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 420 | GCC_WARN_UNDECLARED_SELECTOR = YES; 421 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 422 | GCC_WARN_UNUSED_FUNCTION = YES; 423 | GCC_WARN_UNUSED_VARIABLE = YES; 424 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 425 | MTL_ENABLE_DEBUG_INFO = NO; 426 | SDKROOT = iphoneos; 427 | TARGETED_DEVICE_FAMILY = "1,2"; 428 | VALIDATE_PRODUCT = YES; 429 | }; 430 | name = Release; 431 | }; 432 | 97C147061CF9000F007C117D /* Debug */ = { 433 | isa = XCBuildConfiguration; 434 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 435 | buildSettings = { 436 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 437 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 438 | DEVELOPMENT_TEAM = 3K986X45WA; 439 | ENABLE_BITCODE = NO; 440 | FRAMEWORK_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "$(PROJECT_DIR)/Flutter", 443 | ); 444 | INFOPLIST_FILE = Runner/Info.plist; 445 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 446 | LIBRARY_SEARCH_PATHS = ( 447 | "$(inherited)", 448 | "$(PROJECT_DIR)/Flutter", 449 | ); 450 | PRODUCT_BUNDLE_IDENTIFIER = apiexplorer.com.app; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | VERSIONING_SYSTEM = "apple-generic"; 453 | }; 454 | name = Debug; 455 | }; 456 | 97C147071CF9000F007C117D /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 459 | buildSettings = { 460 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 461 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 462 | DEVELOPMENT_TEAM = 3K986X45WA; 463 | ENABLE_BITCODE = NO; 464 | FRAMEWORK_SEARCH_PATHS = ( 465 | "$(inherited)", 466 | "$(PROJECT_DIR)/Flutter", 467 | ); 468 | INFOPLIST_FILE = Runner/Info.plist; 469 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 470 | LIBRARY_SEARCH_PATHS = ( 471 | "$(inherited)", 472 | "$(PROJECT_DIR)/Flutter", 473 | ); 474 | PRODUCT_BUNDLE_IDENTIFIER = apiexplorer.com.app; 475 | PRODUCT_NAME = "$(TARGET_NAME)"; 476 | VERSIONING_SYSTEM = "apple-generic"; 477 | }; 478 | name = Release; 479 | }; 480 | /* End XCBuildConfiguration section */ 481 | 482 | /* Begin XCConfigurationList section */ 483 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 484 | isa = XCConfigurationList; 485 | buildConfigurations = ( 486 | 97C147031CF9000F007C117D /* Debug */, 487 | 97C147041CF9000F007C117D /* Release */, 488 | ); 489 | defaultConfigurationIsVisible = 0; 490 | defaultConfigurationName = Release; 491 | }; 492 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 493 | isa = XCConfigurationList; 494 | buildConfigurations = ( 495 | 97C147061CF9000F007C117D /* Debug */, 496 | 97C147071CF9000F007C117D /* Release */, 497 | ); 498 | defaultConfigurationIsVisible = 0; 499 | defaultConfigurationName = Release; 500 | }; 501 | /* End XCConfigurationList section */ 502 | }; 503 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 504 | } 505 | -------------------------------------------------------------------------------- /data/firebase-remoteconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version_code": "20181022_18:27", 3 | "default_endpoints": [ 4 | { 5 | "endpointTitle": "Magic: The Gathering", 6 | "endpointUrl": "https://api.magicthegathering.io/v1/cards", 7 | "theme": { 8 | "color": "orange" 9 | }, 10 | "categories": ["Games", "Entertainment"], 11 | 12 | "typeOfListing": "gridWithName", 13 | "typeOfDetail": "detail", 14 | "about": { 15 | "web": "https://magicthegathering.io/", 16 | "doc": "https://docs.magicthegathering.io/", 17 | "info": "Join the Community of Developers building with the MTG API", 18 | "logo": "https://magicthegathering.io/images/bg.jpg" 19 | }, 20 | "widgetsOrder": [ 21 | { 22 | "type": "hero" 23 | }, 24 | { 25 | "type": "header", 26 | "right": [ 27 | { 28 | "type": "image" 29 | } 30 | ], 31 | "left": [ 32 | { "id": "name" }, 33 | { "type": "separator" }, 34 | { "id": "MYtags" }, 35 | { "type": "separator" }, 36 | { "id": "MYstats" } 37 | ] 38 | }, 39 | { 40 | "type": "separator" 41 | }, 42 | { 43 | "id": "MYdescription" 44 | }, 45 | { 46 | "type": "separator" 47 | }, 48 | { 49 | "id": "MYfields" 50 | }, 51 | { 52 | "type": "separator" 53 | }, 54 | { 55 | "id": "MYimages" 56 | } 57 | ], 58 | "id": "id", 59 | "name": { 60 | "field": "name" 61 | }, 62 | "xhero": { 63 | "type": "hero" 64 | }, 65 | "MYdescription": { 66 | "field": "text", 67 | "label": "Summary" 68 | }, 69 | "MYstats": { 70 | "type": "stats", 71 | "docu": "type is one of [images, tags, text, stats, fields]", 72 | "fields": [ 73 | { 74 | "field": "toughness", 75 | "label": "Toughness" 76 | }, 77 | { 78 | "field": "power", 79 | "label": "Power" 80 | }, 81 | { 82 | "field": "cmc", 83 | "label": "CMC" 84 | } 85 | ] 86 | }, 87 | "MYtags": { 88 | "type": "tags", 89 | "fields": ["types", "subtypes"] 90 | }, 91 | "MYfields": { 92 | "type": "fields", 93 | "docu": "type is one of [images, tags, text, stats, fields]", 94 | "fields": [ 95 | { 96 | "field": "artist", 97 | "label": "Artist" 98 | }, 99 | { 100 | "field": "setName", 101 | "label": "Set name" 102 | }, 103 | { 104 | "field": "watermark", 105 | "label": "Watermark" 106 | } 107 | ] 108 | }, 109 | "MYimages": { 110 | "type": "images", 111 | "fields": [ 112 | { 113 | "field": "imageUrl", 114 | "type": "thumbnail", 115 | "docu": "type is one of [thumbnail, poster, hero, null]" 116 | } 117 | ] 118 | } 119 | }, 120 | { 121 | "endpointTitle": "2018 Fifa WorldCup Russia", 122 | "endpointUrl": "http://worldcup.sfg.io/matches", 123 | "theme": { 124 | "base": "dark" 125 | }, 126 | "categories": ["Sports"], 127 | 128 | "typeOfListing": "match", 129 | "typeOfDetail": "match", 130 | "about": { 131 | "web": "https://worldcup.sfg.io", 132 | "doc": "https://github.com/estiens/world_cup_json", 133 | "info": "World cup 2018... in JSON" 134 | }, 135 | 136 | "widgetsOrder": [ 137 | { 138 | "type": "header", 139 | "left": [ 140 | { "id": "img-left" }, 141 | { "id": "name-left" }, 142 | { "id": "score-left" } 143 | ], 144 | "right": [ 145 | { "id": "img-right" }, 146 | { "id": "name-right" }, 147 | { "id": "score-right" } 148 | ] 149 | }, 150 | { "type": "separator" }, 151 | { "id": "venue" }, 152 | { "type": "separator" }, 153 | { "id": "officials" }, 154 | { "type": "separator" }, 155 | { "id": "events" } 156 | ], 157 | 158 | "id": "fifa_id", 159 | "name": { 160 | "fields": [ 161 | { "id": "name-left", "field": "{home_team/country}" }, 162 | { "id": "name-right", "field": "{away_team/country}" } 163 | ] 164 | }, 165 | "section": { "field": "stage_name" }, 166 | "scores": { 167 | "fields": [ 168 | { "id": "score-left", "field": "{home_team/goals}" }, 169 | { "id": "score-right", "field": "{away_team/goals}" } 170 | ] 171 | }, 172 | 173 | "venue": { 174 | "type": "text", 175 | "label": "Venue", 176 | "title": "{venue}", 177 | "subtitle": "{location}", 178 | "img": "https://img.fifa.com/image/upload/t_tc1/wulvncwmgnfeixxsp5gv.jpg" 179 | }, 180 | 181 | "fields": { 182 | "label": "Stats", 183 | "type": "fields", 184 | "docu": "type is one of [images, tags, text, stats, fields]", 185 | "fields": [ 186 | { 187 | "left": "{home_team_statistics/red_cards}", 188 | "right": "{away_team_statistics/red_cards}", 189 | "label": "Red cards" 190 | }, 191 | { 192 | "left": "{home_team_statistics/yellow_cards}", 193 | "right": "{away_team_statistics/yellow_cards}", 194 | "label": "Yellow cards" 195 | }, 196 | { "label": "separator" }, 197 | { 198 | "left": "{home_team/penalties}", 199 | "right": "{away_team/penalties}", 200 | "label": "Penalties" 201 | }, 202 | { 203 | "left": "{home_team_statistics/ball_possession}", 204 | "right": "{away_team_statistics/ball_possession}", 205 | "label": "Ball possession" 206 | }, 207 | { 208 | "left": "{home_team_statistics/tactics}", 209 | "right": "{away_team_statistics/tactics}", 210 | "label": "Tactics" 211 | }, 212 | { 213 | "left": "{home_team_statistics/num_passes}", 214 | "right": "{away_team_statistics/num_passes}", 215 | "label": "Passes" 216 | }, 217 | { 218 | "left": "{home_team_statistics/fouls_committed}", 219 | "right": "{away_team_statistics/fouls_committed}", 220 | "label": "Fouls" 221 | } 222 | ] 223 | }, 224 | "events": { 225 | "type": "timeline", 226 | "fields": ["home_team_events", "away_team_events"], 227 | "sort": "time", 228 | "sort_strip": ["'"], 229 | "transform": "left/right" 230 | }, 231 | 232 | "images": { 233 | "type": "images", 234 | "fields": [ 235 | { 236 | "id": "img-left", 237 | "field": "https://raw.githubusercontent.com/rotoxl/country-flags/master/png100px_by_cioc/{home_team/code}.png", 238 | "type": "poster", 239 | "docu": "one of [thumbnail, poster, hero, null]" 240 | }, 241 | { 242 | "id": "img-right", 243 | "field": "https://raw.githubusercontent.com/rotoxl/country-flags/master/png100px_by_cioc/{away_team/code}.png", 244 | "type": "poster", 245 | "docu": "one of [thumbnail, poster, hero, null]" 246 | } 247 | ] 248 | }, 249 | "officials": { 250 | "type": "tags", 251 | "fields": [{ "field": "officials" }], 252 | "label": "Officials" 253 | } 254 | }, 255 | { 256 | "endpointTitle": "World Countries", 257 | "endpointUrl": "https://restcountries.eu/rest/v2/all", 258 | "theme": { 259 | "color": "indigo" 260 | }, 261 | "categories": ["Education", "Reference"], 262 | 263 | "typeOfListing": "gridWithName", 264 | "typeOfDetail": "detail", 265 | "about": { 266 | "web": "https://restcountries.eu", 267 | "doc": "https://github.com/apilayer/restcountries/", 268 | "info": "Get information about countries via a RESTful API" 269 | }, 270 | "widgetsOrder": [ 271 | { 272 | "type": "hero" 273 | }, 274 | { 275 | "type": "header", 276 | "left": [ 277 | { 278 | "type": "image" 279 | } 280 | ], 281 | "right": [ 282 | { "id": "name" }, 283 | { "type": "separator" }, 284 | { "id": "tags" }, 285 | { "type": "separator" }, 286 | { "id": "stats" } 287 | ] 288 | }, 289 | { 290 | "type": "separator" 291 | }, 292 | { 293 | "id": "fields" 294 | }, 295 | { 296 | "type": "separator" 297 | }, 298 | { 299 | "id": "borders" 300 | } 301 | ], 302 | "id": "alpha3Code", 303 | "name": { 304 | "field": "name" 305 | }, 306 | "hero": { 307 | "type": "hero" 308 | }, 309 | "stats": { 310 | "type": "stats", 311 | "docu": "type is one of [images, tags, text, stats, fields]", 312 | "fields": [ 313 | { 314 | "field": "population", 315 | "label": "Population" 316 | }, 317 | { 318 | "field": "area", 319 | "label": "Area" 320 | } 321 | ] 322 | }, 323 | "tags": { 324 | "type": "tags", 325 | "fields": ["region", "subregion"] 326 | }, 327 | "fields": { 328 | "label": "Fields", 329 | "type": "fields", 330 | "docu": "type is one of [images, tags, text, stats, fields]", 331 | "fields": [ 332 | { 333 | "field": "capital", 334 | "label": "Capital" 335 | }, 336 | { 337 | "field": "{currencies/0/name}", 338 | "label": "Currencies" 339 | }, 340 | { 341 | "field": "topLevelDomain", 342 | "label": "Top level domain" 343 | } 344 | ] 345 | }, 346 | "images": { 347 | "type": "images", 348 | "fields": [ 349 | { 350 | "field": "https://api.backendless.com/2F26DFBF-433C-51CC-FF56-830CEA93BF00/473FB5A9-D20E-8D3E-FF01-E93D9D780A00/files/CountryFlagsPng/{alpha3Code|lower}.png", 351 | "type": "thumbnail", 352 | "docu": "type is one of [thumbnail, poster, hero, null]" 353 | } 354 | ] 355 | }, 356 | "borders": { 357 | "type": "related", 358 | 359 | "label": "Country borders", 360 | "shape": "circle", 361 | "fields": [ 362 | { 363 | "field": "borders", 364 | "linkType": "linkToSameEndPoint", 365 | "___related_field": "alpha3Code", 366 | "___related_image": "https://api.backendless.com/2F26DFBF-433C-51CC-FF56-830CEA93BF00/473FB5A9-D20E-8D3E-FF01-E93D9D780A00/files/CountryFlagsPng/{alpha3Code|lower}.png" 367 | } 368 | ] 369 | } 370 | }, 371 | { 372 | "endpointTitle": "The Movie DB/Trending Movies", 373 | "endpointUrl": "https://api.themoviedb.org/3/trending/all/day?api_key=a2f45e3ad3af96b2ec9d0542adbfd1da®ion=ES", 374 | "theme": { 375 | "color": "red" 376 | }, 377 | "categories": ["Entertainment"], 378 | 379 | "typeOfListing": "gridWithoutName", 380 | "typeOfDetail": "detail", 381 | "about": { 382 | "web": "https://www.themoviedb.org", 383 | "doc": "https://www.themoviedb.org/documentation/api", 384 | "info": "The Movie Database (TMDb) is a popular, user editable database for movies and TV shows.", 385 | "logo": "https://www.themoviedb.org/assets/1/v4/logos/312x276-primary-green-74212f6247252a023be0f02a5a45794925c3689117da9d20ffe47742a665c518.png" 386 | }, 387 | "widgetsOrder": [ 388 | { "type": "hero" }, 389 | { 390 | "type": "header", 391 | "left": [{ "type": "image" }], 392 | "right": [ 393 | { "id": "name" }, 394 | { "type": "separator" }, 395 | { "id": "stats" } 396 | ] 397 | }, 398 | { "type": "separator" }, 399 | { "id": "description" }, 400 | { "type": "separator" }, 401 | { "id": "fields" }, 402 | { "type": "separator" }, 403 | { "type": "images" } 404 | ], 405 | "id": "id", 406 | "name": { 407 | "field": "{original_title||original_name}" 408 | }, 409 | "description": { 410 | "field": "overview", 411 | "label": "Summary" 412 | }, 413 | "hero": { 414 | "type": "hero" 415 | }, 416 | "stats": { 417 | "type": "stats", 418 | "docu": "type is one of [images, tags, text, stats, fields]", 419 | "fields": [ 420 | { 421 | "field": "vote_average", 422 | "label": "Average" 423 | }, 424 | { 425 | "field": "vote_count", 426 | "label": "Vote count" 427 | }, 428 | { 429 | "field": "popularity", 430 | "label": "Popularity" 431 | } 432 | ] 433 | }, 434 | "fields": { 435 | "label": "Fields", 436 | "type": "fields", 437 | "docu": "type is one of [images, tags, text, stats, fields]", 438 | "fields": [ 439 | { 440 | "field": "release_date", 441 | "label": "Release date" 442 | } 443 | ] 444 | }, 445 | "images": { 446 | "type": "images", 447 | "fields": [ 448 | { 449 | "field": "https://image.tmdb.org/t/p/w500/{poster_path}", 450 | "type": "poster", 451 | "docu": "type is one of [thumbnail, poster, hero, null]" 452 | }, 453 | { 454 | "field": "https://image.tmdb.org/t/p/w500/{backdrop_path}", 455 | "type": "hero", 456 | "docu": "type is one of [thumbnail, poster, hero, null]" 457 | } 458 | ] 459 | } 460 | }, 461 | { 462 | "endpointTitle": "Zomato/NY Restaurants", 463 | "endpointUrl": "https://developers.zomato.com/api/v2.1/search?entity_id=280&entity_type=city", 464 | "headers": { "user-key": "ab6e490707b8f2f51c864618f1ad90f4" }, 465 | "theme": { 466 | "color": "green" 467 | }, 468 | "categories": ["Food & drink", "Lifestyle"], 469 | 470 | "typeOfListing": "gridWithName", 471 | "typeOfDetail": "detail", 472 | "about": { 473 | "web": "https://www.zomato.com", 474 | "doc": "https://developers.zomato.com/documentation", 475 | "info": "Find the best restaurants, cafés, and bars", 476 | "logo": "https://upload.wikimedia.org/wikipedia/en/thumb/0/09/Zomato_company_logo.png/240px-Zomato_company_logo.png" 477 | }, 478 | "widgetsOrder": [ 479 | { "type": "hero" }, 480 | { 481 | "type": "header", 482 | "left": [{ "type": "image" }], 483 | "right": [ 484 | { "id": "name" }, 485 | { "type": "separator" }, 486 | { "id": "tags" }, 487 | { "type": "separator" }, 488 | { "id": "stats" } 489 | ] 490 | }, 491 | { "type": "separator" }, 492 | { "id": "description" }, 493 | { "type": "separator" }, 494 | { "type": "images" } 495 | ], 496 | "id": "{restaurant/id}", 497 | "name": { 498 | "field": "{restaurant/name}" 499 | }, 500 | "description": { 501 | "field": "{restaurant/location/address}\n{restaurant/location/locality}\n{restaurant/location/city}", 502 | "label": "Address" 503 | }, 504 | "hero": { 505 | "type": "hero" 506 | }, 507 | "stats": { 508 | "type": "stats", 509 | "docu": "type is one of [images, tags, text, stats, fields]", 510 | "fields": [ 511 | { 512 | "field": "{restaurant/user_rating/aggregate_rating}", 513 | "label": "Average" 514 | }, 515 | { 516 | "field": "{restaurant/user_rating/votes}", 517 | "label": "Vote count" 518 | } 519 | ] 520 | }, 521 | "tags": { 522 | "type": "tags", 523 | "docu": "type is one of [images, tags, text, stats, fields]", 524 | "fields": [ 525 | { 526 | "field": "{restaurant/cuisines}", 527 | "label": "Average" 528 | } 529 | ] 530 | }, 531 | "__fields": { 532 | "label": "Fields", 533 | "type": "fields", 534 | "docu": "type is one of [images, tags, text, stats, fields]", 535 | "fields": [ 536 | { 537 | "field": "release_date", 538 | "label": "Release date" 539 | } 540 | ] 541 | }, 542 | "images": { 543 | "type": "images", 544 | "fields": [ 545 | { 546 | "field": "{restaurant/featured_image}", 547 | "type": "poster", 548 | "docu": "type is one of [thumbnail, poster, hero, null]" 549 | } 550 | ] 551 | } 552 | }, 553 | { 554 | "endpointTitle": "UFC Fighters", 555 | "endpointUrl": "http://ufc-data-api.ufc.com/api/v3/iphone/fighters", 556 | "theme": { 557 | "color": "grey" 558 | }, 559 | "categories": ["Sports"], 560 | 561 | "typeOfListing": "gridWithName", 562 | "typeOfDetail": "detail", 563 | "about": { 564 | "web": "http://ufc-data-api.ufc.com", 565 | "doc": "http://ufc-data-api.ufc.com/api/v1/us", 566 | "info": "Adept's sports focused fan engagement platform is used by some of the most recognized sports brands in the world. Ask what it can do for you" 567 | }, 568 | "widgetsOrder": [ 569 | { "type": "hero" }, 570 | { 571 | "type": "header", 572 | "left": [{ "type": "image" }], 573 | "right": [ 574 | { "id": "name" }, 575 | { "type": "separator" }, 576 | { "id": "tags" }, 577 | { "type": "separator" }, 578 | { "id": "stats" } 579 | ] 580 | }, 581 | { "type": "separator" }, 582 | { "id": "fields" }, 583 | { "type": "separator" }, 584 | { "type": "images" } 585 | ], 586 | "id": "id", 587 | "name": { 588 | "field": "{nickname} {first_name} {last_name}" 589 | }, 590 | "__description": { 591 | "field": "{restaurant/location/address}\n{restaurant/location/locality}\n{restaurant/location/city}", 592 | "label": "Address" 593 | }, 594 | "hero": { 595 | "type": "hero" 596 | }, 597 | "stats": { 598 | "type": "stats", 599 | "docu": "type is one of [images, tags, text, stats, fields]", 600 | "fields": [ 601 | { 602 | "field": "wins", 603 | "label": "Wins" 604 | }, 605 | { 606 | "field": "draws", 607 | "label": "Draws" 608 | }, 609 | { 610 | "field": "losses", 611 | "label": "Losses" 612 | } 613 | ] 614 | }, 615 | "tags": { 616 | "type": "tags", 617 | "docu": "type is one of [images, tags, text, stats, fields]", 618 | "fields": [ 619 | { 620 | "field": "fighter_status" 621 | }, 622 | { 623 | "field": "rank" 624 | } 625 | ] 626 | }, 627 | "fields": { 628 | "label": "Fields", 629 | "type": "fields", 630 | "docu": "type is one of [images, tags, text, stats, fields]", 631 | "fields": [ 632 | { 633 | "field": "link", 634 | "label": "Link" 635 | } 636 | ] 637 | }, 638 | "images": { 639 | "type": "images", 640 | "fields": [ 641 | { 642 | "field": "profile_image", 643 | "type": "thumbnail", 644 | "docu": "type is one of [thumbnail, poster, hero, null]" 645 | }, 646 | { 647 | "field": "left_full_body_image", 648 | "type": "image", 649 | "docu": "type is one of [thumbnail, poster, hero, null]" 650 | }, 651 | { 652 | "field": "right_full_body_image", 653 | "type": "image", 654 | "docu": "type is one of [thumbnail, poster, hero, null]" 655 | }, 656 | { 657 | "field": "belt_thumbnail", 658 | "type": "image", 659 | "docu": "type is one of [thumbnail, poster, hero, null]" 660 | }, 661 | { 662 | "field": "profile_image", 663 | "type": "hero", 664 | "docu": "type is one of [thumbnail, poster, hero, null]" 665 | } 666 | ] 667 | } 668 | }, 669 | { 670 | "endpointTitle": "Google Books", 671 | "endpointUrl": "https://www.googleapis.com/books/v1/volumes?q=oreilly", 672 | "theme": { 673 | "color": "brown" 674 | }, 675 | "categories": ["Education", "Reference", "Entertainment"], 676 | 677 | "typeOfListing": "gridWithoutName", 678 | "typeOfDetail": "detail", 679 | "about": { 680 | "web": "https://books.google.es", 681 | "doc": "https://developers.google.com/books/docs/v1/reference", 682 | "info": "Google Books is our effort to make book content more discoverable on the Web", 683 | "logo": "http://kinlane-productions.s3.amazonaws.com/api-evangelist-site/company/logos/Screen%20Shot%202017-03-16%20at%204.28.26%20PM.png" 684 | }, 685 | "widgetsOrder": [ 686 | { "type": "hero" }, 687 | { 688 | "type": "header", 689 | "left": [{ "type": "image" }], 690 | "right": [{ "id": "name" }, { "type": "separator" }, { "id": "tags" }] 691 | }, 692 | { "type": "separator" }, 693 | { "id": "description" }, 694 | { "type": "separator" }, 695 | { "type": "images" } 696 | ], 697 | "id": "id", 698 | "name": { 699 | "field": "{volumeInfo/title}" 700 | }, 701 | "description": { 702 | "field": "{searchInfo/textSnippet}", 703 | "label": "Description" 704 | }, 705 | "hero": { 706 | "type": "hero" 707 | }, 708 | "tags": { 709 | "type": "tags", 710 | "docu": "type is one of [images, tags, text, stats, fields]", 711 | "fields": [ 712 | { 713 | "field": "{volumeInfo/maturityRating}" 714 | }, 715 | { 716 | "field": "{volumeInfo/printType}" 717 | }, 718 | { 719 | "field": "{volumeInfo/categories/0}" 720 | } 721 | ] 722 | }, 723 | "fields": { 724 | "label": "Fields", 725 | "type": "fields", 726 | "docu": "type is one of [images, tags, text, stats, fields]", 727 | "fields": [ 728 | { 729 | "field": "{volumeInfo/infoLink}", 730 | "label": "Link" 731 | } 732 | ] 733 | }, 734 | "images": { 735 | "type": "images", 736 | "fields": [ 737 | { 738 | "field": "{volumeInfo/imageLinks/thumbnail}", 739 | "type": "poster" 740 | }, 741 | { 742 | "field": "{volumeInfo/imageLinks/smallThumbnail}", 743 | "type": "thumbnail" 744 | } 745 | ] 746 | } 747 | }, 748 | { 749 | "endpointTitle": "Places", 750 | "endpointUrl": "https://s3-eu-west-1.amazonaws.com/api-explorer-app/places/placesAPI.json", 751 | "theme": { 752 | "color": "green" 753 | }, 754 | "categories": ["Photo & video", "Travel"], 755 | 756 | "typeOfListing": "gridWithoutName", 757 | "typeOfDetail": "hero", 758 | 759 | "about": { 760 | "web": "https://unsplash.com", 761 | "doc": "https://unsplash.com/developers", 762 | "info": "Download free (do whatever you want) high-resolution photos", 763 | "__logo": "" 764 | }, 765 | "widgetsOrder": [ 766 | { "type": "hero" }, 767 | { "id": "place" }, 768 | { "id": "author" } 769 | ], 770 | "id": "src", 771 | 772 | "place": { "field": "place", "style": "title", "effect": "textShadow" }, 773 | "author": { 774 | "field": "author", 775 | "style": "subtitle", 776 | "effect": "textShadow" 777 | }, 778 | "hero": { "type": "hero" }, 779 | 780 | "images": { 781 | "type": "images", 782 | "fields": [ 783 | { 784 | "field": "src", 785 | "type": "hero" 786 | } 787 | ] 788 | } 789 | }, 790 | { 791 | "endpointTitle": "_last.fm: Top Artists (Beta)", 792 | "endpointUrl": "http://ws.audioscrobbler.com/2.0/?method=chart.gettopartists&api_key=a75185adbaec912b811ba1a63a931b59&format=json", 793 | "endpointPath":"artists/artist", 794 | "theme": { 795 | "base": "dark" 796 | }, 797 | "categories": ["Entertainment"], 798 | "dependencies": "_albums", 799 | "__imagesProxy":"https://images.weserv.nl/?url={url}", 800 | 801 | "typeOfListing": "sliver", 802 | "typeOfDetail": "detail", 803 | "about": { 804 | "web": "https://www.last.fm", 805 | "doc": "https://www.last.fm/api", 806 | "info": "Last.fm is a music service that learns what you love.", 807 | "logo": "https://www.last.fm/static/images/logo_static_mob@2x.1104cdd90f6b.png" 808 | }, 809 | "widgetsOrder": [ 810 | { 811 | "type": "hero" 812 | }, 813 | { 814 | "type": "header", 815 | "right": [ 816 | { 817 | "type": "image" 818 | } 819 | ], 820 | "left": [ 821 | { "id": "name", "align":"right"}, 822 | {"type": "separator"}, 823 | {"id":"stats"} 824 | ] 825 | }, 826 | {"type": "separator"}, 827 | {"id": "albums"} 828 | ], 829 | "id": "id", 830 | "name": { 831 | "field": "name" 832 | }, 833 | "hero": { 834 | "type": "hero" 835 | }, 836 | "images": { 837 | "type": "images", 838 | "fields": [ 839 | { 840 | "field": "{image/3/#text}", 841 | "type": "poster", 842 | "docu": "type is one of [thumbnail, poster, hero, null]" 843 | } 844 | ] 845 | }, 846 | "albums": { 847 | "type": "related", 848 | 849 | "label": "Albums", 850 | "fields": [ 851 | { 852 | "linkType": "linkToOtherEndPoint", 853 | 854 | "endPointTitle": "_albums (Beta)", 855 | "endPointParameter": { "i": "name" } 856 | } 857 | ] 858 | }, 859 | "stats": { 860 | "type": "stats", 861 | "fields": [ 862 | { 863 | "field": "playcount", 864 | "label": "Playcount" 865 | }, 866 | { 867 | "field": "listeners", 868 | "label": "Listeners" 869 | } 870 | ] 871 | } 872 | }, 873 | { 874 | "endpointTitle": "_albums (Beta)", 875 | "endpointUrl": "http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist={i}&api_key=a75185adbaec912b811ba1a63a931b59&format=json", 876 | "endpointParameters": [{ "name":"i", "value": "the beatles", "label":"Artist name" }], 877 | "endpointPath":"topalbums/album", 878 | "__imagesProxy":"https://images.weserv.nl/?url={url}", 879 | 880 | "theme": {"color": "yellow"}, 881 | "categories": ["Entertainment"], 882 | 883 | "typeOfListing": "gridWithoutName", 884 | "typeOfDetail": "detail", 885 | "about": { 886 | "web": "https://www.last.fm", 887 | "doc": "https://www.last.fm/api", 888 | "info": "Last.fm is a music service that learns what you love.", 889 | "logo": "https://www.last.fm/static/images/logo_static_mob@2x.1104cdd90f6b.png" 890 | }, 891 | "widgetsOrder": [ 892 | { 893 | "type": "header", 894 | "left": [ 895 | {"type": "image"} 896 | ], 897 | "right": [{ "id": "name" }, {"type": "stats"}] 898 | }, 899 | { 900 | "type": "separator" 901 | }, 902 | { 903 | "id": "description" 904 | } 905 | ], 906 | "id": "mbid", 907 | "name": { 908 | "field": "name" 909 | }, 910 | "images": { 911 | "type": "images", 912 | "fields": [ 913 | { 914 | "field": "{image/3/#text}", 915 | "type": "poster", 916 | "docu": "type is one of [thumbnail, poster, hero, null]" 917 | } 918 | ] 919 | }, 920 | "stats": { 921 | "type": "stats", 922 | "fields": [ 923 | { 924 | "field": "playcount", 925 | "label": "Playcount" 926 | }, 927 | { 928 | "field": "listeners", 929 | "label": "Listeners" 930 | } 931 | ] 932 | } 933 | } 934 | ] 935 | } --------------------------------------------------------------------------------