├── .fvm └── fvm_config.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .metadata ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── rohantaneja │ │ │ │ └── fluttercon │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ ├── production │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── values-night-v31 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── fastlane │ └── Fastfile ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── images │ ├── fluttercon.svg │ ├── logo.png │ └── splash_background.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Production.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon-20@2x.png │ │ │ ├── AppIcon-20@2x~ipad.png │ │ │ ├── AppIcon-20@3x.png │ │ │ ├── AppIcon-20~ipad.png │ │ │ ├── AppIcon-29.png │ │ │ ├── AppIcon-29@2x.png │ │ │ ├── AppIcon-29@2x~ipad.png │ │ │ ├── AppIcon-29@3x.png │ │ │ ├── AppIcon-29~ipad.png │ │ │ ├── AppIcon-40@2x.png │ │ │ ├── AppIcon-40@2x~ipad.png │ │ │ ├── AppIcon-40@3x.png │ │ │ ├── AppIcon-40~ipad.png │ │ │ ├── AppIcon-60@2x~car.png │ │ │ ├── AppIcon-60@3x~car.png │ │ │ ├── AppIcon-83.5@2x~ipad.png │ │ │ ├── AppIcon@2x.png │ │ │ ├── AppIcon@2x~ipad.png │ │ │ ├── AppIcon@3x.png │ │ │ ├── AppIcon~ios-marketing.png │ │ │ ├── AppIcon~ipad.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── LaunchBackgroundProduction.imageset │ │ │ ├── Contents.json │ │ │ └── background.png │ │ ├── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ └── LaunchImageProduction.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ └── LaunchImage@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ ├── LaunchScreenProduction.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── RunnerTests │ └── RunnerTests.swift └── fastlane │ └── Fastfile ├── lib ├── common │ ├── extensions │ │ ├── session_extensions.dart │ │ └── speaker_extensions.dart │ └── widgets │ │ ├── conference_app_bar.dart │ │ ├── error_message_widget.dart │ │ ├── session │ │ ├── favourite_session_icon.dart │ │ ├── session_duration.dart │ │ ├── session_format.dart │ │ ├── session_room.dart │ │ ├── sessions_list_item.dart │ │ └── sessions_tab_bar_view.dart │ │ ├── speaker │ │ └── speakers_list_item.dart │ │ └── tap_to_shrink_effect_gesture_detector.dart ├── di │ └── injector.dart ├── features │ ├── app │ │ └── presentation │ │ │ └── bloc │ │ │ ├── app_bloc.dart │ │ │ ├── app_event.dart │ │ │ ├── app_state.dart │ │ │ └── bloc.dart │ ├── favourites │ │ ├── data │ │ │ └── data_source │ │ │ │ ├── favourites_local_data_source.dart │ │ │ │ └── favourites_local_data_source_impl.dart │ │ ├── domain │ │ │ ├── repository │ │ │ │ ├── favourites_repository.dart │ │ │ │ └── favourites_repository_impl.dart │ │ │ └── use_case │ │ │ │ ├── get_favourite_session_ids_use_case.dart │ │ │ │ └── save_favourite_session_ids_use_case.dart │ │ └── presentation │ │ │ └── pages │ │ │ └── favourite_sessions_page.dart │ ├── home │ │ └── presentation │ │ │ ├── conference_metadata.dart │ │ │ ├── pages │ │ │ ├── home_page.dart │ │ │ ├── sessions_page.dart │ │ │ └── speakers_page.dart │ │ │ └── widgets │ │ │ └── conference_search_app_bar.dart │ ├── session_details │ │ └── presentation │ │ │ └── pages │ │ │ └── session_details_page.dart │ ├── speaker_details │ │ └── presentation │ │ │ └── pages │ │ │ └── speaker_details_page.dart │ └── splash │ │ └── flutter_native_splash-production.yaml ├── fluttercon_app.dart └── main.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Production.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── packages ├── api_client │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib │ │ ├── api_client.dart │ │ └── src │ │ │ └── api_client.dart │ └── pubspec.yaml ├── conference_data │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib │ │ ├── conference_data.dart │ │ └── src │ │ │ ├── conference_data.dart │ │ │ ├── data │ │ │ ├── data_source │ │ │ │ ├── conference_data_local_data_source.dart │ │ │ │ ├── conference_data_local_data_source_impl.dart │ │ │ │ ├── conference_data_remote_data_source.dart │ │ │ │ └── conference_data_remote_data_source_impl.dart │ │ │ └── model │ │ │ │ ├── agenda_model.dart │ │ │ │ ├── category_model.dart │ │ │ │ ├── category_parent_model.dart │ │ │ │ ├── conference_data_model.dart │ │ │ │ ├── link_model.dart │ │ │ │ ├── room_model.dart │ │ │ │ ├── session_model.dart │ │ │ │ └── speaker_model.dart │ │ │ ├── di │ │ │ └── injector.dart │ │ │ └── domain │ │ │ ├── conference_data_source.dart │ │ │ ├── entity │ │ │ ├── agenda.dart │ │ │ ├── category.dart │ │ │ ├── conference_data.dart │ │ │ ├── link.dart │ │ │ ├── room.dart │ │ │ ├── session.dart │ │ │ └── speaker.dart │ │ │ ├── repository │ │ │ ├── conference_data_repository.dart │ │ │ └── conference_data_repository_impl.dart │ │ │ └── use_case │ │ │ └── get_conference_data_use_case.dart │ └── pubspec.yaml └── util │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib │ ├── src │ │ ├── failure.dart │ │ ├── result.dart │ │ └── use_case.dart │ └── util.dart │ └── pubspec.yaml ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── sessions-android.png ├── sessions-ios.png ├── sessions-macos.png ├── sessions-search-android.png ├── sessions-search-ios.png ├── sessions-search-macos.png ├── speakers-android.png ├── speakers-ios.png ├── speakers-macos.png ├── speakers-search-android.png ├── speakers-search-ios.png └── speakers-search-macos.png └── store-badges ├── appstore.png └── playstore.png /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.10.5", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Status 10 | 11 | **READY / IN DEVELOPMENT / HOLD** 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Type of Change 18 | 19 | 20 | 21 | - [ ] ✨ New feature (non-breaking change which adds functionality) 22 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 23 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 24 | - [ ] 🧹 Code refactor 25 | - [ ] ✅ Build configuration change 26 | - [ ] 📝 Documentation 27 | - [ ] 🗑️ Chore 28 | 29 | ## Design Changes 30 | 31 | 32 | 33 | |Before|After| 34 | |:-:|:-:| 35 | |PASTE_BEFORE_HERE|PASTE_AFTER_HERE| 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | # FVM 47 | .fvm/flutter_sdk 48 | 49 | build/ 50 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 796c8ef79279f9c774545b3771238c3098dbefab 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 17 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 18 | - platform: android 19 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 20 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 21 | - platform: ios 22 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 23 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 24 | - platform: macos 25 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 26 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rohan Taneja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluttercon 2 | 3 | iOS, Android and macOS app for [Fluttercon Berlin 2023](https://fluttercon.dev), built with Flutter. 4 | 5 | [![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis) 6 | 7 | ### Download Links 8 | 9 | | iOS | Android | macOS | 10 | |:--:|:--:|:--:| 11 | |[](https://apps.apple.com/us/app/fluttercon-berlin-2023/id6450055384)|[](https://play.google.com/store/apps/details?id=com.rohantaneja.fluttercon)|Not published| 12 | 13 | ### Screenshots 14 | 15 | | | iOS | Android | macOS | 16 | |-----------------|-------------------------------------------------------------|---------------------------------------------------------------------|-----------------------------------------------------------------| 17 | | Sessions | ![Sessions iOS](screenshots/sessions-ios.png) | ![Sessions Android](screenshots/sessions-android.png) | ![Sessions macOS](screenshots/sessions-macos.png) | 18 | | Speakers | ![Speakers iOS](screenshots/speakers-ios.png) | ![Speakers Android](screenshots/speakers-android.png) | ![Speakers macOS](screenshots/speakers-macos.png) | 19 | | Sessions Search | ![Sessions Search iOS](screenshots/sessions-search-ios.png) | ![Sessions Search Android](screenshots/sessions-search-android.png) | ![Sessions Search macOS](screenshots/sessions-search-macos.png) | 20 | | Speakers Search | ![Speakers Search iOS](screenshots/speakers-search-ios.png) | ![Speakers Search Android](screenshots/speakers-search-android.png) | ![Speakers Search macOS](screenshots/speakers-search-macos.png) | 21 | 22 | ### Dev Notes 23 | 24 | - The project uses a line length of 120 characters 25 | - The code can ideally be re-used for any conference that uses the Sessionize API 26 | - The base url for the Sessionize API for Fluttercon has been gitignored. To run the app and see conference data, switch 27 | to the [not-for-prod/mock-api-response](https://github.com/rohan20/fluttercon/tree/not-for-prod/mock-api-response) 28 | branch that uses hardcoded Fluttercon 2023 responses instead. 29 | - When running the app, make sure you set the flavor for the build to `production` 30 | - ApiClient requires a baseUrl and so if you are running the app on the `not-for-prod/mock-api-response` branch, this can be set to `http://localhost` 31 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.5.0.0.yaml 2 | 3 | linter: 4 | rules: 5 | lines_longer_than_80_chars: false 6 | public_member_api_docs: false 7 | one_member_abstracts: false 8 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | 15 | fastlane/report.xml 16 | fastlane/Appfile 17 | fastlane/service_account.json 18 | fastlane/README.md 19 | -------------------------------------------------------------------------------- /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 plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.rohantaneja.fluttercon" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.rohantaneja.fluttercon" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion 34 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | signingConfigs { 58 | release { 59 | storeFile file('../fastlane/rhntnj-keystore.jks') 60 | keyAlias 'fluttercon' 61 | keyPassword project.findProperty('fluttercon-key-password') ?: '' 62 | storePassword project.findProperty('keystore-password') ?: '' 63 | } 64 | } 65 | 66 | flavorDimensions "app" 67 | 68 | productFlavors { 69 | production { 70 | dimension "app" 71 | resValue "string", "app_name", "Fluttercon" 72 | applicationId "com.rohantaneja.fluttercon" 73 | } 74 | } 75 | 76 | buildTypes { 77 | debug { 78 | productFlavors.production.signingConfig signingConfigs.debug 79 | } 80 | 81 | release { 82 | productFlavors.production.signingConfig signingConfigs.release 83 | } 84 | } 85 | } 86 | 87 | flutter { 88 | source '../..' 89 | } 90 | 91 | dependencies { 92 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 93 | } 94 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 25 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/rohantaneja/fluttercon/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rohantaneja.fluttercon 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/android/app/src/production/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/production/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/production/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/production/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/production/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/production/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:android) 2 | 3 | APP_ID_PRODUCTION = "com.rohantaneja.fluttercon" 4 | 5 | platform :android do 6 | 7 | private_lane :cleanup do 8 | sh "fvm flutter clean" 9 | sh "fvm flutter packages get" 10 | end 11 | 12 | desc "Build App" 13 | private_lane :build_app do 14 | cleanup 15 | gradle(task: "clean", print_command: true) 16 | gradle( 17 | task: "bundle", 18 | build_type: "Release", 19 | print_command: true 20 | ) 21 | end 22 | 23 | desc "Upload Production App to Google Play Store" 24 | lane :upload_production do 25 | flavor = "production" 26 | 27 | build_app(app_id: APP_ID_PRODUCTION, should_build_app_bundle: true) 28 | upload_to_play_store( 29 | track: "internal", 30 | package_name: APP_ID_PRODUCTION, 31 | json_key: "./fastlane/service_account.json", 32 | aab: "../build/app/outputs/bundle/#{flavor}Release/app-#{flavor}-release.aab", 33 | ) 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/images/fluttercon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/splash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/assets/images/splash_background.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | 36 | fastlane/build/ 37 | fastlane/report.xml 38 | fastlane/README.md 39 | fastlane/.env.default 40 | fastlane/Appfile 41 | fastlane/Matchfile 42 | -------------------------------------------------------------------------------- /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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | # Flutter flavors documentation bug: https://docs.flutter.dev/deployment/flavors#plugin-configurations 8 | # As per the documentation, this should be: 9 | # project 'Runner', { 10 | # 'Debug-Production' => :debug, 11 | # 'Profile-Production' => :release, 12 | # 'Release-Production' => :release, 13 | # } 14 | # 15 | # However, that doesn't work. The following (which is the default when a new Flutter project is created) does: 16 | project 'Runner', { 17 | 'Debug' => :debug, 18 | 'Profile' => :release, 19 | 'Release' => :release, 20 | } 21 | 22 | def flutter_root 23 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 24 | unless File.exist?(generated_xcode_build_settings_path) 25 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 26 | end 27 | 28 | File.foreach(generated_xcode_build_settings_path) do |line| 29 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 30 | return matches[1].strip if matches 31 | end 32 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 33 | end 34 | 35 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 36 | 37 | flutter_ios_podfile_setup 38 | 39 | target 'Runner' do 40 | use_frameworks! 41 | use_modular_headers! 42 | 43 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 44 | target 'RunnerTests' do 45 | inherit! :search_paths 46 | end 47 | end 48 | 49 | post_install do |installer| 50 | installer.pods_project.targets.each do |target| 51 | flutter_additional_ios_build_settings(target) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_native_splash (0.0.1): 4 | - Flutter 5 | - FMDB (2.7.12): 6 | - FMDB/standard (= 2.7.12) 7 | - FMDB/Core (2.7.12) 8 | - FMDB/standard (2.7.12): 9 | - FMDB/Core 10 | - path_provider_foundation (0.0.1): 11 | - Flutter 12 | - FlutterMacOS 13 | - shared_preferences_foundation (0.0.1): 14 | - Flutter 15 | - FlutterMacOS 16 | - sqflite (0.0.3): 17 | - Flutter 18 | - FMDB (>= 2.7.5) 19 | - url_launcher_ios (0.0.1): 20 | - Flutter 21 | 22 | DEPENDENCIES: 23 | - Flutter (from `Flutter`) 24 | - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) 25 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 26 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 27 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 28 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 29 | 30 | SPEC REPOS: 31 | trunk: 32 | - FMDB 33 | 34 | EXTERNAL SOURCES: 35 | Flutter: 36 | :path: Flutter 37 | flutter_native_splash: 38 | :path: ".symlinks/plugins/flutter_native_splash/ios" 39 | path_provider_foundation: 40 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 41 | shared_preferences_foundation: 42 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 43 | sqflite: 44 | :path: ".symlinks/plugins/sqflite/ios" 45 | url_launcher_ios: 46 | :path: ".symlinks/plugins/url_launcher_ios/ios" 47 | 48 | SPEC CHECKSUMS: 49 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 50 | flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef 51 | FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 52 | path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 53 | shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c 54 | sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a 55 | url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 56 | 57 | PODFILE CHECKSUM: 125a6238d4c2be6899f67c499e66e295a37618c7 58 | 59 | COCOAPODS: 1.15.2 60 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Production.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon@2x.png", 5 | "idiom": "iphone", 6 | "scale": "2x", 7 | "size": "60x60" 8 | }, 9 | { 10 | "filename": "AppIcon@3x.png", 11 | "idiom": "iphone", 12 | "scale": "3x", 13 | "size": "60x60" 14 | }, 15 | { 16 | "filename": "AppIcon~ipad.png", 17 | "idiom": "ipad", 18 | "scale": "1x", 19 | "size": "76x76" 20 | }, 21 | { 22 | "filename": "AppIcon@2x~ipad.png", 23 | "idiom": "ipad", 24 | "scale": "2x", 25 | "size": "76x76" 26 | }, 27 | { 28 | "filename": "AppIcon-83.5@2x~ipad.png", 29 | "idiom": "ipad", 30 | "scale": "2x", 31 | "size": "83.5x83.5" 32 | }, 33 | { 34 | "filename": "AppIcon-40@2x.png", 35 | "idiom": "iphone", 36 | "scale": "2x", 37 | "size": "40x40" 38 | }, 39 | { 40 | "filename": "AppIcon-40@3x.png", 41 | "idiom": "iphone", 42 | "scale": "3x", 43 | "size": "40x40" 44 | }, 45 | { 46 | "filename": "AppIcon-40~ipad.png", 47 | "idiom": "ipad", 48 | "scale": "1x", 49 | "size": "40x40" 50 | }, 51 | { 52 | "filename": "AppIcon-40@2x~ipad.png", 53 | "idiom": "ipad", 54 | "scale": "2x", 55 | "size": "40x40" 56 | }, 57 | { 58 | "filename": "AppIcon-20@2x.png", 59 | "idiom": "iphone", 60 | "scale": "2x", 61 | "size": "20x20" 62 | }, 63 | { 64 | "filename": "AppIcon-20@3x.png", 65 | "idiom": "iphone", 66 | "scale": "3x", 67 | "size": "20x20" 68 | }, 69 | { 70 | "filename": "AppIcon-20~ipad.png", 71 | "idiom": "ipad", 72 | "scale": "1x", 73 | "size": "20x20" 74 | }, 75 | { 76 | "filename": "AppIcon-20@2x~ipad.png", 77 | "idiom": "ipad", 78 | "scale": "2x", 79 | "size": "20x20" 80 | }, 81 | { 82 | "filename": "AppIcon-29.png", 83 | "idiom": "iphone", 84 | "scale": "1x", 85 | "size": "29x29" 86 | }, 87 | { 88 | "filename": "AppIcon-29@2x.png", 89 | "idiom": "iphone", 90 | "scale": "2x", 91 | "size": "29x29" 92 | }, 93 | { 94 | "filename": "AppIcon-29@3x.png", 95 | "idiom": "iphone", 96 | "scale": "3x", 97 | "size": "29x29" 98 | }, 99 | { 100 | "filename": "AppIcon-29~ipad.png", 101 | "idiom": "ipad", 102 | "scale": "1x", 103 | "size": "29x29" 104 | }, 105 | { 106 | "filename": "AppIcon-29@2x~ipad.png", 107 | "idiom": "ipad", 108 | "scale": "2x", 109 | "size": "29x29" 110 | }, 111 | { 112 | "filename": "AppIcon-60@2x~car.png", 113 | "idiom": "car", 114 | "scale": "2x", 115 | "size": "60x60" 116 | }, 117 | { 118 | "filename": "AppIcon-60@3x~car.png", 119 | "idiom": "car", 120 | "scale": "3x", 121 | "size": "60x60" 122 | }, 123 | { 124 | "filename": "AppIcon~ios-marketing.png", 125 | "idiom": "ios-marketing", 126 | "scale": "1x", 127 | "size": "1024x1024" 128 | } 129 | ], 130 | "info": { 131 | "author": "iconkitchen", 132 | "version": 1 133 | } 134 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackgroundProduction.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackgroundProduction.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchBackgroundProduction.imageset/background.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/ios/Runner/Assets.xcassets/LaunchImageProduction.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreenProduction.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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Fluttercon 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | fluttercon 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | $(LAUNCH_SCREEN_STORYBOARD) 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiresFullScreen 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | CADisableMinimumFrameDurationOnPhone 40 | 41 | UIApplicationSupportsIndirectInputEvents 42 | 43 | UIStatusBarHidden 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | TEAM_ID = "YV55MHHS8A" 4 | GIT_BRANCH = TEAM_ID 5 | 6 | BUNDLE_ID_PRODUCTION = "com.rohantaneja.fluttercon" 7 | 8 | def generate_identifiers(app_bundle_id, extensions) 9 | bundle_ids = [app_bundle_id] 10 | 11 | extensions.each { |appExtension| 12 | bundle_ids.push(app_bundle_id + "." + appExtension) 13 | } 14 | 15 | bundle_ids 16 | end 17 | 18 | platform :ios do 19 | 20 | desc "Delete all certificates and provisioning profiles for development, enterprise and appstore" 21 | lane :delete_all_certificates_and_provisioning_profiles do 22 | delete_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, readonly: false) 23 | delete_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, type: "appstore", readonly: false) 24 | end 25 | 26 | desc "Delete certificates and provisioning profiles for the provided type [development/adhoc/enterprise/appstore]" 27 | lane :delete_certificates_and_provisioning_profiles do |options| 28 | bundle_id = options[:bundle_id] 29 | type = options[:type] == nil ? "development" : options[:type] 30 | 31 | match_nuke( 32 | type: type, 33 | team_id: TEAM_ID, 34 | app_identifier: bundle_id, 35 | git_branch: GIT_BRANCH, 36 | force: options[:force] == nil ? false : options[:force], 37 | readonly: options[:readonly] == nil ? true : options[:readonly], 38 | verbose: options[:verbose], 39 | profile_name: type == "development" ? get_profile_name_development(bundle_id) : get_profile_name_appstore(bundle_id) 40 | ) 41 | end 42 | 43 | # This will NOT re-create certificates and provisioning profiles if they exist on the dev portal and are valid. 44 | # 45 | # Use this lane for situations like adding a new entitlement to an app identifier on the dev portal. Fastlane will 46 | # automatically detect that the certificates on dev portal are valid so it will not re-create them. But since 47 | # provisioning profiles on dev portal won't have this new capability from the app identifier, it will re-create them. 48 | desc "Regenerate all provisioning profiles for development, enterprise and appstore" 49 | lane :regenerate_all_provisioning_profiles do |options| 50 | readonly = false 51 | force = options[:force] 52 | 53 | sync_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, readonly: readonly, force: force) 54 | sync_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, readonly: readonly, force: force, type: "appstore") 55 | end 56 | 57 | desc "Sync all certificates and provisioning profiles for development, enterprise and appstore" 58 | lane :sync_all_certificates_and_provisioning_profiles do |options| 59 | readonly = options[:readonly] 60 | 61 | sync_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, readonly: readonly) 62 | sync_certificates_and_provisioning_profiles(bundle_id: BUNDLE_ID_PRODUCTION, type: "appstore", readonly: readonly) 63 | end 64 | 65 | desc "Sync certificates and provisioning profiles for the provided type [development/adhoc/enterprise/appstore]" 66 | lane :sync_certificates_and_provisioning_profiles do |options| 67 | bundle_id = options[:bundle_id] 68 | type = options[:type] == nil ? "development" : options[:type] 69 | 70 | match( 71 | type: type, 72 | team_id: TEAM_ID, 73 | app_identifier: bundle_id, 74 | git_branch: GIT_BRANCH, 75 | force: options[:force] == nil ? false : options[:force], 76 | readonly: options[:readonly] == nil ? true : options[:readonly], 77 | verbose: options[:verbose], 78 | profile_name: type == "development" ? get_profile_name_development(bundle_id) : get_profile_name_appstore(bundle_id) 79 | ) 80 | end 81 | 82 | desc "Upload Production App to TestFlight" 83 | lane :upload_production do 84 | bundle_id = BUNDLE_ID_PRODUCTION 85 | 86 | build_production 87 | # TODO: Add back after Firebase integration: upload_debug_symbols_to_crashlytics(bundle_id: bundle_id) 88 | upload_to_testflight( 89 | app_identifier: bundle_id, 90 | skip_submission: true, 91 | skip_waiting_for_build_processing: true, 92 | team_id: TEAM_ID 93 | ) 94 | end 95 | 96 | desc "Build - Production" 97 | lane :build_production do 98 | bundle_id = BUNDLE_ID_PRODUCTION 99 | 100 | sync_certificates_and_provisioning_profiles(bundle_id: bundle_id, type: "appstore") 101 | cleanup 102 | gym( 103 | scheme: "Production", 104 | export_method: "app-store", 105 | clean: true, 106 | include_bitcode: false, 107 | include_symbols: true, 108 | export_team_id: TEAM_ID, 109 | export_options: { 110 | signingStyle: "manual", 111 | provisioningProfiles: { 112 | bundle_id => "match AppStore #{bundle_id}", 113 | } 114 | } 115 | ) 116 | end 117 | 118 | def get_profile_name_development(bundle_id) 119 | return "match Development #{bundle_id}" 120 | end 121 | 122 | def get_profile_name_appstore(bundle_id) 123 | return "match AppStore #{bundle_id}" 124 | end 125 | 126 | private_lane :cleanup do 127 | sh "fvm flutter clean" 128 | sh "fvm flutter packages get" 129 | cocoapods(clean_install: true) 130 | end 131 | 132 | def get_scheme(bundle_id) 133 | case (bundle_id) 134 | when BUNDLE_ID_PRODUCTION 135 | return "production" 136 | else 137 | UI.crash!("Scheme " + bundle_id + " seems to be invalid. Please check again.") 138 | end 139 | end 140 | 141 | end 142 | -------------------------------------------------------------------------------- /lib/common/extensions/session_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:conference_data/conference_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:fluttercon/features/home/presentation/conference_metadata.dart'; 5 | 6 | extension SessionExt on Session { 7 | List getSessionSpeakers({required List speakers}) { 8 | return speakers.where((speaker) => speakerIds.contains(speaker.id)).toList(); 9 | } 10 | 11 | List getSessionCategories({required List categories}) { 12 | return categories.where((category) => categoryIds.contains(category.id)).toList(); 13 | } 14 | 15 | Room getSessionRoom({required List rooms}) { 16 | return rooms.firstWhere((room) => room.id == roomId); 17 | } 18 | 19 | Category? getSessionFormatCategory({required List categories}) { 20 | return categories.firstWhereOrNull((category) { 21 | final isCategoryTypeSessionFormat = category.typeId == ConferenceMetadata.categoryTypeSessionFormatId; 22 | 23 | return isCategoryTypeSessionFormat && categoryIds.contains(category.id); 24 | }); 25 | } 26 | } 27 | 28 | extension SessionFormatExt on String { 29 | Color get sessionFormatBorderColor { 30 | switch (this) { 31 | case ConferenceMetadata.lightningTalkId: 32 | return Colors.blue.shade500; 33 | case ConferenceMetadata.sessionId: 34 | return Colors.orange.shade500; 35 | case ConferenceMetadata.workshopId || ConferenceMetadata.keynoteId || ConferenceMetadata.panelDiscussionId: 36 | return Colors.green.shade500; 37 | default: 38 | return Colors.grey.shade500; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/common/extensions/speaker_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | 3 | extension SpeakerExt on Speaker { 4 | List getSpeakerSessions({required List sessions}) { 5 | return sessions.where((session) => session.speakerIds.contains(id)).toList(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/common/widgets/conference_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_svg/flutter_svg.dart'; 5 | 6 | class ConferenceAppBar extends StatelessWidget implements PreferredSizeWidget { 7 | const ConferenceAppBar({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return AppBar( 12 | centerTitle: true, 13 | title: SvgPicture.asset( 14 | 'assets/images/fluttercon.svg', 15 | width: min(MediaQuery.of(context).size.width * 0.4, 150), 16 | ), 17 | ); 18 | } 19 | 20 | @override 21 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/widgets/error_message_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorMessageWidget extends StatelessWidget { 4 | const ErrorMessageWidget({this.text = 'Error', this.onRefresh, super.key}); 5 | 6 | final String text; 7 | final RefreshCallback? onRefresh; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final child = Center(child: Text(text, textAlign: TextAlign.center)); 12 | 13 | if (onRefresh == null) { 14 | return child; 15 | } 16 | 17 | return RefreshIndicator( 18 | onRefresh: onRefresh!, 19 | child: CustomScrollView( 20 | slivers: [ 21 | SliverFillRemaining(child: child), 22 | ], 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/common/widgets/session/favourite_session_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 4 | 5 | class FavouriteSessionIcon extends StatelessWidget { 6 | const FavouriteSessionIcon({ 7 | required this.sessionId, 8 | this.allowTap = true, 9 | this.inactiveColor, 10 | super.key, 11 | }); 12 | 13 | final String sessionId; 14 | final bool allowTap; 15 | final Color? inactiveColor; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return GestureDetector( 20 | behavior: HitTestBehavior.opaque, 21 | onTap: allowTap ? () => context.read().add(SessionFavouriteIconTappedEvent(sessionId: sessionId)) : null, 22 | child: BlocBuilder( 23 | builder: (context, state) { 24 | return Icon( 25 | state.isFavouriteSession(id: sessionId) ? Icons.favorite_rounded : Icons.favorite_outline_rounded, 26 | color: state.isFavouriteSession(id: sessionId) ? Colors.red.shade400 : inactiveColor ?? Colors.grey.shade300, 27 | size: 24, 28 | ); 29 | }, 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/common/widgets/session/session_duration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SessionDuration extends StatelessWidget { 4 | const SessionDuration({ 5 | required this.durationInMinutes, 6 | this.isNotATalk = false, 7 | super.key, 8 | }); 9 | 10 | final int durationInMinutes; 11 | final bool isNotATalk; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 17 | decoration: BoxDecoration( 18 | color: isNotATalk ? Colors.grey.shade50 : durationInMinutes.backgroundColor, 19 | borderRadius: BorderRadius.circular(8), 20 | border: Border.all(color: isNotATalk ? Colors.grey.shade500 : durationInMinutes.borderColor, width: 0.5), 21 | ), 22 | child: Text( 23 | '${durationInMinutes}m', 24 | style: Theme.of(context).textTheme.labelSmall, 25 | ), 26 | ); 27 | } 28 | } 29 | 30 | extension DurationExt on int { 31 | Color get backgroundColor { 32 | switch (this) { 33 | case 20: 34 | return Colors.blue.shade50; 35 | case 40: 36 | return Colors.orange.shade50; 37 | default: 38 | return Colors.green.shade50; 39 | } 40 | } 41 | 42 | Color get borderColor { 43 | switch (this) { 44 | case 20: 45 | return Colors.blue.shade500; 46 | case 40: 47 | return Colors.orange.shade500; 48 | default: 49 | return Colors.green.shade500; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/common/widgets/session/session_format.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttercon/common/extensions/session_extensions.dart'; 4 | import 'package:fluttercon/features/home/presentation/conference_metadata.dart'; 5 | 6 | class SessionFormat extends StatelessWidget { 7 | const SessionFormat({ 8 | required this.sessionFormat, 9 | this.hideSessionFormatIfItIsSession = false, 10 | super.key, 11 | }); 12 | 13 | final Category? sessionFormat; 14 | final bool hideSessionFormatIfItIsSession; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | if (sessionFormat == null || sessionFormat!.id == ConferenceMetadata.sessionId && hideSessionFormatIfItIsSession) { 19 | return const SizedBox(); 20 | } 21 | 22 | return Container( 23 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 24 | decoration: BoxDecoration( 25 | color: sessionFormat!.id.backgroundColor, 26 | borderRadius: BorderRadius.circular(8), 27 | border: Border.all(color: sessionFormat!.id.sessionFormatBorderColor, width: 0.5), 28 | ), 29 | child: Text( 30 | sessionFormat!.name, 31 | style: Theme.of(context).textTheme.labelSmall, 32 | ), 33 | ); 34 | } 35 | } 36 | 37 | extension CategoryExt on String { 38 | Color get backgroundColor { 39 | switch (this) { 40 | case ConferenceMetadata.lightningTalkId: 41 | return Colors.blue.shade50; 42 | case ConferenceMetadata.sessionId: 43 | return Colors.orange.shade50; 44 | case ConferenceMetadata.workshopId || ConferenceMetadata.keynoteId || ConferenceMetadata.panelDiscussionId: 45 | return Colors.green.shade50; 46 | default: 47 | return Colors.grey.shade200; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/widgets/session/session_room.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SessionRoom extends StatelessWidget { 4 | const SessionRoom({ 5 | required this.roomName, 6 | super.key, 7 | }); 8 | 9 | final String roomName; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Flexible( 14 | child: Container( 15 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 16 | decoration: BoxDecoration( 17 | color: Colors.transparent, 18 | borderRadius: BorderRadius.circular(8), 19 | border: Border.all(color: Colors.grey.shade800, width: 0.2), 20 | ), 21 | child: Row( 22 | mainAxisSize: MainAxisSize.min, 23 | children: [ 24 | Icon(Icons.location_on, size: 16, color: Colors.grey.shade400), 25 | const SizedBox(width: 4), 26 | Flexible( 27 | child: Text( 28 | roomName, 29 | maxLines: 1, 30 | overflow: TextOverflow.ellipsis, 31 | style: Theme.of(context).textTheme.bodySmall, 32 | ), 33 | ), 34 | ], 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/common/widgets/speaker/speakers_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:conference_data/conference_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:fluttercon/common/widgets/tap_to_shrink_effect_gesture_detector.dart'; 5 | import 'package:fluttercon/features/speaker_details/presentation/pages/speaker_details_page.dart'; 6 | 7 | const _speakerImageRadius = 24.0; 8 | 9 | class SpeakersListItem extends StatelessWidget { 10 | const SpeakersListItem({ 11 | required this.speaker, 12 | this.backgroundColor = Colors.transparent, 13 | this.allowTap = true, 14 | this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 12), 15 | super.key, 16 | }); 17 | 18 | final Speaker speaker; 19 | final Color backgroundColor; 20 | final bool allowTap; 21 | final EdgeInsets padding; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return TapToShrinkEffectGestureDetector( 26 | isEffectEnabled: allowTap, 27 | onTap: () { 28 | if (allowTap) { 29 | Navigator.of(context).push( 30 | MaterialPageRoute( 31 | builder: (_) => SpeakerDetailsPage(speaker), 32 | ), 33 | ); 34 | } 35 | }, 36 | child: Container( 37 | padding: padding, 38 | color: backgroundColor, 39 | child: Row( 40 | children: [ 41 | _SpeakerImage(name: speaker.fullName, imageUrl: speaker.profilePictureUrl), 42 | const SizedBox(width: 16), 43 | Expanded( 44 | child: Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | Text( 48 | speaker.fullName, 49 | style: Theme.of(context).textTheme.titleSmall, 50 | ), 51 | if (speaker.tagLine.isNotEmpty) ...{ 52 | const SizedBox(height: 4), 53 | Text( 54 | speaker.tagLine, 55 | style: Theme.of(context).textTheme.bodySmall, 56 | ), 57 | }, 58 | ], 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | 68 | class _SpeakerImage extends StatelessWidget { 69 | const _SpeakerImage({ 70 | required this.name, 71 | required this.imageUrl, 72 | }); 73 | 74 | final String name; 75 | final String imageUrl; 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return Container( 80 | decoration: BoxDecoration( 81 | shape: BoxShape.circle, 82 | border: Border.all(color: Theme.of(context).colorScheme.primary), 83 | ), 84 | child: CachedNetworkImage( 85 | imageUrl: imageUrl, 86 | imageBuilder: (_, imageProvider) { 87 | return CircleAvatar( 88 | radius: _speakerImageRadius, 89 | backgroundImage: imageProvider, 90 | ); 91 | }, 92 | placeholder: (_, __) => _SpeakerImageFallback(name: name), 93 | errorWidget: (_, __, ___) => _SpeakerImageFallback(name: name), 94 | ), 95 | ); 96 | } 97 | } 98 | 99 | class _SpeakerImageFallback extends StatelessWidget { 100 | const _SpeakerImageFallback({ 101 | required this.name, 102 | }); 103 | 104 | final String name; 105 | 106 | @override 107 | Widget build(BuildContext context) { 108 | return CircleAvatar( 109 | radius: _speakerImageRadius, 110 | child: Text( 111 | name[0], 112 | style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Theme.of(context).colorScheme.primary), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/common/widgets/tap_to_shrink_effect_gesture_detector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A different tap effect for tappable widgets. Feels like pushing a real-life button. 4 | /// 5 | /// Source: 6 | /// https://gist.github.com/rohan20/b1a15c09bbb8d18c4caa9e8c41a108c0?permalink_comment_id=3770115#gistcomment-3770115 7 | class TapToShrinkEffectGestureDetector extends StatefulWidget { 8 | const TapToShrinkEffectGestureDetector({ 9 | required this.onTap, 10 | required this.child, 11 | this.isEffectEnabled = true, 12 | super.key, 13 | }); 14 | 15 | final VoidCallback? onTap; 16 | final Widget child; 17 | final bool isEffectEnabled; 18 | 19 | @override 20 | State createState() => _TapToShrinkEffectGestureDetectorState(); 21 | } 22 | 23 | class _TapToShrinkEffectGestureDetectorState extends State 24 | with SingleTickerProviderStateMixin { 25 | static const clickAnimationDurationMillis = 50; 26 | 27 | double _scaleTransformValue = 1; 28 | 29 | // needed for the "click" tap effect 30 | late final AnimationController animationController; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | animationController = AnimationController( 36 | vsync: this, 37 | duration: const Duration(milliseconds: clickAnimationDurationMillis), 38 | upperBound: 0.05, 39 | )..addListener(() { 40 | setState(() => _scaleTransformValue = 1 - animationController.value); 41 | }); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | animationController.dispose(); 47 | super.dispose(); 48 | } 49 | 50 | void _shrinkButtonSize() { 51 | animationController.forward(); 52 | } 53 | 54 | void _restoreButtonSize() { 55 | Future.delayed( 56 | const Duration(milliseconds: clickAnimationDurationMillis), 57 | () => animationController.reverse(), 58 | ); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return GestureDetector( 64 | onTap: () { 65 | Future.delayed( 66 | Duration(milliseconds: widget.isEffectEnabled ? clickAnimationDurationMillis : 0), 67 | () => widget.onTap?.call(), 68 | ); 69 | 70 | if (widget.isEffectEnabled) { 71 | _shrinkButtonSize(); 72 | _restoreButtonSize(); 73 | } 74 | }, 75 | onTapDown: widget.isEffectEnabled ? (_) => _shrinkButtonSize() : null, 76 | onTapCancel: widget.isEffectEnabled ? _restoreButtonSize : null, 77 | child: Transform.scale( 78 | scale: _scaleTransformValue, 79 | child: widget.child, 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/di/injector.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:fluttercon/features/favourites/data/data_source/favourites_local_data_source.dart'; 3 | import 'package:fluttercon/features/favourites/data/data_source/favourites_local_data_source_impl.dart'; 4 | import 'package:fluttercon/features/favourites/domain/repository/favourites_repository.dart'; 5 | import 'package:fluttercon/features/favourites/domain/repository/favourites_repository_impl.dart'; 6 | import 'package:fluttercon/features/favourites/domain/use_case/get_favourite_session_ids_use_case.dart'; 7 | import 'package:fluttercon/features/favourites/domain/use_case/save_favourite_session_ids_use_case.dart'; 8 | import 'package:get_it/get_it.dart'; 9 | import 'package:shared_preferences/shared_preferences.dart'; 10 | 11 | final injector = Injector(); 12 | 13 | class Injector { 14 | Injector() : _injector = GetIt.instance; 15 | 16 | final GetIt _injector; 17 | 18 | T get() => _injector.get(); 19 | 20 | void registerLazySingleton(T Function() factoryFunc) { 21 | _injector.registerLazySingleton(factoryFunc); 22 | } 23 | 24 | void registerFactory(T Function() factoryFunc) => _injector.registerFactory(factoryFunc); 25 | } 26 | 27 | Future initDependencies() async { 28 | await ConferenceData.init(); 29 | 30 | injector.registerFactory(GetConferenceDataUseCase.new); 31 | 32 | // Favourites 33 | final sharedPreferences = await SharedPreferences.getInstance(); 34 | 35 | injector 36 | ..registerLazySingleton(() => FavouritesLocalDataSourceImpl(sharedPreferences)) 37 | ..registerLazySingleton( 38 | () => FavouritesRepositoryImpl(injector.get()), 39 | ) 40 | ..registerFactory( 41 | () => SaveFavouriteSessionIdsUseCase(injector.get()), 42 | ) 43 | ..registerFactory( 44 | () => GetFavouriteSessionIdsUseCase(injector.get()), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/features/app/presentation/bloc/app_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:conference_data/conference_data.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:fluttercon/di/injector.dart'; 6 | import 'package:fluttercon/features/app/presentation/bloc/app_event.dart'; 7 | import 'package:fluttercon/features/app/presentation/bloc/app_state.dart'; 8 | import 'package:fluttercon/features/favourites/domain/use_case/get_favourite_session_ids_use_case.dart'; 9 | import 'package:fluttercon/features/favourites/domain/use_case/save_favourite_session_ids_use_case.dart'; 10 | 11 | class AppBloc extends Bloc { 12 | AppBloc() : super(const AppState()) { 13 | on(_onAppLaunchedEvent); 14 | on(_onSessionFavouriteIconTappedEvent); 15 | on(_onSearchButtonPressedEvent); 16 | on(_onSearchCancelledEvent); 17 | on(_onSearchTextChangedEvent); 18 | on(_onPullToRefreshSessionsListEvent); 19 | on(_onPullToRefreshSpeakersListEvent); 20 | } 21 | 22 | FutureOr _onAppLaunchedEvent(event, Emitter emit) async { 23 | await _getConferenceDataAndEmitState(emit: emit); 24 | } 25 | 26 | Future _getConferenceDataAndEmitState({ 27 | required Emitter emit, 28 | bool? forceLatestData, 29 | }) async { 30 | emit(state.copyWith(isLoading: true)); 31 | 32 | final conferenceDataResult = await injector.get().call(forceLatestData); 33 | 34 | if (conferenceDataResult.isError()) { 35 | emit(state.copyWith(isLoading: false, isError: true)); 36 | } else { 37 | final conferenceData = conferenceDataResult.getSuccess()!; 38 | 39 | final favouriteSessionIds = await _getFavouriteSessionIds(); 40 | 41 | emit( 42 | state.copyWith( 43 | isLoading: false, 44 | isError: false, 45 | sessions: conferenceData.sessions, 46 | speakers: conferenceData.speakers, 47 | categories: conferenceData.categories, 48 | rooms: conferenceData.rooms, 49 | favouriteSessionIds: favouriteSessionIds, 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | FutureOr _onSessionFavouriteIconTappedEvent( 56 | SessionFavouriteIconTappedEvent event, 57 | Emitter emit, 58 | ) async { 59 | // Emit new state using in-memory logic so that the UI-state of the 'favourite' icon updates instantly 60 | if (state.isFavouriteSession(id: event.sessionId)) { 61 | emit(state.copyWith(favouriteSessionIds: Set.of(state.favouriteSessionIds)..remove(event.sessionId))); 62 | } else { 63 | emit(state.copyWith(favouriteSessionIds: Set.of(state.favouriteSessionIds)..add(event.sessionId))); 64 | } 65 | 66 | // Then persist the new state and re-emit from the persisted state so that it acts as the ultimate source of truth 67 | await injector.get().call(List.from(state.favouriteSessionIds)); 68 | 69 | final favouriteSessionIds = await _getFavouriteSessionIds(); 70 | emit(state.copyWith(favouriteSessionIds: favouriteSessionIds)); 71 | } 72 | 73 | Future> _getFavouriteSessionIds() async { 74 | final favouriteSessionIdsResult = await injector.get().call(); 75 | final favouriteSessionIds = favouriteSessionIdsResult.when( 76 | (_) => Set.from(state.favouriteSessionIds), // keep the old favourite session ids 77 | (favouriteSessionIds) => favouriteSessionIds.toSet(), 78 | ); 79 | 80 | return favouriteSessionIds; 81 | } 82 | 83 | FutureOr _onSearchButtonPressedEvent(_, Emitter emit) { 84 | if (state.isInSearchMode) { 85 | _exitSearchMode(emit: emit); 86 | } else { 87 | emit(state.copyWith(isInSearchMode: true)); 88 | } 89 | } 90 | 91 | FutureOr _onSearchCancelledEvent(_, Emitter emit) { 92 | _exitSearchMode(emit: emit); 93 | } 94 | 95 | void _exitSearchMode({required Emitter emit}) { 96 | emit(state.copyWith(isInSearchMode: false, searchTerm: '')); 97 | } 98 | 99 | FutureOr _onSearchTextChangedEvent(SearchTextChangedEvent event, Emitter emit) { 100 | emit(state.copyWith(searchTerm: event.searchTerm)); 101 | } 102 | 103 | FutureOr _onPullToRefreshSessionsListEvent(_, Emitter emit) async { 104 | await _getConferenceDataAndEmitState(emit: emit, forceLatestData: true); 105 | } 106 | 107 | FutureOr _onPullToRefreshSpeakersListEvent(_, Emitter emit) async { 108 | await _getConferenceDataAndEmitState(emit: emit, forceLatestData: true); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/features/app/presentation/bloc/app_event.dart: -------------------------------------------------------------------------------- 1 | abstract class AppEvent {} 2 | 3 | class AppLaunchedEvent extends AppEvent {} 4 | 5 | class SessionFavouriteIconTappedEvent extends AppEvent { 6 | SessionFavouriteIconTappedEvent({required this.sessionId}); 7 | 8 | final String sessionId; 9 | } 10 | 11 | class SearchButtonPressedEvent extends AppEvent {} 12 | 13 | class SearchCancelledEvent extends AppEvent {} 14 | 15 | class SearchTextChangedEvent extends AppEvent { 16 | SearchTextChangedEvent(this.searchTerm); 17 | 18 | final String searchTerm; 19 | } 20 | 21 | class PullToRefreshSessionsListEvent extends AppEvent {} 22 | 23 | class PullToRefreshSpeakersListEvent extends AppEvent {} 24 | -------------------------------------------------------------------------------- /lib/features/app/presentation/bloc/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:fluttercon/common/extensions/session_extensions.dart'; 3 | import 'package:fluttercon/features/home/presentation/conference_metadata.dart'; 4 | 5 | class AppState { 6 | const AppState({ 7 | this.isLoading = false, 8 | this.isError = false, 9 | this.sessions = const [], 10 | this.speakers = const [], 11 | this.categories = const [], 12 | this.rooms = const [], 13 | this.favouriteSessionIds = const {}, 14 | this.isInSearchMode = false, 15 | this.searchTerm = '', 16 | }); 17 | 18 | final bool isLoading; 19 | final bool isError; 20 | final List sessions; 21 | final List speakers; 22 | final List categories; 23 | final List rooms; 24 | final Set favouriteSessionIds; 25 | final bool isInSearchMode; 26 | final String searchTerm; 27 | 28 | AppState copyWith({ 29 | bool? isLoading, 30 | bool? isError, 31 | List? sessions, 32 | List? speakers, 33 | List? categories, 34 | List? rooms, 35 | Set? favouriteSessionIds, 36 | bool? isInSearchMode, 37 | String? searchTerm, 38 | }) { 39 | return AppState( 40 | isLoading: isLoading ?? this.isLoading, 41 | isError: isError ?? this.isError, 42 | sessions: sessions ?? this.sessions, 43 | speakers: speakers ?? this.speakers, 44 | categories: categories ?? this.categories, 45 | rooms: rooms ?? this.rooms, 46 | favouriteSessionIds: favouriteSessionIds ?? this.favouriteSessionIds, 47 | isInSearchMode: isInSearchMode ?? this.isInSearchMode, 48 | searchTerm: searchTerm ?? this.searchTerm, 49 | ); 50 | } 51 | 52 | List get _sessionsFilteredBySearchTerm { 53 | if (!isInSearchMode || searchTerm.isEmpty) { 54 | return List.of(sessions); 55 | } 56 | 57 | return List.of(sessions).where((session) { 58 | final sessionTitleMatchesSearchTerm = session.title.containsIgnoreCase(searchTerm); 59 | 60 | final sessionSpeakers = session.getSessionSpeakers(speakers: speakers); 61 | 62 | final speakerNameOrTagLineMatchSearchTerm = sessionSpeakers.any((speaker) { 63 | return speaker.fullName.containsIgnoreCase(searchTerm) || speaker.tagLine.containsIgnoreCase(searchTerm); 64 | }); 65 | 66 | return sessionTitleMatchesSearchTerm || speakerNameOrTagLineMatchSearchTerm; 67 | }).toList(); 68 | } 69 | 70 | List get _sessionsSortedByStartTime { 71 | return _sessionsFilteredBySearchTerm.toList()..sort((a, b) => a.startsAt.compareTo(b.startsAt)); 72 | } 73 | 74 | List _getSessionsForDay(DateTime day) { 75 | return _sessionsSortedByStartTime.where((session) => session.startsAt.isSameDayAs(day)).toList(); 76 | } 77 | 78 | List get day1SessionsSortedByStartTime => _getSessionsForDay(ConferenceMetadata.day1); 79 | 80 | List get day1FavouriteSessionsSortedByStartTime { 81 | return day1SessionsSortedByStartTime.where(_isFavouriteSession).toList(); 82 | } 83 | 84 | List get day2SessionsSortedByStartTime => _getSessionsForDay(ConferenceMetadata.day2); 85 | 86 | List get day2FavouriteSessionsSortedByStartTime { 87 | return day2SessionsSortedByStartTime.where(_isFavouriteSession).toList(); 88 | } 89 | 90 | bool _isFavouriteSession(Session session) => favouriteSessionIds.contains(session.id); 91 | 92 | bool isFavouriteSession({required String id}) => favouriteSessionIds.contains(id); 93 | 94 | int get filteredSessionsCount => _sessionsFilteredBySearchTerm.length; 95 | 96 | int get filteredFavouriteSessionsCount { 97 | return _sessionsFilteredBySearchTerm.where(_isFavouriteSession).length; 98 | } 99 | 100 | List get _speakersFilteredBySearchTerm { 101 | if (!isInSearchMode || searchTerm.isEmpty) { 102 | return List.of(speakers); 103 | } 104 | 105 | return List.of(speakers).where((speaker) { 106 | return speaker.fullName.containsIgnoreCase(searchTerm) || speaker.tagLine.containsIgnoreCase(searchTerm); 107 | }).toList(); 108 | } 109 | 110 | List get filteredSpeakers => _speakersFilteredBySearchTerm; 111 | 112 | int get filteredSpeakersCount => filteredSpeakers.length; 113 | } 114 | 115 | extension _DateTimeExt on DateTime { 116 | bool isSameDayAs(DateTime other) { 117 | return year == other.year && month == other.month && day == other.day; 118 | } 119 | } 120 | 121 | extension _StringExt on String { 122 | bool containsIgnoreCase(String other) => toLowerCase().contains(other.toLowerCase()); 123 | } 124 | -------------------------------------------------------------------------------- /lib/features/app/presentation/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'package:fluttercon/features/app/presentation/bloc/app_bloc.dart'; 2 | export 'package:fluttercon/features/app/presentation/bloc/app_event.dart'; 3 | export 'package:fluttercon/features/app/presentation/bloc/app_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/favourites/data/data_source/favourites_local_data_source.dart: -------------------------------------------------------------------------------- 1 | abstract class FavouritesLocalDataSource { 2 | Future saveFavouriteSessionIds(List sessionIds); 3 | 4 | Future> getFavouriteSessionIds(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/features/favourites/data/data_source/favourites_local_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluttercon/features/favourites/data/data_source/favourites_local_data_source.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class FavouritesLocalDataSourceImpl implements FavouritesLocalDataSource { 5 | FavouritesLocalDataSourceImpl(this.sharedPreferences); 6 | 7 | static const _favouriteSessionIdsKey = 'favouriteSessionIds-2024-usa'; 8 | 9 | final SharedPreferences sharedPreferences; 10 | 11 | @override 12 | Future saveFavouriteSessionIds(List sessionIds) async { 13 | await sharedPreferences.setStringList(_favouriteSessionIdsKey, sessionIds); 14 | // Delete legacy data from older conferences (applicable in case the user didn't uninstall the app between conferences) 15 | await sharedPreferences.remove('favouriteSessionIds'); 16 | } 17 | 18 | @override 19 | Future> getFavouriteSessionIds() async { 20 | return sharedPreferences.getStringList(_favouriteSessionIdsKey) ?? []; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/features/favourites/domain/repository/favourites_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:util/util.dart'; 2 | 3 | abstract class FavouritesRepository { 4 | Future> saveFavouriteSessionIds(List sessionIds); 5 | 6 | Future>> getFavouriteSessionIds(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/features/favourites/domain/repository/favourites_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluttercon/features/favourites/data/data_source/favourites_local_data_source.dart'; 2 | import 'package:fluttercon/features/favourites/domain/repository/favourites_repository.dart'; 3 | import 'package:util/util.dart'; 4 | 5 | class FavouritesRepositoryImpl implements FavouritesRepository { 6 | FavouritesRepositoryImpl(this.favouritesLocalDataSource); 7 | 8 | final FavouritesLocalDataSource favouritesLocalDataSource; 9 | 10 | @override 11 | Future> saveFavouriteSessionIds(List sessionIds) async { 12 | try { 13 | return Success(await favouritesLocalDataSource.saveFavouriteSessionIds(sessionIds)); 14 | } catch (_) { 15 | return const Error(LocalFailure()); 16 | } 17 | } 18 | 19 | @override 20 | Future>> getFavouriteSessionIds() async { 21 | try { 22 | return Success(await favouritesLocalDataSource.getFavouriteSessionIds()); 23 | } catch (_) { 24 | return const Error(LocalFailure()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/features/favourites/domain/use_case/get_favourite_session_ids_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluttercon/features/favourites/domain/repository/favourites_repository.dart'; 2 | import 'package:util/util.dart'; 3 | 4 | class GetFavouriteSessionIdsUseCase extends UseCase, NoParams> { 5 | GetFavouriteSessionIdsUseCase(this.favouritesRepository); 6 | 7 | final FavouritesRepository favouritesRepository; 8 | 9 | @override 10 | Future>> call([NoParams? params]) async { 11 | return favouritesRepository.getFavouriteSessionIds(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/favourites/domain/use_case/save_favourite_session_ids_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluttercon/features/favourites/domain/repository/favourites_repository.dart'; 2 | import 'package:util/util.dart'; 3 | 4 | class SaveFavouriteSessionIdsUseCase extends UseCase> { 5 | SaveFavouriteSessionIdsUseCase(this.favouritesRepository); 6 | 7 | final FavouritesRepository favouritesRepository; 8 | 9 | @override 10 | Future> call([List? params]) async { 11 | return favouritesRepository.saveFavouriteSessionIds(params ?? []); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/favourites/presentation/pages/favourite_sessions_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fluttercon/common/widgets/error_message_widget.dart'; 4 | import 'package:fluttercon/common/widgets/session/sessions_tab_bar_view.dart'; 5 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 6 | 7 | class FavouriteSessionsPage extends StatelessWidget { 8 | const FavouriteSessionsPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return BlocBuilder( 13 | builder: (context, state) { 14 | if (state.isLoading) { 15 | return const Center( 16 | child: CircularProgressIndicator(), 17 | ); 18 | } else if (state.isError) { 19 | return ErrorMessageWidget( 20 | onRefresh: () async => context.read().add(PullToRefreshSessionsListEvent()), 21 | ); 22 | } else { 23 | return SessionsTabBarView( 24 | day1SessionsSortedByStartTime: state.day1FavouriteSessionsSortedByStartTime, 25 | day2SessionsSortedByStartTime: state.day2FavouriteSessionsSortedByStartTime, 26 | speakers: state.speakers, 27 | categories: state.categories, 28 | rooms: state.rooms, 29 | emptySessionsMessage: state.isInSearchMode && state.searchTerm.isNotEmpty 30 | ? "No favourite sessions found for this day for the search term '${state.searchTerm}'" 31 | : 'No favourite sessions found for this day', 32 | ); 33 | } 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/features/home/presentation/conference_metadata.dart: -------------------------------------------------------------------------------- 1 | class ConferenceMetadata { 2 | const ConferenceMetadata._(); 3 | 4 | static final DateTime day1 = DateTime(2024, 09, 19, 20); 5 | static final DateTime day2 = DateTime(2024, 09, 20, 20); 6 | 7 | // Get these from 'https://sessionize.com/api/v2//view/all' 8 | static const String lightningTalkId = '254923'; 9 | static const String sessionId = '254924'; 10 | static const String workshopId = '254925'; 11 | static const String keynoteId = '254927'; 12 | static const String panelDiscussionId = '254926'; 13 | 14 | static const String categoryTypeSessionFormatId = '72066'; 15 | } 16 | -------------------------------------------------------------------------------- /lib/features/home/presentation/pages/sessions_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fluttercon/common/widgets/error_message_widget.dart'; 4 | import 'package:fluttercon/common/widgets/session/sessions_tab_bar_view.dart'; 5 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 6 | 7 | class SessionsPage extends StatelessWidget { 8 | const SessionsPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return BlocBuilder( 13 | builder: (context, state) { 14 | if (state.isLoading) { 15 | return const Center( 16 | child: CircularProgressIndicator(), 17 | ); 18 | } else if (state.isError) { 19 | return ErrorMessageWidget( 20 | onRefresh: () async => context.read().add(PullToRefreshSessionsListEvent()), 21 | ); 22 | } else { 23 | return SessionsTabBarView( 24 | day1SessionsSortedByStartTime: state.day1SessionsSortedByStartTime, 25 | day2SessionsSortedByStartTime: state.day2SessionsSortedByStartTime, 26 | speakers: state.speakers, 27 | categories: state.categories, 28 | rooms: state.rooms, 29 | emptySessionsMessage: state.isInSearchMode && state.searchTerm.isNotEmpty 30 | ? "No sessions found for this day for the search term '${state.searchTerm}'" 31 | : 'No sessions found for this day', 32 | ); 33 | } 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/features/home/presentation/pages/speakers_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:fluttercon/common/widgets/error_message_widget.dart'; 5 | import 'package:fluttercon/common/widgets/speaker/speakers_list_item.dart'; 6 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 7 | 8 | class SpeakersPage extends StatelessWidget { 9 | const SpeakersPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocBuilder( 14 | builder: (context, state) { 15 | if (state.isLoading) { 16 | return const Center( 17 | child: CircularProgressIndicator(), 18 | ); 19 | } else if (state.isError) { 20 | return ErrorMessageWidget( 21 | onRefresh: () async => context.read().add(PullToRefreshSpeakersListEvent()), 22 | ); 23 | } else { 24 | return _SpeakersList( 25 | speakers: state.filteredSpeakers, 26 | emptySpeakersMessage: state.isInSearchMode && state.searchTerm.isNotEmpty 27 | ? "No speakers found for the search term '${state.searchTerm}'" 28 | : 'No speakers found', 29 | ); 30 | } 31 | }, 32 | ); 33 | } 34 | } 35 | 36 | class _SpeakersList extends StatelessWidget { 37 | const _SpeakersList({ 38 | required this.speakers, 39 | required this.emptySpeakersMessage, 40 | }); 41 | 42 | final List speakers; 43 | final String emptySpeakersMessage; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return RefreshIndicator( 48 | onRefresh: () async => context.read().add(PullToRefreshSpeakersListEvent()), 49 | child: speakers.isEmpty 50 | ? CustomScrollView( 51 | slivers: [ 52 | SliverFillRemaining( 53 | child: Center(child: Text(emptySpeakersMessage, textAlign: TextAlign.center)), 54 | ), 55 | ], 56 | ) 57 | : ListView.builder( 58 | padding: const EdgeInsets.only(top: 12, bottom: 90), 59 | itemCount: speakers.length, 60 | itemBuilder: (BuildContext context, int index) { 61 | final speaker = speakers[index]; 62 | 63 | return SpeakersListItem( 64 | speaker: speaker, 65 | backgroundColor: index.isEven ? Colors.transparent : Colors.grey.shade50, 66 | ); 67 | }, 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/features/home/presentation/widgets/conference_search_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 4 | 5 | class ConferenceSearchAppBar extends StatelessWidget implements PreferredSizeWidget { 6 | const ConferenceSearchAppBar({this.focusNode, super.key}); 7 | 8 | final FocusNode? focusNode; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return AppBar( 13 | title: TextField( 14 | autofocus: true, 15 | focusNode: focusNode, 16 | decoration: InputDecoration( 17 | hintText: 'Talk title / Speaker name or tagline', 18 | hintStyle: Theme.of(context).textTheme.labelSmall!.copyWith( 19 | fontSize: 14, 20 | color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), 21 | ), 22 | border: InputBorder.none, 23 | prefixIcon: const Icon(Icons.search), 24 | suffixIcon: IconButton( 25 | icon: const Icon(Icons.close), 26 | onPressed: () => context.read().add(SearchCancelledEvent()), 27 | ), 28 | ), 29 | onChanged: (query) => context.read().add(SearchTextChangedEvent(query)), 30 | ), 31 | ); 32 | } 33 | 34 | @override 35 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/session_details/presentation/pages/session_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:fluttercon/common/extensions/session_extensions.dart'; 5 | import 'package:fluttercon/common/widgets/conference_app_bar.dart'; 6 | import 'package:fluttercon/common/widgets/session/favourite_session_icon.dart'; 7 | import 'package:fluttercon/common/widgets/session/session_duration.dart'; 8 | import 'package:fluttercon/common/widgets/session/session_format.dart'; 9 | import 'package:fluttercon/common/widgets/session/session_room.dart'; 10 | import 'package:fluttercon/common/widgets/speaker/speakers_list_item.dart'; 11 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 12 | import 'package:intl/intl.dart'; 13 | 14 | class SessionDetailsPage extends StatelessWidget { 15 | const SessionDetailsPage(this.session, {super.key}); 16 | 17 | final Session session; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: const ConferenceAppBar(), 23 | body: Padding( 24 | padding: const EdgeInsets.symmetric(horizontal: 16), 25 | child: SingleChildScrollView( 26 | child: BlocBuilder( 27 | builder: (context, state) { 28 | final sessionSpeakers = session.getSessionSpeakers(speakers: state.speakers); 29 | final sessionCategories = session.getSessionCategories(categories: state.categories); 30 | final sessionFormatCategory = session.getSessionFormatCategory(categories: sessionCategories); 31 | final roomName = session.getSessionRoom(rooms: state.rooms).name; 32 | 33 | return Column( 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | const SizedBox(height: 16), 37 | Align(child: _SessionTitle(session.title)), 38 | const SizedBox(height: 24), 39 | Align(child: _SessionDateTime(session.startsAt)), 40 | const SizedBox(height: 12), 41 | Row( 42 | mainAxisAlignment: MainAxisAlignment.center, 43 | children: [ 44 | SessionFormat(sessionFormat: sessionFormatCategory), 45 | const SizedBox(width: 8), 46 | SessionRoom(roomName: roomName), 47 | const SizedBox(width: 8), 48 | SessionDuration(durationInMinutes: session.duration), 49 | ], 50 | ), 51 | const SizedBox(height: 24), 52 | for (final speaker in sessionSpeakers) ...{ 53 | SpeakersListItem(speaker: speaker), 54 | }, 55 | const SizedBox(height: 16), 56 | Text('Description:', style: Theme.of(context).textTheme.bodyMedium), 57 | const SizedBox(height: 12), 58 | Text( 59 | session.description, 60 | style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14), 61 | ), 62 | const SizedBox(height: 16), 63 | Text('Categories:', style: Theme.of(context).textTheme.bodyMedium), 64 | const SizedBox(height: 12), 65 | _SessionCategories( 66 | categories: List.from(sessionCategories) 67 | ..removeWhere( 68 | (category) => category.id == sessionFormatCategory?.id, 69 | ), 70 | ), 71 | const SizedBox(height: 120), // big enough to ensure that the FAB doesn't hide any content below it 72 | ], 73 | ); 74 | }, 75 | ), 76 | ), 77 | ), 78 | floatingActionButton: FloatingActionButton( 79 | onPressed: () => context.read().add(SessionFavouriteIconTappedEvent(sessionId: session.id)), 80 | child: FavouriteSessionIcon( 81 | sessionId: session.id, 82 | allowTap: false, // to prevent the icon from being clicked directly which would mean no FAB splash effect 83 | inactiveColor: Colors.black45, 84 | ), 85 | ), 86 | ); 87 | } 88 | } 89 | 90 | class _SessionCategories extends StatelessWidget { 91 | const _SessionCategories({ 92 | required this.categories, 93 | }); 94 | 95 | final List categories; 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Wrap( 100 | spacing: 4, 101 | runSpacing: 8, 102 | children: categories.map( 103 | (category) { 104 | return Container( 105 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 106 | decoration: BoxDecoration( 107 | color: Colors.deepPurple.shade50, 108 | borderRadius: BorderRadius.circular(8), 109 | ), 110 | child: Text( 111 | category.name, 112 | style: Theme.of(context).textTheme.labelSmall, 113 | ), 114 | ); 115 | }, 116 | ).toList(), 117 | ); 118 | } 119 | } 120 | 121 | class _SessionTitle extends StatelessWidget { 122 | const _SessionTitle(this.title); 123 | 124 | final String title; 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | return Text( 129 | title, 130 | style: Theme.of(context).textTheme.titleLarge, 131 | textAlign: TextAlign.center, 132 | ); 133 | } 134 | } 135 | 136 | class _SessionDateTime extends StatelessWidget { 137 | const _SessionDateTime(this.startsAt); 138 | 139 | final DateTime startsAt; 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return Text.rich( 144 | TextSpan( 145 | children: [ 146 | WidgetSpan( 147 | child: Icon(Icons.access_time, size: 16, color: Colors.black87.withOpacity(0.8)), 148 | ), 149 | const WidgetSpan(child: SizedBox(width: 4)), 150 | TextSpan( 151 | text: DateFormat('EEE, dd MMM').format(startsAt), 152 | style: Theme.of(context).textTheme.labelLarge, 153 | ), 154 | TextSpan( 155 | text: ' at ', 156 | style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14), 157 | ), 158 | TextSpan( 159 | text: DateFormat('HH:mm').format(startsAt), 160 | style: Theme.of(context).textTheme.labelLarge, 161 | ), 162 | ], 163 | ), 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/features/speaker_details/presentation/pages/speaker_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:fluttercon/common/widgets/conference_app_bar.dart'; 6 | import 'package:fluttercon/common/widgets/session/sessions_list_item.dart'; 7 | import 'package:fluttercon/common/widgets/speaker/speakers_list_item.dart'; 8 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 9 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 10 | import 'package:url_launcher/url_launcher.dart'; 11 | 12 | class SpeakerDetailsPage extends StatelessWidget { 13 | const SpeakerDetailsPage(this.speaker, {super.key}); 14 | 15 | final Speaker speaker; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | appBar: const ConferenceAppBar(), 21 | body: Padding( 22 | padding: const EdgeInsets.symmetric(horizontal: 16), 23 | child: SingleChildScrollView( 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | const SizedBox(height: 16), 28 | SpeakersListItem( 29 | speaker: speaker, 30 | allowTap: false, 31 | padding: EdgeInsets.zero, 32 | ), 33 | if (speaker.bio.trim().isNotEmpty) ...{ 34 | const SizedBox(height: 16), 35 | Text('Bio:', style: Theme.of(context).textTheme.bodyMedium), 36 | const SizedBox(height: 12), 37 | Text( 38 | speaker.bio.trim(), 39 | style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14), 40 | ), 41 | }, 42 | const SizedBox(height: 16), 43 | if (speaker.links.isNotEmpty) ...{ 44 | Text('Links:', style: Theme.of(context).textTheme.bodyMedium), 45 | const SizedBox(height: 12), 46 | _SpeakerLinks(links: speaker.links), 47 | }, 48 | Builder( 49 | builder: (context) { 50 | final speakerSessions = context.watch().state.sessions.where( 51 | (session) => session.speakerIds.any((speakerId) => speakerId == speaker.id), 52 | ); 53 | 54 | if (speakerSessions.isEmpty) { 55 | return const SizedBox.shrink(); 56 | } 57 | 58 | return Column( 59 | crossAxisAlignment: CrossAxisAlignment.start, 60 | children: [ 61 | const SizedBox(height: 16), 62 | Text('Sessions:', style: Theme.of(context).textTheme.bodyMedium), 63 | const SizedBox(height: 12), 64 | for (final session in speakerSessions) ...{ 65 | SessionsListItem( 66 | session: session, 67 | sessionTimeVisibility: SessionTimeVisibility.gone, 68 | padding: const EdgeInsets.only(bottom: 12), 69 | ), 70 | }, 71 | ], 72 | ); 73 | }, 74 | ), 75 | const SizedBox(height: 32), 76 | ], 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | 84 | class _SpeakerLinks extends StatelessWidget { 85 | const _SpeakerLinks({ 86 | required this.links, 87 | }); 88 | 89 | final List links; 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | return Wrap( 94 | spacing: 4, 95 | runSpacing: 8, 96 | children: links.map( 97 | (link) { 98 | return GestureDetector( 99 | onTap: () async { 100 | final linkUri = Uri.parse(link.url); 101 | 102 | if (await canLaunchUrl(linkUri)) { 103 | try { 104 | await launchUrl(linkUri); 105 | } catch (e) { 106 | // ignore: use_build_context_synchronously 107 | _showFailedToLaunchUrlSnackbar(context: context, url: link.url); 108 | } 109 | } else { 110 | // ignore: use_build_context_synchronously 111 | _showFailedToLaunchUrlSnackbar(context: context, url: link.url); 112 | } 113 | }, 114 | child: Container( 115 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 116 | decoration: BoxDecoration( 117 | color: Theme.of(context).primaryColor.withOpacity(0.1), 118 | borderRadius: BorderRadius.circular(8), 119 | ), 120 | child: Row( 121 | mainAxisSize: MainAxisSize.min, 122 | children: [ 123 | Icon(link.iconData, size: 14, color: Colors.grey.shade700), 124 | const SizedBox(width: 6), 125 | Text( 126 | link.title, 127 | style: Theme.of(context).textTheme.labelSmall, 128 | ), 129 | ], 130 | ), 131 | ), 132 | ); 133 | }, 134 | ).toList(), 135 | ); 136 | } 137 | 138 | void _showFailedToLaunchUrlSnackbar({required BuildContext context, required String url}) { 139 | if (!context.mounted) { 140 | return; 141 | } 142 | 143 | ScaffoldMessenger.of(context).showSnackBar( 144 | SnackBar( 145 | content: Text('Could not open url: $url'), 146 | action: SnackBarAction( 147 | label: 'Copy', 148 | onPressed: () => Clipboard.setData(ClipboardData(text: url)), 149 | ), 150 | ), 151 | ); 152 | } 153 | } 154 | 155 | extension _LinkExt on Link { 156 | IconData get iconData { 157 | switch (type.toLowerCase()) { 158 | case 'twitter': 159 | return FontAwesomeIcons.twitter; 160 | case 'instagram': 161 | return FontAwesomeIcons.instagram; 162 | case 'linkedin': 163 | return FontAwesomeIcons.linkedin; 164 | default: 165 | return FontAwesomeIcons.globe; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/features/splash/flutter_native_splash-production.yaml: -------------------------------------------------------------------------------- 1 | flutter_native_splash: 2 | # background_image generated via: https://angrytools.com/gradient/image/ 3 | background_image: assets/images/splash_background.png 4 | image: assets/images/logo.png 5 | 6 | android_12: 7 | # background_image doesn't work with Android 12, hence a background color is being used: 8 | # https://pub.dev/packages/flutter_native_splash#android-12-support 9 | color: "#8ad7fc" 10 | 11 | web: false 12 | 13 | android_gravity: center 14 | ios_content_mode: center 15 | -------------------------------------------------------------------------------- /lib/fluttercon_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fluttercon/features/app/presentation/bloc/bloc.dart'; 4 | import 'package:fluttercon/features/home/presentation/pages/home_page.dart'; 5 | import 'package:google_fonts/google_fonts.dart'; 6 | 7 | class FlutterconApp extends StatelessWidget { 8 | const FlutterconApp({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return MultiBlocProvider( 13 | providers: [ 14 | BlocProvider( 15 | create: (_) => AppBloc()..add(AppLaunchedEvent()), 16 | ), 17 | ], 18 | child: MaterialApp( 19 | title: 'Fluttercon', 20 | theme: ThemeData( 21 | colorScheme: ColorScheme.fromSeed( 22 | // Color picked from the 'con' part of 'Fluttercon' logo on https://fluttercon.dev 23 | seedColor: const Color.fromRGBO(1, 53, 255, 1), 24 | ), 25 | useMaterial3: true, 26 | textTheme: GoogleFonts.capriolaTextTheme(), 27 | ), 28 | home: const HomePage(), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttercon/di/injector.dart'; 3 | import 'package:fluttercon/fluttercon_app.dart'; 4 | 5 | Future main() async { 6 | WidgetsFlutterBinding.ensureInitialized(); 7 | 8 | await initDependencies(); 9 | 10 | runApp(const FlutterconApp()); 11 | } 12 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import path_provider_foundation 9 | import shared_preferences_foundation 10 | import sqflite 11 | import url_launcher_macos 12 | 13 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 14 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 15 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 16 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 17 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 18 | } 19 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | # Issue: https://github.com/flutter/flutter/issues/122684 7 | # Temporary fix (used currently below): https://github.com/flutter/flutter/issues/122684#issuecomment-1568139743 8 | # Permanent fix (not yet released): https://github.com/flutter/flutter/pull/127997 9 | project 'Runner', { 10 | 'Debug Production' => :debug, 11 | 'Profile Production' => :release, 12 | 'Release Production' => :release, 13 | } 14 | 15 | def flutter_root 16 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 17 | unless File.exist?(generated_xcode_build_settings_path) 18 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 19 | end 20 | 21 | File.foreach(generated_xcode_build_settings_path) do |line| 22 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 23 | return matches[1].strip if matches 24 | end 25 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 26 | end 27 | 28 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 29 | 30 | flutter_macos_podfile_setup 31 | 32 | target 'Runner' do 33 | use_frameworks! 34 | use_modular_headers! 35 | 36 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 37 | target 'RunnerTests' do 38 | inherit! :search_paths 39 | end 40 | end 41 | 42 | post_install do |installer| 43 | installer.pods_project.targets.each do |target| 44 | flutter_additional_macos_build_settings(target) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - FMDB (2.7.5): 4 | - FMDB/standard (= 2.7.5) 5 | - FMDB/standard (2.7.5) 6 | - path_provider_foundation (0.0.1): 7 | - Flutter 8 | - FlutterMacOS 9 | - shared_preferences_foundation (0.0.1): 10 | - Flutter 11 | - FlutterMacOS 12 | - sqflite (0.0.2): 13 | - FlutterMacOS 14 | - FMDB (>= 2.7.5) 15 | - url_launcher_macos (0.0.1): 16 | - FlutterMacOS 17 | 18 | DEPENDENCIES: 19 | - FlutterMacOS (from `Flutter/ephemeral`) 20 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) 21 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) 22 | - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) 23 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 24 | 25 | SPEC REPOS: 26 | trunk: 27 | - FMDB 28 | 29 | EXTERNAL SOURCES: 30 | FlutterMacOS: 31 | :path: Flutter/ephemeral 32 | path_provider_foundation: 33 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin 34 | shared_preferences_foundation: 35 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin 36 | sqflite: 37 | :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos 38 | url_launcher_macos: 39 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 40 | 41 | SPEC CHECKSUMS: 42 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 43 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 44 | path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 45 | shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c 46 | sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea 47 | url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 48 | 49 | PODFILE CHECKSUM: e335a526d353f8faa28f74c9b08e24040ae645bf 50 | 51 | COCOAPODS: 1.11.3 52 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Production.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = fluttercon 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.rohantaneja.fluttercon 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.rohantaneja. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /packages/api_client/.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 | build/ 7 | pubspec.lock -------------------------------------------------------------------------------- /packages/api_client/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.5.0.0.yaml 2 | 3 | linter: 4 | rules: 5 | lines_longer_than_80_chars: false 6 | public_member_api_docs: false 7 | one_member_abstracts: false 8 | -------------------------------------------------------------------------------- /packages/api_client/lib/api_client.dart: -------------------------------------------------------------------------------- 1 | /// Client to make network calls 2 | library api_client; 3 | 4 | export 'src/api_client.dart'; 5 | -------------------------------------------------------------------------------- /packages/api_client/lib/src/api_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | /// {@template api_client_error} 6 | /// Error throw when accessing api failed. 7 | /// 8 | /// Check [cause] and [stackTrace] for specific details. 9 | /// {@endtemplate} 10 | class ApiClientError implements Exception { 11 | /// {@macro api_client_error} 12 | ApiClientError(this.cause, this.stackTrace); 13 | 14 | /// Error cause. 15 | final dynamic cause; 16 | 17 | /// The stack trace of the error. 18 | final StackTrace stackTrace; 19 | 20 | @override 21 | String toString() { 22 | return cause.toString(); 23 | } 24 | } 25 | 26 | /// {@template api_client} 27 | /// Client to access the api 28 | /// {@endtemplate} 29 | class ApiClient { 30 | /// {@macro api_client} 31 | ApiClient({ 32 | required String baseUrl, 33 | Duration connectTimeout = const Duration(seconds: 5), 34 | Duration receiveTimeout = const Duration(seconds: 10), 35 | Duration sendTimeout = const Duration(seconds: 10), 36 | }) : _dio = Dio( 37 | BaseOptions( 38 | baseUrl: baseUrl, 39 | connectTimeout: connectTimeout, 40 | receiveTimeout: receiveTimeout, 41 | sendTimeout: sendTimeout, 42 | ), 43 | ); 44 | 45 | final Dio _dio; 46 | 47 | /// Sends a GET request to the specified [path]. 48 | Future> get( 49 | String path, 50 | ) async { 51 | final response = await _dio.get( 52 | path, 53 | options: Options( 54 | headers: { 55 | HttpHeaders.contentTypeHeader: ContentType.json.value, 56 | }, 57 | ), 58 | ); 59 | 60 | return response; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/api_client/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: api_client 2 | description: Client to make network calls 3 | version: 0.1.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=3.0.5 <4.0.0' 8 | 9 | dependencies: 10 | dio: ^5.2.1+1 11 | 12 | dev_dependencies: 13 | mocktail: ^0.3.0 14 | test: ^1.19.2 15 | very_good_analysis: ^5.0.0+1 16 | -------------------------------------------------------------------------------- /packages/conference_data/.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 | build/ 7 | pubspec.lock 8 | 9 | lib/src/conference_secrets.dart 10 | -------------------------------------------------------------------------------- /packages/conference_data/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.5.0.0.yaml 2 | 3 | linter: 4 | rules: 5 | lines_longer_than_80_chars: false 6 | public_member_api_docs: false 7 | one_member_abstracts: false 8 | -------------------------------------------------------------------------------- /packages/conference_data/lib/conference_data.dart: -------------------------------------------------------------------------------- 1 | /// All things related to the conference data 2 | library conference_data; 3 | 4 | export 'src/conference_data.dart'; 5 | export 'src/domain/conference_data_source.dart'; 6 | export 'src/domain/entity/category.dart'; 7 | export 'src/domain/entity/link.dart'; 8 | export 'src/domain/entity/room.dart'; 9 | export 'src/domain/entity/session.dart'; 10 | export 'src/domain/entity/speaker.dart'; 11 | export 'src/domain/use_case/get_conference_data_use_case.dart'; 12 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/conference_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:api_client/api_client.dart'; 2 | import 'package:conference_data/src/conference_secrets.dart'; 3 | import 'package:conference_data/src/data/data_source/conference_data_local_data_source.dart'; 4 | import 'package:conference_data/src/data/data_source/conference_data_local_data_source_impl.dart'; 5 | import 'package:conference_data/src/data/data_source/conference_data_remote_data_source.dart'; 6 | import 'package:conference_data/src/data/data_source/conference_data_remote_data_source_impl.dart'; 7 | import 'package:conference_data/src/di/injector.dart'; 8 | import 'package:conference_data/src/domain/repository/conference_data_repository.dart'; 9 | import 'package:conference_data/src/domain/repository/conference_data_repository_impl.dart'; 10 | import 'package:shared_preferences/shared_preferences.dart'; 11 | 12 | class ConferenceData { 13 | static Future init() async { 14 | await _initDependencies(); 15 | } 16 | 17 | static Future _initDependencies() async { 18 | final apiClient = ApiClient(baseUrl: conferenceBaseUrl); 19 | 20 | final sharedPreferences = await SharedPreferences.getInstance(); 21 | 22 | injector 23 | ..registerLazySingleton( 24 | () => ConferenceDataRemoteDataSourceImpl(apiClient: apiClient), 25 | ) 26 | ..registerLazySingleton( 27 | () => ConferenceDataLocalDataSourceImpl(sharedPreferences), 28 | ) 29 | ..registerLazySingleton( 30 | () => ConferenceDataRepositoryImpl( 31 | conferenceDataRemoteDataSource: injector(), 32 | conferenceDataLocalDataSource: injector(), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/data_source/conference_data_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/agenda_model.dart'; 2 | import 'package:conference_data/src/data/model/conference_data_model.dart'; 3 | 4 | abstract class ConferenceDataLocalDataSource { 5 | Future saveConferenceData(ConferenceDataModel conferenceDataModel); 6 | 7 | Future getConferenceData(); 8 | 9 | Future saveAgenda(AgendaModel agendaModel); 10 | 11 | Future getAgenda(); 12 | } 13 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/data_source/conference_data_local_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:conference_data/src/data/data_source/conference_data_local_data_source.dart'; 4 | import 'package:conference_data/src/data/model/agenda_model.dart'; 5 | import 'package:conference_data/src/data/model/conference_data_model.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | const _conferenceDataKey = 'conferenceData-2024-usa'; 9 | const _agendaKey = 'agenda-2024-usa'; 10 | 11 | class ConferenceDataLocalDataSourceImpl implements ConferenceDataLocalDataSource { 12 | ConferenceDataLocalDataSourceImpl(this.sharedPreferences); 13 | 14 | final SharedPreferences sharedPreferences; 15 | 16 | @override 17 | Future saveConferenceData(ConferenceDataModel conferenceDataModel) async { 18 | try { 19 | await sharedPreferences.setString(_conferenceDataKey, jsonEncode(conferenceDataModel.toJson())); 20 | // Delete legacy data from older conferences (applicable in case the user didn't uninstall the app between conferences) 21 | await sharedPreferences.remove('conferenceData'); 22 | } catch (e) { 23 | throw Exception(e); 24 | } 25 | } 26 | 27 | @override 28 | Future getConferenceData() async { 29 | try { 30 | final conferenceDataJson = sharedPreferences.getString(_conferenceDataKey); 31 | 32 | if (conferenceDataJson == null) { 33 | return ConferenceDataModel(); 34 | } 35 | 36 | return ConferenceDataModel.fromJson(jsonDecode(conferenceDataJson) as Map); 37 | } catch (e) { 38 | throw Exception(e); 39 | } 40 | } 41 | 42 | @override 43 | Future saveAgenda(AgendaModel agendaModel) async { 44 | try { 45 | await sharedPreferences.setString(_agendaKey, jsonEncode(agendaModel.toJson())); 46 | // Delete legacy data from older conferences (applicable in case the user didn't uninstall the app between conferences) 47 | await sharedPreferences.remove('agenda'); 48 | } catch (e) { 49 | throw Exception(e); 50 | } 51 | } 52 | 53 | @override 54 | Future getAgenda() async { 55 | try { 56 | final agendaJson = sharedPreferences.getString(_agendaKey); 57 | 58 | if (agendaJson == null) { 59 | return AgendaModel(); 60 | } 61 | 62 | return AgendaModel.fromLocalJson(jsonDecode(agendaJson) as Map); 63 | } catch (e) { 64 | throw Exception(e); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/data_source/conference_data_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/agenda_model.dart'; 2 | import 'package:conference_data/src/data/model/conference_data_model.dart'; 3 | 4 | abstract class ConferenceDataRemoteDataSource { 5 | Future getConferenceData(); 6 | 7 | Future getAgenda(); 8 | } 9 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/data_source/conference_data_remote_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:api_client/api_client.dart'; 4 | import 'package:conference_data/src/data/data_source/conference_data_remote_data_source.dart'; 5 | import 'package:conference_data/src/data/model/agenda_model.dart'; 6 | import 'package:conference_data/src/data/model/conference_data_model.dart'; 7 | 8 | class ConferenceDataRemoteDataSourceImpl implements ConferenceDataRemoteDataSource { 9 | const ConferenceDataRemoteDataSourceImpl({required this.apiClient}); 10 | 11 | final ApiClient apiClient; 12 | 13 | @override 14 | Future getConferenceData() async { 15 | try { 16 | final response = await apiClient.get('view/All'); 17 | 18 | if (response.statusCode != HttpStatus.ok) { 19 | throw ApiClientError( 20 | 'GET view/All returned status ${response.statusCode} with the following response: "${response.data}', 21 | StackTrace.current, 22 | ); 23 | } 24 | 25 | return ConferenceDataModel.fromJson(response.data as Map); 26 | } on ApiClientError { 27 | rethrow; 28 | } catch (e) { 29 | throw Exception(e); 30 | } 31 | } 32 | 33 | @override 34 | Future getAgenda() async { 35 | try { 36 | final response = await apiClient.get('view/GridSmart'); 37 | 38 | if (response.statusCode != HttpStatus.ok) { 39 | throw ApiClientError( 40 | 'GET view/GridSmart returned status ${response.statusCode} with the following response: "${response.data}', 41 | StackTrace.current, 42 | ); 43 | } 44 | 45 | return AgendaModel.fromJson(response.data as List); 46 | } on ApiClientError { 47 | rethrow; 48 | } catch (e) { 49 | throw Exception(e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/agenda_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/session_model.dart'; 2 | 3 | class AgendaModel { 4 | AgendaModel({ 5 | this.sessions = const [], 6 | }); 7 | 8 | factory AgendaModel.fromJson(List jsonList) { 9 | final sessionModelsList = jsonList 10 | .map>((agendaDay) => (agendaDay as Map)['rooms'] as List) 11 | .expand((List roomJson) => roomJson) 12 | .expand((roomJson) => (roomJson as Map)['sessions'] as List) 13 | .map( 14 | (sessionJson) { 15 | final speakerIdsJsonList = ((sessionJson as Map)['speakers'] as List) 16 | .map((speakerJson) => (speakerJson as Map)['id']) 17 | .toList(); 18 | 19 | final categoryItemsJsonList = (sessionJson['categories'] as List) 20 | .map((categoryJson) => categoryJson as Map) 21 | .expand((categoryJson) => categoryJson['categoryItems'] as List) 22 | .map((categoryItemJson) => (categoryItemJson as Map)['id']) 23 | .toList(); 24 | 25 | sessionJson 26 | ..remove('speakers') 27 | ..putIfAbsent('speakers', () => speakerIdsJsonList) 28 | ..putIfAbsent('categoryItems', () => categoryItemsJsonList) 29 | ..remove('categories'); 30 | 31 | return SessionModel.fromJson(sessionJson); 32 | }, 33 | ).toList(); 34 | 35 | return AgendaModel( 36 | sessions: sessionModelsList, 37 | ); 38 | } 39 | 40 | factory AgendaModel.fromLocalJson(Map json) { 41 | return AgendaModel( 42 | sessions: json['sessions'] != null 43 | ? (json['sessions'] as List) 44 | .map((sessionJson) => SessionModel.fromJson(sessionJson as Map)) 45 | .toList() 46 | : [], 47 | ); 48 | } 49 | 50 | Map toJson() { 51 | return { 52 | 'sessions': sessions.map((session) => session.toJson()).toList(), 53 | }; 54 | } 55 | 56 | final List sessions; 57 | 58 | @override 59 | String toString() { 60 | return 'AgendaModel(sessions: $sessions)'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/category_model.dart: -------------------------------------------------------------------------------- 1 | class CategoryModel { 2 | CategoryModel.fromJson(Map json) 3 | : _id = json['id'] as int, 4 | _name = json['name'] as String; 5 | 6 | final int _id; 7 | final String _name; 8 | 9 | String get id => _id.toString(); 10 | 11 | String get name => _name; 12 | 13 | @override 14 | String toString() => 'CategoryModel(id: $id, name: $name)'; 15 | } 16 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/category_parent_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/category_model.dart'; 2 | 3 | class CategoryParentModel { 4 | CategoryParentModel.fromJson(Map json) 5 | : _id = json['id'] as int, 6 | _title = json['title'] as String, 7 | _categories = json['items'] as List; 8 | 9 | Map toJson() { 10 | return { 11 | 'id': _id, 12 | 'title': _title, 13 | 'items': _categories, 14 | }; 15 | } 16 | 17 | final int _id; 18 | final String _title; 19 | final List _categories; 20 | 21 | String get id => _id.toString(); 22 | 23 | String get title => _title; 24 | 25 | List get categories => _categories 26 | .map((dynamic category) => CategoryModel.fromJson(category as Map)) 27 | .toList(); 28 | 29 | @override 30 | String toString() => 'CategoryParentModel(id: $id, title: $title, categories: $categories)'; 31 | } 32 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/conference_data_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/category_parent_model.dart'; 2 | import 'package:conference_data/src/data/model/room_model.dart'; 3 | import 'package:conference_data/src/data/model/session_model.dart'; 4 | import 'package:conference_data/src/data/model/speaker_model.dart'; 5 | 6 | class ConferenceDataModel { 7 | ConferenceDataModel({ 8 | this.sessions = const [], 9 | this.speakers = const [], 10 | this.categoryParents = const [], 11 | this.rooms = const [], 12 | }); 13 | 14 | factory ConferenceDataModel.fromJson(Map json) { 15 | return ConferenceDataModel( 16 | sessions: json['sessions'] != null 17 | ? (json['sessions'] as List) 18 | .map((sessionJson) => SessionModel.fromJson(sessionJson as Map)) 19 | .toList() 20 | : [], 21 | speakers: json['speakers'] != null 22 | ? (json['speakers'] as List) 23 | .map((speakerJson) => SpeakerModel.fromJson(speakerJson as Map)) 24 | .toList() 25 | : [], 26 | categoryParents: json['categories'] != null 27 | ? (json['categories'] as List) 28 | .map( 29 | (categoryParentJson) => CategoryParentModel.fromJson(categoryParentJson as Map), 30 | ) 31 | .toList() 32 | : [], 33 | rooms: json['rooms'] != null 34 | ? (json['rooms'] as List) 35 | .map((roomJson) => RoomModel.fromJson(roomJson as Map)) 36 | .toList() 37 | : [], 38 | ); 39 | } 40 | 41 | Map toJson() { 42 | return { 43 | 'sessions': sessions.map((session) => session.toJson()).toList(), 44 | 'speakers': speakers.map((speaker) => speaker.toJson()).toList(), 45 | 'categories': categoryParents.map((categoryParent) => categoryParent.toJson()).toList(), 46 | 'rooms': rooms.map((room) => room.toJson()).toList(), 47 | }; 48 | } 49 | 50 | final List sessions; 51 | final List speakers; 52 | final List categoryParents; 53 | final List rooms; 54 | 55 | @override 56 | String toString() { 57 | return 'ConferenceDataModel(sessions: $sessions, speakers: $speakers, categoryParents: $categoryParents)'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/link_model.dart: -------------------------------------------------------------------------------- 1 | class LinkModel { 2 | LinkModel.fromJson(Map json) 3 | : _title = json['title'] as String, 4 | _url = json['url'] as String, 5 | _type = json['linkType'] as String; 6 | 7 | final String _title; 8 | final String _url; 9 | final String _type; 10 | 11 | String get title => _title; 12 | 13 | String get url => _url; 14 | 15 | String get type => _type; 16 | 17 | @override 18 | String toString() => 'LinkModel{_title: $_title, _url: $_url, _type: $_type}'; 19 | } 20 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/room_model.dart: -------------------------------------------------------------------------------- 1 | class RoomModel { 2 | RoomModel.fromJson(Map json) 3 | : _id = json['id'] as int, 4 | _name = json['name'] as String; 5 | 6 | Map toJson() { 7 | return { 8 | 'id': _id, 9 | 'name': _name, 10 | }; 11 | } 12 | 13 | final int _id; 14 | final String _name; 15 | 16 | String get id => _id.toString(); 17 | 18 | String get name => _name; 19 | 20 | @override 21 | String toString() => 'RoomModel(id: $id, name: $name)'; 22 | } 23 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/session_model.dart: -------------------------------------------------------------------------------- 1 | class SessionModel { 2 | SessionModel({ 3 | required String id, 4 | required String title, 5 | required String startsAt, 6 | required String endsAt, 7 | String? description, 8 | bool? isServiceSession, 9 | // 'Plenum' definition: an assembly of all the members of a group or committee. 10 | // If true, this usually implies a session that requires all attendees to be present which automatically implies that 11 | // no other session is happening at the same time. For example, the 'Keynote' session or the 'Lunch' session or 12 | // the 'Registration & Check-in' session. 13 | bool? isPlenumSession, 14 | List? speakerIds, 15 | List? categoryIds, 16 | int? roomId, 17 | String? room, 18 | }) : _id = id, 19 | _title = title, 20 | _description = description, 21 | _startsAt = startsAt, 22 | _endsAt = endsAt, 23 | _isServiceSession = isServiceSession, 24 | _isPlenumSession = isPlenumSession, 25 | _speakerIds = speakerIds, 26 | _categoryIds = categoryIds, 27 | _roomId = roomId, 28 | _room = room; 29 | 30 | factory SessionModel.fromJson(Map json) { 31 | return SessionModel( 32 | id: json['id'] as String, 33 | title: json['title'] as String, 34 | startsAt: json['startsAt'] as String, 35 | endsAt: json['endsAt'] as String, 36 | description: json['description'] as String?, 37 | isServiceSession: json['isServiceSession'] as bool?, 38 | isPlenumSession: json['isPlenumSession'] as bool?, 39 | speakerIds: json['speakers'] as List?, 40 | categoryIds: json['categoryItems'] as List?, 41 | roomId: json['roomId'] as int?, 42 | room: json['room'] as String?, 43 | ); 44 | } 45 | 46 | Map toJson() { 47 | return { 48 | 'id': _id, 49 | 'title': _title, 50 | 'startsAt': _startsAt, 51 | 'endsAt': _endsAt, 52 | 'description': _description, 53 | 'isServiceSession': _isServiceSession, 54 | 'isPlenumSession': _isPlenumSession, 55 | 'speakers': _speakerIds, 56 | 'categoryItems': _categoryIds, 57 | 'roomId': _roomId, 58 | 'room': _room, 59 | }; 60 | } 61 | 62 | String _id; 63 | String _title; 64 | String _startsAt; 65 | String _endsAt; 66 | String? _description; 67 | bool? _isServiceSession; 68 | bool? _isPlenumSession; 69 | List? _speakerIds; 70 | List? _categoryIds; 71 | int? _roomId; 72 | String? _room; 73 | 74 | @override 75 | String toString() { 76 | return 'SessionModel(id: $_id, title: $_title, startsAt: $_startsAt, endsAt: $_endsAt, description: $_description, isServiceSession: $_isServiceSession, isPlenumSession: $_isPlenumSession, speakerIds: $_speakerIds, categoryIds: $_categoryIds, roomId: $_roomId, room: $_room)'; 77 | } 78 | 79 | String get id => _id; 80 | 81 | String get title => _title; 82 | 83 | DateTime get startsAt => DateTime.parse(_startsAt); 84 | 85 | DateTime get endsAt => DateTime.parse(_endsAt); 86 | 87 | String get description => _description ?? ''; 88 | 89 | bool get isServiceSession => _isServiceSession ?? false; 90 | 91 | bool get isPlenumSession => _isPlenumSession ?? false; 92 | 93 | List get speakerIds => (_speakerIds ?? const []).map((speakerId) => speakerId.toString()).toList(); 94 | 95 | List get categoryIds => (_categoryIds ?? const []).map((categoryId) => categoryId?.toString() ?? '').toList(); 96 | 97 | String get roomId => (_roomId ?? -1).toString(); 98 | 99 | String get room => _room ?? ''; 100 | } 101 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/data/model/speaker_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/link_model.dart'; 2 | 3 | class SpeakerModel { 4 | SpeakerModel({ 5 | required String id, 6 | required String fullName, 7 | String? bio, 8 | String? tagLine, 9 | String? profilePictureUrl, 10 | List? sessionIds, 11 | List? links, 12 | }) : _id = id, 13 | _fullName = fullName, 14 | _bio = bio, 15 | _tagLine = tagLine, 16 | _profilePictureUrl = profilePictureUrl, 17 | _sessionIds = sessionIds, 18 | _links = links; 19 | 20 | factory SpeakerModel.fromJson(Map json) { 21 | return SpeakerModel( 22 | id: json['id'] as String, 23 | fullName: json['fullName'] as String, 24 | bio: json['bio'] as String?, 25 | tagLine: json['tagLine'] as String?, 26 | profilePictureUrl: json['profilePicture'] as String?, 27 | sessionIds: json['sessions'] as List?, 28 | links: json['links'] as List?, 29 | ); 30 | } 31 | 32 | Map toJson() { 33 | return { 34 | 'id': _id, 35 | 'fullName': _fullName, 36 | 'bio': _bio, 37 | 'tagLine': _tagLine, 38 | 'profilePicture': _profilePictureUrl, 39 | 'sessions': _sessionIds, 40 | 'links': _links, 41 | }; 42 | } 43 | 44 | String _id; 45 | String _fullName; 46 | String? _bio; 47 | String? _tagLine; 48 | String? _profilePictureUrl; 49 | List? _sessionIds; 50 | List? _links; 51 | 52 | @override 53 | String toString() { 54 | return 'SpeakerModel{_id: $_id, _fullName: $_fullName, _bio: $_bio, _tagLine: $_tagLine, _profilePictureUrl: $_profilePictureUrl, _sessionIds: $_sessionIds, _links: $_links}'; 55 | } 56 | 57 | String get id => _id; 58 | 59 | String get fullName => _fullName; 60 | 61 | String get bio => _bio ?? ''; 62 | 63 | String get tagLine => _tagLine ?? ''; 64 | 65 | String get profilePictureUrl => _profilePictureUrl ?? ''; 66 | 67 | List get sessionIds => _sessionIds?.map((dynamic id) => id.toString()).toList() ?? []; 68 | 69 | List get links { 70 | return _links?.map((dynamic link) => LinkModel.fromJson(link as Map)).toList() ?? []; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/di/injector.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | final injector = GetIt.asNewInstance(); 4 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/conference_data_source.dart: -------------------------------------------------------------------------------- 1 | enum ConferenceDataSource { 2 | cached, 3 | latest, 4 | } 5 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/agenda.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/agenda_model.dart'; 2 | import 'package:conference_data/src/domain/entity/session.dart'; 3 | 4 | class Agenda { 5 | Agenda({ 6 | this.sessions = const [], 7 | }); 8 | 9 | factory Agenda.fromAgendaModel(AgendaModel agendaModel) { 10 | return Agenda( 11 | sessions: agendaModel.sessions.map(Session.fromSessionModel).toList(), 12 | ); 13 | } 14 | 15 | final List sessions; 16 | 17 | @override 18 | String toString() { 19 | return 'Agenda(sessions: $sessions)'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/category_model.dart'; 2 | 3 | class Category { 4 | const Category({ 5 | required this.id, 6 | required this.name, 7 | required this.typeId, 8 | required this.type, 9 | }); 10 | 11 | Category.fromCategoryModel(CategoryModel categoryModel) 12 | : id = categoryModel.id, 13 | name = categoryModel.name, 14 | typeId = '', 15 | type = ''; 16 | 17 | final String id; 18 | final String name; 19 | final String typeId; 20 | final String type; 21 | 22 | Category copyWith({ 23 | String? typeId, 24 | String? type, 25 | }) { 26 | return Category( 27 | id: id, 28 | name: name, 29 | typeId: typeId ?? this.typeId, 30 | type: type ?? this.type, 31 | ); 32 | } 33 | 34 | @override 35 | String toString() => 'Category(id: $id, name: $name, typeId: $typeId, type: $type)'; 36 | } 37 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/conference_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/conference_data_model.dart'; 2 | import 'package:conference_data/src/domain/entity/category.dart'; 3 | import 'package:conference_data/src/domain/entity/room.dart'; 4 | import 'package:conference_data/src/domain/entity/session.dart'; 5 | import 'package:conference_data/src/domain/entity/speaker.dart'; 6 | 7 | class ConferenceData { 8 | ConferenceData({ 9 | this.sessions = const [], 10 | this.speakers = const [], 11 | this.categories = const [], 12 | this.rooms = const [], 13 | }); 14 | 15 | factory ConferenceData.fromConferenceDataModel(ConferenceDataModel conferenceDataModel) { 16 | final categories = []; 17 | 18 | for (final categoryParent in conferenceDataModel.categoryParents) { 19 | for (final category in categoryParent.categories) { 20 | categories.add( 21 | Category.fromCategoryModel(category).copyWith( 22 | typeId: categoryParent.id, 23 | type: categoryParent.title, 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | return ConferenceData( 30 | sessions: conferenceDataModel.sessions.map(Session.fromSessionModel).toList(), 31 | speakers: conferenceDataModel.speakers.map(Speaker.fromSpeakerModel).toList(), 32 | categories: categories, 33 | rooms: conferenceDataModel.rooms.map(Room.fromRoomModel).toList(), 34 | ); 35 | } 36 | 37 | final List sessions; 38 | final List speakers; 39 | final List categories; 40 | final List rooms; 41 | 42 | ConferenceData copyWith({List? sessions}) { 43 | return ConferenceData( 44 | sessions: sessions ?? this.sessions, 45 | speakers: speakers, 46 | categories: categories, 47 | rooms: rooms, 48 | ); 49 | } 50 | 51 | @override 52 | String toString() { 53 | return 'ConferenceData(sessions: $sessions, speakers: $speakers, categories: $categories, rooms: $rooms)'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/link.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/link_model.dart'; 2 | 3 | class Link { 4 | Link({ 5 | required this.url, 6 | required this.title, 7 | required this.type, 8 | }); 9 | 10 | factory Link.fromLinkModel(LinkModel linkModel) { 11 | return Link( 12 | title: linkModel.title, 13 | url: linkModel.url, 14 | type: linkModel.type, 15 | ); 16 | } 17 | 18 | final String url; 19 | final String title; 20 | final String type; 21 | 22 | @override 23 | String toString() { 24 | return 'Link{url: $url, title: $title, type: $type}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/room.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/room_model.dart'; 2 | 3 | class Room { 4 | const Room({ 5 | required this.id, 6 | required this.name, 7 | }); 8 | 9 | Room.fromRoomModel(RoomModel roomModel) 10 | : id = roomModel.id, 11 | name = roomModel.name; 12 | 13 | final String id; 14 | final String name; 15 | 16 | @override 17 | String toString() => 'Room(id: $id, name: $name)'; 18 | } 19 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/session.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/data/model/session_model.dart'; 2 | 3 | class Session { 4 | Session({ 5 | required this.id, 6 | required this.title, 7 | required this.startsAt, 8 | required this.endsAt, 9 | required this.description, 10 | required this.isServiceSession, 11 | required this.isPlenumSession, 12 | required this.roomId, 13 | required this.room, 14 | required this.speakerIds, 15 | required this.categoryIds, 16 | }); 17 | 18 | factory Session.fromSessionModel(SessionModel sessionModel) { 19 | return Session( 20 | id: sessionModel.id, 21 | title: sessionModel.title, 22 | startsAt: sessionModel.startsAt, 23 | endsAt: sessionModel.endsAt, 24 | description: sessionModel.description, 25 | isServiceSession: sessionModel.isServiceSession, 26 | isPlenumSession: sessionModel.isPlenumSession, 27 | roomId: sessionModel.roomId, 28 | room: sessionModel.room, 29 | speakerIds: sessionModel.speakerIds, 30 | categoryIds: sessionModel.categoryIds, 31 | ); 32 | } 33 | 34 | final String id; 35 | final String title; 36 | final DateTime startsAt; 37 | final DateTime endsAt; 38 | final String description; 39 | final bool isServiceSession; 40 | final bool isPlenumSession; 41 | final String roomId; 42 | final String room; 43 | final List speakerIds; 44 | final List categoryIds; 45 | 46 | int get duration => endsAt.difference(startsAt).inMinutes; 47 | 48 | bool get isNotATalk => isServiceSession; 49 | 50 | @override 51 | String toString() { 52 | return 'Session(id: $id, title: $title, startsAt: $startsAt, endsAt: $endsAt, description: $description, isServiceSession: $isServiceSession, isPlenumSession: $isPlenumSession, roomId: $roomId, room: $room, speakerIds: $speakerIds, categoryIds: $categoryIds)'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/entity/speaker.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/conference_data.dart'; 2 | import 'package:conference_data/src/data/model/speaker_model.dart'; 3 | 4 | class Speaker { 5 | Speaker({ 6 | required this.id, 7 | required this.fullName, 8 | required this.bio, 9 | required this.tagLine, 10 | required this.profilePictureUrl, 11 | required this.sessionIds, 12 | this.links = const [], 13 | }); 14 | 15 | factory Speaker.fromSpeakerModel(SpeakerModel speakerModel) { 16 | return Speaker( 17 | id: speakerModel.id, 18 | fullName: speakerModel.fullName, 19 | bio: speakerModel.bio, 20 | tagLine: speakerModel.tagLine, 21 | profilePictureUrl: speakerModel.profilePictureUrl, 22 | links: speakerModel.links.map(Link.fromLinkModel).toList(), 23 | sessionIds: speakerModel.sessionIds, 24 | ); 25 | } 26 | 27 | final String id; 28 | final String fullName; 29 | final String bio; 30 | final String tagLine; 31 | final String profilePictureUrl; 32 | final List sessionIds; 33 | final List links; 34 | 35 | @override 36 | String toString() { 37 | return 'Speaker{id: $id, fullName: $fullName, bio: $bio, tagLine: $tagLine, profilePictureUrl: $profilePictureUrl, sessionIds: $sessionIds, links: $links}'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/repository/conference_data_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/domain/conference_data_source.dart'; 2 | import 'package:conference_data/src/domain/entity/agenda.dart'; 3 | import 'package:conference_data/src/domain/entity/conference_data.dart'; 4 | import 'package:util/util.dart'; 5 | 6 | abstract class ConferenceDataRepository { 7 | Future> getConferenceData({ConferenceDataSource? source}); 8 | 9 | Future> getAgenda({ConferenceDataSource? source}); 10 | } 11 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/repository/conference_data_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:api_client/api_client.dart'; 2 | import 'package:conference_data/src/data/data_source/conference_data_local_data_source.dart'; 3 | import 'package:conference_data/src/data/data_source/conference_data_remote_data_source.dart'; 4 | import 'package:conference_data/src/domain/conference_data_source.dart'; 5 | import 'package:conference_data/src/domain/entity/agenda.dart'; 6 | import 'package:conference_data/src/domain/entity/conference_data.dart'; 7 | import 'package:conference_data/src/domain/repository/conference_data_repository.dart'; 8 | import 'package:util/util.dart'; 9 | 10 | class ConferenceDataRepositoryImpl implements ConferenceDataRepository { 11 | ConferenceDataRepositoryImpl({ 12 | required this.conferenceDataRemoteDataSource, 13 | required this.conferenceDataLocalDataSource, 14 | }); 15 | 16 | final ConferenceDataRemoteDataSource conferenceDataRemoteDataSource; 17 | final ConferenceDataLocalDataSource conferenceDataLocalDataSource; 18 | 19 | @override 20 | Future> getConferenceData({ 21 | ConferenceDataSource? source = ConferenceDataSource.cached, 22 | }) async { 23 | try { 24 | final sourceOrFallbackSource = source ?? ConferenceDataSource.cached; 25 | 26 | try { 27 | if (sourceOrFallbackSource == ConferenceDataSource.cached) { 28 | final cachedConferenceDataModel = await conferenceDataLocalDataSource.getConferenceData(); 29 | 30 | if (cachedConferenceDataModel.sessions.isNotEmpty) { 31 | return Success(ConferenceData.fromConferenceDataModel(cachedConferenceDataModel)); 32 | } else { 33 | return Success(await _getLatestConferenceDataAndCacheIt()); 34 | } 35 | } else { 36 | return Success(await _getLatestConferenceDataAndCacheIt()); 37 | } 38 | } on ApiClientError { 39 | rethrow; 40 | } catch (e) { 41 | return Success(await _getLatestConferenceDataAndCacheIt()); 42 | } 43 | } on ApiClientError { 44 | return const Error(ServerFailure()); 45 | } catch (e) { 46 | return const Error(LocalFailure()); 47 | } 48 | } 49 | 50 | Future _getLatestConferenceDataAndCacheIt() async { 51 | final conferenceDataModel = await conferenceDataRemoteDataSource.getConferenceData(); 52 | 53 | try { 54 | await conferenceDataLocalDataSource.saveConferenceData(conferenceDataModel); 55 | } catch (_) { 56 | return ConferenceData.fromConferenceDataModel(conferenceDataModel); 57 | } 58 | 59 | return ConferenceData.fromConferenceDataModel(conferenceDataModel); 60 | } 61 | 62 | @override 63 | Future> getAgenda({ 64 | ConferenceDataSource? source = ConferenceDataSource.cached, 65 | }) async { 66 | try { 67 | final sourceOrFallbackSource = source ?? ConferenceDataSource.cached; 68 | 69 | try { 70 | if (sourceOrFallbackSource == ConferenceDataSource.cached) { 71 | final cachedAgendaModel = await conferenceDataLocalDataSource.getAgenda(); 72 | 73 | if (cachedAgendaModel.sessions.isNotEmpty) { 74 | return Success(Agenda.fromAgendaModel(cachedAgendaModel)); 75 | } else { 76 | return Success(await _getLatestAgendaAndCacheIt()); 77 | } 78 | } else { 79 | return Success(await _getLatestAgendaAndCacheIt()); 80 | } 81 | } on ApiClientError { 82 | rethrow; 83 | } catch (e) { 84 | return Success(await _getLatestAgendaAndCacheIt()); 85 | } 86 | } on ApiClientError { 87 | return const Error(ServerFailure()); 88 | } catch (e) { 89 | return const Error(LocalFailure()); 90 | } 91 | } 92 | 93 | Future _getLatestAgendaAndCacheIt() async { 94 | final agendaModel = await conferenceDataRemoteDataSource.getAgenda(); 95 | 96 | try { 97 | await conferenceDataLocalDataSource.saveAgenda(agendaModel); 98 | } catch (_) { 99 | return Agenda.fromAgendaModel(agendaModel); 100 | } 101 | 102 | return Agenda.fromAgendaModel(agendaModel); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/conference_data/lib/src/domain/use_case/get_conference_data_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:conference_data/src/di/injector.dart'; 2 | import 'package:conference_data/src/domain/conference_data_source.dart'; 3 | import 'package:conference_data/src/domain/entity/agenda.dart'; 4 | import 'package:conference_data/src/domain/entity/conference_data.dart'; 5 | import 'package:conference_data/src/domain/entity/session.dart'; 6 | import 'package:conference_data/src/domain/repository/conference_data_repository.dart'; 7 | import 'package:util/util.dart'; 8 | 9 | class GetConferenceDataUseCase extends UseCase { 10 | GetConferenceDataUseCase(); 11 | 12 | @override 13 | // ignore: avoid_renaming_method_parameters 14 | Future> call([bool? forceLatest]) async { 15 | final conferenceDataRepository = injector.get(); 16 | 17 | final dataSource = (forceLatest ?? false) ? ConferenceDataSource.latest : null; 18 | 19 | final conferenceDataAndAgendaResults = await Future.wait( 20 | [ 21 | conferenceDataRepository.getConferenceData(source: dataSource), 22 | conferenceDataRepository.getAgenda(source: dataSource), 23 | ], 24 | ); 25 | 26 | final conferenceDataResult = conferenceDataAndAgendaResults[0] as Result; 27 | final agendaResult = conferenceDataAndAgendaResults[1] as Result; 28 | 29 | if (conferenceDataResult.isError()) { 30 | return Error(conferenceDataResult.getError()!); 31 | } else { 32 | final conferenceData = conferenceDataResult.getSuccess()!; 33 | 34 | // List from [ConferenceData] is just a list of talks. 35 | // List from [Agenda] is the same list of talks plus breaks, registration, lunch, etc. events. 36 | final agendaSessions = agendaResult.when( 37 | (_) => [], 38 | (agenda) => agenda.sessions, 39 | ); 40 | 41 | if (agendaSessions.isEmpty) { 42 | return Success(conferenceData); 43 | } 44 | 45 | return Success(conferenceData.copyWith(sessions: agendaSessions)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/conference_data/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: conference_data 2 | description: All things related to the conference data 3 | version: 0.1.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=3.0.5 <4.0.0' 8 | 9 | dependencies: 10 | api_client: 11 | path: ../api_client 12 | get_it: ^7.6.0 13 | shared_preferences: ^2.1.2 14 | util: 15 | path: ../util 16 | 17 | dev_dependencies: 18 | mocktail: ^0.3.0 19 | test: ^1.19.2 20 | very_good_analysis: ^5.0.0+1 21 | -------------------------------------------------------------------------------- /packages/util/.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 | build/ 7 | pubspec.lock -------------------------------------------------------------------------------- /packages/util/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.5.0.0.yaml 2 | 3 | linter: 4 | rules: 5 | lines_longer_than_80_chars: false 6 | public_member_api_docs: false 7 | one_member_abstracts: false 8 | -------------------------------------------------------------------------------- /packages/util/lib/src/failure.dart: -------------------------------------------------------------------------------- 1 | abstract class Failure { 2 | const Failure(); 3 | } 4 | 5 | class ServerFailure extends Failure { 6 | const ServerFailure(); 7 | } 8 | 9 | class LocalFailure extends Failure { 10 | const LocalFailure(); 11 | } 12 | -------------------------------------------------------------------------------- /packages/util/lib/src/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Base Result class 4 | /// 5 | /// Receives two values [E] and [S] 6 | /// as [E] is an error and [S] is a success. 7 | /// 8 | /// Source: https://github.com/higorlapa/result/blob/main/lib/multiple_result.dart 9 | @sealed 10 | abstract class Result { 11 | /// Default constructor. 12 | const Result(); 13 | 14 | /// Returns the current result. 15 | /// 16 | /// It may be a [Success] or an [Error]. 17 | /// Check with 18 | /// ```dart 19 | /// result.isSuccess(); 20 | /// ``` 21 | /// or 22 | /// ```dart 23 | /// result.isError(); 24 | /// ``` 25 | /// 26 | /// before casting the value; 27 | dynamic get(); 28 | 29 | /// Returns the value of [S]. 30 | S? getSuccess(); 31 | 32 | /// Returns the value of [E]. 33 | E? getError(); 34 | 35 | /// Returns true if the current result is an [Error]. 36 | bool isError(); 37 | 38 | /// Returns true if the current result is a [success]. 39 | bool isSuccess(); 40 | 41 | /// Return the result in one of these functions. 42 | /// 43 | /// if the result is an error, it will be returned in 44 | /// [whenError], 45 | /// if it is a success it will be returned in [whenSuccess]. 46 | W when( 47 | W Function(E error) whenError, 48 | W Function(S success) whenSuccess, 49 | ); 50 | } 51 | 52 | /// Success Result. 53 | /// 54 | /// return it when the result of a [Result] is 55 | /// the expected value. 56 | @immutable 57 | class Success implements Result { 58 | /// Receives the [S] param as 59 | /// the successful result. 60 | const Success( 61 | this._success, 62 | ); 63 | 64 | final S _success; 65 | 66 | @override 67 | S get() { 68 | return _success; 69 | } 70 | 71 | @override 72 | bool isError() => false; 73 | 74 | @override 75 | bool isSuccess() => true; 76 | 77 | @override 78 | int get hashCode => _success.hashCode; 79 | 80 | @override 81 | bool operator ==(Object other) => 82 | other is Success && other._success == _success; 83 | 84 | @override 85 | W when( 86 | W Function(E error) whenError, 87 | W Function(S success) whenSuccess, 88 | ) { 89 | return whenSuccess(_success); 90 | } 91 | 92 | @override 93 | E? getError() => null; 94 | 95 | @override 96 | S? getSuccess() => _success; 97 | } 98 | 99 | /// Error Result. 100 | /// 101 | /// return it when the result of a [Result] is 102 | /// not the expected value. 103 | @immutable 104 | class Error implements Result { 105 | /// Receives the [E] param as 106 | /// the error result. 107 | const Error(this._error); 108 | 109 | final E _error; 110 | 111 | @override 112 | E get() { 113 | return _error; 114 | } 115 | 116 | @override 117 | bool isError() => true; 118 | 119 | @override 120 | bool isSuccess() => false; 121 | 122 | @override 123 | int get hashCode => _error.hashCode; 124 | 125 | @override 126 | bool operator ==(Object other) => other is Error && other._error == _error; 127 | 128 | @override 129 | W when( 130 | W Function(E error) whenError, 131 | W Function(S succcess) whenSuccess, 132 | ) { 133 | return whenError(_error); 134 | } 135 | 136 | @override 137 | E? getError() => _error; 138 | 139 | @override 140 | S? getSuccess() => null; 141 | } 142 | 143 | /// Default success class. 144 | /// 145 | /// Instead of returning void, as 146 | /// ```dart 147 | /// Result 148 | /// ``` 149 | /// return 150 | /// ```dart 151 | /// Result 152 | /// ``` 153 | class SuccessResult { 154 | const SuccessResult._internal(); 155 | } 156 | 157 | /// Default success case. 158 | const success = SuccessResult._internal(); 159 | -------------------------------------------------------------------------------- /packages/util/lib/src/use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:util/src/failure.dart'; 2 | import 'package:util/src/result.dart'; 3 | 4 | abstract class UseCase { 5 | Future> call([Params params]); 6 | } 7 | 8 | class NoParams { 9 | const NoParams(); 10 | } 11 | -------------------------------------------------------------------------------- /packages/util/lib/util.dart: -------------------------------------------------------------------------------- 1 | /// Contains some re-usable classes such as Result, UseCase, Failure, etc. 2 | library util; 3 | 4 | export 'src/failure.dart'; 5 | export 'src/result.dart'; 6 | export 'src/use_case.dart'; 7 | -------------------------------------------------------------------------------- /packages/util/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: util 2 | description: Contains some re-usable classes such as Result, UseCase, Failure, etc. 3 | version: 0.1.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=3.0.5 <4.0.0' 8 | 9 | dev_dependencies: 10 | mocktail: ^0.3.0 11 | test: ^1.19.2 12 | very_good_analysis: ^5.0.0+1 13 | dependencies: 14 | meta: ^1.9.1 15 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fluttercon 2 | description: Fluttercon Berlin 2023 3 | publish_to: 'none' 4 | 5 | version: 1.1.0+10 6 | 7 | environment: 8 | sdk: '>=3.0.5 <4.0.0' 9 | flutter: ^3.10.5 10 | 11 | dependencies: 12 | api_client: 13 | path: packages/api_client 14 | cached_network_image: ^3.2.3 15 | collection: ^1.17.1 16 | conference_data: 17 | path: packages/conference_data 18 | cupertino_icons: ^1.0.2 19 | dotted_border: ^2.0.0+3 20 | flutter: 21 | sdk: flutter 22 | flutter_bloc: ^8.1.3 23 | flutter_native_splash: ^2.3.1 24 | flutter_svg: ^2.0.7 25 | font_awesome_flutter: ^10.4.0 26 | get_it: ^7.6.0 27 | google_fonts: ^4.0.4 28 | intl: ^0.18.1 29 | shared_preferences: ^2.1.2 30 | url_launcher: ^6.1.11 31 | util: 32 | path: packages/util 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | very_good_analysis: ^5.0.0+1 38 | 39 | flutter: 40 | uses-material-design: true 41 | 42 | assets: 43 | - assets/images/ -------------------------------------------------------------------------------- /screenshots/sessions-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-android.png -------------------------------------------------------------------------------- /screenshots/sessions-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-ios.png -------------------------------------------------------------------------------- /screenshots/sessions-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-macos.png -------------------------------------------------------------------------------- /screenshots/sessions-search-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-search-android.png -------------------------------------------------------------------------------- /screenshots/sessions-search-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-search-ios.png -------------------------------------------------------------------------------- /screenshots/sessions-search-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/sessions-search-macos.png -------------------------------------------------------------------------------- /screenshots/speakers-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-android.png -------------------------------------------------------------------------------- /screenshots/speakers-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-ios.png -------------------------------------------------------------------------------- /screenshots/speakers-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-macos.png -------------------------------------------------------------------------------- /screenshots/speakers-search-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-search-android.png -------------------------------------------------------------------------------- /screenshots/speakers-search-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-search-ios.png -------------------------------------------------------------------------------- /screenshots/speakers-search-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/screenshots/speakers-search-macos.png -------------------------------------------------------------------------------- /store-badges/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/store-badges/appstore.png -------------------------------------------------------------------------------- /store-badges/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohan20/fluttercon/d7691898ddd62939293996a972b710c6acdf0111/store-badges/playstore.png --------------------------------------------------------------------------------