├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── screenshots │ ├── app_store.png │ ├── event_details.png │ ├── google_play.png │ ├── launch_pwa.png │ ├── now_in_theaters.png │ └── showtimes.png ├── ci-script.sh ├── core ├── analysis_options.yaml ├── lib │ ├── core.dart │ └── src │ │ ├── i18n │ │ ├── inkino_en.arb │ │ ├── inkino_fi.arb │ │ ├── inkino_messages_all.dart │ │ ├── inkino_messages_en.dart │ │ ├── inkino_messages_fi.dart │ │ └── messages.dart │ │ ├── models │ │ ├── actor.dart │ │ ├── content_descriptor.dart │ │ ├── event.dart │ │ ├── loading_status.dart │ │ ├── show.dart │ │ ├── show_cache.dart │ │ └── theater.dart │ │ ├── networking │ │ ├── finnkino_api.dart │ │ ├── image_url_rewriter.dart │ │ └── tmdb_api.dart │ │ ├── parsers │ │ ├── content_descriptor_parser.dart │ │ ├── event_parser.dart │ │ ├── gallery_parser.dart │ │ ├── show_parser.dart │ │ └── theater_parser.dart │ │ ├── preloaded_data.dart │ │ ├── redux │ │ ├── _common │ │ │ ├── common_actions.dart │ │ │ └── search.dart │ │ ├── actor │ │ │ ├── actor_actions.dart │ │ │ ├── actor_middleware.dart │ │ │ ├── actor_reducer.dart │ │ │ └── actor_selectors.dart │ │ ├── app │ │ │ ├── app_reducer.dart │ │ │ └── app_state.dart │ │ ├── event │ │ │ ├── event_actions.dart │ │ │ ├── event_middleware.dart │ │ │ ├── event_reducer.dart │ │ │ ├── event_selectors.dart │ │ │ └── event_state.dart │ │ ├── show │ │ │ ├── show_actions.dart │ │ │ ├── show_middleware.dart │ │ │ ├── show_reducer.dart │ │ │ ├── show_selectors.dart │ │ │ └── show_state.dart │ │ ├── store.dart │ │ └── theater │ │ │ ├── theater_middleware.dart │ │ │ ├── theater_reducer.dart │ │ │ ├── theater_selectors.dart │ │ │ └── theater_state.dart │ │ ├── tmdb_config.dart.sample │ │ ├── utils │ │ ├── clock.dart │ │ ├── event_name_cleaner.dart │ │ ├── http_utils.dart │ │ └── xml_utils.dart │ │ └── viewmodels │ │ ├── events_page_view_model.dart │ │ ├── showtime_page_view_model.dart │ │ └── theater_list_view_model.dart ├── pubspec.yaml └── test │ ├── mocks.dart │ ├── networking │ ├── finnkino_api_test.dart │ └── imgix_url_rewriter_test.dart │ ├── parsers │ ├── event_parser_test.dart │ ├── event_test_seeds.ignore.dart │ ├── show_parser_test.dart │ ├── show_test_seeds.ignore.dart │ ├── theater_parser_test.dart │ └── theater_test_seeds.ignore.dart │ ├── redux │ ├── actor_middleware_test.dart │ ├── actor_reducer_test.dart │ ├── event_middleware_test.dart │ ├── event_reducer_test.dart │ ├── event_selector_test.dart │ ├── search_test.dart │ ├── show_middleware_test.dart │ ├── show_reducer_test.dart │ ├── show_selector_test.dart │ └── theater_middleware_test.dart │ ├── utils │ └── event_name_cleaner_test.dart │ └── viewmodels │ ├── events_page_view_model_test.dart │ ├── showtimes_page_view_model_test.dart │ └── theater_list_view_model_test.dart ├── mobile ├── .metadata ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── Gemfile │ ├── app │ │ ├── build.gradle │ │ ├── release │ │ │ ├── app-release.apk │ │ │ └── output.json │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── roughike │ │ │ │ └── inkino │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── fastlane │ │ ├── Appfile │ │ ├── Fastfile │ │ └── metadata │ │ │ └── android │ │ │ └── en-US │ │ │ ├── changelogs │ │ │ ├── 1.txt │ │ │ ├── 2.txt │ │ │ ├── 3.txt │ │ │ ├── 4.txt │ │ │ ├── 5.txt │ │ │ └── 6.txt │ │ │ ├── full_description.txt │ │ │ ├── images │ │ │ └── icon.png │ │ │ ├── short_description.txt │ │ │ ├── title.txt │ │ │ └── video.txt │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── images │ │ ├── 1x1_transparent.png │ │ ├── background_image.jpg │ │ ├── logo.png │ │ └── powered_by_tmdb.png ├── ios │ ├── .gitignore │ ├── .symlinks │ │ ├── flutter │ │ └── plugins │ │ │ ├── key_value_store_flutter │ │ │ ├── path_provider │ │ │ ├── shared_preferences │ │ │ └── url_launcher │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Gemfile │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.app.dSYM.zip │ ├── Runner.ipa │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── ios_app_icon-1024.png │ │ │ │ ├── ios_app_icon-20.png │ │ │ │ ├── ios_app_icon-20@2x.png │ │ │ │ ├── ios_app_icon-20@3x.png │ │ │ │ ├── ios_app_icon-29.png │ │ │ │ ├── ios_app_icon-29@2x.png │ │ │ │ ├── ios_app_icon-29@3x.png │ │ │ │ ├── ios_app_icon-40.png │ │ │ │ ├── ios_app_icon-40@2x.png │ │ │ │ ├── ios_app_icon-40@3x.png │ │ │ │ ├── ios_app_icon-60@2x.png │ │ │ │ ├── ios_app_icon-60@3x.png │ │ │ │ ├── ios_app_icon-76.png │ │ │ │ ├── ios_app_icon-76@2x.png │ │ │ │ └── ios_app_icon-83.5@2x.png │ │ │ ├── Contents.json │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m │ └── fastlane │ │ ├── Appfile │ │ ├── Fastfile │ │ └── report.xml ├── lib │ ├── assets.dart │ ├── main.dart │ ├── message_provider.dart │ └── ui │ │ ├── common │ │ ├── info_message_view.dart │ │ ├── loading_view.dart │ │ ├── platform_adaptive_progress_indicator.dart │ │ └── widget_utils.dart │ │ ├── event_details │ │ ├── actor_scroller.dart │ │ ├── event_backdrop_photo.dart │ │ ├── event_details_page.dart │ │ ├── event_details_scroll_effects.dart │ │ ├── event_gallery_grid.dart │ │ └── storyline_widget.dart │ │ ├── events │ │ ├── event_grid.dart │ │ ├── event_grid_item.dart │ │ ├── event_poster.dart │ │ ├── event_release_date_information.dart │ │ └── events_page.dart │ │ ├── inkino_app_bar.dart │ │ ├── inkino_bottom_bar.dart │ │ ├── main_page.dart │ │ ├── showtimes │ │ ├── showtime_date_selector.dart │ │ ├── showtime_list.dart │ │ ├── showtime_list_tile.dart │ │ └── showtimes_page.dart │ │ └── theater_list │ │ ├── theater_list.dart │ │ └── theater_selector_popup.dart ├── pubspec.yaml └── test │ ├── mocks.dart │ └── ui │ ├── common │ └── loading_view_test.dart │ ├── event_details │ └── event_details_page_test.dart │ ├── events │ └── events_page_test.dart │ ├── showtimes │ ├── showtime_date_selector_test.dart │ └── showtimes_page_test.dart │ └── theater_list │ └── theater_list_test.dart ├── release-all.sh └── web ├── analysis_options.yaml ├── build.yaml ├── deploy.sh ├── firebase.json ├── lib ├── app_component.dart ├── app_component.html ├── app_component.scss └── src │ ├── _breakpoints.scss │ ├── _common.scss │ ├── app_bar │ ├── app_bar_component.dart │ ├── app_bar_component.html │ ├── app_bar_component.scss │ ├── nav_bar │ │ ├── nav_bar_component.dart │ │ ├── nav_bar_component.html │ │ └── nav_bar_component.scss │ ├── scroll_utils.dart │ └── search_bar │ │ ├── search_bar_component.dart │ │ ├── search_bar_component.html │ │ └── search_bar_component.scss │ ├── common │ ├── content_rating │ │ ├── content_rating_component.dart │ │ ├── content_rating_component.html │ │ └── content_rating_component.scss │ ├── event_poster │ │ ├── event_poster_component.dart │ │ ├── event_poster_component.html │ │ ├── event_poster_component.scss │ │ ├── lazy_image_component.dart │ │ └── lazy_image_component.scss │ ├── loading_view │ │ ├── loading_view_component.dart │ │ ├── loading_view_component.html │ │ ├── loading_view_component.scss │ │ ├── spinner_component.dart │ │ ├── spinner_component.html │ │ └── spinner_component.scss │ ├── showtime_item │ │ ├── showtime_item_component.dart │ │ ├── showtime_item_component.html │ │ └── showtime_item_component.scss │ └── theater_selector │ │ ├── theater_dropdown_controller.dart │ │ ├── theater_selector_component.dart │ │ ├── theater_selector_component.html │ │ ├── theater_selector_component.scss │ │ ├── theater_selector_dropdown_menu_component.dart │ │ ├── theater_selector_dropdown_menu_component.html │ │ └── theater_selector_dropdown_menu_component.scss │ ├── event_details │ ├── actor_scroller │ │ ├── actor_image_component.dart │ │ ├── actor_image_component.html │ │ ├── actor_image_component.scss │ │ ├── actor_scroller_component.dart │ │ ├── actor_scroller_component.html │ │ └── actor_scroller_component.scss │ ├── event_details_component.dart │ ├── event_details_component.html │ ├── event_details_component.scss │ └── landscape_image │ │ ├── event_landscape_image_component.dart │ │ ├── event_landscape_image_component.html │ │ └── event_landscape_image_component.scss │ ├── events │ ├── events_page_component.dart │ ├── events_page_component.html │ └── events_page_component.scss │ ├── restore_scroll_position.dart │ ├── routes.dart │ └── showtimes │ ├── date_selector_component.dart │ ├── date_selector_component.html │ ├── date_selector_component.scss │ ├── showtimes_page_component.dart │ ├── showtimes_page_component.html │ └── showtimes_page_component.scss ├── pubspec.yaml ├── test └── sample_test.dart └── web ├── images ├── arrow_drop_down.svg ├── back.svg ├── background-image.jpg ├── close.svg ├── coming-soon.svg ├── fallback-icon.svg ├── favicon.png ├── icon-192.png ├── icon-48.png ├── icon-512.png ├── icon-96.png ├── info.svg ├── logo.png ├── now-in-theaters.svg ├── place.svg ├── profile.svg ├── search.svg ├── showtimes.svg └── theaters.svg ├── index.html ├── main.dart ├── manifest.json ├── privacy.html ├── pwa.dart └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | android/google-play-service-account.json 2 | android/key.properties 3 | 4 | _Store 5 | .atom/ 6 | .idea/ 7 | .vscode/ 8 | 9 | .packages 10 | .pub/ 11 | .dart_tool/ 12 | pubspec.lock 13 | 14 | Podfile 15 | Podfile.lock 16 | Pods/ 17 | .symlinks/ 18 | **/Flutter/App.framework/ 19 | **/Flutter/Flutter.framework/ 20 | **/Flutter/Generated.xcconfig 21 | **/Flutter/flutter_assets/ 22 | ServiceDefinitions.json 23 | xcuserdata/ 24 | 25 | local.properties 26 | .gradle/ 27 | gradlew 28 | gradlew.bat 29 | gradle-wrapper.jar 30 | *.iml 31 | 32 | GeneratedPluginRegistrant.h 33 | GeneratedPluginRegistrant.m 34 | GeneratedPluginRegistrant.java 35 | build/ 36 | .flutter-plugins 37 | .idea/workspace.xml 38 | .firebaserc 39 | .firebase/ 40 | .firebase-debug.log 41 | .DS_Store 42 | 43 | core/lib/src/tmdb_config.dart 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: true 4 | addons: 5 | apt: 6 | sources: 7 | - ubuntu-toolchain-r-test 8 | packages: 9 | - libstdc++6 10 | - fonts-droid 11 | 12 | before_script: 13 | # Install the standalone Dart SDK 14 | - sudo apt-get update 15 | - sudo apt-get install apt-transport-https 16 | - sudo sh -c 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' 17 | - sudo sh -c 'curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list' 18 | - sudo apt-get update 19 | - sudo apt-get install dart 20 | - export PATH="$PATH:/usr/lib/dart/bin" 21 | 22 | # Install Flutter 23 | - git clone https://github.com/flutter/flutter.git -b stable --depth 1 24 | - export PATH="$PATH:`pwd`/flutter/bin" 25 | - flutter doctor 26 | 27 | script: 28 | - ./ci-script.sh 29 | 30 | cache: 31 | directories: 32 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /assets/screenshots/app_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/app_store.png -------------------------------------------------------------------------------- /assets/screenshots/event_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/event_details.png -------------------------------------------------------------------------------- /assets/screenshots/google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/google_play.png -------------------------------------------------------------------------------- /assets/screenshots/launch_pwa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/launch_pwa.png -------------------------------------------------------------------------------- /assets/screenshots/now_in_theaters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/now_in_theaters.png -------------------------------------------------------------------------------- /assets/screenshots/showtimes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/assets/screenshots/showtimes.png -------------------------------------------------------------------------------- /ci-script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | # Rename tmdb_config.dart.sample file so that the project compiles 8 | (cd core/lib/src && mv tmdb_config.dart.sample tmdb_config.dart) 9 | 10 | # Get all packages for core, mobile and web 11 | (cd core && pub get) 12 | (cd web && pub get) 13 | (cd mobile && flutter packages get) 14 | 15 | # Analyze core, mobile and web 16 | (cd core && dartanalyzer ./ --fatal-infos --fatal-warnings) 17 | (cd mobile && dartanalyzer ./ --fatal-infos --fatal-warnings) 18 | (cd web && dartanalyzer ./ --fatal-infos --fatal-warnings) 19 | 20 | # Run tests for core, mobile and web 21 | echo "--- Running tests in core... ---" 22 | (cd core && pub run test) 23 | 24 | echo "--- Running tests in mobile... ---" 25 | (cd mobile && flutter test) 26 | 27 | echo "--- Running tests in web... ---" 28 | (cd web && pub run build_runner test --fail-on-severe -- -p chrome) -------------------------------------------------------------------------------- /core/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - hash_and_equals 10 | - iterable_contains_unrelated_type 11 | - list_remove_unrelated_type 12 | - test_types_in_equals 13 | - unrelated_type_equality_checks 14 | - valid_regexps 15 | -------------------------------------------------------------------------------- /core/lib/core.dart: -------------------------------------------------------------------------------- 1 | export 'src/models/actor.dart'; 2 | export 'src/models/content_descriptor.dart'; 3 | export 'src/models/event.dart'; 4 | export 'src/models/loading_status.dart'; 5 | export 'src/models/show.dart'; 6 | export 'src/models/show_cache.dart'; 7 | export 'src/models/theater.dart'; 8 | 9 | export 'src/i18n/messages.dart'; 10 | export 'src/i18n/inkino_messages_all.dart'; 11 | 12 | export 'src/networking/finnkino_api.dart'; 13 | 14 | export 'src/redux/_common/search.dart'; 15 | export 'src/redux/_common/common_actions.dart'; 16 | export 'src/redux/actor/actor_actions.dart'; 17 | export 'src/redux/actor/actor_selectors.dart'; 18 | export 'src/redux/app/app_state.dart'; 19 | export 'src/redux/event/event_actions.dart'; 20 | export 'src/redux/event/event_selectors.dart'; 21 | export 'src/redux/show/show_actions.dart'; 22 | export 'src/redux/show/show_selectors.dart'; 23 | export 'src/redux/store.dart'; 24 | 25 | export 'src/viewmodels/theater_list_view_model.dart'; 26 | export 'src/viewmodels/events_page_view_model.dart'; 27 | export 'src/viewmodels/showtime_page_view_model.dart'; -------------------------------------------------------------------------------- /core/lib/src/i18n/inkino_messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:intl/intl.dart'; 8 | import 'package:intl/message_lookup_by_library.dart'; 9 | // ignore: implementation_imports 10 | import 'package:intl/src/intl_helpers.dart'; 11 | 12 | import 'package:core/src/i18n/inkino_messages_en.dart' deferred as messages_en; 13 | import 'package:core/src/i18n/inkino_messages_fi.dart' deferred as messages_fi; 14 | 15 | typedef Future LibraryLoader(); 16 | Map _deferredLibraries = { 17 | 'en': () => messages_en.loadLibrary(), 18 | 'fi': () => messages_fi.loadLibrary(), 19 | }; 20 | 21 | MessageLookupByLibrary _findExact(localeName) { 22 | switch (localeName) { 23 | case 'en': 24 | return messages_en.messages; 25 | case 'fi': 26 | return messages_fi.messages; 27 | default: 28 | return null; 29 | } 30 | } 31 | 32 | /// User programs should call this before using [localeName] for messages. 33 | Future initializeMessages(String localeName) async { 34 | var availableLocale = Intl.verifiedLocale( 35 | localeName, 36 | (locale) => _deferredLibraries[locale] != null, 37 | onFailure: (_) => null); 38 | if (availableLocale == null) { 39 | return new Future.value(false); 40 | } 41 | var lib = _deferredLibraries[availableLocale]; 42 | await (lib == null ? new Future.value(false) : lib()); 43 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 44 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 45 | return new Future.value(true); 46 | } 47 | 48 | bool _messagesExistFor(String locale) { 49 | try { 50 | return _findExact(locale) != null; 51 | } catch (e) { 52 | return false; 53 | } 54 | } 55 | 56 | MessageLookupByLibrary _findGeneratedMessagesFor(locale) { 57 | var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, 58 | onFailure: (_) => null); 59 | if (actualLocale == null) return null; 60 | return _findExact(actualLocale); 61 | } 62 | -------------------------------------------------------------------------------- /core/lib/src/models/actor.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class Actor { 4 | Actor({ 5 | @required this.name, 6 | this.avatarUrl, 7 | }); 8 | 9 | final String name; 10 | final String avatarUrl; 11 | 12 | @override 13 | bool operator ==(Object other) => 14 | identical(this, other) || 15 | other is Actor && 16 | runtimeType == other.runtimeType && 17 | name == other.name && 18 | avatarUrl == other.avatarUrl; 19 | 20 | @override 21 | int get hashCode => 22 | name.hashCode ^ 23 | avatarUrl.hashCode; 24 | } 25 | -------------------------------------------------------------------------------- /core/lib/src/models/content_descriptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class ContentDescriptor { 4 | ContentDescriptor({ 5 | @required this.name, 6 | @required this.imageUrl, 7 | }); 8 | 9 | final String name; 10 | final String imageUrl; 11 | 12 | @override 13 | bool operator ==(Object other) => 14 | identical(this, other) || 15 | other is ContentDescriptor && 16 | runtimeType == other.runtimeType && 17 | name == other.name && 18 | imageUrl == other.imageUrl; 19 | 20 | @override 21 | int get hashCode => 22 | name.hashCode ^ 23 | imageUrl.hashCode; 24 | 25 | @override 26 | String toString() { 27 | return 'ContentDescriptor{name: $name, imageUrl: $imageUrl}'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/lib/src/models/loading_status.dart: -------------------------------------------------------------------------------- 1 | enum LoadingStatus { 2 | idle, 3 | loading, 4 | error, 5 | success, 6 | } -------------------------------------------------------------------------------- /core/lib/src/models/show.dart: -------------------------------------------------------------------------------- 1 | import 'package:kt_dart/collection.dart'; 2 | 3 | import 'content_descriptor.dart'; 4 | import 'event.dart'; 5 | 6 | class Show { 7 | Show({ 8 | this.id, 9 | this.eventId, 10 | this.title, 11 | this.originalTitle, 12 | this.ageRating, 13 | this.ageRatingUrl, 14 | this.url, 15 | this.presentationMethod, 16 | this.theaterAndAuditorium, 17 | this.start, 18 | this.end, 19 | this.images, 20 | this.contentDescriptors, 21 | }); 22 | 23 | final String id; 24 | final String eventId; 25 | final String title; 26 | final String originalTitle; 27 | final String ageRating; 28 | final String ageRatingUrl; 29 | final String url; 30 | final String presentationMethod; 31 | final String theaterAndAuditorium; 32 | final DateTime start; 33 | final DateTime end; 34 | final EventImageData images; 35 | final KtList contentDescriptors; 36 | 37 | @override 38 | bool operator ==(Object other) => 39 | identical(this, other) || 40 | other is Show && 41 | runtimeType == other.runtimeType && 42 | id == other.id && 43 | eventId == other.eventId && 44 | title == other.title && 45 | originalTitle == other.originalTitle && 46 | ageRating == other.ageRating && 47 | ageRatingUrl == other.ageRatingUrl && 48 | url == other.url && 49 | presentationMethod == other.presentationMethod && 50 | theaterAndAuditorium == other.theaterAndAuditorium && 51 | start == other.start && 52 | end == other.end && 53 | images == other.images && 54 | contentDescriptors == other.contentDescriptors; 55 | 56 | @override 57 | int get hashCode => 58 | id.hashCode ^ 59 | eventId.hashCode ^ 60 | title.hashCode ^ 61 | originalTitle.hashCode ^ 62 | ageRating.hashCode ^ 63 | ageRatingUrl.hashCode ^ 64 | url.hashCode ^ 65 | presentationMethod.hashCode ^ 66 | theaterAndAuditorium.hashCode ^ 67 | start.hashCode ^ 68 | end.hashCode ^ 69 | images.hashCode ^ 70 | contentDescriptors.hashCode; 71 | } 72 | -------------------------------------------------------------------------------- /core/lib/src/models/show_cache.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/redux/app/app_state.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | import 'theater.dart'; 5 | 6 | /// Used as a lookup key for caching showtimes. 7 | /// 8 | /// When we've already loaded showtimes for a specific theater on specific date, 9 | /// we don't want to load them again. This pair class is used as a key in a Map 10 | /// structure, and the values are the showtimes. 11 | class DateTheaterPair { 12 | static final ddMMyyyy = new DateFormat('dd.MM.yyyy'); 13 | 14 | DateTheaterPair(this.dateTime, this.theater); 15 | 16 | DateTheaterPair.fromState(AppState state) 17 | : dateTime = state.showState.selectedDate, 18 | theater = state.theaterState.currentTheater; 19 | 20 | final DateTime dateTime; 21 | final Theater theater; 22 | 23 | @override 24 | bool operator ==(Object other) => 25 | identical(this, other) || 26 | other is DateTheaterPair && 27 | runtimeType == other.runtimeType && 28 | dateTime == other.dateTime && 29 | theater == other.theater; 30 | 31 | @override 32 | int get hashCode => dateTime.hashCode ^ theater.hashCode; 33 | } 34 | -------------------------------------------------------------------------------- /core/lib/src/models/theater.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class Theater { 4 | Theater({ 5 | @required this.id, 6 | @required this.name, 7 | }); 8 | 9 | final String id; 10 | final String name; 11 | 12 | @override 13 | bool operator ==(Object other) => 14 | identical(this, other) || 15 | other is Theater && 16 | runtimeType == other.runtimeType && 17 | id == other.id && 18 | name == other.name; 19 | 20 | @override 21 | int get hashCode => id.hashCode ^ name.hashCode; 22 | } 23 | -------------------------------------------------------------------------------- /core/lib/src/networking/finnkino_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:core/src/models/event.dart'; 5 | import 'package:core/src/models/show.dart'; 6 | import 'package:core/src/models/theater.dart'; 7 | import 'package:core/src/parsers/event_parser.dart'; 8 | import 'package:core/src/parsers/show_parser.dart'; 9 | import 'package:http/http.dart'; 10 | import 'package:intl/intl.dart'; 11 | import 'package:kt_dart/collection.dart'; 12 | 13 | class FinnkinoApi { 14 | static final ddMMyyyy = DateFormat('dd.MM.yyyy'); 15 | static final enBaseUrl = 'https://www.finnkino.fi/en/xml'; 16 | static final fiBaseUrl = 'https://www.finkino.fi/xml'; 17 | 18 | static bool useFinnish = false; 19 | 20 | FinnkinoApi(this.client); 21 | 22 | final Client client; 23 | 24 | String get localizedPath => useFinnish ? '' : '/en'; 25 | 26 | Uri get kScheduleBaseUrl => 27 | Uri.https('www.finnkino.fi', '$localizedPath/xml/Schedule'); 28 | 29 | Uri get kEventsBaseUrl => 30 | Uri.https('www.finnkino.fi', '$localizedPath/xml/Events'); 31 | 32 | Future> getSchedule(Theater theater, DateTime date) async { 33 | final dt = ddMMyyyy.format(date ?? new DateTime.now()); 34 | final response = await client.get( 35 | kScheduleBaseUrl.replace(queryParameters: { 36 | 'area': theater.id, 37 | 'dt': dt, 38 | 'includeGallery': 'true', 39 | }), 40 | ); 41 | 42 | return ShowParser.parse(utf8.decode(response.bodyBytes)); 43 | } 44 | 45 | Future> getNowInTheatersEvents(Theater theater) async { 46 | final response = await client.get( 47 | kEventsBaseUrl.replace(queryParameters: { 48 | 'area': theater.id, 49 | 'listType': 'NowInTheatres', 50 | 'includeGallery': 'true', 51 | }), 52 | ); 53 | 54 | return EventParser.parse(utf8.decode(response.bodyBytes)); 55 | } 56 | 57 | Future> getUpcomingEvents() async { 58 | final response = await client.get( 59 | kEventsBaseUrl.replace(queryParameters: { 60 | 'listType': 'ComingSoon', 61 | 'includeGallery': 'true', 62 | }), 63 | ); 64 | 65 | return EventParser.parse(utf8.decode(response.bodyBytes)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/lib/src/networking/image_url_rewriter.dart: -------------------------------------------------------------------------------- 1 | final _finnkinoBaseUrl = RegExp(r'https?://media.finnkino.fi'); 2 | const _imgixBaseUrl = 'https://inkino.imgix.net'; 3 | const _imgixQueryParams = '?auto=format,compress'; 4 | 5 | final notYetRated = RegExp(r'.*not.*yet.*rated.*', caseSensitive: false); 6 | 7 | String rewriteImageUrl(String originalUrl) { 8 | if (originalUrl == null) { 9 | return null; 10 | } 11 | 12 | if (originalUrl.contains(notYetRated)) { 13 | /// Finnkino XML API might return a "Not yet rated" as an image url for a 14 | /// content age rating image. And you might know that "Not yet rated" is not 15 | /// a valid url. 16 | originalUrl = originalUrl.replaceFirst( 17 | notYetRated, 18 | 'https://media.finnkino.fi/images/rating_large_Tulossa.png', 19 | ); 20 | } 21 | 22 | return originalUrl.replaceFirst(_finnkinoBaseUrl, _imgixBaseUrl) + 23 | _imgixQueryParams; 24 | } 25 | -------------------------------------------------------------------------------- /core/lib/src/networking/tmdb_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:core/src/models/actor.dart'; 5 | import 'package:core/src/models/event.dart'; 6 | import 'package:core/src/tmdb_config.dart'; 7 | import 'package:http/http.dart'; 8 | import 'package:kt_dart/collection.dart'; 9 | 10 | /// If this has a red underline, it means that the lib/tmdb_config.dart file 11 | /// is not present on the project. Refer to the README for instructions 12 | /// on how to do so. 13 | 14 | class TMDBApi { 15 | TMDBApi(this.client); 16 | final Client client; 17 | 18 | static final String baseUrl = 'api.themoviedb.org'; 19 | 20 | Future> findAvatarsForActors( 21 | Event event, KtList actors) async { 22 | int movieId = await _findMovieId(event.originalTitle); 23 | 24 | if (movieId != null) { 25 | return _getActorAvatars(movieId); 26 | } 27 | 28 | return actors; 29 | } 30 | 31 | Future _findMovieId(String movieTitle) async { 32 | final searchUri = Uri.https(baseUrl, '3/search/movie', { 33 | 'api_key': TMDBConfig.apiKey, 34 | 'query': movieTitle, 35 | }); 36 | 37 | final response = await client.get(searchUri); 38 | Map movieSearchJson = 39 | json.decode(utf8.decode(response.bodyBytes)); 40 | final searchResults = 41 | (movieSearchJson['results'] as List).cast>(); 42 | 43 | if (searchResults.isNotEmpty) { 44 | return searchResults.first['id']; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | Future> _getActorAvatars(int movieId) async { 51 | final actorUri = Uri.https( 52 | baseUrl, 53 | '3/movie/$movieId/credits', 54 | {'api_key': TMDBConfig.apiKey}, 55 | ); 56 | 57 | final response = await client.get(actorUri); 58 | Map movieActors = 59 | json.decode(utf8.decode(response.bodyBytes)); 60 | 61 | return _parseActorAvatars( 62 | (movieActors['cast'] as List).cast>()); 63 | } 64 | 65 | KtList _parseActorAvatars(List> movieCast) { 66 | final actorsWithAvatars = mutableListOf(); 67 | 68 | movieCast.forEach((Map castMember) { 69 | String pp = castMember['profile_path']; 70 | final profilePath = 71 | pp != null ? 'https://image.tmdb.org/t/p/w200$pp' : null; 72 | 73 | actorsWithAvatars.add(Actor( 74 | name: castMember['name'], 75 | avatarUrl: profilePath, 76 | )); 77 | }); 78 | 79 | return actorsWithAvatars; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/lib/src/parsers/content_descriptor_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/content_descriptor.dart'; 2 | import 'package:core/src/networking/image_url_rewriter.dart'; 3 | import 'package:core/src/utils/xml_utils.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:xml/xml.dart'; 6 | 7 | class ContentDescriptorParser { 8 | static KtList parse(Iterable roots) { 9 | if (roots == null) { 10 | return emptyList(); 11 | } 12 | 13 | var contentDescriptors = 14 | listFrom(roots).first().findElements('ContentDescriptor'); 15 | return listFrom(contentDescriptors).map((element) { 16 | return ContentDescriptor( 17 | name: tagContentsOrNull(element, 'Name'), 18 | imageUrl: rewriteImageUrl(tagContentsOrNull(element, 'ImageURL')), 19 | ); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/lib/src/parsers/gallery_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/networking/image_url_rewriter.dart'; 3 | import 'package:core/src/utils/xml_utils.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:xml/xml.dart'; 6 | 7 | class GalleryParser { 8 | static KtList parse(Iterable roots) { 9 | if (roots == null || roots.isEmpty) { 10 | return emptyList(); 11 | } 12 | 13 | var galleryImage = listFrom(roots).first().findElements('GalleryImage'); 14 | return listFrom(galleryImage).map((node) { 15 | return GalleryImage( 16 | thumbnailLocation: 17 | rewriteImageUrl(tagContents(node, 'ThumbnailLocation')), 18 | location: rewriteImageUrl(tagContents(node, 'Location')), 19 | ); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/lib/src/parsers/show_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/show.dart'; 2 | import 'package:core/src/networking/image_url_rewriter.dart'; 3 | import 'package:core/src/parsers/content_descriptor_parser.dart'; 4 | import 'package:core/src/parsers/event_parser.dart'; 5 | import 'package:core/src/utils/event_name_cleaner.dart'; 6 | import 'package:core/src/utils/xml_utils.dart'; 7 | import 'package:kt_dart/collection.dart'; 8 | import 'package:xml/xml.dart' as xml; 9 | 10 | class ShowParser { 11 | static KtList parse(String xmlString) { 12 | final document = xml.parse(xmlString); 13 | final shows = document.findAllElements('Show'); 14 | 15 | return listFrom(shows).map((node) { 16 | final title = tagContents(node, 'Title'); 17 | final originalTitle = tagContents(node, 'OriginalTitle'); 18 | 19 | return Show( 20 | id: tagContents(node, 'ID'), 21 | eventId: tagContents(node, 'EventID'), 22 | title: EventNameCleaner.cleanup(title), 23 | originalTitle: EventNameCleaner.cleanup(originalTitle), 24 | ageRating: tagContentsOrNull(node, 'Rating'), 25 | ageRatingUrl: 26 | rewriteImageUrl(tagContentsOrNull(node, 'RatingImageUrl')), 27 | url: tagContents(node, 'ShowURL'), 28 | presentationMethod: tagContents(node, 'PresentationMethod'), 29 | theaterAndAuditorium: tagContents(node, 'TheatreAndAuditorium'), 30 | start: DateTime.parse(tagContents(node, 'dttmShowStart')), 31 | end: DateTime.parse(tagContents(node, 'dttmShowEnd')), 32 | images: EventImageDataParser.parse(node.findElements('Images')), 33 | contentDescriptors: ContentDescriptorParser.parse( 34 | node.findElements('ContentDescriptors')), 35 | ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/lib/src/parsers/theater_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:core/src/utils/xml_utils.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | import 'package:xml/xml.dart' as xml; 5 | 6 | final RegExp _nameExpr = new RegExp(r'([A-Z])([A-Z]+)'); 7 | 8 | class TheaterParser { 9 | /// Entirely redundant theater, which isn't actually even a theater. 10 | /// The API returns this as "Valitse alue/teatteri", which means "choose a 11 | /// theater". Thanks Finnkino. 12 | static const String kChooseTheaterId = '1029'; 13 | 14 | static KtList parse(String xmlString) { 15 | final document = xml.parse(xmlString); 16 | final theaters = document.findAllElements('TheatreArea').map((node) { 17 | final id = tagContents(node, 'ID'); 18 | var normalizedName = _normalize(tagContents(node, 'Name')); 19 | 20 | if (id == kChooseTheaterId) { 21 | normalizedName = 'All theaters'; 22 | } 23 | 24 | return Theater( 25 | id: id, 26 | name: normalizedName, 27 | ); 28 | }); 29 | 30 | return listFrom(theaters); 31 | } 32 | 33 | static String _normalize(String text) { 34 | return text.replaceAllMapped(_nameExpr, (match) { 35 | return '${match.group(1)}${match.group(2).toLowerCase()}'; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/lib/src/redux/_common/common_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/actor.dart'; 2 | import 'package:core/src/models/event.dart'; 3 | import 'package:core/src/models/theater.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | 6 | class InitAction {} 7 | 8 | class InitCompleteAction { 9 | InitCompleteAction( 10 | this.theaters, 11 | this.selectedTheater, 12 | ); 13 | 14 | final KtList theaters; 15 | final Theater selectedTheater; 16 | } 17 | 18 | class FetchComingSoonEventsIfNotLoadedAction {} 19 | 20 | class ChangeCurrentTheaterAction { 21 | ChangeCurrentTheaterAction(this.selectedTheater); 22 | final Theater selectedTheater; 23 | } 24 | 25 | class UpdateActorsForEventAction { 26 | UpdateActorsForEventAction(this.event, this.actors); 27 | 28 | final Event event; 29 | final KtList actors; 30 | } 31 | -------------------------------------------------------------------------------- /core/lib/src/redux/_common/search.dart: -------------------------------------------------------------------------------- 1 | class SearchQueryChangedAction { 2 | SearchQueryChangedAction(this.query); 3 | final String query; 4 | } 5 | 6 | String searchQueryReducer(String searchQuery, dynamic action) { 7 | if (action is SearchQueryChangedAction) { 8 | return action.query; 9 | } 10 | 11 | return searchQuery; 12 | } 13 | -------------------------------------------------------------------------------- /core/lib/src/redux/actor/actor_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/actor.dart'; 2 | import 'package:core/src/models/event.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | 5 | class FetchActorAvatarsAction { 6 | FetchActorAvatarsAction(this.event); 7 | final Event event; 8 | } 9 | 10 | class ActorsUpdatedAction { 11 | ActorsUpdatedAction(this.actors); 12 | final KtList actors; 13 | } 14 | 15 | class ReceivedActorAvatarsAction { 16 | ReceivedActorAvatarsAction(this.actors); 17 | final KtList actors; 18 | } 19 | -------------------------------------------------------------------------------- /core/lib/src/redux/actor/actor_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:core/src/networking/tmdb_api.dart'; 4 | import 'package:core/src/redux/_common/common_actions.dart'; 5 | import 'package:core/src/redux/actor/actor_actions.dart'; 6 | import 'package:core/src/redux/app/app_state.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | class ActorMiddleware extends MiddlewareClass { 10 | ActorMiddleware(this.tmdbApi); 11 | final TMDBApi tmdbApi; 12 | 13 | @override 14 | Future call( 15 | Store store, dynamic action, NextDispatcher next) async { 16 | next(action); 17 | 18 | if (action is FetchActorAvatarsAction) { 19 | next(ActorsUpdatedAction(action.event.actors)); 20 | 21 | try { 22 | final actorsWithAvatars = await tmdbApi.findAvatarsForActors( 23 | action.event, 24 | action.event.actors, 25 | ); 26 | 27 | // TMDB API might have a more comprehensive list of actors than the 28 | // Finnkino API, so we update the event with the actors we get from 29 | // the TMDB API. 30 | next(UpdateActorsForEventAction(action.event, actorsWithAvatars)); 31 | next(ReceivedActorAvatarsAction(actorsWithAvatars)); 32 | } catch (e) { 33 | // We don't need to handle this. If fetching actor avatars 34 | // fails, we don't care: the UI just simply won't display 35 | // any actor avatars and falls back to placeholder icons 36 | // instead. 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/lib/src/redux/actor/actor_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/actor.dart'; 2 | import 'package:core/src/redux/actor/actor_actions.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | 5 | KtMap actorReducer(KtMap state, dynamic action) { 6 | if (action is ActorsUpdatedAction) { 7 | return _updateActors(state, action); 8 | } else if (action is ReceivedActorAvatarsAction) { 9 | return _updateActorAvatars(state, action); 10 | } 11 | 12 | return state; 13 | } 14 | 15 | KtMap _updateActors( 16 | KtMap state, ActorsUpdatedAction action) { 17 | final actors = state.toMutableMap(); 18 | action.actors.forEach((actor) { 19 | actors.putIfAbsent(actor.name, Actor(name: actor.name)); 20 | }); 21 | return actors.toMap(); 22 | } 23 | 24 | KtMap _updateActorAvatars( 25 | KtMap state, ReceivedActorAvatarsAction action) { 26 | final actorsWithAvatars = state.toMutableMap(); 27 | action.actors.forEach((actor) { 28 | actorsWithAvatars[actor.name] = Actor( 29 | name: actor.name, 30 | avatarUrl: actor.avatarUrl, 31 | ); 32 | }); 33 | 34 | return actorsWithAvatars.toMap(); 35 | } 36 | -------------------------------------------------------------------------------- /core/lib/src/redux/actor/actor_selectors.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/actor.dart'; 2 | import 'package:core/src/models/event.dart'; 3 | import 'package:core/src/redux/app/app_state.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | 6 | KtList actorsForEventSelector(AppState state, Event event) { 7 | return state.actorsByName.values 8 | .filter((actor) => event.actors.contains(actor)); 9 | } 10 | -------------------------------------------------------------------------------- /core/lib/src/redux/app/app_reducer.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:core/src/redux/_common/search.dart'; 3 | import 'package:core/src/redux/actor/actor_reducer.dart'; 4 | import 'package:core/src/redux/app/app_state.dart'; 5 | import 'package:core/src/redux/event/event_reducer.dart'; 6 | import 'package:core/src/redux/show/show_reducer.dart'; 7 | import 'package:core/src/redux/theater/theater_reducer.dart'; 8 | 9 | AppState appReducer(AppState state, dynamic action) { 10 | return new AppState( 11 | searchQuery: searchQueryReducer(state.searchQuery, action), 12 | actorsByName: actorReducer(state.actorsByName, action), 13 | theaterState: theaterReducer(state.theaterState, action), 14 | showState: showReducer(state.showState, action), 15 | eventState: eventReducer(state.eventState, action), 16 | ); 17 | } -------------------------------------------------------------------------------- /core/lib/src/redux/app/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/actor.dart'; 2 | import 'package:core/src/redux/event/event_state.dart'; 3 | import 'package:core/src/redux/show/show_state.dart'; 4 | import 'package:core/src/redux/theater/theater_state.dart'; 5 | import 'package:kt_dart/collection.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | @immutable 9 | class AppState { 10 | AppState({ 11 | @required this.searchQuery, 12 | @required this.actorsByName, 13 | @required this.theaterState, 14 | @required this.showState, 15 | @required this.eventState, 16 | }); 17 | 18 | final String searchQuery; 19 | final KtMap actorsByName; 20 | final TheaterState theaterState; 21 | final ShowState showState; 22 | final EventState eventState; 23 | 24 | factory AppState.initial() { 25 | return AppState( 26 | searchQuery: null, 27 | actorsByName: emptyMap(), 28 | theaterState: TheaterState.initial(), 29 | showState: ShowState.initial(), 30 | eventState: EventState.initial(), 31 | ); 32 | } 33 | 34 | AppState copyWith({ 35 | String searchQuery, 36 | KtMap actorsByName, 37 | TheaterState theaterState, 38 | ShowState showState, 39 | EventState eventState, 40 | }) { 41 | return AppState( 42 | searchQuery: searchQuery ?? this.searchQuery, 43 | actorsByName: actorsByName ?? this.actorsByName, 44 | theaterState: theaterState ?? this.theaterState, 45 | showState: showState ?? this.showState, 46 | eventState: eventState ?? this.eventState, 47 | ); 48 | } 49 | 50 | @override 51 | bool operator ==(Object other) => 52 | identical(this, other) || 53 | other is AppState && 54 | runtimeType == other.runtimeType && 55 | searchQuery == other.searchQuery && 56 | actorsByName == other.actorsByName && 57 | theaterState == other.theaterState && 58 | showState == other.showState && 59 | eventState == other.eventState; 60 | 61 | @override 62 | int get hashCode => 63 | searchQuery.hashCode ^ 64 | actorsByName.hashCode ^ 65 | theaterState.hashCode ^ 66 | showState.hashCode ^ 67 | eventState.hashCode; 68 | } 69 | -------------------------------------------------------------------------------- /core/lib/src/redux/event/event_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:kt_dart/collection.dart'; 3 | 4 | class RefreshEventsAction { 5 | RefreshEventsAction(this.type); 6 | final EventListType type; 7 | } 8 | 9 | class RequestingEventsAction { 10 | RequestingEventsAction(this.type); 11 | final EventListType type; 12 | } 13 | 14 | class ReceivedInTheatersEventsAction { 15 | ReceivedInTheatersEventsAction(this.events); 16 | final KtList events; 17 | } 18 | 19 | class ReceivedComingSoonEventsAction { 20 | ReceivedComingSoonEventsAction(this.events); 21 | final KtList events; 22 | } 23 | 24 | class ErrorLoadingEventsAction { 25 | ErrorLoadingEventsAction(this.type); 26 | final EventListType type; 27 | } 28 | -------------------------------------------------------------------------------- /core/lib/src/redux/event/event_selectors.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/models/show.dart'; 3 | import 'package:core/src/redux/app/app_state.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:reselect/reselect.dart'; 6 | 7 | final nowInTheatersSelector = createSelector2( 8 | (AppState state) => state.eventState.nowInTheatersEvents, 9 | (AppState state) => state.searchQuery, 10 | _eventsOrEventSearch, 11 | ); 12 | 13 | final comingSoonSelector = createSelector2( 14 | (AppState state) => state.eventState.comingSoonEvents, 15 | (AppState state) => state.searchQuery, 16 | _eventsOrEventSearch, 17 | ); 18 | 19 | Event eventByIdSelector(AppState state, String id) { 20 | final predicate = (event) => event.id == id; 21 | return nowInTheatersSelector(state).firstOrNull(predicate) ?? 22 | comingSoonSelector(state).firstOrNull(predicate); 23 | } 24 | 25 | Event eventForShowSelector(AppState state, Show show) { 26 | return state.eventState.nowInTheatersEvents 27 | .filter((event) => event.id == show.eventId) 28 | .first(); 29 | } 30 | 31 | KtList _eventsOrEventSearch(KtList events, String searchQuery) { 32 | return searchQuery == null 33 | ? _uniqueEvents(events) 34 | : _eventsWithSearchQuery(events, searchQuery); 35 | } 36 | 37 | /// Since Finnkino XML API considers "The Grinch" and "The Grinch 2D" to be two 38 | /// completely different events, we might get a lot of duplication. We have to 39 | /// do this hack because it is quite boring to display four movie posters that 40 | /// are exactly the same. 41 | KtList _uniqueEvents(KtList original) { 42 | return original 43 | // reverse because last unique key wins 44 | .reversed() 45 | .associateBy((event) => event.originalTitle) 46 | .values 47 | .reversed(); 48 | } 49 | 50 | KtList _eventsWithSearchQuery( 51 | KtList original, String searchQuery) { 52 | final searchQueryPattern = RegExp(searchQuery, caseSensitive: false); 53 | 54 | return original.filter((event) { 55 | return event.title.contains(searchQueryPattern) || 56 | event.originalTitle.contains(searchQueryPattern); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /core/lib/src/redux/event/event_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/models/loading_status.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | @immutable 7 | class EventState { 8 | EventState({ 9 | @required this.nowInTheatersStatus, 10 | @required this.nowInTheatersEvents, 11 | @required this.comingSoonStatus, 12 | @required this.comingSoonEvents, 13 | }); 14 | 15 | final LoadingStatus nowInTheatersStatus; 16 | final KtList nowInTheatersEvents; 17 | final LoadingStatus comingSoonStatus; 18 | final KtList comingSoonEvents; 19 | 20 | factory EventState.initial() { 21 | return EventState( 22 | nowInTheatersStatus: LoadingStatus.idle, 23 | nowInTheatersEvents: emptyList(), 24 | comingSoonStatus: LoadingStatus.idle, 25 | comingSoonEvents: emptyList(), 26 | ); 27 | } 28 | 29 | EventState copyWith({ 30 | LoadingStatus nowInTheatersStatus, 31 | KtList nowInTheatersEvents, 32 | LoadingStatus comingSoonStatus, 33 | KtList comingSoonEvents, 34 | }) { 35 | return EventState( 36 | nowInTheatersStatus: nowInTheatersStatus ?? this.nowInTheatersStatus, 37 | comingSoonStatus: comingSoonStatus ?? this.comingSoonStatus, 38 | nowInTheatersEvents: nowInTheatersEvents ?? this.nowInTheatersEvents, 39 | comingSoonEvents: comingSoonEvents ?? this.comingSoonEvents, 40 | ); 41 | } 42 | 43 | @override 44 | bool operator ==(Object other) => 45 | identical(this, other) || 46 | other is EventState && 47 | runtimeType == other.runtimeType && 48 | nowInTheatersStatus == other.nowInTheatersStatus && 49 | comingSoonStatus == other.comingSoonStatus && 50 | nowInTheatersEvents == other.nowInTheatersEvents && 51 | comingSoonEvents == other.comingSoonEvents; 52 | 53 | @override 54 | int get hashCode => 55 | nowInTheatersStatus.hashCode ^ 56 | comingSoonStatus.hashCode ^ 57 | nowInTheatersEvents.hashCode ^ 58 | comingSoonEvents.hashCode; 59 | } 60 | -------------------------------------------------------------------------------- /core/lib/src/redux/show/show_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/show.dart'; 2 | import 'package:core/src/models/show_cache.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | 5 | class UpdateShowDatesAction {} 6 | 7 | class ShowDatesUpdatedAction { 8 | ShowDatesUpdatedAction(this.dates); 9 | final KtList dates; 10 | } 11 | 12 | class FetchShowsIfNotLoadedAction {} 13 | 14 | class RequestingShowsAction {} 15 | 16 | class RefreshShowsAction {} 17 | 18 | class ReceivedShowsAction { 19 | ReceivedShowsAction(this.cacheKey, this.shows); 20 | final DateTheaterPair cacheKey; 21 | final KtList shows; 22 | } 23 | 24 | class ErrorLoadingShowsAction {} 25 | 26 | class ChangeCurrentDateAction { 27 | ChangeCurrentDateAction(this.date); 28 | final DateTime date; 29 | } 30 | -------------------------------------------------------------------------------- /core/lib/src/redux/show/show_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/loading_status.dart'; 2 | import 'package:core/src/redux/_common/common_actions.dart'; 3 | import 'package:core/src/redux/show/show_actions.dart'; 4 | import 'package:core/src/redux/show/show_state.dart'; 5 | 6 | ShowState showReducer(ShowState state, dynamic action) { 7 | if (action is ChangeCurrentTheaterAction) { 8 | return state.copyWith(selectedDate: state.dates.first()); 9 | } else if (action is ChangeCurrentDateAction) { 10 | return state.copyWith(selectedDate: action.date); 11 | } else if (action is RequestingShowsAction) { 12 | return state.copyWith(loadingStatus: LoadingStatus.loading); 13 | } else if (action is ReceivedShowsAction) { 14 | final newShows = state.shows.toMutableMap(); 15 | newShows[action.cacheKey] = action.shows; 16 | 17 | return state.copyWith( 18 | loadingStatus: LoadingStatus.success, 19 | shows: newShows, 20 | ); 21 | } else if (action is ErrorLoadingShowsAction) { 22 | return state.copyWith(loadingStatus: LoadingStatus.error); 23 | } else if (action is ShowDatesUpdatedAction) { 24 | return state.copyWith( 25 | availableDates: action.dates, 26 | selectedDate: action.dates.first(), 27 | ); 28 | } 29 | 30 | return state; 31 | } 32 | -------------------------------------------------------------------------------- /core/lib/src/redux/show/show_selectors.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/models/show.dart'; 3 | import 'package:core/src/models/show_cache.dart'; 4 | import 'package:core/src/redux/app/app_state.dart'; 5 | import 'package:kt_dart/collection.dart'; 6 | import 'package:memoize/memoize.dart'; 7 | import 'package:reselect/reselect.dart'; 8 | 9 | Show showByIdSelector(AppState state, String id) { 10 | return showsSelector(state).firstOrNull((show) => show.id == id) ?? 11 | _findFromAllShows(state, id); 12 | } 13 | 14 | /// Selects a list of shows based on the currently selected date and theater. 15 | /// 16 | /// If the current AppState contains a search query, returns only shows that match 17 | /// that search query. Otherwise returns all matching shows for current theater 18 | /// and date. 19 | final showsSelector = createSelector3>, String, KtList>( 21 | (state) => DateTheaterPair.fromState(state), 22 | (state) => state.showState.shows, 23 | (state) => state.searchQuery, 24 | (key, KtMap> shows, searchQuery) { 25 | KtList matchingShows = shows.getOrDefault(key, emptyList()); 26 | if (searchQuery == null) { 27 | return matchingShows; 28 | } else { 29 | return _showsWithSearchQuery(matchingShows, searchQuery); 30 | } 31 | }, 32 | ); 33 | 34 | final showsForEventSelector = 35 | memo2, Event, KtList>((shows, event) { 36 | return shows.filter((show) => show.originalTitle == event.originalTitle); 37 | }); 38 | 39 | KtList _showsWithSearchQuery(KtList shows, String searchQuery) { 40 | final searchQueryPattern = new RegExp(searchQuery, caseSensitive: false); 41 | 42 | return shows.filter((show) => 43 | show.title.contains(searchQueryPattern) || 44 | show.originalTitle.contains(searchQueryPattern)); 45 | } 46 | 47 | /// Goes through the list of showtimes for every single theater. 48 | /// 49 | /// Skips all memoization and searches for correct show time through all shows 50 | /// instead of shows specific to current theater and date. Used as a fallback 51 | /// when [showByIdSelector] fails. 52 | Show _findFromAllShows(AppState state, String id) { 53 | final allShows = state.showState.shows.values; 54 | return allShows 55 | .firstOrNull( 56 | (shows) => shows.firstOrNull((show) => show.id == id) != null) 57 | ?.firstOrNull(); 58 | } 59 | -------------------------------------------------------------------------------- /core/lib/src/redux/show/show_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/loading_status.dart'; 2 | import 'package:core/src/models/show.dart'; 3 | import 'package:core/src/models/show_cache.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | @immutable 8 | class ShowState { 9 | ShowState({ 10 | @required this.loadingStatus, 11 | @required this.dates, 12 | @required this.selectedDate, 13 | @required this.shows, 14 | }); 15 | 16 | final LoadingStatus loadingStatus; 17 | final KtList dates; 18 | final DateTime selectedDate; 19 | final KtMap> shows; 20 | 21 | factory ShowState.initial() { 22 | return ShowState( 23 | loadingStatus: LoadingStatus.idle, 24 | dates: emptyList(), 25 | selectedDate: null, 26 | shows: emptyMap(), 27 | ); 28 | } 29 | 30 | ShowState copyWith({ 31 | LoadingStatus loadingStatus, 32 | KtList availableDates, 33 | DateTime selectedDate, 34 | KtMap> shows, 35 | }) { 36 | return ShowState( 37 | loadingStatus: loadingStatus ?? this.loadingStatus, 38 | dates: availableDates ?? this.dates, 39 | selectedDate: selectedDate ?? this.selectedDate, 40 | shows: shows ?? this.shows, 41 | ); 42 | } 43 | 44 | @override 45 | bool operator ==(Object other) => 46 | identical(this, other) || 47 | other is ShowState && 48 | runtimeType == other.runtimeType && 49 | loadingStatus == other.loadingStatus && 50 | dates == other.dates && 51 | selectedDate == other.selectedDate && 52 | shows == other.shows; 53 | 54 | @override 55 | int get hashCode => 56 | loadingStatus.hashCode ^ 57 | dates.hashCode ^ 58 | selectedDate.hashCode ^ 59 | shows.hashCode; 60 | } 61 | -------------------------------------------------------------------------------- /core/lib/src/redux/store.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/networking/finnkino_api.dart'; 2 | import 'package:core/src/networking/tmdb_api.dart'; 3 | import 'package:core/src/redux/actor/actor_middleware.dart'; 4 | import 'package:core/src/redux/app/app_reducer.dart'; 5 | import 'package:core/src/redux/app/app_state.dart'; 6 | import 'package:core/src/redux/event/event_middleware.dart'; 7 | import 'package:core/src/redux/show/show_middleware.dart'; 8 | import 'package:core/src/redux/theater/theater_middleware.dart'; 9 | import 'package:http/http.dart'; 10 | import 'package:key_value_store/key_value_store.dart'; 11 | import 'package:redux/redux.dart'; 12 | 13 | Store createStore(Client client, KeyValueStore keyValueStore) { 14 | final tmdbApi = TMDBApi(client); 15 | final finnkinoApi = FinnkinoApi(client); 16 | 17 | return Store( 18 | appReducer, 19 | initialState: AppState.initial(), 20 | distinct: true, 21 | middleware: [ 22 | ActorMiddleware(tmdbApi), 23 | TheaterMiddleware(keyValueStore), 24 | ShowMiddleware(finnkinoApi), 25 | EventMiddleware(finnkinoApi), 26 | ], 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /core/lib/src/redux/theater/theater_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:core/src/models/theater.dart'; 4 | import 'package:core/src/parsers/theater_parser.dart'; 5 | import 'package:core/src/preloaded_data.dart'; 6 | import 'package:core/src/redux/_common/common_actions.dart'; 7 | import 'package:core/src/redux/app/app_state.dart'; 8 | import 'package:key_value_store/key_value_store.dart'; 9 | import 'package:kt_dart/collection.dart'; 10 | import 'package:redux/redux.dart'; 11 | 12 | class TheaterMiddleware extends MiddlewareClass { 13 | static const String kDefaultTheaterId = 'default_theater_id'; 14 | 15 | TheaterMiddleware(this.keyValueStore); 16 | 17 | final KeyValueStore keyValueStore; 18 | 19 | @override 20 | Future call( 21 | Store store, dynamic action, NextDispatcher next) async { 22 | if (action is InitAction) { 23 | await _init(action, next); 24 | } else if (action is ChangeCurrentTheaterAction) { 25 | await _changeCurrentTheater(action, next); 26 | } else { 27 | next(action); 28 | } 29 | } 30 | 31 | Future _init(InitAction action, NextDispatcher next) async { 32 | var theaterXml = PreloadedData.theaters; 33 | var theaters = TheaterParser.parse(theaterXml); 34 | var currentTheater = _getDefaultTheater(theaters); 35 | 36 | next(InitCompleteAction(theaters, currentTheater)); 37 | } 38 | 39 | Future _changeCurrentTheater( 40 | ChangeCurrentTheaterAction action, NextDispatcher next) async { 41 | keyValueStore.setString(kDefaultTheaterId, action.selectedTheater.id); 42 | next(action); 43 | } 44 | 45 | Theater _getDefaultTheater(KtList allTheaters) { 46 | var persistedTheaterId = keyValueStore.getString(kDefaultTheaterId); 47 | 48 | if (persistedTheaterId != null) { 49 | return allTheaters.single((theater) { 50 | return theater.id == persistedTheaterId; 51 | }); 52 | } 53 | 54 | /// Default to Helsinki -> no reason to load movie information for the 55 | /// whole country at first. 56 | return allTheaters.singleOrNull((theater) => theater.id == '1033') ?? 57 | allTheaters.first; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/lib/src/redux/theater/theater_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/redux/_common/common_actions.dart'; 2 | import 'package:core/src/redux/theater/theater_state.dart'; 3 | 4 | TheaterState theaterReducer(TheaterState state, dynamic action) { 5 | if (action is InitCompleteAction) { 6 | return state.copyWith( 7 | currentTheater: action.selectedTheater, theaters: action.theaters); 8 | } else if (action is ChangeCurrentTheaterAction) { 9 | return state.copyWith(currentTheater: action.selectedTheater); 10 | } 11 | 12 | return state; 13 | } -------------------------------------------------------------------------------- /core/lib/src/redux/theater/theater_selectors.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:core/src/redux/app/app_state.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | 5 | Theater currentTheaterSelector(AppState state) => 6 | state.theaterState.currentTheater; 7 | 8 | KtList theatersSelector(AppState state) => state.theaterState.theaters; 9 | -------------------------------------------------------------------------------- /core/lib/src/redux/theater/theater_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:kt_dart/collection.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | class TheaterState { 7 | TheaterState({ 8 | @required this.currentTheater, 9 | @required this.theaters, 10 | }); 11 | 12 | final Theater currentTheater; 13 | final KtList theaters; 14 | 15 | factory TheaterState.initial() { 16 | return TheaterState( 17 | currentTheater: null, 18 | theaters: emptyList(), 19 | ); 20 | } 21 | 22 | TheaterState copyWith({ 23 | Theater currentTheater, 24 | KtList theaters, 25 | }) { 26 | return TheaterState( 27 | currentTheater: currentTheater ?? this.currentTheater, 28 | theaters: theaters ?? this.theaters, 29 | ); 30 | } 31 | 32 | @override 33 | bool operator ==(Object other) => 34 | identical(this, other) || 35 | other is TheaterState && 36 | runtimeType == other.runtimeType && 37 | currentTheater == other.currentTheater && 38 | theaters == other.theaters; 39 | 40 | @override 41 | int get hashCode => currentTheater.hashCode ^ theaters.hashCode; 42 | } 43 | -------------------------------------------------------------------------------- /core/lib/src/tmdb_config.dart.sample: -------------------------------------------------------------------------------- 1 | class TMDBConfig { 2 | /// The TMDB API is mostly used for loading actor avatars. 3 | /// 4 | /// Having a real API key here is optional; if this doesn't 5 | /// contain the real API key, the app will still work, but 6 | /// the actor avatars won't load. 7 | static final String apiKey = ''; 8 | } -------------------------------------------------------------------------------- /core/lib/src/utils/clock.dart: -------------------------------------------------------------------------------- 1 | typedef DateTime DateTimeGetter(); 2 | 3 | /// A class that has only one purpose: getting the current time. 4 | /// 5 | /// Why? Because otherwise it's hard to test any code that depends on 6 | /// the current time. 7 | /// 8 | /// To see how to test with this, see: 9 | /// 10 | /// * [test/redux/show_middleware_test.dart] 11 | /// 12 | /// "Show usages" of [Clock.resetDateTimeGetter] will yield an up to date 13 | /// list of all tests that use this class. 14 | class Clock { 15 | /// The default date time getter, which returns the current date and time. 16 | static final defaultDateTimeGetter = () => DateTime.now(); 17 | 18 | /// Resets the current mock implementation (if any) for [getCurrentTime] 19 | /// method back to an implementation that returns the current date and time. 20 | static void resetDateTimeGetter() => getCurrentTime = defaultDateTimeGetter; 21 | 22 | /// Used by production code to check the current date and time. 23 | /// 24 | /// Switch this to custom implementation in tests in order to test production 25 | /// code that depends on current date and time. 26 | static DateTimeGetter getCurrentTime = defaultDateTimeGetter; 27 | } -------------------------------------------------------------------------------- /core/lib/src/utils/event_name_cleaner.dart: -------------------------------------------------------------------------------- 1 | class EventNameCleaner { 2 | /// Strips unneeded noise out of the event names. 3 | /// 4 | /// For example: 5 | /// 6 | /// "Avengers: Infinity War (2D)" -> "Avengers: Infinity War" 7 | /// "Avengers: Infinity War (2D dub)" -> "Avengers: Infinity War" 8 | /// 9 | /// For more, see test/event_name_cleaner_test.dart. 10 | static final _pattern = RegExp( 11 | r"(\s([23]D$|\(([23]D|dub|orig|spanish|swe|sing-along).*|\s*-\s*erikoisnäytös|\s*-\s*preview))", 12 | caseSensitive: false, 13 | ); 14 | 15 | static String cleanup(String name) { 16 | final matches = _pattern.allMatches(name); 17 | final hasNoise = matches.isNotEmpty; 18 | 19 | if (hasNoise) { 20 | // "noise" means (2D dub), (3D dub), etc. 21 | final noise = matches.first.group(1); 22 | return name.replaceFirst(noise, ''); 23 | } 24 | 25 | return name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/lib/src/utils/http_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | final _httpClient = HttpClient(); 6 | 7 | Future getRequest(Uri uri) async { 8 | final request = await _httpClient.getUrl(uri); 9 | final response = await request.close(); 10 | 11 | return response.transform(utf8.decoder).join(); 12 | } 13 | -------------------------------------------------------------------------------- /core/lib/src/utils/xml_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart' as xml; 2 | 3 | String tagContents(xml.XmlElement node, String tagName) { 4 | final contents = tagContentsOrNull(node, tagName); 5 | 6 | if (contents == null) { 7 | throw ArgumentError('Contents for $tagName were unexpectedly null.'); 8 | } 9 | 10 | return contents; 11 | } 12 | 13 | String tagContentsOrNull(xml.XmlElement node, String tagName) { 14 | final matches = node.findElements(tagName); 15 | 16 | if (matches.isNotEmpty) { 17 | return matches.single.text; 18 | } 19 | 20 | return null; 21 | } -------------------------------------------------------------------------------- /core/lib/src/viewmodels/events_page_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/models/loading_status.dart'; 3 | import 'package:core/src/redux/app/app_state.dart'; 4 | import 'package:core/src/redux/event/event_actions.dart'; 5 | import 'package:core/src/redux/event/event_selectors.dart'; 6 | import 'package:kt_dart/collection.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:redux/redux.dart'; 9 | 10 | class EventsPageViewModel { 11 | EventsPageViewModel({ 12 | @required this.status, 13 | @required this.events, 14 | @required this.refreshEvents, 15 | }); 16 | 17 | final LoadingStatus status; 18 | final KtList events; 19 | final Function refreshEvents; 20 | 21 | static EventsPageViewModel fromStore( 22 | Store store, 23 | EventListType type, 24 | ) { 25 | return EventsPageViewModel( 26 | status: type == EventListType.nowInTheaters 27 | ? store.state.eventState.nowInTheatersStatus 28 | : store.state.eventState.comingSoonStatus, 29 | events: type == EventListType.nowInTheaters 30 | ? nowInTheatersSelector(store.state) 31 | : comingSoonSelector(store.state), 32 | refreshEvents: () => store.dispatch(RefreshEventsAction(type)), 33 | ); 34 | } 35 | 36 | @override 37 | bool operator ==(Object other) => 38 | identical(this, other) || 39 | other is EventsPageViewModel && 40 | runtimeType == other.runtimeType && 41 | status == other.status && 42 | events == other.events; 43 | 44 | @override 45 | int get hashCode => status.hashCode ^ events.hashCode; 46 | } 47 | -------------------------------------------------------------------------------- /core/lib/src/viewmodels/showtime_page_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/loading_status.dart'; 2 | import 'package:core/src/models/show.dart'; 3 | import 'package:core/src/redux/app/app_state.dart'; 4 | import 'package:core/src/redux/show/show_actions.dart'; 5 | import 'package:core/src/redux/show/show_selectors.dart'; 6 | import 'package:kt_dart/collection.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:redux/redux.dart'; 9 | 10 | class ShowtimesPageViewModel { 11 | ShowtimesPageViewModel({ 12 | @required this.status, 13 | @required this.dates, 14 | @required this.selectedDate, 15 | @required this.shows, 16 | @required this.changeCurrentDate, 17 | @required this.refreshShowtimes, 18 | }); 19 | 20 | final LoadingStatus status; 21 | final KtList dates; 22 | final DateTime selectedDate; 23 | final KtList shows; 24 | final Function(DateTime) changeCurrentDate; 25 | final Function refreshShowtimes; 26 | 27 | static ShowtimesPageViewModel fromStore(Store store) { 28 | return ShowtimesPageViewModel( 29 | selectedDate: store.state.showState.selectedDate, 30 | dates: store.state.showState.dates, 31 | status: store.state.showState.loadingStatus, 32 | shows: showsSelector(store.state), 33 | changeCurrentDate: (newDate) { 34 | store.dispatch(ChangeCurrentDateAction(newDate)); 35 | }, 36 | refreshShowtimes: () => store.dispatch(RefreshShowsAction()), 37 | ); 38 | } 39 | 40 | @override 41 | bool operator ==(Object other) => 42 | identical(this, other) || 43 | other is ShowtimesPageViewModel && 44 | runtimeType == other.runtimeType && 45 | status == other.status && 46 | dates == other.dates && 47 | selectedDate == other.selectedDate && 48 | shows == other.shows; 49 | 50 | @override 51 | int get hashCode => 52 | status.hashCode ^ dates.hashCode ^ selectedDate.hashCode ^ shows.hashCode; 53 | } 54 | -------------------------------------------------------------------------------- /core/lib/src/viewmodels/theater_list_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:core/src/redux/_common/common_actions.dart'; 3 | import 'package:core/src/redux/app/app_state.dart'; 4 | import 'package:core/src/redux/theater/theater_selectors.dart'; 5 | import 'package:kt_dart/collection.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | class TheaterListViewModel { 10 | TheaterListViewModel({ 11 | @required this.currentTheater, 12 | @required this.theaters, 13 | @required this.changeCurrentTheater, 14 | }); 15 | 16 | final Theater currentTheater; 17 | final KtList theaters; 18 | final Function(Theater) changeCurrentTheater; 19 | 20 | static TheaterListViewModel fromStore(Store store) { 21 | return TheaterListViewModel( 22 | currentTheater: currentTheaterSelector(store.state), 23 | theaters: theatersSelector(store.state), 24 | changeCurrentTheater: (theater) { 25 | store.dispatch(ChangeCurrentTheaterAction(theater)); 26 | }, 27 | ); 28 | } 29 | 30 | @override 31 | bool operator ==(Object other) => 32 | identical(this, other) || 33 | other is TheaterListViewModel && 34 | runtimeType == other.runtimeType && 35 | currentTheater == other.currentTheater && 36 | theaters == other.theaters; 37 | 38 | @override 39 | int get hashCode => currentTheater.hashCode ^ theaters.hashCode; 40 | } 41 | -------------------------------------------------------------------------------- /core/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: core 2 | description: Common shared code for inKino mobile and web. 3 | version: 0.0.1 4 | #homepage: https://www.example.com 5 | #author: ironman 6 | 7 | environment: 8 | sdk: '>=2.0.0-dev.58.0 <3.0.0' 9 | 10 | dependencies: 11 | redux: ^3.0.0 12 | xml: ^3.0.1 13 | intl: any 14 | http: ^0.12.0 15 | reselect: ^0.4.0 16 | key_value_store: ^1.0.0 17 | kt_dart: ^0.5.0 18 | 19 | dev_dependencies: 20 | test: ^1.3.0 21 | mockito: ^3.0.0 22 | intl_translation: 0.17.0 23 | -------------------------------------------------------------------------------- /core/test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:core/src/networking/finnkino_api.dart'; 4 | import 'package:core/src/redux/app/app_state.dart'; 5 | import 'package:key_value_store/key_value_store.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | class MockFile extends Mock implements File {} 10 | 11 | class MockFinnkinoApi extends Mock implements FinnkinoApi {} 12 | 13 | class MockStore extends Mock implements Store {} 14 | class MockKeyValueStore extends Mock implements KeyValueStore {} -------------------------------------------------------------------------------- /core/test/networking/imgix_url_rewriter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/networking/image_url_rewriter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('ImgixUrlRewriter', () { 6 | test('url rewriting tests', () { 7 | expect( 8 | rewriteImageUrl( 9 | 'http://media.finnkino.fi/1012/Event_11960/gallery/THUMB_AntManAndTheWasp_1200d.jpg', 10 | ), 11 | 'https://inkino.imgix.net/1012/Event_11960/gallery/THUMB_AntManAndTheWasp_1200d.jpg?auto=format,compress', 12 | ); 13 | 14 | expect( 15 | rewriteImageUrl( 16 | 'https://media.finnkino.fi/1012/Event_11960/gallery/THUMB_AntManAndTheWasp_1200d.jpg', 17 | ), 18 | 'https://inkino.imgix.net/1012/Event_11960/gallery/THUMB_AntManAndTheWasp_1200d.jpg?auto=format,compress', 19 | ); 20 | 21 | expect(rewriteImageUrl(null), null); 22 | expect( 23 | rewriteImageUrl('Not yet rated'), 24 | 'https://inkino.imgix.net/images/rating_large_Tulossa.png?auto=format,compress', 25 | ); 26 | expect( 27 | rewriteImageUrl('https://media.finnkino.fi/images/rating_large_Not%20yet%20rated.png'), 28 | 'https://inkino.imgix.net/images/rating_large_Tulossa.png?auto=format,compress', 29 | ); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /core/test/parsers/theater_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:core/src/parsers/theater_parser.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'theater_test_seeds.ignore.dart'; 7 | 8 | void main() { 9 | group('TheaterParser', () { 10 | test('parsing test', () { 11 | KtList deserialized = TheaterParser.parse(theatersXml); 12 | expect(deserialized.size, 3); 13 | 14 | expect(deserialized[0].id, '1029'); 15 | expect(deserialized[0].name, 'All theaters'); 16 | 17 | expect(deserialized[1].id, '001'); 18 | expect(deserialized[1].name, 'Gotham: Theater One'); 19 | 20 | expect(deserialized[2].id, '002'); 21 | expect(deserialized[2].name, 'Gotham: Theater Two'); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /core/test/parsers/theater_test_seeds.ignore.dart: -------------------------------------------------------------------------------- 1 | const String theatersXml = ''' 2 | 3 | 4 | 1029 5 | Valitse alue/teatteri 6 | 7 | 8 | 001 9 | Gotham: THEATER ONE 10 | 11 | 12 | 002 13 | Gotham: THEATER TWO 14 | 15 | '''; 16 | -------------------------------------------------------------------------------- /core/test/redux/search_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/redux/_common/search.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('searchQueryReducer', () { 6 | test('search reducer tests', () { 7 | final state = null; 8 | final reducedState = 9 | searchQueryReducer(state, SearchQueryChangedAction('test')); 10 | 11 | expect(reducedState, 'test'); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /core/test/redux/show_reducer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/redux/show/show_actions.dart'; 2 | import 'package:core/src/redux/show/show_reducer.dart'; 3 | import 'package:core/src/redux/show/show_state.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('ShowReducer', () { 9 | test( 10 | 'when called with ShowDatesUpdatedAction, should update state with 7 days from today', 11 | () { 12 | final initialState = ShowState.initial(); 13 | final reducedState = showReducer( 14 | initialState, 15 | ShowDatesUpdatedAction( 16 | listOf( 17 | DateTime(2018, 1, 1), 18 | DateTime(2018, 1, 2), 19 | ), 20 | ), 21 | ); 22 | 23 | expect( 24 | reducedState.dates, 25 | listOf( 26 | DateTime(2018, 1, 1), 27 | DateTime(2018, 1, 2), 28 | ), 29 | ); 30 | 31 | // Should also select the first date in the list 32 | expect(reducedState.selectedDate, DateTime(2018, 1, 1)); 33 | }, 34 | ); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /core/test/utils/event_name_cleaner_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/utils/event_name_cleaner.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('$EventNameCleaner', () { 6 | String cleanup(String noise) => EventNameCleaner.cleanup(noise); 7 | 8 | test('cleans up unneeded noise from movie names', () { 9 | expect(cleanup('Avengers: Infinity War (2D)'), 'Avengers: Infinity War'); 10 | expect(cleanup('Avengers: Infinity War (3D)'), 'Avengers: Infinity War'); 11 | expect(cleanup('Avengers: Infinity War 2D'), 'Avengers: Infinity War'); 12 | expect(cleanup('Avengers: Infinity War 3D'), 'Avengers: Infinity War'); 13 | expect(cleanup('Avengers: Infinity War (dub)'), 'Avengers: Infinity War'); 14 | expect(cleanup('Avengers: Infinity War (2D dub)'), 'Avengers: Infinity War'); 15 | expect(cleanup('Avengers: Infinity War (2D orig)'), 'Avengers: Infinity War'); 16 | expect(cleanup('Avengers: Infinity War (2D spanish)'), 'Avengers: Infinity War'); 17 | expect(cleanup('Avengers: Infinity War (2D) (dub)'), 'Avengers: Infinity War'); 18 | expect(cleanup('Avengers: Infinity War (2D) (orig)'), 'Avengers: Infinity War'); 19 | expect(cleanup('Avengers: Infinity War (2D) (spanish)'), 'Avengers: Infinity War'); 20 | expect(cleanup('Avengers: Infinity War (3D dub)'), 'Avengers: Infinity War'); 21 | expect(cleanup('Avengers: Infinity War (3D orig)'), 'Avengers: Infinity War'); 22 | expect(cleanup('Avengers: Infinity War (3D spanish)'), 'Avengers: Infinity War'); 23 | expect(cleanup('Avengers: Infinity War (swe)'), 'Avengers: Infinity War'); 24 | expect(cleanup('Bohemian Rhapsody -erikoisnäytös'), 'Bohemian Rhapsody'); 25 | expect(cleanup('Mamma Mia! Here We Go Again (SING-ALONG)'), 'Mamma Mia! Here We Go Again'); 26 | expect(cleanup('Fantastic Beasts: The Crimes of Grindelwald - preview'), 'Fantastic Beasts: The Crimes of Grindelwald'); 27 | 28 | // These should stay the same 29 | expect(cleanup('BPM (beats per minute)'), 'BPM (beats per minute)'); 30 | expect(cleanup('That awesome spanish girl'), 'That awesome spanish girl'); 31 | expect(cleanup('The 3D movie'), 'The 3D movie'); 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /core/test/viewmodels/events_page_view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/event.dart'; 2 | import 'package:core/src/models/loading_status.dart'; 3 | import 'package:core/src/viewmodels/events_page_view_model.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('EventsPageViewModel', () { 9 | test('equal', () { 10 | final first = EventsPageViewModel( 11 | status: LoadingStatus.success, 12 | events: listOf( 13 | Event(id: 'abc123'), 14 | ), 15 | refreshEvents: () {}, 16 | ); 17 | 18 | final second = EventsPageViewModel( 19 | status: LoadingStatus.success, 20 | events: listOf( 21 | Event(id: 'abc123'), 22 | ), 23 | refreshEvents: () {}, 24 | ); 25 | 26 | expect(first, second); 27 | }); 28 | 29 | test('not equal', () { 30 | final first = EventsPageViewModel( 31 | status: LoadingStatus.success, 32 | events: listOf( 33 | Event(id: 'abc123'), 34 | ), 35 | refreshEvents: () {}, 36 | ); 37 | 38 | final second = EventsPageViewModel( 39 | status: LoadingStatus.success, 40 | events: listOf( 41 | Event(id: 'xyz456'), 42 | ), 43 | refreshEvents: () {}, 44 | ); 45 | 46 | expect(first, isNot(second)); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /core/test/viewmodels/showtimes_page_view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/loading_status.dart'; 2 | import 'package:core/src/models/show.dart'; 3 | import 'package:core/src/viewmodels/showtime_page_view_model.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('ShowtimesPageViewModel', () { 9 | test('equal', () { 10 | final first = ShowtimesPageViewModel( 11 | status: LoadingStatus.success, 12 | dates: listOf(DateTime(2018)), 13 | selectedDate: null, 14 | shows: listOf(Show(id: 'abc123')), 15 | changeCurrentDate: (DateTime newDate) {}, 16 | refreshShowtimes: () {}, 17 | ); 18 | 19 | final second = ShowtimesPageViewModel( 20 | status: LoadingStatus.success, 21 | dates: listOf(DateTime(2018)), 22 | selectedDate: null, 23 | shows: listOf(Show(id: 'abc123')), 24 | changeCurrentDate: (DateTime newDate) {}, 25 | refreshShowtimes: () {}, 26 | ); 27 | 28 | expect(first, second); 29 | }); 30 | 31 | test('not equal', () { 32 | final first = ShowtimesPageViewModel( 33 | status: LoadingStatus.success, 34 | dates: listOf(DateTime(2018)), 35 | selectedDate: null, 36 | shows: listOf(Show(id: 'abc123')), 37 | changeCurrentDate: (DateTime newDate) {}, 38 | refreshShowtimes: () {}, 39 | ); 40 | 41 | final second = ShowtimesPageViewModel( 42 | status: LoadingStatus.success, 43 | dates: listOf(DateTime(2018)), 44 | selectedDate: null, 45 | shows: listOf(Show(id: 'xyz456')), 46 | changeCurrentDate: (DateTime newDate) {}, 47 | refreshShowtimes: () {}, 48 | ); 49 | 50 | expect(first, isNot(second)); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /core/test/viewmodels/theater_list_view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/src/models/theater.dart'; 2 | import 'package:core/src/viewmodels/theater_list_view_model.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('TheaterListViewModel', () { 8 | test('equal', () { 9 | final first = TheaterListViewModel( 10 | currentTheater: Theater(id: 'abc123', name: 'Test 1'), 11 | theaters: listOf( 12 | Theater(id: 'abc123', name: 'Test 1'), 13 | Theater(id: 'xyz456', name: 'Test 2'), 14 | ), 15 | changeCurrentTheater: (Theater newTheater) {}, 16 | ); 17 | 18 | final second = TheaterListViewModel( 19 | currentTheater: Theater(id: 'abc123', name: 'Test 1'), 20 | theaters: listOf( 21 | Theater(id: 'abc123', name: 'Test 1'), 22 | Theater(id: 'xyz456', name: 'Test 2'), 23 | ), 24 | changeCurrentTheater: (Theater newTheater) {}, 25 | ); 26 | 27 | expect(first, second); 28 | }); 29 | 30 | test('not equal', () { 31 | final first = TheaterListViewModel( 32 | currentTheater: Theater(id: 'abc123', name: 'Test 1'), 33 | theaters: listOf( 34 | Theater(id: 'abc123', name: 'Test 1'), 35 | Theater(id: 'xyz456', name: 'Test 2'), 36 | ), 37 | changeCurrentTheater: (Theater newTheater) {}, 38 | ); 39 | 40 | final second = TheaterListViewModel( 41 | currentTheater: Theater(id: 'abc123', name: 'Test 1'), 42 | theaters: listOf( 43 | Theater(id: 'efg123', name: 'Test 3'), 44 | Theater(id: 'hjk456', name: 'Test 4'), 45 | ), 46 | changeCurrentTheater: (Theater newTheater) {}, 47 | ); 48 | 49 | expect(first, isNot(second)); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /mobile/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: ab4506cad2a860e1cb6186c0957eeb86024a7c6b 8 | channel: dev 9 | -------------------------------------------------------------------------------- /mobile/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | language: 3 | enableSuperMixins: true 4 | 5 | linter: 6 | rules: 7 | - prefer_const_constructors -------------------------------------------------------------------------------- /mobile/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | *.jks 12 | key.properties 13 | google-play-service-account.json -------------------------------------------------------------------------------- /mobile/android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /mobile/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 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | def keystorePropertiesFile = rootProject.file("key.properties") 18 | def keystoreProperties = null 19 | 20 | if (keystorePropertiesFile.exists()) { 21 | keystoreProperties = new Properties() 22 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 23 | } 24 | 25 | android { 26 | compileSdkVersion 27 27 | 28 | lintOptions { 29 | disable 'InvalidPackage' 30 | } 31 | 32 | defaultConfig { 33 | applicationId "com.roughike.inkino" 34 | minSdkVersion 16 35 | targetSdkVersion 27 36 | versionCode 7 37 | versionName "2.0.1" 38 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 39 | } 40 | 41 | signingConfigs { 42 | release { 43 | if (keystoreProperties != null) { 44 | keyAlias keystoreProperties['keyAlias'] 45 | keyPassword keystoreProperties['keyPassword'] 46 | storeFile file(keystoreProperties['storeFile']) 47 | storePassword keystoreProperties['storePassword'] 48 | } 49 | } 50 | } 51 | buildTypes { 52 | release { 53 | signingConfig keystoreProperties != null ? 54 | signingConfigs.release : signingConfigs.debug 55 | } 56 | } 57 | } 58 | 59 | flutter { 60 | source '../..' 61 | } 62 | 63 | dependencies { 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 66 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 67 | } 68 | -------------------------------------------------------------------------------- /mobile/android/app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/release/app-release.apk -------------------------------------------------------------------------------- /mobile/android/app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":5},"path":"app-release.apk","properties":{"packageId":"com.roughike.inkino","split":"","minSdkVersion":"16"}}] -------------------------------------------------------------------------------- /mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 27 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/java/com/roughike/inkino/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.roughike.inkino; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.0.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /mobile/android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file "google-play-service-account.json" 2 | package_name "com.roughike.inkino" 3 | -------------------------------------------------------------------------------- /mobile/android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:android) 2 | 3 | platform :android do 4 | desc "Submit a new Beta Build to Google Play internal testing" 5 | lane :internal do 6 | upload_to_play_store(apk: "../build/app/outputs/apk/release/app-release.apk", track: "internal") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Initial upload for Fastlane. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Initial release. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Initial release. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Initial release. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | New look, smaller app size and some UI tweaks! Also the usual performance improvements. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | New look, smaller app size and some UI tweaks! Also the usual performance improvements. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | inKino is the unofficial Finnkino client that's minimalistic, fast, and delightful to use. 2 | 3 | - View movie details and trailers 4 | - See showtimes for your local theater 5 | - Search for movies and showtimes 6 | - Buy or reserve tickets 7 | 8 | With inKino, you have movie information and showtimes right at your fingertips. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | inKino is the fastest way for browsing Finnkino's movies and showtimes. -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | inKino - Showtime browser for Finnkino theaters -------------------------------------------------------------------------------- /mobile/android/fastlane/metadata/android/en-US/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/android/fastlane/metadata/android/en-US/video.txt -------------------------------------------------------------------------------- /mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /mobile/assets/images/1x1_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/assets/images/1x1_transparent.png -------------------------------------------------------------------------------- /mobile/assets/images/background_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/assets/images/background_image.jpg -------------------------------------------------------------------------------- /mobile/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/assets/images/logo.png -------------------------------------------------------------------------------- /mobile/assets/images/powered_by_tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/assets/images/powered_by_tmdb.png -------------------------------------------------------------------------------- /mobile/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | *.pbxuser 16 | *.mode1v3 17 | *.mode2v3 18 | *.perspectivev3 19 | 20 | !default.pbxuser 21 | !default.mode1v3 22 | !default.mode2v3 23 | !default.perspectivev3 24 | 25 | xcuserdata 26 | 27 | *.moved-aside 28 | 29 | *.pyc 30 | *sync/ 31 | Icon? 32 | .tags* 33 | 34 | /Flutter/app.flx 35 | /Flutter/app.zip 36 | /Flutter/flutter_assets/ 37 | /Flutter/App.framework 38 | /Flutter/Flutter.framework 39 | /Flutter/Generated.xcconfig 40 | /ServiceDefinitions.json 41 | 42 | Pods/ 43 | -------------------------------------------------------------------------------- /mobile/ios/.symlinks/flutter: -------------------------------------------------------------------------------- 1 | /Users/iirokrankka/flutter/bin/cache/artifacts/engine -------------------------------------------------------------------------------- /mobile/ios/.symlinks/plugins/key_value_store_flutter: -------------------------------------------------------------------------------- 1 | /Users/iirokrankka/flutter/.pub-cache/hosted/pub.dartlang.org/key_value_store_flutter-1.0.0 -------------------------------------------------------------------------------- /mobile/ios/.symlinks/plugins/path_provider: -------------------------------------------------------------------------------- 1 | /Users/iirokrankka/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1 -------------------------------------------------------------------------------- /mobile/ios/.symlinks/plugins/shared_preferences: -------------------------------------------------------------------------------- 1 | /Users/iirokrankka/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.4.3 -------------------------------------------------------------------------------- /mobile/ios/.symlinks/plugins/url_launcher: -------------------------------------------------------------------------------- 1 | /Users/iirokrankka/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-3.0.3 -------------------------------------------------------------------------------- /mobile/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 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /mobile/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /mobile/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /mobile/ios/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /mobile/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mobile/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - key_value_store_flutter (0.0.1): 4 | - Flutter 5 | - path_provider (0.0.1): 6 | - Flutter 7 | - shared_preferences (0.0.1): 8 | - Flutter 9 | - url_launcher (0.0.1): 10 | - Flutter 11 | 12 | DEPENDENCIES: 13 | - Flutter (from `.symlinks/flutter/ios`) 14 | - key_value_store_flutter (from `.symlinks/plugins/key_value_store_flutter/ios`) 15 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 16 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 17 | - url_launcher (from `.symlinks/plugins/url_launcher/ios`) 18 | 19 | EXTERNAL SOURCES: 20 | Flutter: 21 | :path: ".symlinks/flutter/ios" 22 | key_value_store_flutter: 23 | :path: ".symlinks/plugins/key_value_store_flutter/ios" 24 | path_provider: 25 | :path: ".symlinks/plugins/path_provider/ios" 26 | shared_preferences: 27 | :path: ".symlinks/plugins/shared_preferences/ios" 28 | url_launcher: 29 | :path: ".symlinks/plugins/url_launcher/ios" 30 | 31 | SPEC CHECKSUMS: 32 | Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 33 | key_value_store_flutter: 5bf1aa677945ff2a2fc075b0e26f3bcaaecf049f 34 | path_provider: 09407919825bfe3c2deae39453b7a5b44f467873 35 | shared_preferences: 5a1d487c427ee18fcd3ea1f2a131569481834b53 36 | url_launcher: 92b89c1029a0373879933c21642958c874539095 37 | 38 | PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2 39 | 40 | COCOAPODS: 1.5.3 41 | -------------------------------------------------------------------------------- /mobile/ios/Runner.app.dSYM.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner.app.dSYM.zip -------------------------------------------------------------------------------- /mobile/ios/Runner.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner.ipa -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /mobile/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-1024.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-20@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-29@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-40@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-60@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-60@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-76.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-76@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/ios_app_icon-83.5@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mobile/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 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /mobile/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. -------------------------------------------------------------------------------- /mobile/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 | -------------------------------------------------------------------------------- /mobile/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 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | inKino 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | inkino 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.0.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 8 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | arm64 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /mobile/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mobile/ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("com.roughike.inkino.ios") # The bundle identifier of your app 2 | apple_id("iiro.krankka@icloud.com") # Your Apple email address 3 | 4 | itc_team_id("119010849") # App Store Connect Team ID 5 | team_id("9U3A2H2X3Y") # Developer Portal Team ID 6 | 7 | # For more information about the Appfile, see: 8 | # https://docs.fastlane.tools/advanced/#appfile 9 | -------------------------------------------------------------------------------- /mobile/ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | desc "Push a new beta build to TestFlight" 5 | lane :beta do 6 | build_app(workspace: "Runner.xcworkspace", scheme: "Runner") 7 | upload_to_testflight 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /mobile/lib/assets.dart: -------------------------------------------------------------------------------- 1 | class ImageAssets { 2 | static const String transparentImage = 'assets/images/1x1_transparent.png'; 3 | static const String backgroundImage = 'assets/images/background_image.jpg'; 4 | static const String poweredByTMDBLogo = 'assets/images/powered_by_tmdb.png'; 5 | } 6 | 7 | class OtherAssets { 8 | static const String preloadedTheaters = 'assets/preloaded_data/theaters.xml'; 9 | } 10 | -------------------------------------------------------------------------------- /mobile/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:core/core.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_localizations/flutter_localizations.dart'; 7 | import 'package:flutter_redux/flutter_redux.dart'; 8 | import 'package:http/http.dart'; 9 | import 'package:inkino/message_provider.dart'; 10 | import 'package:inkino/ui/main_page.dart'; 11 | import 'package:key_value_store_flutter/key_value_store_flutter.dart'; 12 | import 'package:redux/redux.dart'; 13 | import 'package:shared_preferences/shared_preferences.dart'; 14 | 15 | Future main() async { 16 | final prefs = await SharedPreferences.getInstance(); 17 | final keyValueStore = FlutterKeyValueStore(prefs); 18 | final store = createStore(Client(), keyValueStore); 19 | 20 | FinnkinoApi.useFinnish = ui.window.locale.languageCode == 'fi'; 21 | runApp(InKinoApp(store)); 22 | } 23 | 24 | final supportedLocales = const [ 25 | const Locale('en', 'US'), 26 | const Locale('fi', 'FI'), 27 | ]; 28 | 29 | final localizationsDelegates = [ 30 | const InKinoLocalizationsDelegate(), 31 | GlobalMaterialLocalizations.delegate, 32 | GlobalWidgetsLocalizations.delegate, 33 | ]; 34 | 35 | class InKinoApp extends StatefulWidget { 36 | InKinoApp(this.store); 37 | final Store store; 38 | 39 | @override 40 | _InKinoAppState createState() => _InKinoAppState(); 41 | } 42 | 43 | class _InKinoAppState extends State { 44 | @override 45 | void initState() { 46 | super.initState(); 47 | widget.store.dispatch(InitAction()); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return StoreProvider( 53 | store: widget.store, 54 | child: MaterialApp( 55 | title: 'inKino', 56 | theme: ThemeData( 57 | primaryColor: const Color(0xFF1C306D), 58 | accentColor: const Color(0xFFFFAD32), 59 | scaffoldBackgroundColor: Colors.transparent, 60 | ), 61 | home: const MainPage(), 62 | supportedLocales: supportedLocales, 63 | localizationsDelegates: localizationsDelegates, 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /mobile/lib/message_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:core/core.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:intl/intl.dart'; 7 | 8 | class MessageProvider { 9 | MessageProvider(this.messages); 10 | final Messages messages; 11 | 12 | static Future load(Locale locale) { 13 | final String name = 14 | locale.countryCode.isEmpty ? locale.languageCode : locale.toString(); 15 | final String localeName = Intl.canonicalizedLocale(name); 16 | 17 | return initializeMessages(localeName).then((_) { 18 | Intl.defaultLocale = localeName; 19 | return MessageProvider(Messages()); 20 | }); 21 | } 22 | 23 | static Messages of(BuildContext context) { 24 | return Localizations.of(context, MessageProvider).messages; 25 | } 26 | } 27 | 28 | class InKinoLocalizationsDelegate 29 | extends LocalizationsDelegate { 30 | const InKinoLocalizationsDelegate(); 31 | 32 | @override 33 | bool isSupported(Locale locale) => ['en', 'fi'].contains(locale.languageCode); 34 | 35 | @override 36 | Future load(Locale locale) => MessageProvider.load(locale); 37 | 38 | @override 39 | bool shouldReload(InKinoLocalizationsDelegate old) => false; 40 | } 41 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/platform_adaptive_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class PlatformAdaptiveProgressIndicator extends StatelessWidget { 7 | const PlatformAdaptiveProgressIndicator() : super(); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Platform.isIOS 12 | ? const CupertinoActivityIndicator() 13 | : const CircularProgressIndicator(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/widget_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | void addIfNonNull(Widget widget, List children) { 4 | if (widget != null) { 5 | children.add(widget); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mobile/lib/ui/events/event_release_date_information.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:inkino/message_provider.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | class EventReleaseDateInformation extends StatelessWidget { 7 | static final _releaseDateFormat = DateFormat('dd.MM.yyyy'); 8 | 9 | EventReleaseDateInformation(this.event); 10 | final Event event; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final messages = MessageProvider.of(context); 15 | 16 | return Container( 17 | color: Colors.black87, 18 | padding: const EdgeInsets.only( 19 | top: 5.0, 20 | right: 20.0, 21 | bottom: 5.0, 22 | left: 10.0, 23 | ), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Text( 28 | messages.releaseDate, 29 | style: TextStyle( 30 | color: Theme.of(context).accentColor, 31 | fontSize: 12.0, 32 | fontWeight: FontWeight.bold, 33 | ), 34 | ), 35 | const SizedBox(height: 2.0), 36 | Text( 37 | _releaseDateFormat.format(event.releaseDate), 38 | style: const TextStyle( 39 | color: const Color(0xFFFEFEFE), 40 | fontWeight: FontWeight.w300, 41 | fontSize: 16.0, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mobile/lib/ui/events/events_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_redux/flutter_redux.dart'; 5 | import 'package:inkino/message_provider.dart'; 6 | import 'package:inkino/ui/common/info_message_view.dart'; 7 | import 'package:inkino/ui/common/loading_view.dart'; 8 | import 'package:inkino/ui/common/platform_adaptive_progress_indicator.dart'; 9 | import 'package:inkino/ui/events/event_grid.dart'; 10 | 11 | class EventsPage extends StatelessWidget { 12 | EventsPage(this.listType); 13 | final EventListType listType; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return StoreConnector( 18 | distinct: true, 19 | onInit: (store) => store.dispatch(FetchComingSoonEventsIfNotLoadedAction()), 20 | converter: (store) => EventsPageViewModel.fromStore(store, listType), 21 | builder: (_, viewModel) => EventsPageContent(viewModel, listType), 22 | ); 23 | } 24 | } 25 | 26 | class EventsPageContent extends StatelessWidget { 27 | EventsPageContent(this.viewModel, this.listType); 28 | final EventsPageViewModel viewModel; 29 | final EventListType listType; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final messages = MessageProvider.of(context); 34 | return LoadingView( 35 | status: viewModel.status, 36 | loadingContent: const PlatformAdaptiveProgressIndicator(), 37 | errorContent: ErrorView( 38 | description: messages.errorLoadingEvents, 39 | onRetry: viewModel.refreshEvents, 40 | ), 41 | successContent: EventGrid( 42 | listType: listType, 43 | events: viewModel.events, 44 | onReloadCallback: viewModel.refreshEvents, 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mobile/lib/ui/inkino_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class InkinoBottomBar extends StatelessWidget { 5 | InkinoBottomBar({ 6 | @required this.currentIndex, 7 | @required this.onTap, 8 | @required this.items, 9 | }); 10 | 11 | final int currentIndex; 12 | final ValueChanged onTap; 13 | final List items; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | /// Yes - I'm using CupertinoTabBar on both Android and iOS. It looks dope. 18 | /// I'm not a designer and only God can judge me. (╯°□°)╯︵ ┻━┻ 19 | return CupertinoTabBar( 20 | backgroundColor: Colors.black54, 21 | inactiveColor: Colors.white54, 22 | activeColor: Colors.white, 23 | iconSize: 24.0, 24 | currentIndex: currentIndex, 25 | onTap: onTap, 26 | items: items, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mobile/lib/ui/showtimes/showtimes_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_redux/flutter_redux.dart'; 5 | import 'package:inkino/ui/common/info_message_view.dart'; 6 | import 'package:inkino/ui/common/loading_view.dart'; 7 | import 'package:inkino/ui/common/platform_adaptive_progress_indicator.dart'; 8 | import 'package:inkino/ui/showtimes/showtime_date_selector.dart'; 9 | import 'package:inkino/ui/showtimes/showtime_list.dart'; 10 | 11 | class ShowtimesPage extends StatelessWidget { 12 | const ShowtimesPage(); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return StoreConnector( 17 | distinct: true, 18 | onInit: (store) => store.dispatch(FetchShowsIfNotLoadedAction()), 19 | converter: (store) => ShowtimesPageViewModel.fromStore(store), 20 | builder: (_, viewModel) => ShowtimesPageContent(viewModel), 21 | ); 22 | } 23 | } 24 | 25 | class ShowtimesPageContent extends StatelessWidget { 26 | ShowtimesPageContent(this.viewModel); 27 | final ShowtimesPageViewModel viewModel; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Column( 32 | crossAxisAlignment: CrossAxisAlignment.stretch, 33 | children: [ 34 | ShowtimeDateSelector(viewModel), 35 | Expanded( 36 | child: LoadingView( 37 | status: viewModel.status, 38 | loadingContent: const PlatformAdaptiveProgressIndicator(), 39 | errorContent: ErrorView(onRetry: viewModel.refreshShowtimes), 40 | successContent: ShowtimeList(viewModel.status, viewModel.shows), 41 | ), 42 | ), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mobile/lib/ui/theater_list/theater_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_redux/flutter_redux.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | class TheaterList extends StatelessWidget { 7 | TheaterList({ 8 | @required this.onTheaterTapped, 9 | }); 10 | 11 | final VoidCallback onTheaterTapped; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return StoreConnector( 16 | distinct: true, 17 | converter: (store) => TheaterListViewModel.fromStore(store), 18 | builder: (BuildContext context, TheaterListViewModel viewModel) { 19 | return TheaterListContent( 20 | onTheaterTapped: onTheaterTapped, 21 | viewModel: viewModel, 22 | ); 23 | }, 24 | ); 25 | } 26 | } 27 | 28 | class TheaterListContent extends StatelessWidget { 29 | TheaterListContent({ 30 | @required this.onTheaterTapped, 31 | @required this.viewModel, 32 | }); 33 | 34 | final VoidCallback onTheaterTapped; 35 | final TheaterListViewModel viewModel; 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scrollbar( 40 | child: ListView.builder( 41 | padding: EdgeInsets.zero, 42 | itemCount: viewModel.theaters.size, 43 | itemBuilder: (BuildContext context, int index) { 44 | final theater = viewModel.theaters[index]; 45 | final isSelected = viewModel.currentTheater.id == theater.id; 46 | final backgroundColor = 47 | isSelected ? Colors.black54 : Colors.transparent; 48 | final foregroundColor = 49 | isSelected ? Colors.white : Colors.white.withOpacity(0.56); 50 | 51 | return Material( 52 | color: backgroundColor, 53 | child: ListTile( 54 | onTap: () { 55 | viewModel.changeCurrentTheater(theater); 56 | onTheaterTapped(); 57 | }, 58 | selected: isSelected, 59 | title: Text( 60 | theater.name, 61 | style: TextStyle(color: foregroundColor), 62 | ), 63 | ), 64 | ); 65 | }, 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mobile/lib/ui/theater_list/theater_selector_popup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:inkino/ui/theater_list/theater_list.dart'; 3 | 4 | class TheaterSelectorPopup extends PopupRoute { 5 | final opacityTween = Tween(begin: 0.0, end: 1.0); 6 | final positionTween = Tween( 7 | begin: const Offset(0.0, -1.0), 8 | end: Offset.zero, 9 | ); 10 | 11 | @override 12 | Color get barrierColor => null; 13 | 14 | @override 15 | bool get barrierDismissible => true; 16 | 17 | @override 18 | String get barrierLabel => null; 19 | 20 | @override 21 | Duration get transitionDuration => kThemeAnimationDuration; 22 | 23 | @override 24 | Widget buildTransitions(BuildContext context, Animation animation, 25 | Animation secondaryAnimation, Widget child) { 26 | final topPadding = MediaQuery.of(context).padding.top + kToolbarHeight; 27 | final curve = CurvedAnimation( 28 | parent: animation, 29 | curve: Curves.fastOutSlowIn, 30 | reverseCurve: Curves.decelerate, 31 | ); 32 | 33 | return Padding( 34 | padding: EdgeInsets.only(top: topPadding), 35 | child: ClipRect( 36 | child: FadeTransition( 37 | opacity: opacityTween.animate(curve), 38 | child: SlideTransition( 39 | position: positionTween.animate(curve), 40 | child: child, 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | 47 | @override 48 | Widget buildPage(BuildContext context, Animation animation, 49 | Animation secondaryAnimation) { 50 | return Container( 51 | color: const Color(0xFF152451), 52 | child: TheaterList(onTheaterTapped: () => Navigator.pop(context)), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mobile/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: inkino 2 | description: A new Flutter application. 3 | 4 | dependencies: 5 | core: 6 | path: ../core 7 | flutter: 8 | sdk: flutter 9 | flutter_localizations: 10 | sdk: flutter 11 | xml: ^3.0.0 12 | flutter_redux: ^0.5.0 13 | intl: ^0.15.2 14 | url_launcher: ^3.0.0 15 | path_provider: ^0.4.0 16 | key_value_store_flutter: ^1.0.0 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | mockito: ^3.0.0 22 | image_test_utils: ^1.0.0 23 | 24 | flutter: 25 | uses-material-design: true 26 | assets: 27 | - assets/images/1x1_transparent.png 28 | - assets/images/powered_by_tmdb.png 29 | - assets/images/background_image.jpg 30 | - assets/images/logo.png -------------------------------------------------------------------------------- /mobile/test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class MockFile extends Mock implements File {} 8 | 9 | class MockAssetBundle extends Mock implements AssetBundle {} 10 | 11 | class MockPreferences extends Mock implements SharedPreferences {} 12 | -------------------------------------------------------------------------------- /mobile/test/ui/common/loading_view_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:inkino/ui/common/loading_view.dart'; 5 | 6 | void main() { 7 | group('LoadingView', () { 8 | const loading = Key('test-loading'); 9 | const error = Key('test-error'); 10 | const success = Key('test-success'); 11 | 12 | Future _pumpWithLoadingStatus( 13 | WidgetTester tester, LoadingStatus status) async { 14 | return tester.pumpWidget(MaterialApp( 15 | home: LoadingView( 16 | status: status, 17 | loadingContent: Container(key: loading), 18 | errorContent: Container(key: error), 19 | successContent: Container(key: success), 20 | ), 21 | )); 22 | } 23 | 24 | testWidgets('loading', (WidgetTester tester) async { 25 | await _pumpWithLoadingStatus(tester, LoadingStatus.loading); 26 | await tester.pumpAndSettle(); 27 | 28 | expect(find.byKey(loading), findsOneWidget); 29 | expect(find.byKey(error), findsNothing); 30 | expect(find.byKey(success), findsNothing); 31 | }); 32 | 33 | testWidgets('error', (WidgetTester tester) async { 34 | await _pumpWithLoadingStatus(tester, LoadingStatus.error); 35 | await tester.pumpAndSettle(); 36 | 37 | expect(find.byKey(loading), findsNothing); 38 | expect(find.byKey(error), findsOneWidget); 39 | expect(find.byKey(success), findsNothing); 40 | }); 41 | 42 | testWidgets('success', (WidgetTester tester) async { 43 | await _pumpWithLoadingStatus(tester, LoadingStatus.success); 44 | await tester.pumpAndSettle(); 45 | 46 | expect(find.byKey(loading), findsNothing); 47 | expect(find.byKey(error), findsNothing); 48 | expect(find.byKey(success), findsOneWidget); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /mobile/test/ui/showtimes/showtime_date_selector_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:inkino/ui/showtimes/showtime_date_selector.dart'; 5 | import 'package:kt_dart/collection.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | class MockShowtimesPageViewModel extends Mock 9 | implements ShowtimesPageViewModel {} 10 | 11 | void main() { 12 | group('ShowtimeDateSelector', () { 13 | final dates = listOf( 14 | DateTime(2018, 1, 1), 15 | DateTime(2018, 1, 2), 16 | ); 17 | 18 | MockShowtimesPageViewModel mockViewModel; 19 | 20 | setUp(() { 21 | mockViewModel = MockShowtimesPageViewModel(); 22 | }); 23 | 24 | Future _buildDateSelector(WidgetTester tester) { 25 | return tester.pumpWidget(MaterialApp( 26 | home: ShowtimeDateSelector(mockViewModel), 27 | )); 28 | } 29 | 30 | testWidgets( 31 | 'when there are dates, should show them in UI', 32 | (WidgetTester tester) async { 33 | when(mockViewModel.dates).thenReturn(dates); 34 | 35 | await _buildDateSelector(tester); 36 | 37 | // Monday is the first day of 2018. 38 | expect(find.text('Mon'), findsOneWidget); 39 | expect(find.text('Tue'), findsOneWidget); 40 | }, 41 | ); 42 | 43 | testWidgets( 44 | 'when tapping a date, calls changeCurrentDate on the viewmodel with new date', 45 | (WidgetTester tester) async { 46 | DateTime date; 47 | 48 | when(mockViewModel.dates).thenReturn(dates); 49 | when(mockViewModel.changeCurrentDate) 50 | .thenReturn((newDate) => date = newDate); 51 | 52 | await _buildDateSelector(tester); 53 | await tester.tap(find.text('Tue')); 54 | 55 | expect(date.year, 2018); 56 | expect(date.month, 1); 57 | expect(date.day, 2); 58 | }, 59 | ); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /mobile/test/ui/theater_list/theater_list_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:inkino/ui/theater_list/theater_list.dart'; 5 | import 'package:kt_dart/collection.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | class MockTheaterListViewModel extends Mock implements TheaterListViewModel {} 9 | 10 | void main() { 11 | group('TheaterList', () { 12 | final theaters = listOf( 13 | Theater(id: '1', name: 'Test Theater #1'), 14 | Theater(id: '2', name: 'Test Theater #2'), 15 | ); 16 | 17 | MockTheaterListViewModel mockViewModel; 18 | bool theaterTappedCallbackCalled; 19 | 20 | setUp(() { 21 | mockViewModel = MockTheaterListViewModel(); 22 | when(mockViewModel.currentTheater).thenReturn(null); 23 | when(mockViewModel.theaters).thenReturn(emptyList()); 24 | 25 | theaterTappedCallbackCalled = false; 26 | }); 27 | 28 | Future _buildTheaterList(WidgetTester tester) { 29 | return tester.pumpWidget(MaterialApp( 30 | home: TheaterListContent( 31 | onTheaterTapped: () { 32 | theaterTappedCallbackCalled = true; 33 | }, 34 | viewModel: mockViewModel, 35 | ), 36 | )); 37 | } 38 | 39 | testWidgets( 40 | 'when theaters exist, should show theam in the UI', 41 | (WidgetTester tester) async { 42 | when(mockViewModel.currentTheater).thenReturn(theaters.first()); 43 | when(mockViewModel.theaters).thenReturn(theaters); 44 | 45 | await _buildTheaterList(tester); 46 | 47 | expect(find.text('Test Theater #1'), findsOneWidget); 48 | expect(find.text('Test Theater #2'), findsOneWidget); 49 | }, 50 | ); 51 | 52 | testWidgets( 53 | 'when theater tapped, should call both changeCurrentTheater and onTheaterTapped', 54 | (WidgetTester tester) async { 55 | Theater theater; 56 | when(mockViewModel.currentTheater).thenReturn(theaters.first()); 57 | when(mockViewModel.theaters).thenReturn(theaters); 58 | when(mockViewModel.changeCurrentTheater) 59 | .thenReturn((newTheater) => theater = newTheater); 60 | 61 | await _buildTheaterList(tester); 62 | await tester.tap(find.text('Test Theater #2')); 63 | 64 | expect(theater.id, '2'); 65 | expect(theater.name, 'Test Theater #2'); 66 | 67 | expect(theaterTappedCallbackCalled, isTrue); 68 | }, 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /release-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | (cd mobile && flutter build apk && flutter build ios) 8 | 9 | (cd mobile/android && fastlane internal) 10 | (cd mobile/ios && fastlane beta) 11 | (cd web && ./deploy.sh && firebase deploy) -------------------------------------------------------------------------------- /web/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: [build/**] 3 | errors: 4 | uri_has_not_been_generated: ignore 5 | plugins: 6 | - angular 7 | 8 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 9 | linter: 10 | rules: 11 | - cancel_subscriptions 12 | - hash_and_equals 13 | - iterable_contains_unrelated_type 14 | - list_remove_unrelated_type 15 | - test_types_in_equals 16 | - unrelated_type_equality_checks 17 | - valid_regexps 18 | 19 | global_options: 20 | angular|angular: 21 | options: 22 | no-emit-component-factories: True 23 | no-emit-injectable-factories: True 24 | -------------------------------------------------------------------------------- /web/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | build_web_compilers|entrypoint: 5 | options: 6 | dart2js_args: 7 | - --minify 8 | - --omit-implicit-checks 9 | - --fast-startup 10 | - --trust-primitives 11 | - --trust-type-annotations 12 | -------------------------------------------------------------------------------- /web/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Yes, I know this is bad. It uses a custom chunk-based file fingerprinter, 3 | # but it's not ready just yet and has shortcomings that have to be patched 4 | # by this bash script. I'm working on it. 5 | rm -rf build 6 | webdev build 7 | cd build/images && mv * ../ && cd ../ 8 | 9 | dart ../../../fingerprint/bin/fingerprint.dart 10 | 11 | for i in $(find . -type f | egrep '\.(svg|png|jpeg|jpg)$'); do 12 | mv "$i" ./images/$i 13 | done 14 | 15 | cd ../ 16 | # firebase deploy -------------------------------------------------------------------------------- /web/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**", 8 | "**/packages/**" 9 | ], 10 | "headers": [ 11 | { 12 | "source": "**/*.@(jpg|jpeg|gif|png|svg|js)", 13 | "headers": [ 14 | { 15 | "key": "Cache-Control", 16 | "value": "max-age=31536000" 17 | } 18 | ] 19 | } 20 | ], 21 | "rewrites": [ 22 | { 23 | "source": "**", 24 | "destination": "/index.html" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/lib/app_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:angular_router/angular_router.dart'; 5 | import 'package:core/core.dart'; 6 | import 'package:redux/redux.dart'; 7 | import 'package:web/src/app_bar/app_bar_component.dart'; 8 | import 'package:web/src/common/theater_selector/theater_dropdown_controller.dart'; 9 | 10 | import 'src/routes.dart'; 11 | 12 | @Component( 13 | selector: 'my-app', 14 | styleUrls: ['app_component.css'], 15 | templateUrl: 'app_component.html', 16 | directives: [ 17 | AppBarComponent, 18 | routerDirectives, 19 | ], 20 | exports: [Routes], 21 | ) 22 | class AppComponent implements OnInit, AfterContentInit { 23 | AppComponent(this._store, this._loader); 24 | final Store _store; 25 | final ComponentLoader _loader; 26 | 27 | @ViewChild('theaterContainer', read: ViewContainerRef) 28 | ViewContainerRef theaterContainer; 29 | 30 | TheaterDropdownController _theaterController; 31 | bool get theaterDropdownVisible => _theaterController?.visible == true; 32 | bool get theaterDropdownActive => _theaterController?.isDestroyed == false; 33 | 34 | @override 35 | void ngOnInit() => _store.dispatch(InitAction()); 36 | 37 | @override 38 | void ngAfterContentInit() => document.body.classes.add('loaded'); 39 | 40 | void toggleTheaterDropdown() async { 41 | if (!theaterDropdownActive) { 42 | _theaterController = await TheaterDropdownController.loadAndShow( 43 | _loader, 44 | theaterContainer, 45 | background: '#152451', 46 | ); 47 | } else { 48 | _theaterController.hideAndDestroy(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /web/lib/app_component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 |
-------------------------------------------------------------------------------- /web/lib/app_component.scss: -------------------------------------------------------------------------------- 1 | @import 'src/common'; 2 | @import 'src/breakpoints'; 3 | 4 | main { 5 | margin: 56px 0; 6 | 7 | @include screen-size-tablet { 8 | margin: 60px 20px; 9 | } 10 | 11 | @include screen-size-laptop { 12 | margin: 60px 0; 13 | } 14 | } 15 | 16 | .theater-container { 17 | display: none; 18 | position: fixed; 19 | top: 56px; 20 | right: 0; 21 | bottom: 0; 22 | left: 0; 23 | 24 | &.visible { 25 | display: block; 26 | } 27 | } -------------------------------------------------------------------------------- /web/lib/src/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | @mixin screen-size-nexus-5x { 2 | @media only screen and(min-width: 412px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin screen-size-phablet { 8 | @media only screen and(min-width: 650px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin screen-size-tablet { 14 | @media only screen and (min-width: 768px) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin screen-size-laptop { 20 | @media only screen and (min-width: 1024px) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin screen-size-huge { 26 | @media only screen and (min-width: 1800px) { 27 | @content; 28 | } 29 | } -------------------------------------------------------------------------------- /web/lib/src/_common.scss: -------------------------------------------------------------------------------- 1 | @import 'breakpoints'; 2 | 3 | .app-bar-button { 4 | width: 56px; 5 | height: 56px; 6 | padding: 14px; 7 | cursor: pointer; 8 | user-select: none; 9 | transition: background-color 250ms ease, opacity 150ms ease, transform 250ms ease; 10 | 11 | &.active { 12 | background: #152451; 13 | } 14 | 15 | @include screen-size-tablet { 16 | width: 60px; 17 | height: 60px; 18 | } 19 | } 20 | 21 | .scrolling-blocked { 22 | position: fixed; 23 | left: 0; 24 | right: 0; 25 | z-index: 1; 26 | overflow: hidden; 27 | } 28 | 29 | .page-title { 30 | display: none; 31 | 32 | @include screen-size-tablet { 33 | display: flex; 34 | flex-flow: row; 35 | align-items: center; 36 | justify-content: space-between; 37 | padding-top: 20px; 38 | 39 | h3 { 40 | color: #ffffff; 41 | font-weight: 600; 42 | font-size: 30px; 43 | text-transform: uppercase; 44 | } 45 | } 46 | } 47 | 48 | @include screen-size-laptop { 49 | .content-wrapper { 50 | margin: 0 auto; 51 | width: 70%; 52 | min-width: 850px; 53 | padding: 100px 0; 54 | } 55 | } 56 | 57 | // Needs the parent to have a position:relative directive. 58 | @mixin full-size-overlay { 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | right: 0; 63 | bottom: 0; 64 | } -------------------------------------------------------------------------------- /web/lib/src/app_bar/app_bar_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:angular_router/angular_router.dart'; 5 | import 'package:core/core.dart'; 6 | import 'package:redux/redux.dart'; 7 | import 'package:web/src/app_bar/nav_bar/nav_bar_component.dart'; 8 | import 'package:web/src/app_bar/scroll_utils.dart'; 9 | import 'package:web/src/app_bar/search_bar/search_bar_component.dart'; 10 | import 'package:web/src/routes.dart'; 11 | 12 | @Component( 13 | selector: 'app-bar', 14 | templateUrl: 'app_bar_component.html', 15 | styleUrls: ['app_bar_component.css'], 16 | directives: [ 17 | NavBarComponent, 18 | SearchBarComponent, 19 | routerDirectives, 20 | ], 21 | exports: [RoutePaths], 22 | ) 23 | class AppBarComponent implements OnInit, OnDestroy { 24 | AppBarComponent(this.messages, this._store, this._router); 25 | 26 | final Messages messages; 27 | final Store _store; 28 | final Router _router; 29 | 30 | String get theaterName => _store.state.theaterState.currentTheater.name; 31 | 32 | @Input() 33 | bool theaterDropdownVisible = false; 34 | 35 | @Input() 36 | bool theaterDropdownActive = false; 37 | 38 | bool hide = false; 39 | bool isEventDetailsPage = false; 40 | 41 | StreamSubscription _routeListener; 42 | Timer _scrollTimer; 43 | 44 | @Output() 45 | Stream get theaterButtonClicked => _theaterButtonClicked.stream; 46 | final _theaterButtonClicked = StreamController(); 47 | 48 | void openTheaterDropdown() => _theaterButtonClicked.add(null); 49 | 50 | @override 51 | void ngOnInit() { 52 | _listenForRoutes(); 53 | _scrollTimer = listenForScrollDirectionChanges((newDirection) { 54 | if (!isEventDetailsPage && !theaterDropdownVisible) { 55 | hide = newDirection == ScrollDirection.down; 56 | } 57 | }); 58 | } 59 | 60 | void _listenForRoutes() { 61 | _routeListener = _router.onRouteActivated.listen((route) { 62 | final path = route.routePath.path; 63 | isEventDetailsPage = path == RoutePaths.eventDetails.path || 64 | path == RoutePaths.showDetails.path; 65 | 66 | hide = isEventDetailsPage; 67 | }); 68 | } 69 | 70 | @override 71 | void ngOnDestroy() { 72 | _theaterButtonClicked.close(); 73 | _routeListener.cancel(); 74 | _scrollTimer.cancel(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/lib/src/app_bar/app_bar_component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 | 19 | 20 | 21 |
22 | 23 |
24 | Change selected theater 29 | 30 | 31 |
32 |
33 |
-------------------------------------------------------------------------------- /web/lib/src/app_bar/app_bar_component.scss: -------------------------------------------------------------------------------- 1 | @import '../common'; 2 | @import '../breakpoints'; 3 | 4 | header { 5 | background: #1C306D; 6 | box-shadow: 0 10px 60px rgba(0, 0, 0, .4); 7 | width: 100%; 8 | height: 56px; 9 | position: fixed; 10 | top: 0; 11 | opacity: 1; 12 | z-index: 3000; 13 | transition: opacity 350ms ease, top 350ms ease; 14 | 15 | &.hidden { 16 | top: -56px; 17 | opacity: 0; 18 | } 19 | } 20 | 21 | .wrapper { 22 | display: flex; 23 | justify-content: space-between; 24 | align-content: center; 25 | width: 100%; 26 | padding-left: 20px; 27 | } 28 | 29 | .left { 30 | display: flex; 31 | } 32 | 33 | .right { 34 | display: flex; 35 | } 36 | 37 | .logo { 38 | position: relative; 39 | display: flex; 40 | color: #ffffff; 41 | align-items: center; 42 | text-decoration: none; 43 | user-select: none; 44 | cursor: pointer; 45 | 46 | img { 47 | width: 28px; 48 | height: 28px; 49 | margin-top: 2px; 50 | } 51 | 52 | h1 { 53 | font-size: 20px; 54 | font-weight: 500; 55 | color: #FEFEFE; 56 | } 57 | 58 | .mobile-logo-focus-trap { 59 | @include full-size-overlay; 60 | } 61 | } 62 | 63 | .name-and-selected-theater { 64 | display: flex; 65 | flex-direction: column; 66 | margin-left: 6px; 67 | } 68 | 69 | .theater-name { 70 | font-size: 12px; 71 | opacity: 0.7; 72 | white-space: nowrap; 73 | text-overflow: ellipsis; 74 | } 75 | 76 | @include screen-size-tablet { 77 | header { 78 | height: 60px; 79 | 80 | &.hidden { 81 | top: -60px; 82 | } 83 | } 84 | 85 | .theater-name { 86 | display: none; 87 | } 88 | 89 | .name-and-selected-theater { 90 | margin-left: 10px; 91 | } 92 | 93 | .logo { 94 | img { 95 | width: 32px; 96 | height: 32px; 97 | } 98 | 99 | h1 { 100 | font-size: 30px; 101 | } 102 | } 103 | 104 | .mobile-logo-focus-trap { 105 | display: none; 106 | } 107 | 108 | .app-bar-button.select-theater { 109 | display: none; 110 | } 111 | } 112 | 113 | @include screen-size-laptop { 114 | .wrapper { 115 | width: 70%; 116 | min-width: 850px; 117 | margin: 0 auto; 118 | padding: 0; 119 | } 120 | } -------------------------------------------------------------------------------- /web/lib/src/app_bar/nav_bar/nav_bar_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:angular_router/angular_router.dart'; 3 | import 'package:core/core.dart'; 4 | import 'package:web/src/routes.dart'; 5 | 6 | @Component( 7 | selector: 'nav-bar', 8 | templateUrl: 'nav_bar_component.html', 9 | styleUrls: ['nav_bar_component.css'], 10 | directives: [ 11 | routerDirectives, 12 | ], 13 | exports: [RoutePaths], 14 | ) 15 | class NavBarComponent { 16 | NavBarComponent(this.messages); 17 | final Messages messages; 18 | 19 | @Input() 20 | bool theaterDropdownActive = false; 21 | } 22 | -------------------------------------------------------------------------------- /web/lib/src/app_bar/nav_bar/nav_bar_component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/lib/src/app_bar/nav_bar/nav_bar_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../breakpoints'; 2 | 3 | nav { 4 | position: fixed; 5 | box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.5); 6 | bottom: 0; 7 | left: 0; 8 | display: flex; 9 | justify-content: space-around; 10 | background: rgba(0, 0, 0, 0.8); 11 | width: 100%; 12 | height: 68px; 13 | opacity: 1; 14 | transition: opacity 350ms ease, bottom 350ms ease; 15 | 16 | &.hidden { 17 | opacity: 0; 18 | bottom: -60px; 19 | } 20 | } 21 | 22 | nav a { 23 | padding: 10px 16px 2px 16px; 24 | text-decoration: none; 25 | text-align: center; 26 | user-select: none; 27 | 28 | .icon { 29 | width: 28px; 30 | height: 28px; 31 | opacity: 0.6; 32 | transition: opacity 0.15s linear; 33 | } 34 | 35 | span { 36 | display: flex; 37 | align-items: center; 38 | color: rgba(255, 255, 255, 0.6); 39 | padding: 0 2px 5px 2px; 40 | font-size: 14px; 41 | transition: border-bottom-width 0.15s linear, padding-bottom 0.15s linear, color 0.15s linear; 42 | } 43 | 44 | &.active-route { 45 | color: #fff; 46 | padding-bottom: 0; 47 | 48 | .icon { 49 | opacity: 1; 50 | } 51 | 52 | span { 53 | padding-bottom: 0; 54 | color: #ffffff; 55 | } 56 | } 57 | } 58 | 59 | @include screen-size-tablet { 60 | nav { 61 | margin-left: 40px; 62 | height: 60px; 63 | position: unset; 64 | box-shadow: none; 65 | justify-content: unset; 66 | background: unset; 67 | width: unset; 68 | 69 | a { 70 | padding-bottom: 0; 71 | 72 | span { 73 | height: 100%; 74 | font-size: 16px; 75 | border-bottom: 0 solid #fdbd2c; 76 | } 77 | } 78 | 79 | .icon { 80 | display: none; 81 | } 82 | 83 | .active-route span { 84 | border-bottom-width: 5px; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /web/lib/src/app_bar/scroll_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | 4 | enum ScrollDirection { up, down } 5 | 6 | typedef void ScrollDirectionChangedCallback(ScrollDirection newDirection); 7 | 8 | Timer listenForScrollDirectionChanges(ScrollDirectionChangedCallback callback) { 9 | var previousTop = 0; 10 | 11 | return Timer.periodic(const Duration(milliseconds: 250), (_) { 12 | final top = document.body.getBoundingClientRect().top; 13 | 14 | if (top > previousTop || top > -160) { 15 | callback(ScrollDirection.up); 16 | } else if (top < previousTop) { 17 | callback(ScrollDirection.down); 18 | } 19 | 20 | previousTop = top; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /web/lib/src/app_bar/search_bar/search_bar_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | 4 | import 'package:angular/angular.dart'; 5 | import 'package:core/core.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | @Component( 9 | selector: 'search-bar', 10 | templateUrl: 'search_bar_component.html', 11 | styleUrls: ['search_bar_component.css'], 12 | ) 13 | class SearchBarComponent { 14 | SearchBarComponent(this.messages, this.store); 15 | final Messages messages; 16 | final Store store; 17 | 18 | @HostBinding('attr.expanded') 19 | String get hostExpanded => searchOpen ? '' : null; 20 | 21 | @ViewChild('searchField') 22 | InputElement searchField; 23 | 24 | bool searchOpen = false; 25 | 26 | void toggleSearch() { 27 | searchOpen = !searchOpen; 28 | 29 | if (searchOpen) { 30 | Timer(const Duration(milliseconds: 250), () => searchField.focus()); 31 | } else { 32 | searchField.value = ''; // Clear the search when closed. 33 | updateSearchQuery(null); 34 | } 35 | } 36 | 37 | void updateSearchQuery(String newQuery) => 38 | store.dispatch(SearchQueryChangedAction(newQuery)); 39 | } 40 | -------------------------------------------------------------------------------- /web/lib/src/app_bar/search_bar/search_bar_component.html: -------------------------------------------------------------------------------- 1 | Stop searching movies 6 | 7 | 12 | 13 |
14 | Search for movies 15 | Stop searching movies 16 |
-------------------------------------------------------------------------------- /web/lib/src/app_bar/search_bar/search_bar_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../common'; 2 | @import '../../breakpoints'; 3 | 4 | :host { 5 | display: flex; 6 | background: #1F3169; 7 | 8 | &[expanded] { 9 | position: fixed; 10 | left: 0; 11 | right: 0; 12 | 13 | @include screen-size-tablet { 14 | position: unset; 15 | } 16 | } 17 | } 18 | 19 | .back { 20 | display: none; 21 | 22 | &.visible { 23 | display: block; 24 | padding-left: 16px; 25 | cursor: pointer; 26 | 27 | @include screen-size-tablet { 28 | display: none; 29 | } 30 | } 31 | } 32 | 33 | input { 34 | display: none; 35 | background: transparent; 36 | width: 100%; 37 | color: #ffffff; 38 | font-size: 16px; 39 | padding: 8px; 40 | 41 | &, &:focus { 42 | background-color: transparent; 43 | border: none; 44 | outline: none; 45 | } 46 | 47 | &::placeholder { 48 | color: rgba(255, 255, 255, 0.5); 49 | } 50 | 51 | &.visible { 52 | display: block; 53 | } 54 | } 55 | 56 | .buttons { 57 | position: relative; 58 | width: 56px; 59 | height: 56px; 60 | 61 | @include screen-size-tablet { 62 | width: 60px; 63 | height: 60px; 64 | } 65 | } 66 | 67 | .buttons img { 68 | position: absolute; 69 | opacity: 0; 70 | right: 0; 71 | transform: scale(0.2); 72 | 73 | &.visible { 74 | opacity: 1; 75 | transform: scale(1.0); 76 | } 77 | } -------------------------------------------------------------------------------- /web/lib/src/common/content_rating/content_rating_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:kt_dart/collection.dart'; 4 | 5 | @Component( 6 | selector: 'content-rating', 7 | templateUrl: 'content_rating_component.html', 8 | styleUrls: ['content_rating_component.css'], 9 | directives: [NgFor], 10 | ) 11 | class ContentRatingComponent { 12 | @Input() 13 | Show show; 14 | 15 | @Input() 16 | Event event; 17 | 18 | String get ageRating => show?.ageRating ?? event?.ageRating; 19 | String get ageRatingUrl => show?.ageRatingUrl ?? event?.ageRatingUrl; 20 | 21 | KtList get contentDescriptors => show?.contentDescriptors ?? event?.contentDescriptors; 22 | } 23 | -------------------------------------------------------------------------------- /web/lib/src/common/content_rating/content_rating_component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/lib/src/common/content_rating/content_rating_component.scss: -------------------------------------------------------------------------------- 1 | $small: 20px; 2 | $medium: 30px; 3 | $default: $small; 4 | 5 | @mixin _size($size) { 6 | img { 7 | width: $size; 8 | height: $size; 9 | } 10 | } 11 | 12 | :host { 13 | &[size="small"] { 14 | @include _size($small); 15 | } 16 | 17 | &[size="medium"] { 18 | @include _size($medium); 19 | } 20 | } 21 | 22 | img { 23 | width: $default; 24 | height: $default; 25 | vertical-align: middle; 26 | margin-right: 6px; 27 | 28 | &:last-child { 29 | margin-right: 0; 30 | } 31 | } -------------------------------------------------------------------------------- /web/lib/src/common/event_poster/event_poster_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:web/src/common/content_rating/content_rating_component.dart'; 5 | import 'package:web/src/common/event_poster/lazy_image_component.dart'; 6 | 7 | @Component( 8 | selector: 'event-poster', 9 | styleUrls: ['event_poster_component.css'], 10 | templateUrl: 'event_poster_component.html', 11 | directives: [ 12 | ContentRatingComponent, 13 | LazyImageComponent, 14 | NgIf, 15 | NgFor, 16 | ], 17 | ) 18 | class EventPosterComponent { 19 | static final _releaseDateFormat = DateFormat('dd.MM.yyyy'); 20 | 21 | EventPosterComponent(this.messages); 22 | final Messages messages; 23 | 24 | @Input() 25 | Event event; 26 | 27 | @Input() 28 | bool isComingSoon = false; 29 | 30 | @Input() 31 | bool hasDetails = true; 32 | 33 | @Input() 34 | bool isTouchable = true; 35 | 36 | String get releaseDate => _releaseDateFormat.format(event.releaseDate); 37 | } 38 | -------------------------------------------------------------------------------- /web/lib/src/common/event_poster/event_poster_component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 | 6 | 9 | 10 |
11 | 14 | 15 | 16 |

{{ event.title }}

17 |

{{ event.genres }}

18 |
19 | 20 |
21 |

{{ messages.releaseDate }}

22 |

{{ releaseDate }}

23 |
24 | -------------------------------------------------------------------------------- /web/lib/src/common/event_poster/event_poster_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../common'; 2 | @import '../../breakpoints'; 3 | 4 | :host { 5 | position: relative; 6 | } 7 | 8 | .fallback-icon { 9 | @include full-size-overlay; 10 | 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | z-index: -1; 15 | background: linear-gradient(#424242, #222222); 16 | 17 | img { 18 | display: block; 19 | width: 70%; 20 | } 21 | } 22 | 23 | .event-information { 24 | @include full-size-overlay; 25 | 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: flex-end; 29 | background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 30%, #000); 30 | padding: 1.2em; 31 | 32 | strong { 33 | font-weight: 500; 34 | font-size: 12pt; 35 | color: #ffffff; 36 | } 37 | 38 | p.genres { 39 | margin-top: 0.2em; 40 | font-size: 10pt; 41 | color: rgba(255, 255, 255, 0.7); 42 | } 43 | 44 | content-rating { 45 | margin-bottom: 10px; 46 | } 47 | } 48 | 49 | .release-date-information { 50 | position: absolute; 51 | top: 10px; 52 | left: 0; 53 | background: rgba(0, 0, 0, 0.8); 54 | padding: 5px 20px 5px 10px; 55 | 56 | .label { 57 | color: #FFBE00; 58 | font-size: 12px; 59 | font-weight: bold; 60 | } 61 | 62 | .date { 63 | color: #FEFEFE; 64 | font-size: 16px; 65 | font-weight: 300; 66 | margin-top: 2px; 67 | } 68 | 69 | @include screen-size-tablet { 70 | padding: 10px 40px 10px 20px; 71 | 72 | .date { 73 | font-size: 20px; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /web/lib/src/common/event_poster/lazy_image_component.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | max-height: 100%; 4 | width: 100%; 5 | opacity: 0; 6 | transition: opacity 750ms ease; 7 | } 8 | -------------------------------------------------------------------------------- /web/lib/src/common/loading_view/loading_view_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html' as html; 3 | 4 | import 'package:angular/angular.dart'; 5 | import 'package:core/core.dart'; 6 | import 'package:web/src/common/loading_view/spinner_component.dart'; 7 | 8 | @Component( 9 | selector: 'loading-view', 10 | templateUrl: 'loading_view_component.html', 11 | styleUrls: ['loading_view_component.css'], 12 | directives: [ 13 | SpinnerComponent, 14 | NgIf, 15 | ], 16 | ) 17 | class LoadingViewComponent implements OnDestroy { 18 | LoadingViewComponent(this.messages); 19 | final Messages messages; 20 | 21 | LoadingStatus _status; 22 | 23 | @Input() 24 | bool contentEmpty = false; 25 | 26 | @Input() 27 | String errorTitle; 28 | 29 | @Input() 30 | String errorMessage; 31 | 32 | String get emptyTitle => contentEmpty ? messages.allEmpty : null; 33 | String get emptyMessage => contentEmpty ? messages.noMoviesForToday : null; 34 | 35 | @Output() 36 | Stream get actionButtonClicked => _tryAgainController.stream; 37 | final _tryAgainController = StreamController(); 38 | 39 | @Input() 40 | set status(LoadingStatus status) { 41 | _clearOutInvisibleContent = false; 42 | _status = status; 43 | 44 | Timer( 45 | const Duration(milliseconds: 450), 46 | () => _clearOutInvisibleContent = true, 47 | ); 48 | } 49 | 50 | bool get loadingContentVisible => _status == LoadingStatus.loading; 51 | bool get loadingContentPresent => 52 | loadingContentVisible || !_clearOutInvisibleContent; 53 | 54 | bool get successContentVisible => _status == LoadingStatus.success; 55 | bool get successContentPresent => 56 | successContentVisible || !_clearOutInvisibleContent; 57 | 58 | bool get errorContentVisible => 59 | _status == LoadingStatus.error || 60 | (_status != LoadingStatus.loading && contentEmpty); 61 | bool get errorContentPresent => 62 | errorContentVisible || !_clearOutInvisibleContent; 63 | 64 | // After animations have finished, all invisible content gets removed from DOM. 65 | bool _clearOutInvisibleContent = false; 66 | 67 | void onTryAgainClicked(html.Event event) { 68 | event.preventDefault(); 69 | _tryAgainController.add(null); 70 | } 71 | 72 | @override 73 | void ngOnDestroy() => _tryAgainController.close(); 74 | } 75 | -------------------------------------------------------------------------------- /web/lib/src/common/loading_view/loading_view_component.html: -------------------------------------------------------------------------------- 1 |
2 |
5 | 6 |
7 | 8 |
11 | 12 |
13 | 14 |
17 |
18 | 19 |
20 | 21 |

{{ emptyTitle ?? errorTitle ?? messages.oops}}

22 |

23 | {{ emptyMessage ?? errorMessage ?? messages.loadingMoviesError }}
24 | (this might be caused by your ad blocker) 25 |

26 | 27 | 28 | {{ messages.tryAgain }} 29 | 30 |
31 |
-------------------------------------------------------------------------------- /web/lib/src/common/loading_view/loading_view_component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 100vh; 4 | } 5 | 6 | .loading-content { 7 | position: absolute; 8 | width: 100%; 9 | height: 75%; 10 | transition: opacity 150ms ease; 11 | 12 | &.visible { 13 | opacity: 1; 14 | } 15 | } 16 | 17 | .error-content { 18 | position: absolute; 19 | width: 100%; 20 | height: 75%; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | opacity: 0; 26 | transition: opacity 450ms ease; 27 | 28 | &.visible { 29 | opacity: 1; 30 | } 31 | } 32 | 33 | .success-content { 34 | position: absolute; 35 | width: 100%; 36 | height: 100%; 37 | opacity: 0; 38 | transition: opacity 450ms ease; 39 | 40 | &.visible { 41 | opacity: 1; 42 | } 43 | } 44 | 45 | .icon { 46 | border-radius: 50px; 47 | background: rgba(255, 255, 255, 0.12); 48 | 49 | img { 50 | display: block; 51 | width: 96px; 52 | height: 96px; 53 | } 54 | } 55 | 56 | .title { 57 | margin-top: 16px; 58 | max-width: 350px; 59 | text-align: center; 60 | font-size: 24px; 61 | color: #ffffff; 62 | } 63 | 64 | .message { 65 | margin-top: 8px; 66 | max-width: 250px; 67 | text-align: center; 68 | color: rgba(255, 255, 255, 0.7); 69 | } 70 | 71 | .try-again { 72 | margin-top: 12px; 73 | color: #ffffff; 74 | padding: 8px; 75 | text-decoration: none; 76 | font-weight: 600; 77 | user-select: none; 78 | } 79 | 80 | -------------------------------------------------------------------------------- /web/lib/src/common/loading_view/spinner_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | 3 | @Component( 4 | selector: 'spinner', 5 | templateUrl: 'spinner_component.html', 6 | styleUrls: ['spinner_component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | ) 9 | class SpinnerComponent {} 10 | -------------------------------------------------------------------------------- /web/lib/src/common/loading_view/spinner_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
-------------------------------------------------------------------------------- /web/lib/src/common/loading_view/spinner_component.scss: -------------------------------------------------------------------------------- 1 | // From: https://loading.io/css/ 2 | .container{display:inline-block;position:absolute;width:64px;height:64px;top:50%;left:50%;margin-top:-32px;margin-left:-32px}.container div{animation:lds-roller 1.2s cubic-bezier(.5,0,.5,1) infinite;transform-origin:32px 32px}.container div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#fff;margin:-3px 0 0 -3px}.container div:nth-child(1){animation-delay:-36ms}.container div:nth-child(1):after{top:50px;left:50px}.container div:nth-child(2){animation-delay:-72ms}.container div:nth-child(2):after{top:54px;left:45px}.container div:nth-child(3){animation-delay:-108ms}.container div:nth-child(3):after{top:57px;left:39px}.container div:nth-child(4){animation-delay:-144ms}.container div:nth-child(4):after{top:58px;left:32px}.container div:nth-child(5){animation-delay:-.18s}.container div:nth-child(5):after{top:57px;left:25px}.container div:nth-child(6){animation-delay:-216ms}.container div:nth-child(6):after{top:54px;left:19px}.container div:nth-child(7){animation-delay:-252ms}.container div:nth-child(7):after{top:50px;left:14px}.container div:nth-child(8){animation-delay:-288ms}.container div:nth-child(8):after{top:45px;left:10px}@keyframes lds-roller{0%{transform:rotate(0)}100%{transform:rotate(360deg)}} -------------------------------------------------------------------------------- /web/lib/src/common/showtime_item/showtime_item_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html' as html; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:core/core.dart'; 5 | import 'package:web/src/common/content_rating/content_rating_component.dart'; 6 | 7 | @Component( 8 | selector: 'showtime-item', 9 | styleUrls: ['showtime_item_component.css'], 10 | templateUrl: 'showtime_item_component.html', 11 | directives: [ContentRatingComponent, NgIf, NgFor], 12 | pipes: [DatePipe], 13 | ) 14 | class ShowtimeItemComponent { 15 | ShowtimeItemComponent(this.messages); 16 | final Messages messages; 17 | 18 | @Input() 19 | Show show; 20 | 21 | void openTickets(html.Event event) { 22 | html.window.open(show.url, 'Tickets for ${show.title}'); 23 | event.stopImmediatePropagation(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/lib/src/common/showtime_item/showtime_item_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ show.start | date: 'HH:mm' }}

4 |

{{ show.end | date: 'HH:mm' }}

5 |
6 |
7 |

{{ show.title }}

8 |
9 |

{{ show.theaterAndAuditorium }}

10 |

11 | {{ show.presentationMethod }} 12 | 13 |

14 |
15 |
16 |
17 | 18 |
19 | 20 | 22 | 23 | 24 |
-------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_dropdown_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | import 'theater_selector_dropdown_menu_component.template.dart' as dropdown; 7 | 8 | class TheaterDropdownController { 9 | static const animationDuration = Duration(milliseconds: 250); 10 | 11 | TheaterDropdownController._(this._menu); 12 | ComponentRef _menu; 13 | 14 | bool get isDestroyed => _menu == null; 15 | bool visible = false; 16 | 17 | static Future loadAndShow( 18 | ComponentLoader loader, 19 | ViewContainerRef container, { 20 | String background = 'rgba(26, 26, 26, 0.9)', 21 | }) async { 22 | final menu = await loader.loadNextToLocation( 23 | dropdown.TheaterSelectorDropdownMenuComponentNgFactory, 24 | container, 25 | ); 26 | 27 | final controller = TheaterDropdownController._(menu); 28 | menu.instance 29 | ..controller = controller 30 | ..background = background; 31 | 32 | return controller 33 | ..visible = true 34 | .._menuAnimation(visible: true); 35 | } 36 | 37 | void hideAndDestroy() { 38 | visible = false; 39 | _menuAnimation( 40 | visible: false, 41 | afterAnimation: () { 42 | _menu.destroy(); 43 | _menu = null; 44 | }, 45 | ); 46 | } 47 | 48 | void _menuAnimation({@required bool visible, void afterAnimation()}) { 49 | Timer( 50 | const Duration(milliseconds: 25), 51 | () => _menu?.instance?.isOpen = visible, 52 | ); 53 | 54 | if (afterAnimation != null) { 55 | Timer(animationDuration, afterAnimation); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:redux/redux.dart'; 4 | import 'package:web/src/common/theater_selector/theater_dropdown_controller.dart'; 5 | 6 | @Component( 7 | selector: 'theater-selector', 8 | styleUrls: ['theater_selector_component.css'], 9 | templateUrl: 'theater_selector_component.html', 10 | ) 11 | class TheaterSelectorComponent { 12 | TheaterSelectorComponent(this._store, this._loader); 13 | final Store _store; 14 | final ComponentLoader _loader; 15 | 16 | TheaterListViewModel get _viewModel => TheaterListViewModel.fromStore(_store); 17 | Theater get currentTheater => _viewModel.currentTheater; 18 | 19 | @ViewChild('menuContainer', read: ViewContainerRef) 20 | ViewContainerRef menuContainer; 21 | 22 | TheaterDropdownController _menuController; 23 | bool get theaterDropdownVisible => 24 | _menuController != null && _menuController.isDestroyed == false; 25 | 26 | void toggleMenu() async { 27 | if (!theaterDropdownVisible) { 28 | _menuController = await TheaterDropdownController.loadAndShow( 29 | _loader, 30 | menuContainer, 31 | ); 32 | } else { 33 | hideMenu(); 34 | } 35 | } 36 | 37 | void hideMenu() { 38 | _menuController.hideAndDestroy(); 39 | _menuController = null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_component.html: -------------------------------------------------------------------------------- 1 |
2 | A map pin. 3 | {{ currentTheater.name }} 4 | Drop down arrow. 5 |
6 | 7 | -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../breakpoints'; 2 | 3 | :host { 4 | position: relative; 5 | z-index: 2001; 6 | } 7 | 8 | .button { 9 | position: relative; 10 | width: 100%; 11 | height: 36px; 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | border: 1px solid #717DAD; 16 | border-radius: 5px; 17 | padding: 6px; 18 | user-select: none; 19 | cursor: pointer; 20 | 21 | img { 22 | width: 24px; 23 | height: 24px; 24 | } 25 | 26 | .button-text { 27 | flex-grow: 1; 28 | font-size: 16px; 29 | padding: 0 8px; 30 | color: #FEFEFE; 31 | } 32 | } 33 | 34 | .menu { 35 | display: none; 36 | position: absolute; 37 | top: 36px; 38 | width: 100%; 39 | height: 400px; 40 | 41 | &.visible { 42 | display: block; 43 | } 44 | } 45 | 46 | @include screen-size-phablet { 47 | :host { 48 | min-width: 250px; 49 | } 50 | } -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_dropdown_menu_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:redux/redux.dart'; 4 | import 'package:web/src/common/theater_selector/theater_dropdown_controller.dart'; 5 | 6 | @Component( 7 | selector: 'theater-selector-dropdown-menu', 8 | templateUrl: 'theater_selector_dropdown_menu_component.html', 9 | styleUrls: ['theater_selector_dropdown_menu_component.css'], 10 | directives: [NgFor], 11 | ) 12 | class TheaterSelectorDropdownMenuComponent { 13 | TheaterSelectorDropdownMenuComponent(this._store); 14 | final Store _store; 15 | 16 | TheaterDropdownController controller; 17 | String background; 18 | 19 | TheaterListViewModel get _viewModel => TheaterListViewModel.fromStore(_store); 20 | Theater get selectedTheater => _viewModel.currentTheater; 21 | List get theaters => _viewModel.theaters.list; 22 | 23 | bool get focusTrapVisible => isOpen; 24 | bool isOpen = false; 25 | 26 | void onTheaterClicked(Theater newTheater) { 27 | _viewModel.changeCurrentTheater(newTheater); 28 | controller.hideAndDestroy(); 29 | } 30 | 31 | void hideAndDestroy() => controller.hideAndDestroy(); 32 | } 33 | -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_dropdown_menu_component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 6 | 14 | -------------------------------------------------------------------------------- /web/lib/src/common/theater_selector/theater_selector_dropdown_menu_component.scss: -------------------------------------------------------------------------------- 1 | .focus-trap { 2 | display: none; 3 | z-index: 2000; 4 | position: fixed; 5 | top: 0; 6 | right: 0; 7 | bottom: 0; 8 | left: 0; 9 | 10 | &.visible { 11 | display: block; 12 | } 13 | } 14 | 15 | .menu { 16 | z-index: 2001; 17 | position: relative; 18 | width: 100%; 19 | opacity: 0; 20 | height: 0; 21 | background: transparent; 22 | overflow-y: scroll; 23 | -webkit-overflow-scrolling: touch; 24 | transition: height 250ms ease, opacity 250ms ease; 25 | 26 | &.opened { 27 | opacity: 1; 28 | height: 100%; 29 | } 30 | } 31 | 32 | .item { 33 | position: relative; 34 | cursor: pointer; 35 | padding: 16px; 36 | background: transparent; 37 | font-size: 16px; 38 | color: rgba(255, 255, 255, 0.56); 39 | user-select: none; 40 | 41 | &.selected { 42 | background: rgba(0, 0, 0, 0.54); 43 | color: #ffffff; 44 | } 45 | 46 | &:hover { 47 | background: rgba(0, 0, 0, 0.2); 48 | color: rgba(255, 255, 255, 0.8); 49 | } 50 | } -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_image_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:angular/angular.dart'; 4 | 5 | @Component( 6 | selector: 'actor-img', 7 | templateUrl: 'actor_image_component.html', 8 | styleUrls: ['actor_image_component.css'], 9 | ) 10 | class ActorImageComponent implements OnInit { 11 | @Input() 12 | String src; 13 | 14 | @ViewChild('actualImage') 15 | ImageElement imageElement; 16 | 17 | @override 18 | void ngOnInit() { 19 | imageElement.addEventListener( 20 | 'load', (_) => imageElement.classes.add('loaded')); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_image_component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_image_component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | background: #1C306D; 4 | border-radius: 50%; 5 | width: 60px; 6 | height: 60px; 7 | } 8 | 9 | img { 10 | position: absolute; 11 | border-radius: 50%; 12 | object-fit: cover; 13 | 14 | &.placeholder { 15 | z-index: 1; 16 | width: 32px; 17 | height: 32px; 18 | top: 13px; 19 | left: 13px; 20 | } 21 | 22 | &.actual { 23 | z-index: 2; 24 | width: 60px; 25 | height: 60px; 26 | opacity: 0; 27 | transition: opacity 250ms ease; 28 | 29 | &.loaded { 30 | opacity: 1; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_scroller_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:web/src/event_details/actor_scroller/actor_image_component.dart'; 4 | 5 | @Component( 6 | selector: 'actor-scroller', 7 | templateUrl: 'actor_scroller_component.html', 8 | styleUrls: ['actor_scroller_component.css'], 9 | directives: [ActorImageComponent, NgFor], 10 | ) 11 | class ActorScrollerComponent { 12 | @Input() 13 | List actors; 14 | } 15 | -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_scroller_component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ actor.name }}

4 |
5 | -------------------------------------------------------------------------------- /web/lib/src/event_details/actor_scroller/actor_scroller_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../breakpoints'; 2 | 3 | :host { 4 | display: flex; 5 | overflow-x: scroll; 6 | overflow-y: hidden; 7 | -webkit-overflow-scrolling: touch; 8 | padding-left: 20px; 9 | 10 | @include screen-size-laptop { 11 | padding-left: 0; 12 | } 13 | 14 | div { 15 | margin-right: 20px; 16 | max-width: 60px; 17 | 18 | &:last-child { 19 | margin-right: 0; 20 | } 21 | 22 | p { 23 | color: #1D1D1B; 24 | font-size: 12px; 25 | margin-top: 10px; 26 | line-height: 14px; 27 | text-align: center; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/lib/src/event_details/event_details_component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 |

{{ event.title }}

9 | 10 |
11 |

{{ event.lengthInMinutes }} min

12 |

13 | {{ messages.director }}: {{ event.director }} 14 |

15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |

{{ messages.storyline }}

28 |

{{ event.synopsis }}

29 |
30 |
31 | 32 |
33 |
34 |

{{ messages.cast }}

35 | 36 |
37 |
38 | 39 | 49 |
50 | -------------------------------------------------------------------------------- /web/lib/src/event_details/landscape_image/event_landscape_image_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html' as html; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:core/core.dart'; 5 | 6 | @Component( 7 | selector: 'event-landscape-image', 8 | templateUrl: 'event_landscape_image_component.html', 9 | styleUrls: ['event_landscape_image_component.css'], 10 | ) 11 | class EventLandscapeImageComponent implements OnInit, OnDestroy { 12 | @Input() 13 | Event event; 14 | 15 | @ViewChild('actualImage') 16 | html.ImageElement imageElement; 17 | 18 | bool _triedWithSecondLandscapeUrl = false; 19 | 20 | @override 21 | void ngOnInit() { 22 | imageElement.addEventListener('load', _onLoad); 23 | imageElement.addEventListener('error', _onError); 24 | } 25 | 26 | @override 27 | void ngOnDestroy() => _clearListeners(); 28 | 29 | void _onLoad(html.Event _) { 30 | imageElement.classes.add('loaded'); 31 | _clearListeners(); 32 | } 33 | 34 | void _onError(html.Event _) { 35 | if (_triedWithSecondLandscapeUrl) { 36 | _clearListeners(); 37 | return; 38 | } 39 | 40 | imageElement.src = event.images.landscapeHd2; 41 | _triedWithSecondLandscapeUrl = true; 42 | } 43 | 44 | void _clearListeners() { 45 | imageElement.removeEventListener('load', _onLoad); 46 | imageElement.removeEventListener('error', _onError); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/lib/src/event_details/landscape_image/event_landscape_image_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /web/lib/src/event_details/landscape_image/event_landscape_image_component.scss: -------------------------------------------------------------------------------- 1 | @import '../../breakpoints'; 2 | 3 | .container { 4 | position: relative; 5 | width: 100%; 6 | height: 225px; 7 | background: linear-gradient(to top, #222222, #424242); 8 | } 9 | 10 | .placeholder { 11 | position: absolute; 12 | width: 100%; 13 | height: 225px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | z-index: 1; 18 | 19 | img { 20 | width: 128px; 21 | height: 128px; 22 | } 23 | } 24 | 25 | .actual { 26 | position: absolute; 27 | object-fit: cover; 28 | z-index: 2; 29 | width: 100%; 30 | height: 225px; 31 | opacity: 0; 32 | transition: opacity 750ms ease; 33 | 34 | &.loaded { 35 | opacity: 1; 36 | } 37 | } 38 | 39 | @include screen-size-laptop { 40 | .container, .placeholder, .actual { 41 | height: 450px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/lib/src/events/events_page_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:angular_router/angular_router.dart'; 3 | import 'package:core/core.dart'; 4 | import 'package:redux/redux.dart'; 5 | import 'package:web/src/common/event_poster/event_poster_component.dart'; 6 | import 'package:web/src/common/loading_view/loading_view_component.dart'; 7 | import 'package:web/src/common/theater_selector/theater_selector_component.dart'; 8 | import 'package:web/src/routes.dart'; 9 | 10 | import '../restore_scroll_position.dart'; 11 | 12 | @Component( 13 | selector: 'events-page', 14 | styleUrls: ['events_page_component.css'], 15 | templateUrl: 'events_page_component.html', 16 | directives: [ 17 | TheaterSelectorComponent, 18 | LoadingViewComponent, 19 | EventPosterComponent, 20 | NgFor, 21 | ], 22 | ) 23 | class EventsPageComponent implements OnActivate { 24 | EventsPageComponent(this._store, this._router, this.messages); 25 | final Store _store; 26 | final Router _router; 27 | final Messages messages; 28 | 29 | EventListType _listType; 30 | 31 | EventsPageViewModel get viewModel => 32 | EventsPageViewModel.fromStore(_store, _listType); 33 | 34 | String get eventTypeTitle => _listType == EventListType.nowInTheaters 35 | ? messages.nowInTheaters 36 | : messages.comingSoon; 37 | 38 | bool get isDisplayingComingSoonMovies => 39 | _listType == EventListType.comingSoon; 40 | 41 | @override 42 | void onActivate(RouterState previous, RouterState current) { 43 | _listType = current.routePath.additionalData; 44 | restoreScrollPositionIfNeeded(previous, RoutePaths.eventDetails); 45 | 46 | if (_listType == EventListType.comingSoon) { 47 | _store.dispatch(FetchComingSoonEventsIfNotLoadedAction()); 48 | } 49 | } 50 | 51 | void openEventDetails(Event event) { 52 | storeCurrentScrollPosition(); 53 | 54 | final url = 55 | RoutePaths.eventDetails.toUrl(parameters: {'eventId': event.id}); 56 | _router.navigate(url); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/lib/src/events/events_page_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ eventTypeTitle }}

4 | 5 |
6 | 7 | 11 |
12 | 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /web/lib/src/events/events_page_component.scss: -------------------------------------------------------------------------------- 1 | @import '../breakpoints'; 2 | @import '../common'; 3 | 4 | .grid-container { 5 | display: flex; 6 | flex-flow: row wrap; 7 | } 8 | 9 | event-poster { 10 | width: calc(100% / 2); 11 | min-height: 243px; 12 | cursor: pointer; 13 | } 14 | 15 | @include screen-size-nexus-5x { 16 | event-poster { 17 | min-height: 312px; 18 | } 19 | } 20 | 21 | @include screen-size-phablet { 22 | event-poster { 23 | width: calc(100% / 3); 24 | min-height: 327px; 25 | } 26 | } 27 | 28 | $horizontal-margin: 1.5em; 29 | $vertical-margin: 1.5em; 30 | 31 | @include screen-size-tablet { 32 | event-poster { 33 | box-shadow: 1px 1px 8px 2px rgba(0, 0, 0, 0.32); 34 | margin-top: $vertical-margin; 35 | margin-right: $horizontal-margin; 36 | width: calc(100% / 3 - 1em); 37 | min-height: 343px; 38 | } 39 | 40 | .grid-container :nth-child(3n) { 41 | margin-right: 0; 42 | } 43 | } 44 | 45 | @include screen-size-laptop { 46 | event-poster { 47 | width: calc(100% / 3 - 1em); 48 | min-height: 404px; 49 | } 50 | 51 | .grid-container { 52 | margin: 0; 53 | } 54 | 55 | .grid-container :nth-child(3n) { 56 | margin-right: 0; 57 | } 58 | } 59 | 60 | @include screen-size-huge { 61 | event-poster { 62 | width: calc(100% / 4 - 1.15em); 63 | min-height: 448px; 64 | } 65 | 66 | .grid-container :nth-child(3n) { 67 | margin-right: $horizontal-margin; 68 | } 69 | 70 | .grid-container :nth-child(4n) { 71 | margin-right: 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /web/lib/src/restore_scroll_position.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | 4 | import 'package:angular_router/angular_router.dart'; 5 | 6 | void storeCurrentScrollPosition() => 7 | window.sessionStorage['scrollY'] = window.scrollY.toString(); 8 | 9 | void restoreScrollPositionIfNeeded( 10 | RouterState previous, RoutePath restoreWhenComingFrom) { 11 | final shouldRestoreScrollPosition = 12 | previous?.routePath?.path == restoreWhenComingFrom.path; 13 | 14 | if (shouldRestoreScrollPosition) { 15 | Timer(Duration.zero, () { 16 | window.scrollTo(0, int.tryParse(window.sessionStorage['scrollY'] ?? '0')); 17 | }); 18 | } else { 19 | window.scrollTo(0, 0); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/lib/src/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular_router/angular_router.dart'; 2 | import 'package:core/core.dart'; 3 | import 'package:web/src/event_details/event_details_component.template.dart' 4 | deferred as event_details; 5 | import 'package:web/src/events/events_page_component.template.dart' 6 | as events_page; 7 | import 'package:web/src/showtimes/showtimes_page_component.template.dart' 8 | deferred as showtimes_page; 9 | 10 | class RoutePaths { 11 | static final nowInTheaters = RoutePath( 12 | path: '/', 13 | additionalData: EventListType.nowInTheaters, 14 | useAsDefault: true, 15 | ); 16 | 17 | static final showtimes = RoutePath(path: 'showtimes'); 18 | static final comingSoon = RoutePath( 19 | path: 'comingSoon', 20 | additionalData: EventListType.comingSoon, 21 | ); 22 | 23 | static final eventDetails = RoutePath(path: 'event/:eventId'); 24 | static final showDetails = RoutePath(path: 'show/:eventId/:showId'); 25 | } 26 | 27 | class Routes { 28 | static final List all = [ 29 | RouteDefinition( 30 | routePath: RoutePaths.nowInTheaters, 31 | useAsDefault: true, 32 | component: events_page.EventsPageComponentNgFactory, 33 | ), 34 | RouteDefinition( 35 | routePath: RoutePaths.comingSoon, 36 | component: events_page.EventsPageComponentNgFactory, 37 | ), 38 | RouteDefinition.defer( 39 | routePath: RoutePaths.showtimes, 40 | loader: () { 41 | return showtimes_page 42 | .loadLibrary() 43 | .then((_) => showtimes_page.ShowtimesPageComponentNgFactory); 44 | }, 45 | ), 46 | RouteDefinition.defer( 47 | routePath: RoutePaths.eventDetails, 48 | loader: () { 49 | return event_details 50 | .loadLibrary() 51 | .then((_) => event_details.EventDetailsComponentNgFactory); 52 | }, 53 | ), 54 | RouteDefinition.defer( 55 | routePath: RoutePaths.showDetails, 56 | loader: () { 57 | return event_details 58 | .loadLibrary() 59 | .then((_) => event_details.EventDetailsComponentNgFactory); 60 | }, 61 | ), 62 | ]; 63 | } 64 | -------------------------------------------------------------------------------- /web/lib/src/showtimes/date_selector_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | 3 | @Component( 4 | selector: 'date-selector', 5 | styleUrls: ['date_selector_component.css'], 6 | templateUrl: 'date_selector_component.html', 7 | directives: [NgFor], 8 | pipes: [DatePipe], 9 | ) 10 | class DateSelectorComponent { 11 | @Input() 12 | List dates; 13 | 14 | @Input() 15 | DateTime selectedDate; 16 | 17 | @Input() 18 | Function(DateTime) newDateSelected; 19 | } 20 | -------------------------------------------------------------------------------- /web/lib/src/showtimes/date_selector_component.html: -------------------------------------------------------------------------------- 1 |
5 |

{{ date | date: 'E' }}

6 |

{{ date | date: 'd' }}

7 |
8 | -------------------------------------------------------------------------------- /web/lib/src/showtimes/date_selector_component.scss: -------------------------------------------------------------------------------- 1 | @import '../breakpoints'; 2 | 3 | :host { 4 | display: flex; 5 | color: rgba(255, 255, 255, 0.4); 6 | justify-content: space-around; 7 | } 8 | 9 | .date-item { 10 | flex: 1; 11 | cursor: pointer; 12 | height: 50px; 13 | position: relative; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | background: #0F1734; 18 | transition: background-color 0.2s ease; 19 | 20 | p { 21 | text-align: center; 22 | } 23 | 24 | .dayname { 25 | color: #717DAD; 26 | font-size: 12px; 27 | font-weight: 500; 28 | transition: color 0.2s ease; 29 | } 30 | 31 | .day { 32 | color: #ffffff; 33 | font-size: 20px; 34 | font-weight: 300; 35 | transition: color 0.2s ease, font-weight 0.2s ease; 36 | } 37 | } 38 | 39 | .selected { 40 | background: #FBBE35; 41 | 42 | .dayname, .day { 43 | color: #0d1732; 44 | } 45 | 46 | .day { 47 | font-weight: 500; 48 | } 49 | } 50 | 51 | @include screen-size-laptop { 52 | .date-item { 53 | height: 60px; 54 | margin-right: 3px; 55 | 56 | &:last-child { 57 | margin-right: 0; 58 | } 59 | 60 | .day { 61 | font-size: 25px; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /web/lib/src/showtimes/showtimes_page_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:angular_router/angular_router.dart'; 3 | import 'package:core/core.dart'; 4 | import 'package:kt_dart/collection.dart'; 5 | import 'package:redux/redux.dart'; 6 | import 'package:web/src/common/loading_view/loading_view_component.dart'; 7 | import 'package:web/src/common/showtime_item/showtime_item_component.dart'; 8 | import 'package:web/src/common/theater_selector/theater_selector_component.dart'; 9 | import 'package:web/src/restore_scroll_position.dart'; 10 | import 'package:web/src/routes.dart'; 11 | import 'package:web/src/showtimes/date_selector_component.dart'; 12 | 13 | @Component( 14 | selector: 'showtimes-page', 15 | styleUrls: ['showtimes_page_component.css'], 16 | templateUrl: 'showtimes_page_component.html', 17 | directives: [ 18 | TheaterSelectorComponent, 19 | LoadingViewComponent, 20 | ShowtimeItemComponent, 21 | DateSelectorComponent, 22 | NgFor, 23 | NgIf, 24 | ], 25 | pipes: [DatePipe], 26 | ) 27 | class ShowtimesPageComponent implements OnActivate { 28 | ShowtimesPageComponent(this._store, this._router, this.messages); 29 | final Store _store; 30 | final Router _router; 31 | final Messages messages; 32 | 33 | @Input('event-filter') 34 | Event eventFilter; 35 | 36 | ShowtimesPageViewModel get viewModel => 37 | ShowtimesPageViewModel.fromStore(_store); 38 | 39 | KtList get shows => eventFilter == null 40 | ? viewModel.shows 41 | : showsForEventSelector(viewModel.shows, eventFilter); 42 | 43 | void openShowDetails(Show show) { 44 | storeCurrentScrollPosition(); 45 | 46 | final event = eventForShowSelector(_store.state, show); 47 | final url = RoutePaths.showDetails.toUrl(parameters: { 48 | 'eventId': event.id, 49 | 'showId': show.id, 50 | }); 51 | 52 | _router.navigate(url); 53 | } 54 | 55 | @override 56 | void onActivate(RouterState previous, _) { 57 | restoreScrollPositionIfNeeded(previous, RoutePaths.showDetails); 58 | _store.dispatch(FetchShowsIfNotLoadedAction()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/lib/src/showtimes/showtimes_page_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ messages.showtimes }}

4 | 5 |
6 | 7 | 10 | 11 | 15 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /web/lib/src/showtimes/showtimes_page_component.scss: -------------------------------------------------------------------------------- 1 | @import '../common'; 2 | @import '../breakpoints'; 3 | 4 | .page-header { 5 | display: flex; 6 | 7 | theater-selector { 8 | margin-left: 30px; 9 | } 10 | } 11 | 12 | .page-title { 13 | margin-bottom: 30px; 14 | } 15 | -------------------------------------------------------------------------------- /web/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: web 2 | description: A web app that uses AngularDart Components 3 | # version: 1.0.0 4 | # homepage: https://www.example.com 5 | # author: ironman 6 | 7 | environment: 8 | sdk: '>=2.0.0 <3.0.0' 9 | 10 | dependencies: 11 | core: 12 | path: ../core 13 | angular: ^5.0.0 14 | angular_router: ^2.0.0-alpha+19 15 | key_value_store_web: ^1.0.0 16 | pwa: ^0.1.11 17 | 18 | dev_dependencies: 19 | angular_test: ^2.0.0 20 | build_runner: ^0.10.0 21 | build_test: ^0.10.3 22 | build_web_compilers: ^0.4.1 23 | sass_builder: ^2.1.1 24 | test: ^1.3.0 25 | -------------------------------------------------------------------------------- /web/test/sample_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | void main() { 4 | /// TODO: Remove this and add proper tests. 5 | test('trues are true and falses are false', () { 6 | expect(true, isTrue); 7 | expect(false, isFalse); 8 | }); 9 | } -------------------------------------------------------------------------------- /web/web/images/arrow_drop_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/web/images/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/web/images/background-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/background-image.jpg -------------------------------------------------------------------------------- /web/web/images/close.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /web/web/images/coming-soon.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /web/web/images/fallback-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/web/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/favicon.png -------------------------------------------------------------------------------- /web/web/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/icon-192.png -------------------------------------------------------------------------------- /web/web/images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/icon-48.png -------------------------------------------------------------------------------- /web/web/images/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/icon-512.png -------------------------------------------------------------------------------- /web/web/images/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/icon-96.png -------------------------------------------------------------------------------- /web/web/images/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/web/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roughike/inKino/d406a23c3b57a0348a13de0cc6fc680b376cab35/web/web/images/logo.png -------------------------------------------------------------------------------- /web/web/images/now-in-theaters.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /web/web/images/place.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/web/images/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/web/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/web/images/showtimes.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/web/images/theaters.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /web/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | inKino - browse the movie selection of Finnish Finnkino cinemas 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /web/web/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:angular/angular.dart'; 4 | import 'package:angular_router/angular_router.dart'; 5 | import 'package:core/core.dart'; 6 | import 'package:http/http.dart'; 7 | import 'package:intl/date_symbol_data_local.dart'; 8 | import 'package:intl/intl.dart'; 9 | import 'package:intl/intl_browser.dart'; 10 | import 'package:key_value_store_web/key_value_store_web.dart'; 11 | import 'package:pwa/client.dart' as pwa; 12 | import 'package:redux/redux.dart'; 13 | import 'package:web/app_component.template.dart' as ng; 14 | 15 | import 'main.template.dart' as self; 16 | 17 | final Store _store = createStore( 18 | Client(), 19 | WebKeyValueStore(window.localStorage), 20 | ); 21 | Store storeFactory() => _store; 22 | 23 | @GenerateInjector([ 24 | const FactoryProvider(Store, storeFactory), 25 | const ClassProvider(Messages), 26 | routerProvidersHash, 27 | ]) 28 | final InjectorFactory rootInjector = self.rootInjector$Injector; 29 | 30 | void main() async { 31 | pwa.Client(); 32 | await _initializeTranslations(); 33 | 34 | runApp(ng.AppComponentNgFactory, createInjector: rootInjector); 35 | } 36 | 37 | void _initializeTranslations() async { 38 | var locale = await findSystemLocale(); 39 | final initializationSuccessful = await initializeMessages(locale); 40 | await initializeDateFormatting(locale); 41 | 42 | if (!initializationSuccessful) { 43 | // If we can't initialize messages for current locale, fall back on English. 44 | locale = 'en'; 45 | await initializeMessages(locale); 46 | await initializeDateFormatting(locale); 47 | } 48 | 49 | FinnkinoApi.useFinnish = locale == 'fi'; 50 | Intl.defaultLocale = locale; 51 | } 52 | -------------------------------------------------------------------------------- /web/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inKino", 3 | "short_name": "inKino", 4 | "description": "A web app that uses AngularDart Components", 5 | "lang": "en-US", 6 | "start_url": "/", 7 | "scope": "/", 8 | "display": "standalone", 9 | "orientation": "any", 10 | "theme_color": "#1C306D", 11 | "background_color": "#ffffff", 12 | "icons": [ 13 | { 14 | "src": "images/icon-48.png", 15 | "sizes": "48x48", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "images/icon-96.png", 20 | "sizes": "96x96", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "images/icon-192.png", 25 | "sizes": "192x192", 26 | "type": "image/png" 27 | }, 28 | { 29 | "src": "images/icon-512.png", 30 | "type": "image/png", 31 | "sizes": "512x512" 32 | } 33 | ], 34 | "related_applications": [ 35 | { 36 | "platform": "play", 37 | "url": "https://play.google.com/store/apps/details?id=com.roughike.inkino", 38 | "id": "com.roughike.inkino" 39 | }, { 40 | "platform": "itunes", 41 | "url": "https://itunes.apple.com/us/app/inkino/id1367181450" 42 | }] 43 | } 44 | -------------------------------------------------------------------------------- /web/web/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | inKino - Privacy Policy 5 | 6 | 7 |
8 |

inKino Privacy Policy

9 |

InKino does not store any of your data.

10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /web/web/pwa.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:pwa/worker.dart'; 3 | 4 | void main() { 5 | final cache = DynamicCache('inkino-cache', maxAge: const Duration(days: 1)); 6 | 7 | Worker() 8 | ..offlineUrls = [ 9 | './', 10 | './main.dart.js', 11 | './main.dart.js_1.part.js', 12 | './main.dart.js_2.part.js', 13 | './main.dart.js_3.part.js', 14 | './main.dart.js_4.part.js', 15 | './main.dart.js_5.part.js', 16 | './main.dart.js_6.part.js', 17 | './images/arrow_drop_down.svg', 18 | './images/back.svg', 19 | './images/background-image.jpg', 20 | './images/close.svg', 21 | './images/coming-soon.svg', 22 | './images/fallback-icon.svg', 23 | './images/favicon.png', 24 | './images/info.svg', 25 | './images/logo.png', 26 | './images/now-in-theaters.svg', 27 | './images/place.svg', 28 | './images/profile.svg', 29 | './images/search.svg', 30 | './images/showtimes.svg', 31 | './images/theaters.svg', 32 | './manifest.json', 33 | ] 34 | ..router.registerGetUrl( 35 | FinnkinoApi.enBaseUrl, (request) => cache.cacheFirst(request)) 36 | ..router.registerGetUrl( 37 | FinnkinoApi.fiBaseUrl, (request) => cache.cacheFirst(request)) 38 | ..run(version: '6'); 39 | } 40 | -------------------------------------------------------------------------------- /web/web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: --------------------------------------------------------------------------------