diff --git a/.gitignore b/.gitignore
index 8551635f..a8342be8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +1,43 @@
-.DS_Store
+android/google-play-service-account.json
+android/key.properties
+
+_Store
.atom/
-.idea
+.idea/
.vscode/
+
.packages
.pub/
-build/
-ios/.generated/
-packages
+.dart_tool/
pubspec.lock
+
+Podfile
+Podfile.lock
+Pods/
+.symlinks/
+**/Flutter/App.framework/
+**/Flutter/Flutter.framework/
+**/Flutter/Generated.xcconfig
+**/Flutter/flutter_assets/
+ServiceDefinitions.json
+xcuserdata/
+
+local.properties
+.gradle/
+gradlew
+gradlew.bat
+gradle-wrapper.jar
+*.iml
+
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+GeneratedPluginRegistrant.java
+build/
.flutter-plugins
-lib/tmdb_config.dart
-ios/Podfile.lock
+.idea/workspace.xml
+.firebaserc
+.firebase/
+.firebase-debug.log
+.DS_Store
-android/google-play-service-account.json
-android/key.properties
\ No newline at end of file
+core/lib/src/tmdb_config.dart
diff --git a/README.md b/README.md
index 89aaddc4..bab228df 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,61 @@
-# inKino - a showtime browser for Finnkino cinemas
+# inKino - a multiplatform Dart project with code sharing between Flutter and web
-[![Build Status](https://travis-ci.org/roughike/inKino.svg?branch=development)](https://travis-ci.org/roughike/inKino) [![Coverage Status](https://coveralls.io/repos/github/roughike/inKino/badge.svg?branch=development)](https://coveralls.io/github/roughike/inKino?branch=development)
-
-
+
## What is inKino?
-inKino is a minimal app for browsing movies and showtimes for [Finnkino](https://finnkino.fi/) cinemas. It's made with [Flutter](https://flutter.io/), uses [flutter_redux](https://github.com/brianegan/flutter_redux), and has an [extensive set of unit and widget tests](https://github.com/roughike/inKino/tree/development/test). It also has smooth transition animations and handles offline use cases gracefully.
+inKino is a _multiplatform_ Dart app for browsing movies and showtimes for Finnkino cinemas.
-While I built inKino for my own needs, it is also intented to showcase good app structure and a clean, well-organized Flutter codebase. The app uses the [Finnkino XML API](https://finnkino.fi/xml) for fetching movies and showtimes, and the [TMDB API](https://www.themoviedb.org/documentation/api) for fetching the actor avatars.
+InKino showcases Redux, has an extensive set of automated tests and **40% code sharing between Flutter and web**.
+The Android & iOS apps are made with a single [Flutter](http://flutter.io) codebase. The progressive web app is made with [AngularDart](https://webdev.dartlang.org/angular).
+This project is generally something that I believe is a good example of a multiplatform Dart project.
-The source code is **100% Dart**, and everything resides in the [/lib](https://github.com/roughike/inKino/tree/development/lib) folder.
+I plan on doing a full article series on multiplatform Dart stuff, so you might want to [check out my blog](https://iirokrankka.com) and subscribe to it.
-
+
+
+
+## Folder structure
+
+There's three different folders. Each of them is a Dart project.
+
+* **core**: contains the pure Dart business logic, such API communication, Redux, XML parsing, sanitization, i18n, models and utilities.
+It also has a great test coverage.
+* **mobile**: this is the Flutter project. It imports **core**, and it's a 100% shared codebase for the native Android & iOS apps that go on app stores.
+* **web**: the AngularDart progressive web app. Also imports **core**, and it's the thing that is live at https://inkino.app.
+
+To work on these projects, open each one of them in an editor of your choice.
+
+For example, if you want to do a new feature and you do it for the Flutter project first, you'd open both **core** and **mobile** in separate editor windows.
+To clarify, you'd do `File -> Open...` for core and then `File -> Open...` again for mobile.
+
+## Development environment setup
+
+* [Install Dart for the web](https://webdev.dartlang.org/tools/sdk#install). The customized Dart version Flutter ships with is not suitable for web development.
+* Install [webdev](https://webdev.dartlang.org/tools/webdev) by running `pub global activate webdev`. This requires that you ran your Dart installation properly and Dart is part of your PATH.
+* Install an IDE. You can't go wrong with [WebStorm](https://webdev.dartlang.org/tools/webstorm). If that doesn't tickle your fancy, [there are other options too](https://www.dartlang.org/tools#ides).
+* Install the Dart plugin for your IDE.
+
+Finally, if you haven't already, [install Flutter](https://flutter.io/docs/get-started/install).
+And the Flutter plugin for your IDE.
+At the time of being, inKino is built with **Flutter 0.10.2**.
+
+If you don't like IDEs, [you can apparently use Emacs or Vim too](https://news.ycombinator.com/item?id=16822780).
+
## Building the project
### Renaming the TMDB configuration file
-If you try to build the project straight away, you'll get an error complaining that a `tmdb_config.dart` file is missing. To resolve that, run this on your terminal in the project root:
+You don't need a TMDB API key, but the actor images won't load without it.
+
+If you try to build the project straight away, you'll get an error complaining that a tmdb_config.dart file is missing.
+To resolve that, run this on your terminal in the project root:
```bash
-cd lib && mv tmdb_config.dart.sample tmdb_config.dart && cd ..
+cd core/lib/src && mv tmdb_config.dart.sample tmdb_config.dart && cd ../../..
```
**OR**
@@ -32,12 +64,19 @@ If you don't trust in random bash scripts copied from the Internet, you can just
### Building from source
-To build the project, ensure that you have a recent version of the Flutter SDK installed. Then either run `flutter run` in the project root or use your IDE of choice. To run the tests, run `flutter test` in the project root.
+First, ensure that you followed the "Development environment setup" section above.
+
+* To run the **web project**, first run `pub get` initially, and then `webdev serve` in the root of the web project.
+* To run the **Flutter project**, open it in your editor and click the play button, or run `flutter run` on your terminal.
## Contributing
-Contributions are welcome! However, if it's going to be a major change, please create an issue first. Before starting to work on something, please comment on a specific issue and say you'd like to work on it.
+Contributions are welcome!
+However, if it's going to be a major change, please create an issue first.
+Before starting to work on something, please comment on a specific issue and say you'd like to work on it.
## Thanks
-Special thanks to [Thibaud Colas](https://twitter.com/thibaud_colas), [Brian Egan](https://twitter.com/brianegan), [Alessandro Aime](https://twitter.com/aimealessandro) and [Juho Rautioaho](https://github.com/Jraut) for giving their extra pair of eyes for reviewing the source code.
+Special thanks to [Olli Haataja](https://www.linkedin.com/in/olli-haataja-46b96b120/) for the design.
+
+Additional thanks for the initial release go to [Thibaud Colas](https://twitter.com/thibaud_colas), [Brian Egan](https://twitter.com/brianegan), [Alessandro Aime](https://twitter.com/aimealessandro) and [Juho Rautioaho](https://github.com/Jraut) for giving their extra pair of eyes for reviewing the source code.
diff --git a/screenshots/app_store.png b/_screenshots/app_store.png
similarity index 100%
rename from screenshots/app_store.png
rename to _screenshots/app_store.png
diff --git a/_screenshots/event_details.png b/_screenshots/event_details.png
new file mode 100644
index 00000000..f8c05371
Binary files /dev/null and b/_screenshots/event_details.png differ
diff --git a/screenshots/google_play.png b/_screenshots/google_play.png
similarity index 100%
rename from screenshots/google_play.png
rename to _screenshots/google_play.png
diff --git a/_screenshots/launch_pwa.png b/_screenshots/launch_pwa.png
new file mode 100644
index 00000000..47fce876
Binary files /dev/null and b/_screenshots/launch_pwa.png differ
diff --git a/_screenshots/now_in_theaters.png b/_screenshots/now_in_theaters.png
new file mode 100644
index 00000000..c3fc7e32
Binary files /dev/null and b/_screenshots/now_in_theaters.png differ
diff --git a/_screenshots/showtimes.png b/_screenshots/showtimes.png
new file mode 100644
index 00000000..30f8d574
Binary files /dev/null and b/_screenshots/showtimes.png differ
diff --git a/android.iml b/android.iml
deleted file mode 100644
index 462b903e..00000000
--- a/android.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk
deleted file mode 100644
index 38503a9a..00000000
Binary files a/android/app/release/app-release.apk and /dev/null differ
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 13372aef..00000000
Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/android/gradlew b/android/gradlew
deleted file mode 100755
index 9d82f789..00000000
--- a/android/gradlew
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
- echo "$*"
-}
-
-die ( ) {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=$((i+1))
- done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
deleted file mode 100644
index 8a0b282a..00000000
--- a/android/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/core/analysis_options.yaml b/core/analysis_options.yaml
new file mode 100644
index 00000000..97d4b470
--- /dev/null
+++ b/core/analysis_options.yaml
@@ -0,0 +1,14 @@
+analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# Lint rules and documentation, see http://dart-lang.github.io/linter/lints
+linter:
+ rules:
+ - cancel_subscriptions
+ - hash_and_equals
+ - iterable_contains_unrelated_type
+ - list_remove_unrelated_type
+ - test_types_in_equals
+ - unrelated_type_equality_checks
+ - valid_regexps
diff --git a/core/lib/core.dart b/core/lib/core.dart
new file mode 100644
index 00000000..0f1d4fb9
--- /dev/null
+++ b/core/lib/core.dart
@@ -0,0 +1,27 @@
+export 'src/models/actor.dart';
+export 'src/models/content_descriptor.dart';
+export 'src/models/event.dart';
+export 'src/models/loading_status.dart';
+export 'src/models/show.dart';
+export 'src/models/show_cache.dart';
+export 'src/models/theater.dart';
+
+export 'src/i18n/messages.dart';
+export 'src/i18n/inkino_messages_all.dart';
+
+export 'src/networking/finnkino_api.dart';
+
+export 'src/redux/_common/search.dart';
+export 'src/redux/_common/common_actions.dart';
+export 'src/redux/actor/actor_actions.dart';
+export 'src/redux/actor/actor_selectors.dart';
+export 'src/redux/app/app_state.dart';
+export 'src/redux/event/event_actions.dart';
+export 'src/redux/event/event_selectors.dart';
+export 'src/redux/show/show_actions.dart';
+export 'src/redux/show/show_selectors.dart';
+export 'src/redux/store.dart';
+
+export 'src/viewmodels/theater_list_view_model.dart';
+export 'src/viewmodels/events_page_view_model.dart';
+export 'src/viewmodels/showtime_page_view_model.dart';
\ No newline at end of file
diff --git a/core/lib/src/i18n/inkino_en.arb b/core/lib/src/i18n/inkino_en.arb
new file mode 100644
index 00000000..ce7688c4
--- /dev/null
+++ b/core/lib/src/i18n/inkino_en.arb
@@ -0,0 +1,148 @@
+{
+ "@@last_modified": "2018-11-03T15:02:36.546976",
+ "appName": "inKino",
+ "@appName": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "nowInTheaters": "Now in theaters",
+ "@nowInTheaters": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "showtimes": "Showtimes",
+ "@showtimes": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "comingSoon": "Coming soon",
+ "@comingSoon": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "oops": "Oops!",
+ "@oops": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "loadingMoviesError": "There was an error while\nloading movies.",
+ "@loadingMoviesError": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tryAgain": "TRY AGAIN",
+ "@tryAgain": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "director": "Director",
+ "@director": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "storyline": "Storyline",
+ "@storyline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "collapseStoryline": "touch to collapse",
+ "@collapseStoryline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "expandStoryline": "touch to expand",
+ "@expandStoryline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "cast": "Cast",
+ "@cast": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "gallery": "Gallery",
+ "@gallery": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "releaseDate": "Release date",
+ "@releaseDate": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "at": "at",
+ "@at": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tickets": "Tickets",
+ "@tickets": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "allEmpty": "All empty!",
+ "@allEmpty": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "noMovies": "Didn't find any movies at\nall.",
+ "@noMovies": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "errorLoadingEvents": "Error loading events.",
+ "@errorLoadingEvents": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "noMoviesForToday": "Didn't find any movies\nabout to start for today. ¯\\_(ツ)_/¯",
+ "@noMoviesForToday": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "about": "About",
+ "@about": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "aboutInKino": "About inKino",
+ "@aboutInKino": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "gotIt": "Okay, got it!",
+ "@gotIt": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "aboutInKinoDescription": "inKino is the unofficial Finnkino client that is minimalistic, fast, and delightful to use.",
+ "@aboutInKinoDescription": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "appDevelopedWith": "The app was developed with",
+ "@appDevelopedWith": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "checkoutRepo": "and it's open source; check out the source code yourself from",
+ "@checkoutRepo": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "githubRepo": "the GitHub repo",
+ "@githubRepo": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tmdbAttribution": "This product uses the TMDb API but is not endorsed or certified by TMDb.",
+ "@tmdbAttribution": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "searchHint": "Search movies & showtimes...",
+ "@searchHint": {
+ "type": "text",
+ "placeholders": {}
+ }
+}
\ No newline at end of file
diff --git a/core/lib/src/i18n/inkino_fi.arb b/core/lib/src/i18n/inkino_fi.arb
new file mode 100644
index 00000000..92b5e4bc
--- /dev/null
+++ b/core/lib/src/i18n/inkino_fi.arb
@@ -0,0 +1,147 @@
+{
+ "appName": "inKino",
+ "@appName": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "nowInTheaters": "Ohjelmistossa",
+ "@nowInTheaters": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "showtimes": "Näytösajat",
+ "@showtimes": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "comingSoon": "Tulossa",
+ "@comingSoon": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "oops": "Oho!",
+ "@oops": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "loadingMoviesError": "Ongelma elokuvien latauksessa.",
+ "@loadingMoviesError": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tryAgain": "YRITÄ UUDELLEEN",
+ "@tryAgain": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "director": "Ohjaaja",
+ "@director": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "storyline": "Juoni",
+ "@storyline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "collapseStoryline": "näytä enemmän",
+ "@collapseStoryline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "expandStoryline": "näytä vähemmän",
+ "@expandStoryline": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "cast": "Näyttelijät",
+ "@cast": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "gallery": "Galleria",
+ "@gallery": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "releaseDate": "Julkaisupäivä",
+ "@releaseDate": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "at": "klo",
+ "@at": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tickets": "Liput",
+ "@tickets": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "allEmpty": "Tyhjää täynnä!",
+ "@allEmpty": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "noMovies": "Elokuvia ei löytynyt.",
+ "@noMovies": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "errorLoadingEvents": "Elokuvia ladattaessa tapahtui virhe.",
+ "@errorLoadingEvents": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "noMoviesForToday": "Tälle päivälle ei löytynyt yhtään alkavia elokuvia. ¯ \\ _ (ツ) _ / ¯",
+ "@noMoviesForToday": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "about": "Tietoa",
+ "@about": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "aboutInKino": "Tietoa InKinosta",
+ "@aboutInKino": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "gotIt": "Selvä homma!",
+ "@gotIt": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "aboutInKinoDescription": "inKino on epävirallinen Finnkino-sovellus, joka on minimalistinen, nopea ja ihastuttava käyttää.",
+ "@aboutInKinoDescription": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "appDevelopedWith": "Sovellus on kehitetty käyttämällä",
+ "@appDevelopedWith": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "checkoutRepo": "ja sen lähdekoodi on julkisesti saatavilla",
+ "@checkoutRepo": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "githubRepo": "GitHubissa",
+ "@githubRepo": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "tmdbAttribution": "Tämä tuote käyttää TMDb:n rajapintaa, mutta sitä ei ole TMDb:n suosittelema tai sertifioima.",
+ "@tmdbAttribution": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "searchHint": "Hae elokuvia ja näytösaikoja...",
+ "@searchHint": {
+ "type": "text",
+ "placeholders": {}
+ }
+}
\ No newline at end of file
diff --git a/core/lib/src/i18n/inkino_messages_all.dart b/core/lib/src/i18n/inkino_messages_all.dart
new file mode 100644
index 00000000..42afe11f
--- /dev/null
+++ b/core/lib/src/i18n/inkino_messages_all.dart
@@ -0,0 +1,61 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that looks up messages for specific locales by
+// delegating to the appropriate library.
+
+import 'dart:async';
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+// ignore: implementation_imports
+import 'package:intl/src/intl_helpers.dart';
+
+import 'package:core/src/i18n/inkino_messages_en.dart' deferred as messages_en;
+import 'package:core/src/i18n/inkino_messages_fi.dart' deferred as messages_fi;
+
+typedef Future LibraryLoader();
+Map _deferredLibraries = {
+ 'en': () => messages_en.loadLibrary(),
+ 'fi': () => messages_fi.loadLibrary(),
+};
+
+MessageLookupByLibrary _findExact(localeName) {
+ switch (localeName) {
+ case 'en':
+ return messages_en.messages;
+ case 'fi':
+ return messages_fi.messages;
+ default:
+ return null;
+ }
+}
+
+/// User programs should call this before using [localeName] for messages.
+Future initializeMessages(String localeName) async {
+ var availableLocale = Intl.verifiedLocale(
+ localeName,
+ (locale) => _deferredLibraries[locale] != null,
+ onFailure: (_) => null);
+ if (availableLocale == null) {
+ return new Future.value(false);
+ }
+ var lib = _deferredLibraries[availableLocale];
+ await (lib == null ? new Future.value(false) : lib());
+ initializeInternalMessageLookup(() => new CompositeMessageLookup());
+ messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
+ return new Future.value(true);
+}
+
+bool _messagesExistFor(String locale) {
+ try {
+ return _findExact(locale) != null;
+ } catch (e) {
+ return false;
+ }
+}
+
+MessageLookupByLibrary _findGeneratedMessagesFor(locale) {
+ var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
+ onFailure: (_) => null);
+ if (actualLocale == null) return null;
+ return _findExact(actualLocale);
+}
diff --git a/core/lib/src/i18n/inkino_messages_en.dart b/core/lib/src/i18n/inkino_messages_en.dart
new file mode 100644
index 00000000..7e7fd12a
--- /dev/null
+++ b/core/lib/src/i18n/inkino_messages_en.dart
@@ -0,0 +1,52 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a en locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+// ignore: unused_element
+final _keepAnalysisHappy = Intl.defaultLocale;
+
+// ignore: non_constant_identifier_names
+typedef MessageIfAbsent(String message_str, List args);
+
+class MessageLookup extends MessageLookupByLibrary {
+ get localeName => 'en';
+
+ final messages = _notInlinedMessages(_notInlinedMessages);
+ static _notInlinedMessages(_) => {
+ "about" : MessageLookupByLibrary.simpleMessage("About"),
+ "aboutInKino" : MessageLookupByLibrary.simpleMessage("About inKino"),
+ "aboutInKinoDescription" : MessageLookupByLibrary.simpleMessage("inKino is the unofficial Finnkino client that is minimalistic, fast, and delightful to use."),
+ "allEmpty" : MessageLookupByLibrary.simpleMessage("All empty!"),
+ "appDevelopedWith" : MessageLookupByLibrary.simpleMessage("The app was developed with"),
+ "appName" : MessageLookupByLibrary.simpleMessage("inKino"),
+ "at" : MessageLookupByLibrary.simpleMessage("at"),
+ "cast" : MessageLookupByLibrary.simpleMessage("Cast"),
+ "checkoutRepo" : MessageLookupByLibrary.simpleMessage("and it\'s open source; check out the source code yourself from"),
+ "collapseStoryline" : MessageLookupByLibrary.simpleMessage("touch to collapse"),
+ "comingSoon" : MessageLookupByLibrary.simpleMessage("Coming soon"),
+ "director" : MessageLookupByLibrary.simpleMessage("Director"),
+ "errorLoadingEvents" : MessageLookupByLibrary.simpleMessage("Error loading events."),
+ "expandStoryline" : MessageLookupByLibrary.simpleMessage("touch to expand"),
+ "gallery" : MessageLookupByLibrary.simpleMessage("Gallery"),
+ "githubRepo" : MessageLookupByLibrary.simpleMessage("the GitHub repo"),
+ "gotIt" : MessageLookupByLibrary.simpleMessage("Okay, got it!"),
+ "loadingMoviesError" : MessageLookupByLibrary.simpleMessage("There was an error while\nloading movies."),
+ "noMovies" : MessageLookupByLibrary.simpleMessage("Didn\'t find any movies at\nall."),
+ "noMoviesForToday" : MessageLookupByLibrary.simpleMessage("Didn\'t find any movies\nabout to start for today. ¯\\_(ツ)_/¯"),
+ "nowInTheaters" : MessageLookupByLibrary.simpleMessage("Now in theaters"),
+ "oops" : MessageLookupByLibrary.simpleMessage("Oops!"),
+ "releaseDate" : MessageLookupByLibrary.simpleMessage("Release date"),
+ "searchHint" : MessageLookupByLibrary.simpleMessage("Search movies & showtimes..."),
+ "showtimes" : MessageLookupByLibrary.simpleMessage("Showtimes"),
+ "storyline" : MessageLookupByLibrary.simpleMessage("Storyline"),
+ "tickets" : MessageLookupByLibrary.simpleMessage("Tickets"),
+ "tmdbAttribution" : MessageLookupByLibrary.simpleMessage("This product uses the TMDb API but is not endorsed or certified by TMDb."),
+ "tryAgain" : MessageLookupByLibrary.simpleMessage("TRY AGAIN")
+ };
+}
diff --git a/core/lib/src/i18n/inkino_messages_fi.dart b/core/lib/src/i18n/inkino_messages_fi.dart
new file mode 100644
index 00000000..beff92a5
--- /dev/null
+++ b/core/lib/src/i18n/inkino_messages_fi.dart
@@ -0,0 +1,52 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a fi locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+// ignore: unused_element
+final _keepAnalysisHappy = Intl.defaultLocale;
+
+// ignore: non_constant_identifier_names
+typedef MessageIfAbsent(String message_str, List args);
+
+class MessageLookup extends MessageLookupByLibrary {
+ get localeName => 'fi';
+
+ final messages = _notInlinedMessages(_notInlinedMessages);
+ static _notInlinedMessages(_) => {
+ "about" : MessageLookupByLibrary.simpleMessage("Tietoa"),
+ "aboutInKino" : MessageLookupByLibrary.simpleMessage("Tietoa InKinosta"),
+ "aboutInKinoDescription" : MessageLookupByLibrary.simpleMessage("inKino on epävirallinen Finnkino-sovellus, joka on minimalistinen, nopea ja ihastuttava käyttää."),
+ "allEmpty" : MessageLookupByLibrary.simpleMessage("Tyhjää täynnä!"),
+ "appDevelopedWith" : MessageLookupByLibrary.simpleMessage("Sovellus on kehitetty käyttämällä"),
+ "appName" : MessageLookupByLibrary.simpleMessage("inKino"),
+ "at" : MessageLookupByLibrary.simpleMessage("klo"),
+ "cast" : MessageLookupByLibrary.simpleMessage("Näyttelijät"),
+ "checkoutRepo" : MessageLookupByLibrary.simpleMessage("ja sen lähdekoodi on julkisesti saatavilla"),
+ "collapseStoryline" : MessageLookupByLibrary.simpleMessage("näytä enemmän"),
+ "comingSoon" : MessageLookupByLibrary.simpleMessage("Tulossa"),
+ "director" : MessageLookupByLibrary.simpleMessage("Ohjaaja"),
+ "errorLoadingEvents" : MessageLookupByLibrary.simpleMessage("Elokuvia ladattaessa tapahtui virhe."),
+ "expandStoryline" : MessageLookupByLibrary.simpleMessage("näytä vähemmän"),
+ "gallery" : MessageLookupByLibrary.simpleMessage("Galleria"),
+ "githubRepo" : MessageLookupByLibrary.simpleMessage("GitHubissa"),
+ "gotIt" : MessageLookupByLibrary.simpleMessage("Selvä homma!"),
+ "loadingMoviesError" : MessageLookupByLibrary.simpleMessage("Ongelma elokuvien latauksessa."),
+ "noMovies" : MessageLookupByLibrary.simpleMessage("Elokuvia ei löytynyt."),
+ "noMoviesForToday" : MessageLookupByLibrary.simpleMessage("Tälle päivälle ei löytynyt yhtään alkavia elokuvia. ¯ \\ _ (ツ) _ / ¯"),
+ "nowInTheaters" : MessageLookupByLibrary.simpleMessage("Ohjelmistossa"),
+ "oops" : MessageLookupByLibrary.simpleMessage("Oho!"),
+ "releaseDate" : MessageLookupByLibrary.simpleMessage("Julkaisupäivä"),
+ "searchHint" : MessageLookupByLibrary.simpleMessage("Hae elokuvia ja näytösaikoja..."),
+ "showtimes" : MessageLookupByLibrary.simpleMessage("Näytösajat"),
+ "storyline" : MessageLookupByLibrary.simpleMessage("Juoni"),
+ "tickets" : MessageLookupByLibrary.simpleMessage("Liput"),
+ "tmdbAttribution" : MessageLookupByLibrary.simpleMessage("Tämä tuote käyttää TMDb:n rajapintaa, mutta sitä ei ole TMDb:n suosittelema tai sertifioima."),
+ "tryAgain" : MessageLookupByLibrary.simpleMessage("YRITÄ UUDELLEEN")
+ };
+}
diff --git a/core/lib/src/i18n/messages.dart b/core/lib/src/i18n/messages.dart
new file mode 100644
index 00000000..df4c1926
--- /dev/null
+++ b/core/lib/src/i18n/messages.dart
@@ -0,0 +1,75 @@
+import 'package:intl/intl.dart';
+
+class Messages {
+ String get appName => Intl.message('inKino', name: 'appName');
+ String get nowInTheaters => Intl.message(
+ 'Now in theaters',
+ name: 'nowInTheaters',
+ );
+ String get showtimes => Intl.message(
+ 'Showtimes',
+ name: 'showtimes',
+ );
+ String get comingSoon => Intl.message(
+ 'Coming soon',
+ name: 'comingSoon',
+ );
+
+ String get oops => Intl.message('Oops!', name: 'oops');
+ String get loadingMoviesError => Intl.message(
+ 'There was an error while\nloading movies.',
+ name: 'loadingMoviesError',
+ );
+ String get tryAgain => Intl.message('TRY AGAIN', name: 'tryAgain');
+
+ String get director => Intl.message('Director', name: 'director');
+ String get storyline => Intl.message('Storyline', name: 'storyline');
+ String get collapseStoryline =>
+ Intl.message('touch to collapse', name: 'collapseStoryline');
+ String get expandStoryline =>
+ Intl.message('touch to expand', name: 'expandStoryline');
+ String get cast => Intl.message('Cast', name: 'cast');
+ String get gallery => Intl.message('Gallery', name: 'gallery');
+
+ String get releaseDate => Intl.message('Release date', name: 'releaseDate');
+ String get at => Intl.message(
+ 'at',
+ name: 'at',
+ meaning: 'Means time. For example, "the meeting is at 6PM".',
+ );
+
+ String get tickets => Intl.message('Tickets', name: 'tickets');
+ String get allEmpty => Intl.message('All empty!', name: 'allEmpty');
+ String get noMovies =>
+ Intl.message('Didn\'t find any movies at\nall.', name: 'noMovies');
+ String get errorLoadingEvents =>
+ Intl.message('Error loading events.', name: 'errorLoadingEvents');
+
+ String get noMoviesForToday => Intl.message(
+ 'Didn\'t find any movies\nabout to start for today. ¯\\_(ツ)_/¯',
+ name: 'noMoviesForToday',
+ );
+ String get about => Intl.message('About', name: 'about');
+ String get aboutInKino => Intl.message('About inKino', name: 'aboutInKino');
+ String get gotIt => Intl.message('Okay, got it!', name: 'gotIt');
+ String get aboutInKinoDescription => Intl.message(
+ 'inKino is the unofficial Finnkino client that is minimalistic, fast, and delightful to use.',
+ name: 'aboutInKinoDescription',
+ );
+ String get appDevelopedWith => Intl.message(
+ 'The app was developed with',
+ name: 'appDevelopedWith',
+ );
+ String get checkoutRepo => Intl.message(
+ 'and it\'s open source; check out the source code yourself from',
+ name: 'checkoutRepo',
+ );
+ String get githubRepo => Intl.message('the GitHub repo', name: 'githubRepo');
+ String get tmdbAttribution => Intl.message(
+ 'This product uses the TMDb API but is not endorsed or certified by TMDb.',
+ name: 'tmdbAttribution',
+ );
+
+ String get searchHint =>
+ Intl.message('Search movies & showtimes...', name: 'searchHint');
+}
diff --git a/lib/models/actor.dart b/core/lib/src/models/actor.dart
similarity index 100%
rename from lib/models/actor.dart
rename to core/lib/src/models/actor.dart
diff --git a/core/lib/src/models/content_descriptor.dart b/core/lib/src/models/content_descriptor.dart
new file mode 100644
index 00000000..53f0125d
--- /dev/null
+++ b/core/lib/src/models/content_descriptor.dart
@@ -0,0 +1,29 @@
+import 'package:meta/meta.dart';
+
+class ContentDescriptor {
+ ContentDescriptor({
+ @required this.name,
+ @required this.imageUrl,
+ });
+
+ final String name;
+ final String imageUrl;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is ContentDescriptor &&
+ runtimeType == other.runtimeType &&
+ name == other.name &&
+ imageUrl == other.imageUrl;
+
+ @override
+ int get hashCode =>
+ name.hashCode ^
+ imageUrl.hashCode;
+
+ @override
+ String toString() {
+ return 'ContentDescriptor{name: $name, imageUrl: $imageUrl}';
+ }
+}
diff --git a/lib/models/event.dart b/core/lib/src/models/event.dart
similarity index 61%
rename from lib/models/event.dart
rename to core/lib/src/models/event.dart
index 440f5811..5f8a33e5 100644
--- a/lib/models/event.dart
+++ b/core/lib/src/models/event.dart
@@ -1,6 +1,8 @@
-import 'package:inkino/models/actor.dart';
import 'package:meta/meta.dart';
+import 'actor.dart';
+import 'content_descriptor.dart';
+
enum EventListType {
nowInTheaters,
comingSoon,
@@ -11,7 +13,9 @@ class Event {
this.id,
this.title,
this.originalTitle,
- this.productionYear,
+ this.releaseDate,
+ this.ageRating,
+ this.ageRatingUrl,
this.genres,
this.directors,
this.actors,
@@ -19,26 +23,36 @@ class Event {
this.shortSynopsis,
this.synopsis,
this.images,
+ this.contentDescriptors,
this.youtubeTrailers,
+ this.galleryImages,
});
final String id;
final String title;
final String originalTitle;
- final String productionYear;
+ final DateTime releaseDate;
+ final String ageRating;
+ final String ageRatingUrl;
final String genres;
final List directors;
final String lengthInMinutes;
final String shortSynopsis;
final String synopsis;
final EventImageData images;
+ final List contentDescriptors;
final List youtubeTrailers;
+ final List galleryImages;
+ String get director =>
+ directors.firstWhere((e) => e != null, orElse: () => null);
List actors;
+ List get genresSeparated => genres.split(', ');
bool get hasSynopsis =>
(shortSynopsis != null && shortSynopsis.isNotEmpty) ||
(synopsis != null && synopsis.isNotEmpty);
+ bool get hasMediumPortraitImage => images.portraitMedium != null;
@override
bool operator ==(Object other) =>
@@ -48,13 +62,16 @@ class Event {
id == other.id &&
title == other.title &&
originalTitle == other.originalTitle &&
- productionYear == other.productionYear &&
+ releaseDate == other.releaseDate &&
+ ageRating == other.ageRating &&
+ ageRatingUrl == other.ageRatingUrl &&
genres == other.genres &&
directors == other.directors &&
lengthInMinutes == other.lengthInMinutes &&
shortSynopsis == other.shortSynopsis &&
synopsis == other.synopsis &&
images == other.images &&
+ contentDescriptors == other.contentDescriptors &&
youtubeTrailers == other.youtubeTrailers &&
actors == other.actors;
@@ -63,17 +80,41 @@ class Event {
id.hashCode ^
title.hashCode ^
originalTitle.hashCode ^
- productionYear.hashCode ^
+ releaseDate.hashCode ^
+ ageRating.hashCode ^
+ ageRatingUrl.hashCode ^
genres.hashCode ^
directors.hashCode ^
lengthInMinutes.hashCode ^
shortSynopsis.hashCode ^
synopsis.hashCode ^
images.hashCode ^
+ contentDescriptors.hashCode ^
youtubeTrailers.hashCode ^
actors.hashCode;
}
+class GalleryImage {
+ GalleryImage({
+ this.location,
+ this.thumbnailLocation,
+ });
+
+ final String location;
+ final String thumbnailLocation;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is GalleryImage &&
+ runtimeType == other.runtimeType &&
+ location == other.location &&
+ thumbnailLocation == other.thumbnailLocation;
+
+ @override
+ int get hashCode => location.hashCode ^ thumbnailLocation.hashCode;
+}
+
class EventImageData {
EventImageData({
@required this.portraitSmall,
@@ -81,6 +122,8 @@ class EventImageData {
@required this.portraitLarge,
@required this.landscapeSmall,
@required this.landscapeBig,
+ @required this.landscapeHd,
+ @required this.landscapeHd2,
});
final String portraitSmall;
@@ -88,6 +131,8 @@ class EventImageData {
final String portraitLarge;
final String landscapeSmall;
final String landscapeBig;
+ final String landscapeHd;
+ final String landscapeHd2;
String get anyAvailableImage =>
portraitSmall ??
@@ -101,7 +146,9 @@ class EventImageData {
portraitMedium = null,
portraitLarge = null,
landscapeSmall = null,
- landscapeBig = null;
+ landscapeBig = null,
+ landscapeHd = null,
+ landscapeHd2 = null;
@override
bool operator ==(Object other) =>
@@ -112,7 +159,9 @@ class EventImageData {
portraitMedium == other.portraitMedium &&
portraitLarge == other.portraitLarge &&
landscapeSmall == other.landscapeSmall &&
- landscapeBig == other.landscapeBig;
+ landscapeBig == other.landscapeBig &&
+ landscapeHd == other.landscapeHd &&
+ landscapeHd2 == other.landscapeHd2;
@override
int get hashCode =>
@@ -120,5 +169,7 @@ class EventImageData {
portraitMedium.hashCode ^
portraitLarge.hashCode ^
landscapeSmall.hashCode ^
- landscapeBig.hashCode;
+ landscapeBig.hashCode ^
+ landscapeHd.hashCode ^
+ landscapeHd2.hashCode;
}
diff --git a/lib/models/loading_status.dart b/core/lib/src/models/loading_status.dart
similarity index 86%
rename from lib/models/loading_status.dart
rename to core/lib/src/models/loading_status.dart
index 3b280d8f..de82fbd4 100644
--- a/lib/models/loading_status.dart
+++ b/core/lib/src/models/loading_status.dart
@@ -1,4 +1,5 @@
enum LoadingStatus {
+ idle,
loading,
error,
success,
diff --git a/lib/models/show.dart b/core/lib/src/models/show.dart
similarity index 65%
rename from lib/models/show.dart
rename to core/lib/src/models/show.dart
index d705d667..dbec00b7 100644
--- a/lib/models/show.dart
+++ b/core/lib/src/models/show.dart
@@ -1,25 +1,36 @@
+import 'content_descriptor.dart';
+import 'event.dart';
+
class Show {
Show({
this.id,
this.eventId,
this.title,
this.originalTitle,
+ this.ageRating,
+ this.ageRatingUrl,
this.url,
this.presentationMethod,
this.theaterAndAuditorium,
this.start,
this.end,
+ this.images,
+ this.contentDescriptors,
});
final String id;
final String eventId;
final String title;
final String originalTitle;
+ final String ageRating;
+ final String ageRatingUrl;
final String url;
final String presentationMethod;
final String theaterAndAuditorium;
final DateTime start;
final DateTime end;
+ final EventImageData images;
+ final List contentDescriptors;
@override
bool operator ==(Object other) =>
@@ -30,11 +41,15 @@ class Show {
eventId == other.eventId &&
title == other.title &&
originalTitle == other.originalTitle &&
+ ageRating == other.ageRating &&
+ ageRatingUrl == other.ageRatingUrl &&
url == other.url &&
presentationMethod == other.presentationMethod &&
theaterAndAuditorium == other.theaterAndAuditorium &&
start == other.start &&
- end == other.end;
+ end == other.end &&
+ images == other.images &&
+ contentDescriptors == other.contentDescriptors;
@override
int get hashCode =>
@@ -42,9 +57,13 @@ class Show {
eventId.hashCode ^
title.hashCode ^
originalTitle.hashCode ^
+ ageRating.hashCode ^
+ ageRatingUrl.hashCode ^
url.hashCode ^
presentationMethod.hashCode ^
theaterAndAuditorium.hashCode ^
start.hashCode ^
- end.hashCode;
+ end.hashCode ^
+ images.hashCode ^
+ contentDescriptors.hashCode;
}
diff --git a/core/lib/src/models/show_cache.dart b/core/lib/src/models/show_cache.dart
new file mode 100644
index 00000000..2f26923e
--- /dev/null
+++ b/core/lib/src/models/show_cache.dart
@@ -0,0 +1,33 @@
+import 'package:core/src/redux/app/app_state.dart';
+import 'package:intl/intl.dart';
+
+import 'theater.dart';
+
+/// Used as a lookup key for caching showtimes.
+///
+/// When we've already loaded showtimes for a specific theater on specific date,
+/// we don't want to load them again. This pair class is used as a key in a Map
+/// structure, and the values are the showtimes.
+class DateTheaterPair {
+ static final ddMMyyyy = new DateFormat('dd.MM.yyyy');
+
+ DateTheaterPair(this.dateTime, this.theater);
+
+ DateTheaterPair.fromState(AppState state)
+ : dateTime = state.showState.selectedDate,
+ theater = state.theaterState.currentTheater;
+
+ final DateTime dateTime;
+ final Theater theater;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is DateTheaterPair &&
+ runtimeType == other.runtimeType &&
+ dateTime == other.dateTime &&
+ theater == other.theater;
+
+ @override
+ int get hashCode => dateTime.hashCode ^ theater.hashCode;
+}
diff --git a/lib/models/theater.dart b/core/lib/src/models/theater.dart
similarity index 100%
rename from lib/models/theater.dart
rename to core/lib/src/models/theater.dart
diff --git a/core/lib/src/networking/finnkino_api.dart b/core/lib/src/networking/finnkino_api.dart
new file mode 100644
index 00000000..94f3204c
--- /dev/null
+++ b/core/lib/src/networking/finnkino_api.dart
@@ -0,0 +1,63 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:core/src/models/event.dart';
+import 'package:core/src/models/show.dart';
+import 'package:core/src/models/theater.dart';
+import 'package:core/src/parsers/event_parser.dart';
+import 'package:core/src/parsers/show_parser.dart';
+import 'package:http/http.dart';
+import 'package:intl/intl.dart';
+
+class FinnkinoApi {
+ static final ddMMyyyy = DateFormat('dd.MM.yyyy');
+ static final enBaseUrl = 'https://www.finnkino.fi/en/xml';
+ static final fiBaseUrl = 'https://www.finkino.fi/xml';
+
+ static bool useFinnish = false;
+
+ FinnkinoApi(this.client);
+ final Client client;
+
+ String get localizedPath => useFinnish ? '' : '/en';
+ Uri get kScheduleBaseUrl =>
+ Uri.https('www.finnkino.fi', '$localizedPath/xml/Schedule');
+ Uri get kEventsBaseUrl =>
+ Uri.https('www.finnkino.fi', '$localizedPath/xml/Events');
+
+ Future> getSchedule(Theater theater, DateTime date) async {
+ final dt = ddMMyyyy.format(date ?? new DateTime.now());
+ final response = await client.get(
+ kScheduleBaseUrl.replace(queryParameters: {
+ 'area': theater.id,
+ 'dt': dt,
+ 'includeGallery': 'true',
+ }),
+ );
+
+ return ShowParser.parse(utf8.decode(response.bodyBytes));
+ }
+
+ Future> getNowInTheatersEvents(Theater theater) async {
+ final response = await client.get(
+ kEventsBaseUrl.replace(queryParameters: {
+ 'area': theater.id,
+ 'listType': 'NowInTheatres',
+ 'includeGallery': 'true',
+ }),
+ );
+
+ return EventParser.parse(utf8.decode(response.bodyBytes));
+ }
+
+ Future> getUpcomingEvents() async {
+ final response = await client.get(
+ kEventsBaseUrl.replace(queryParameters: {
+ 'listType': 'ComingSoon',
+ 'includeGallery': 'true',
+ }),
+ );
+
+ return EventParser.parse(utf8.decode(response.bodyBytes));
+ }
+}
diff --git a/core/lib/src/networking/image_url_rewriter.dart b/core/lib/src/networking/image_url_rewriter.dart
new file mode 100644
index 00000000..91cbeb32
--- /dev/null
+++ b/core/lib/src/networking/image_url_rewriter.dart
@@ -0,0 +1,24 @@
+final _finnkinoBaseUrl = RegExp(r'https?://media.finnkino.fi');
+const _imgixBaseUrl = 'https://inkino.imgix.net';
+const _imgixQueryParams = '?auto=format,compress';
+
+final notYetRated = RegExp(r'.*not.*yet.*rated.*', caseSensitive: false);
+
+String rewriteImageUrl(String originalUrl) {
+ if (originalUrl == null) {
+ return null;
+ }
+
+ if (originalUrl.contains(notYetRated)) {
+ /// Finnkino XML API might return a "Not yet rated" as an image url for a
+ /// content age rating image. And you might know that "Not yet rated" is not
+ /// a valid url.
+ originalUrl = originalUrl.replaceFirst(
+ notYetRated,
+ 'https://media.finnkino.fi/images/rating_large_Tulossa.png',
+ );
+ }
+
+ return originalUrl.replaceFirst(_finnkinoBaseUrl, _imgixBaseUrl) +
+ _imgixQueryParams;
+}
diff --git a/lib/networking/tmdb_api.dart b/core/lib/src/networking/tmdb_api.dart
similarity index 50%
rename from lib/networking/tmdb_api.dart
rename to core/lib/src/networking/tmdb_api.dart
index 88e4fc15..d7f31e39 100644
--- a/lib/networking/tmdb_api.dart
+++ b/core/lib/src/networking/tmdb_api.dart
@@ -1,29 +1,24 @@
import 'dart:async';
import 'dart:convert';
-import 'package:inkino/models/actor.dart';
-import 'package:inkino/models/event.dart';
-/// If this has a red underline, it means that the lib/tmdb_config.dart file
-/// is not present on the project. Refer to the README for instructions
-/// on how to do so.
-import 'package:inkino/tmdb_config.dart';
-import 'package:inkino/utils/http_utils.dart';
-/// If this has a red underline, it means that the lib/tmdb_config.dart file
-/// is not present on the project. Refer to the README for instructions
-/// on how to do so.
+import 'package:core/src/models/actor.dart';
+import 'package:core/src/models/event.dart';
+import 'package:core/src/tmdb_config.dart';
+import 'package:http/http.dart';
/// If this has a red underline, it means that the lib/tmdb_config.dart file
/// is not present on the project. Refer to the README for instructions
/// on how to do so.
-
-
class TMDBApi {
+ TMDBApi(this.client);
+ final Client client;
+
static final String baseUrl = 'api.themoviedb.org';
Future> findAvatarsForActors(
Event event, List actors) async {
- int movieId = await _findMovieId(event.originalTitle, event.productionYear);
+ int movieId = await _findMovieId(event.originalTitle);
if (movieId != null) {
return _getActorAvatars(movieId);
@@ -32,20 +27,16 @@ class TMDBApi {
return actors;
}
- Future _findMovieId(String movieTitle, String movieYear) async {
- var searchUri = Uri.https(
- baseUrl,
- '3/search/movie',
- {
- 'api_key': TMDBConfig.apiKey,
- 'query': movieTitle,
- 'year': movieYear,
- },
- );
+ Future _findMovieId(String movieTitle) async {
+ final searchUri = Uri.https(baseUrl, '3/search/movie', {
+ 'api_key': TMDBConfig.apiKey,
+ 'query': movieTitle,
+ });
- var response = await getRequest(searchUri);
- Map movieSearchJson = json.decode(response);
- var searchResults =
+ final response = await client.get(searchUri);
+ Map movieSearchJson =
+ json.decode(utf8.decode(response.bodyBytes));
+ final searchResults =
(movieSearchJson['results'] as List).cast