├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── drawable
│ │ │ │ ├── show_reel.xml
│ │ │ │ ├── unwatched.xml
│ │ │ │ ├── watched.xml
│ │ │ │ ├── ic_oscar.xml
│ │ │ │ ├── ic_oscar_white.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ ├── splash_screen.xml
│ │ │ │ ├── ic_oscar_statue.xml
│ │ │ │ ├── winner_badge.xml
│ │ │ │ ├── unwatch.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── assets
│ │ │ ├── categoryAliases.json
│ │ │ └── genres.json
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── chrisa
│ │ │ │ └── theoscars
│ │ │ │ ├── core
│ │ │ │ ├── data
│ │ │ │ │ ├── db
│ │ │ │ │ │ ├── genre
│ │ │ │ │ │ │ ├── GenreDataSource.kt
│ │ │ │ │ │ │ ├── GenreSeedDataModel.kt
│ │ │ │ │ │ │ ├── GenreEntity.kt
│ │ │ │ │ │ │ ├── GenreAssetDataSource.kt
│ │ │ │ │ │ │ ├── GenreDao.kt
│ │ │ │ │ │ │ └── GenreHelper.kt
│ │ │ │ │ │ ├── movie
│ │ │ │ │ │ │ ├── MovieDataSource.kt
│ │ │ │ │ │ │ ├── MovieGenreEntity.kt
│ │ │ │ │ │ │ ├── MovieAssetDataSource.kt
│ │ │ │ │ │ │ ├── MovieSeedDataModel.kt
│ │ │ │ │ │ │ ├── MovieEntity.kt
│ │ │ │ │ │ │ ├── MovieDao.kt
│ │ │ │ │ │ │ └── MovieHelper.kt
│ │ │ │ │ │ ├── category
│ │ │ │ │ │ │ ├── CategoryDataSource.kt
│ │ │ │ │ │ │ ├── CategorySeedDataModel.kt
│ │ │ │ │ │ │ ├── CategoryAssetDataSource.kt
│ │ │ │ │ │ │ ├── CategoryDao.kt
│ │ │ │ │ │ │ ├── CategoryHelper.kt
│ │ │ │ │ │ │ └── CategoryEntity.kt
│ │ │ │ │ │ ├── nomination
│ │ │ │ │ │ │ ├── NominationDataSource.kt
│ │ │ │ │ │ │ ├── NominationSeedDataModel.kt
│ │ │ │ │ │ │ ├── NominationAssetDataSource.kt
│ │ │ │ │ │ │ ├── NominationEntity.kt
│ │ │ │ │ │ │ ├── NominationHelper.kt
│ │ │ │ │ │ │ └── NominationDao.kt
│ │ │ │ │ │ ├── categoryalias
│ │ │ │ │ │ │ ├── CategoryAliasDataSource.kt
│ │ │ │ │ │ │ ├── CategoryAliasSeedDataModel.kt
│ │ │ │ │ │ │ ├── CategoryAliasEntity.kt
│ │ │ │ │ │ │ ├── CategoryAliasAssetDataSource.kt
│ │ │ │ │ │ │ ├── CategoryAliasDao.kt
│ │ │ │ │ │ │ └── CategoryAliasHelper.kt
│ │ │ │ │ │ ├── LocalDateConverter.kt
│ │ │ │ │ │ ├── watchlist
│ │ │ │ │ │ │ ├── WatchlistEntity.kt
│ │ │ │ │ │ │ └── WatchlistDao.kt
│ │ │ │ │ │ ├── AssetFileManager.kt
│ │ │ │ │ │ ├── Bootstrapper.kt
│ │ │ │ │ │ ├── DatabaseModule.kt
│ │ │ │ │ │ └── AppDatabase.kt
│ │ │ │ │ ├── DataModule.kt
│ │ │ │ │ ├── LocalDateTimeJsonAdapter.kt
│ │ │ │ │ └── LocalDateJsonAdapter.kt
│ │ │ │ ├── util
│ │ │ │ │ ├── coroutines
│ │ │ │ │ │ ├── CoroutineDispatchers.kt
│ │ │ │ │ │ ├── CoroutineDispatchersImpl.kt
│ │ │ │ │ │ └── CloseableCoroutineScope.kt
│ │ │ │ │ └── YearValidator.kt
│ │ │ │ └── ui
│ │ │ │ │ ├── common
│ │ │ │ │ ├── RadioButtonWithLabel.kt
│ │ │ │ │ └── ComingSoon.kt
│ │ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ │ ├── features
│ │ │ │ ├── search
│ │ │ │ │ ├── domain
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── SearchResultModel.kt
│ │ │ │ │ │ └── SearchMoviesUseCase.kt
│ │ │ │ │ ├── data
│ │ │ │ │ │ └── SearchDataRepository.kt
│ │ │ │ │ └── presentation
│ │ │ │ │ │ └── SearchViewModel.kt
│ │ │ │ ├── watchlist
│ │ │ │ │ ├── domain
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── WatchlistMovieModel.kt
│ │ │ │ │ │ ├── RemoveAllFromWatchlistUseCase.kt
│ │ │ │ │ │ └── WatchlistMoviesUseCase.kt
│ │ │ │ │ ├── data
│ │ │ │ │ │ └── WatchlistDataRepository.kt
│ │ │ │ │ └── presentation
│ │ │ │ │ │ └── WatchlistViewModel.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── domain
│ │ │ │ │ │ ├── InitializeDataUseCase.kt
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── MovieSummaryModel.kt
│ │ │ │ │ │ ├── LoadGenresUseCase.kt
│ │ │ │ │ │ ├── LoadCategoriesUseCase.kt
│ │ │ │ │ │ └── FilterMoviesUseCase.kt
│ │ │ │ │ └── data
│ │ │ │ │ │ └── HomeDataRepository.kt
│ │ │ │ └── movie
│ │ │ │ │ ├── domain
│ │ │ │ │ ├── models
│ │ │ │ │ │ └── MovieDetailModel.kt
│ │ │ │ │ ├── DeleteWatchlistDataUseCase.kt
│ │ │ │ │ ├── InsertWatchlistDataUseCase.kt
│ │ │ │ │ ├── LoadWatchlistDataUseCase.kt
│ │ │ │ │ └── LoadMovieDetailUseCase.kt
│ │ │ │ │ └── data
│ │ │ │ │ └── MovieDataRepository.kt
│ │ │ │ ├── App.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ └── AppNavGraph.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── chrisa
│ │ │ └── theoscars
│ │ │ ├── core
│ │ │ ├── util
│ │ │ │ └── coroutines
│ │ │ │ │ ├── TestCoroutineDispatchersImpl.kt
│ │ │ │ │ └── TestExecutor.kt
│ │ │ └── data
│ │ │ │ └── db
│ │ │ │ ├── FakeAssetFileManager.kt
│ │ │ │ ├── genre
│ │ │ │ └── GenreSeedData.kt
│ │ │ │ ├── categoryalias
│ │ │ │ └── CategoryAliasSeedData.kt
│ │ │ │ └── BootstrapperBuilder.kt
│ │ │ └── features
│ │ │ └── search
│ │ │ └── presentation
│ │ │ └── SearchViewModelTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── chrisa
│ │ └── theoscars
│ │ ├── HiltTestRunner.kt
│ │ ├── core
│ │ └── data
│ │ │ └── db
│ │ │ └── TestDatabaseModule.kt
│ │ ├── features
│ │ └── search
│ │ │ └── SearchScreenTest.kt
│ │ └── util
│ │ └── AndroidComposeTestRuleExt.kt
└── proguard-rules.pro
├── clear_gradle_cache.sh
├── media
└── preview.mp4
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── seed-data.gradle
├── scripts
├── README.md
└── copyright.kt
├── .gitignore
├── .github
├── ci-gradle.properties
└── workflows
│ ├── android-main.yml
│ └── android-feature.yml
├── settings.gradle
├── gradle.properties
├── README.md
└── gradlew.bat
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/clear_gradle_cache.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mv ~/.gradle ~/.invalid || true
--------------------------------------------------------------------------------
/media/preview.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/media/preview.mp4
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
1 | The files in this directory are only used for continuous integration and lint
2 | purposes. They are not part of the Android app.
3 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ichrisanderson/theoscars/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | /.idea/
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #C37C33
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #33000000
4 | @color/immersive_sys_ui
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 11 09:47:25 GMT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-rc-1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.github/ci-gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.daemon=false
2 | org.gradle.parallel=true
3 | org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true
4 | org.gradle.workers.max=2
5 |
6 | kotlin.incremental=false
7 | kotlin.compiler.execution.strategy=in-process
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | jcenter()
7 | maven { url 'https://jitpack.io' }
8 | }
9 | }
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | jcenter()
16 | maven { url 'https://jitpack.io' }
17 | }
18 | }
19 | rootProject.name = "The Oscars"
20 | include ':app'
21 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/scripts/copyright.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright $YEAR Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/show_reel.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/gradle/seed-data.gradle:
--------------------------------------------------------------------------------
1 | // Check for google-services.json file and copy to project dir
2 | copySeedData(findProperty('oscar_seed_data_dir') ?: '', file("src/main/assets"))
3 |
4 | private void copySeedData(seedDir = '', targetDir) {
5 | if (seedDir != '') {
6 | def seedDirectory = new File(seedDir)
7 | if (seedDirectory.exists()) {
8 | writeFile("the-oscars-db", seedDir, targetDir)
9 | }
10 | }
11 | }
12 |
13 | private void writeFile(fileName = '', seedDir, targetDir) {
14 | def seedFile = new File(seedDir, fileName)
15 | def targetFile = new File(targetDir, fileName)
16 | if (seedFile.exists() && !targetFile.exists()) {
17 | targetFile.write(seedFile.text)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/unwatched.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/assets/categoryAliases.json:
--------------------------------------------------------------------------------
1 | [{"id":1,"name":"Best Picture"},{"id":2,"name":"International Feature Film"},{"id":3,"name":"Animated Feature Film"},{"id":4,"name":"Cinematography"},{"id":5,"name":"Documentary Feature Film"},{"id":6,"name":"Actor in a Leading Role"},{"id":7,"name":"Actress in a Leading Role"},{"id":8,"name":"Actor in a Supporting Role"},{"id":9,"name":"Actress in a Supporting Role"},{"id":10,"name":"Directing"},{"id":11,"name":"Writing"},{"id":12,"name":"Documentary Short Film"},{"id":13,"name":"Short Film (Animated)"},{"id":14,"name":"Short Film (Live Action)"},{"id":15,"name":"Sound"},{"id":16,"name":"Music (Original Score)"},{"id":17,"name":"Music (Original Song)"},{"id":18,"name":"Makeup and Hairstyling"},{"id":19,"name":"Costume Design"},{"id":20,"name":"Production Design"},{"id":21,"name":"Visual Effects"},{"id":22,"name":"Film Editing"}]
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | interface GenreDataSource {
20 | fun getGenres(): List
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | interface MovieDataSource {
20 | fun getMovies(): List
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategoryDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | interface CategoryDataSource {
20 | fun getCategories(): List
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | interface NominationDataSource {
20 | fun getNominations(): List
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | interface CategoryAliasDataSource {
20 | fun getCategoryAliases(): List
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/watched.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreSeedDataModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class GenreSeedDataModel(
23 | val id: Long,
24 | val name: String,
25 | )
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/util/coroutines/CoroutineDispatchers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util.coroutines
18 |
19 | import kotlinx.coroutines.CoroutineDispatcher
20 |
21 | interface CoroutineDispatchers {
22 | val io: CoroutineDispatcher
23 | val main: CoroutineDispatcher
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/search/domain/models/SearchResultModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search.domain.models
18 |
19 | data class SearchResultModel(
20 | val movieId: Long,
21 | val title: String,
22 | val year: String,
23 | val posterImagePath: String?,
24 | )
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasSeedDataModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class CategoryAliasSeedDataModel(
23 | val id: Long,
24 | val name: String,
25 | )
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategorySeedDataModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class CategorySeedDataModel(
23 | val id: Long,
24 | val aliasId: Long,
25 | val name: String,
26 | )
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | import androidx.room.Entity
20 | import androidx.room.PrimaryKey
21 |
22 | @Entity(
23 | tableName = "genre",
24 | )
25 | data class GenreEntity(
26 | @PrimaryKey
27 | val id: Long,
28 | val name: String,
29 | )
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | import androidx.room.Entity
20 | import androidx.room.PrimaryKey
21 |
22 | @Entity(
23 | tableName = "categoryAlias",
24 | )
25 | data class CategoryAliasEntity(
26 | @PrimaryKey
27 | val id: Long,
28 | val name: String,
29 | )
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/App.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars
18 |
19 | import android.app.Application
20 | import dagger.hilt.android.HiltAndroidApp
21 | import timber.log.Timber
22 |
23 | @HiltAndroidApp
24 | class App : Application() {
25 |
26 | override fun onCreate() {
27 | super.onCreate()
28 | if (BuildConfig.DEBUG) {
29 | Timber.plant(Timber.DebugTree())
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/util/coroutines/TestCoroutineDispatchersImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util.coroutines
18 |
19 | import kotlinx.coroutines.CoroutineDispatcher
20 |
21 | class TestCoroutineDispatchersImpl(private val dispatcher: CoroutineDispatcher) :
22 | CoroutineDispatchers {
23 | override val io: CoroutineDispatcher
24 | get() = dispatcher
25 | override val main: CoroutineDispatcher
26 | get() = dispatcher
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieGenreEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import androidx.room.Entity
20 | import androidx.room.Index
21 |
22 | @Entity(
23 | tableName = "movieGenre",
24 | primaryKeys = ["movieId", "genreId"],
25 | indices = [
26 | Index("movieId"),
27 | Index("genreId"),
28 | ],
29 | )
30 | data class MovieGenreEntity(
31 | val movieId: Long,
32 | val genreId: Long,
33 | )
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/watchlist/domain/models/WatchlistMovieModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.watchlist.domain.models
18 |
19 | data class WatchlistModel(
20 | val moviesToWatch: List,
21 | val moviesWatched: List,
22 | )
23 | data class WatchlistMovieModel(
24 | val id: Long,
25 | val movieId: Long,
26 | val title: String,
27 | val year: String,
28 | val posterImagePath: String?,
29 | )
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationSeedDataModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | import com.squareup.moshi.Json
20 | import com.squareup.moshi.JsonClass
21 |
22 | @JsonClass(generateAdapter = true)
23 | data class NominationSeedDataModel(
24 | @Json(name = "year_ceremony")
25 | val ceremonyYear: Int,
26 | val categoryId: Long,
27 | val movieId: Long,
28 | val content: String,
29 | val winner: Boolean,
30 | )
31 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/util/YearValidator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util
18 | import java.time.LocalDate
19 |
20 | object YearValidator {
21 | fun isValidYear(year: String): Boolean {
22 | if (year.isEmpty()) return false
23 | val currentYear = LocalDate.now().year
24 | return try {
25 | val number = year.toInt(10)
26 | number in 1928..currentYear
27 | } catch (ex: NumberFormatException) {
28 | false
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/util/coroutines/CoroutineDispatchersImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util.coroutines
18 |
19 | import kotlinx.coroutines.CoroutineDispatcher
20 | import kotlinx.coroutines.Dispatchers
21 | import javax.inject.Inject
22 |
23 | internal class CoroutineDispatchersImpl @Inject constructor() :
24 | CoroutineDispatchers {
25 | override val io: CoroutineDispatcher
26 | get() = Dispatchers.IO
27 | override val main: CoroutineDispatcher
28 | get() = Dispatchers.Main.immediate
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/LocalDateConverter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import androidx.room.TypeConverter
20 | import java.time.LocalDate
21 | import javax.inject.Inject
22 |
23 | class LocalDateConverter @Inject constructor() {
24 | @TypeConverter
25 | fun fromTimestamp(value: Long?): LocalDate? {
26 | return value?.let { LocalDate.ofEpochDay(it) }
27 | }
28 |
29 | @TypeConverter
30 | fun dateToTimestamp(date: LocalDate?): Long? {
31 | return date?.toEpochDay()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/search/data/SearchDataRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search.data
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import com.chrisa.theoscars.core.data.db.nomination.MovieSearchSummary
21 | import javax.inject.Inject
22 |
23 | class SearchDataRepository @Inject constructor(
24 | private val appDatabase: AppDatabase,
25 | ) {
26 |
27 | fun searchMovies(query: String): List {
28 | val dao = appDatabase.nominationDao()
29 | return dao.searchMovies(query)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/assets/genres.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 28,
4 | "name": "Action"
5 | },
6 | {
7 | "id": 12,
8 | "name": "Adventure"
9 | },
10 | {
11 | "id": 16,
12 | "name": "Animation"
13 | },
14 | {
15 | "id": 35,
16 | "name": "Comedy"
17 | },
18 | {
19 | "id": 80,
20 | "name": "Crime"
21 | },
22 | {
23 | "id": 99,
24 | "name": "Documentary"
25 | },
26 | {
27 | "id": 18,
28 | "name": "Drama"
29 | },
30 | {
31 | "id": 10751,
32 | "name": "Family"
33 | },
34 | {
35 | "id": 14,
36 | "name": "Fantasy"
37 | },
38 | {
39 | "id": 36,
40 | "name": "History"
41 | },
42 | {
43 | "id": 27,
44 | "name": "Horror"
45 | },
46 | {
47 | "id": 10402,
48 | "name": "Music"
49 | },
50 | {
51 | "id": 9648,
52 | "name": "Mystery"
53 | },
54 | {
55 | "id": 10749,
56 | "name": "Romance"
57 | },
58 | {
59 | "id": 878,
60 | "name": "Science Fiction"
61 | },
62 | {
63 | "id": 10770,
64 | "name": "TV Movie"
65 | },
66 | {
67 | "id": 53,
68 | "name": "Thriller"
69 | },
70 | {
71 | "id": 10752,
72 | "name": "War"
73 | },
74 | {
75 | "id": 37,
76 | "name": "Western"
77 | }
78 | ]
79 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chrisa/theoscars/HiltTestRunner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars
18 |
19 | import android.app.Application
20 | import android.content.Context
21 | import androidx.test.runner.AndroidJUnitRunner
22 | import dagger.hilt.android.testing.HiltTestApplication
23 | import timber.log.Timber
24 |
25 | class HiltTestRunner : AndroidJUnitRunner() {
26 |
27 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
28 | Timber.plant(Timber.DebugTree())
29 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/domain/InitializeDataUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.domain
18 |
19 | import com.chrisa.theoscars.core.data.db.Bootstrapper
20 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
21 | import kotlinx.coroutines.withContext
22 | import javax.inject.Inject
23 |
24 | class InitializeDataUseCase @Inject constructor(
25 | private val coroutineDispatchers: CoroutineDispatchers,
26 | private val bootstrapper: Bootstrapper,
27 | ) {
28 | suspend fun execute() = withContext(coroutineDispatchers.io) {
29 | bootstrapper.insertData()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreAssetDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | import com.chrisa.theoscars.core.data.db.AssetFileManager
20 | import com.chrisa.theoscars.core.data.db.AssetFileManager.Companion.openFileAsList
21 | import com.squareup.moshi.Moshi
22 | import javax.inject.Inject
23 |
24 | class GenreAssetDataSource @Inject constructor(
25 | private val moshi: Moshi,
26 | private val assetFileManager: AssetFileManager,
27 | ) : GenreDataSource {
28 |
29 | override fun getGenres(): List =
30 | assetFileManager.openFileAsList("genres.json", moshi, GenreSeedDataModel::class.java)
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieAssetDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import com.chrisa.theoscars.core.data.db.AssetFileManager
20 | import com.chrisa.theoscars.core.data.db.AssetFileManager.Companion.openFileAsList
21 | import com.squareup.moshi.Moshi
22 | import javax.inject.Inject
23 |
24 | class MovieAssetDataSource @Inject constructor(
25 | private val moshi: Moshi,
26 | private val assetFileManager: AssetFileManager,
27 | ) : MovieDataSource {
28 |
29 | override fun getMovies(): List =
30 | assetFileManager.openFileAsList("movies.json", moshi, MovieSeedDataModel::class.java)
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_oscar.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 |
24 | @Dao
25 | interface GenreDao {
26 |
27 | @Query("SELECT COUNT(id) FROM genre")
28 | fun countAll(): Int
29 |
30 | @Query("SELECT * FROM genre ORDER BY name")
31 | fun all(): List
32 |
33 | @Insert(onConflict = OnConflictStrategy.REPLACE)
34 | fun insert(genre: GenreEntity)
35 |
36 | @Insert(onConflict = OnConflictStrategy.REPLACE)
37 | fun insertAll(genres: List)
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/genre/GenreHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import javax.inject.Inject
21 |
22 | class GenreHelper @Inject constructor(
23 | appDatabase: AppDatabase,
24 | private val dataSource: GenreAssetDataSource,
25 | ) {
26 | private val dao = appDatabase.genreDao()
27 |
28 | fun insertData() {
29 | val items = dao.countAll()
30 | if (items > 0) return
31 |
32 | val entities = dataSource.getGenres()
33 | .map { GenreEntity(id = it.id, name = it.name) }
34 |
35 | dao.insertAll(entities)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/util/coroutines/CloseableCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util.coroutines
18 |
19 | import kotlinx.coroutines.CoroutineScope
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.SupervisorJob
22 | import kotlinx.coroutines.cancel
23 | import java.io.Closeable
24 | import javax.inject.Inject
25 | import kotlin.coroutines.CoroutineContext
26 |
27 | class CloseableCoroutineScope @Inject constructor() : Closeable, CoroutineScope {
28 | override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
29 | override fun close() {
30 | coroutineContext.cancel()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_oscar_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategoryAssetDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | import com.chrisa.theoscars.core.data.db.AssetFileManager
20 | import com.chrisa.theoscars.core.data.db.AssetFileManager.Companion.openFileAsList
21 | import com.squareup.moshi.Moshi
22 | import javax.inject.Inject
23 |
24 | class CategoryAssetDataSource @Inject constructor(
25 | private val moshi: Moshi,
26 | private val assetFileManager: AssetFileManager,
27 | ) : CategoryDataSource {
28 |
29 | override fun getCategories(): List =
30 | assetFileManager.openFileAsList("categories.json", moshi, CategorySeedDataModel::class.java)
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/DataModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data
18 |
19 | import com.squareup.moshi.Moshi
20 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
21 | import dagger.Module
22 | import dagger.Provides
23 | import dagger.hilt.InstallIn
24 | import dagger.hilt.components.SingletonComponent
25 |
26 | @InstallIn(SingletonComponent::class)
27 | @Module
28 | internal object DataModule {
29 |
30 | @Provides
31 | fun moshi(): Moshi {
32 | return Moshi.Builder()
33 | .add(LocalDateJsonAdapter())
34 | .add(LocalDateTimeJsonAdapter())
35 | .addLast(KotlinJsonAdapterFactory())
36 | .build()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/domain/models/MovieDetailModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.domain.models
18 |
19 | data class MovieDetailModel(
20 | val id: Long,
21 | val backdropImagePath: String?,
22 | val overview: String,
23 | val title: String,
24 | val year: String,
25 | val youTubeVideoKey: String?,
26 | val nominations: List,
27 | )
28 |
29 | data class NominationModel(
30 | val category: String,
31 | val name: String,
32 | val winner: Boolean,
33 | )
34 |
35 | data class WatchlistDataModel(
36 | val id: Long,
37 | val movieId: Long,
38 | val hasWatched: Boolean,
39 | ) {
40 | val isOnWatchlist = id > 0L
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationAssetDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | import com.chrisa.theoscars.core.data.db.AssetFileManager
20 | import com.chrisa.theoscars.core.data.db.AssetFileManager.Companion.openFileAsList
21 | import com.squareup.moshi.Moshi
22 | import javax.inject.Inject
23 |
24 | class NominationAssetDataSource @Inject constructor(
25 | private val moshi: Moshi,
26 | private val assetFileManager: AssetFileManager,
27 | ) : NominationDataSource {
28 |
29 | override fun getNominations(): List =
30 | assetFileManager.openFileAsList("nominations.json", moshi, NominationSeedDataModel::class.java)
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/domain/models/MovieSummaryModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.domain.models
18 |
19 | data class MovieSummaryModel(
20 | val id: Long,
21 | val backdropImagePath: String?,
22 | val overview: String,
23 | val title: String,
24 | val year: String,
25 | val watchlistId: Long?,
26 | val hasWatched: Boolean,
27 | )
28 |
29 | data class CategoryModel(
30 | val name: String,
31 | val id: Long,
32 | )
33 |
34 | data class GenreModel(
35 | val name: String,
36 | val id: Long,
37 | )
38 |
39 | enum class SortOrder {
40 | YEAR,
41 | TITLE,
42 | }
43 |
44 | enum class SortDirection {
45 | ASCENDING,
46 | DESCENDING,
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategoryDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 |
24 | @Dao
25 | interface CategoryDao {
26 |
27 | @Query("SELECT COUNT(id) FROM category")
28 | fun countAll(): Int
29 |
30 | @Query("SELECT * FROM category ORDER BY name")
31 | fun allCategories(): List
32 |
33 | @Insert(onConflict = OnConflictStrategy.REPLACE)
34 | fun insert(category: CategoryEntity)
35 |
36 | @Insert(onConflict = OnConflictStrategy.REPLACE)
37 | fun insertAll(categories: List)
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/domain/DeleteWatchlistDataUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.movie.data.MovieDataRepository
21 | import kotlinx.coroutines.withContext
22 | import javax.inject.Inject
23 |
24 | class DeleteWatchlistDataUseCase @Inject constructor(
25 | private val coroutineDispatchers: CoroutineDispatchers,
26 | private val movieDataRepository: MovieDataRepository,
27 | ) {
28 | suspend fun execute(watchListId: Long) =
29 | withContext(coroutineDispatchers.io) {
30 | movieDataRepository.deleteWatchlistData(watchListId)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasAssetDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | import com.chrisa.theoscars.core.data.db.AssetFileManager
20 | import com.chrisa.theoscars.core.data.db.AssetFileManager.Companion.openFileAsList
21 | import com.squareup.moshi.Moshi
22 | import javax.inject.Inject
23 |
24 | class CategoryAliasAssetDataSource @Inject constructor(
25 | private val moshi: Moshi,
26 | private val assetFileManager: AssetFileManager,
27 | ) : CategoryAliasDataSource {
28 |
29 | override fun getCategoryAliases(): List =
30 | assetFileManager.openFileAsList("categoryAliases.json", moshi, CategoryAliasSeedDataModel::class.java)
31 | }
32 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategoryHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import javax.inject.Inject
21 |
22 | class CategoryHelper @Inject constructor(
23 | private val appDatabase: AppDatabase,
24 | private val dataSource: CategoryDataSource,
25 | ) {
26 | private val dao = appDatabase.categoryDao()
27 |
28 | fun insertData() {
29 | val items = dao.countAll()
30 | if (items > 0) return
31 |
32 | val entities = dataSource.getCategories()
33 | .map {
34 | CategoryEntity(id = it.id, categoryAliasId = it.aliasId, name = it.name)
35 | }
36 |
37 | dao.insertAll(entities)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 |
24 | @Dao
25 | interface CategoryAliasDao {
26 |
27 | @Query("SELECT COUNT(id) FROM categoryAlias")
28 | fun countAll(): Int
29 |
30 | @Query("SELECT * FROM categoryAlias ORDER BY name")
31 | fun allCategoryAliases(): List
32 |
33 | @Insert(onConflict = OnConflictStrategy.REPLACE)
34 | fun insert(categoryAlias: CategoryAliasEntity)
35 |
36 | @Insert(onConflict = OnConflictStrategy.REPLACE)
37 | fun insertAll(categoryAliases: List)
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieSeedDataModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class MovieSeedDataModel(
23 | val id: Long,
24 | val backdropImagePath: String?,
25 | val posterImagePath: String?,
26 | val overview: String,
27 | val title: String,
28 | val releaseYear: Int,
29 | val youTubeVideoKey: String?,
30 | val genreIds: String,
31 | val imdbId: String? = null,
32 | val originalLanguage: String? = null,
33 | val spokenLanguages: String? = null,
34 | val originalTitle: String? = null,
35 | val displayTitle: String? = null,
36 | val metadata: String? = null,
37 | val runtime: Int? = null,
38 | )
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/LocalDateTimeJsonAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data
18 |
19 | import com.squareup.moshi.FromJson
20 | import com.squareup.moshi.ToJson
21 | import java.time.LocalDateTime
22 | import java.time.ZoneId
23 | import java.time.format.DateTimeFormatter
24 |
25 | class LocalDateTimeJsonAdapter {
26 | @ToJson
27 | fun toJson(localDateTime: LocalDateTime): String {
28 | return localDateTime.atZone(ZoneId.of("UTC"))
29 | .format(FORMATTER)
30 | }
31 |
32 | @FromJson
33 | fun fromJson(json: String): LocalDateTime {
34 | return FORMATTER.parse(json, LocalDateTime::from)
35 | }
36 |
37 | companion object {
38 | private val FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import javax.inject.Inject
21 |
22 | class CategoryAliasHelper @Inject constructor(
23 | private val appDatabase: AppDatabase,
24 | private val dataSource: CategoryAliasDataSource,
25 | ) {
26 | private val dao = appDatabase.categoryAliasDao()
27 |
28 | fun insertData() {
29 | val items = dao.countAll()
30 | if (items > 0) return
31 |
32 | val entities = dataSource.getCategoryAliases()
33 | .map {
34 | CategoryAliasEntity(id = it.id, name = it.name)
35 | }
36 |
37 | dao.insertAll(entities)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/watchlist/WatchlistEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.watchlist
18 |
19 | import androidx.room.Entity
20 | import androidx.room.ForeignKey
21 | import androidx.room.Index
22 | import androidx.room.PrimaryKey
23 | import com.chrisa.theoscars.core.data.db.movie.MovieEntity
24 |
25 | @Entity(
26 | tableName = "watchlist",
27 | foreignKeys = [
28 | ForeignKey(
29 | entity = MovieEntity::class,
30 | parentColumns = ["id"],
31 | childColumns = ["movieId"],
32 | ),
33 | ],
34 | indices = [
35 | Index("movieId"),
36 | ],
37 | )
38 | data class WatchlistEntity(
39 | @PrimaryKey(autoGenerate = true)
40 | var id: Long = 0L,
41 | val movieId: Long,
42 | val hasWatched: Boolean,
43 | )
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/category/CategoryEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.category
18 |
19 | import androidx.room.Entity
20 | import androidx.room.ForeignKey
21 | import androidx.room.Index
22 | import androidx.room.PrimaryKey
23 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasEntity
24 |
25 | @Entity(
26 | tableName = "category",
27 | foreignKeys = [
28 | ForeignKey(
29 | entity = CategoryAliasEntity::class,
30 | parentColumns = ["id"],
31 | childColumns = ["categoryAliasId"],
32 | ),
33 | ],
34 | indices = [
35 | Index("categoryAliasId"),
36 | ],
37 | )
38 | data class CategoryEntity(
39 | @PrimaryKey
40 | val id: Long,
41 | val categoryAliasId: Long,
42 | val name: String,
43 | )
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Oscars
2 |
3 | Project built using Jetpack Compose to browse Oscar nominations.
4 |
5 | WIP. More features to come.
6 |
7 | # Features
8 |
9 | - View nominations and open trailers
10 |
11 | https://user-images.githubusercontent.com/272168/224086232-6867bee2-5a66-4bf1-b943-062f60ee77b7.mp4
12 |
13 |
14 |
15 | - Apply filters
16 |
17 | https://user-images.githubusercontent.com/272168/224362450-99c9ceef-96ad-4a1a-8a3c-b91525a5d0c3.mp4
18 |
19 |
20 |
21 | - Search
22 |
23 | https://user-images.githubusercontent.com/272168/224774934-29276df9-46ad-4d94-9b53-a9513bf080f5.mp4
24 |
25 | # Built using
26 |
27 | - Kotlin coroutines
28 | - Room
29 | - Dagger Hilt
30 | - Jetpack Compose
31 |
32 | By Chris Anderson (https://github.com/ichrisanderson)
33 |
34 | ## License
35 |
36 | ```
37 | Copyright 2023 Chris Anderson
38 |
39 | This program is free software: you can redistribute it and/or modify
40 | it under the terms of the GNU General Public License as published by
41 | the Free Software Foundation, either version 3 of the License, or
42 | (at your option) any later version.
43 |
44 | This program is distributed in the hope that it will be useful,
45 | but WITHOUT ANY WARRANTY; without even the implied warranty of
46 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 | GNU General Public License for more details.
48 |
49 | You should have received a copy of the GNU General Public License
50 | along with this program. If not, see .
51 | ```
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import androidx.room.Entity
20 | import androidx.room.PrimaryKey
21 |
22 | @Entity(
23 | tableName = "movie",
24 | )
25 | data class MovieEntity(
26 | @PrimaryKey
27 | val id: Long,
28 | val backdropImagePath: String?,
29 | val posterImagePath: String?,
30 | val overview: String,
31 | val title: String,
32 | val releaseYear: Int,
33 | val youTubeVideoKey: String?,
34 | val imdbId: String? = null,
35 | val originalLanguage: String? = null,
36 | val spokenLanguages: String? = null,
37 | val originalTitle: String? = null,
38 | val displayTitle: String? = null,
39 | val metadata: String? = null,
40 | val runtime: Int? = null,
41 | val isTvMovie: Boolean = false,
42 | )
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars
18 |
19 | import android.os.Bundle
20 | import androidx.activity.ComponentActivity
21 | import androidx.activity.compose.setContent
22 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
23 | import androidx.core.view.WindowCompat
24 | import com.chrisa.theoscars.core.ui.theme.OscarsTheme
25 | import dagger.hilt.android.AndroidEntryPoint
26 |
27 | @AndroidEntryPoint
28 | class MainActivity : ComponentActivity() {
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | installSplashScreen()
32 | super.onCreate(savedInstanceState)
33 | WindowCompat.setDecorFitsSystemWindows(window, false)
34 | setContent {
35 | OscarsTheme {
36 | AppNavGraph(this)
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_screen.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/util/coroutines/TestExecutor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.util.coroutines
18 |
19 | import java.util.LinkedList
20 | import java.util.concurrent.Executor
21 |
22 | class TestExecutor : Executor {
23 | /**
24 | * If true, adding a new task will drain all existing tasks.
25 | */
26 | var autoRun: Boolean = true
27 |
28 | private val mTasks = LinkedList()
29 |
30 | override fun execute(command: Runnable) {
31 | mTasks.add(command)
32 | if (autoRun) {
33 | executeAll()
34 | }
35 | }
36 |
37 | fun executeAll(): Boolean {
38 | val consumed = !mTasks.isEmpty()
39 |
40 | var task = mTasks.poll()
41 | while (task != null) {
42 | task.run()
43 | task = mTasks.poll()
44 | }
45 | return consumed
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 |
24 | @Dao
25 | interface MovieDao {
26 |
27 | @Query("SELECT COUNT(id) FROM movie")
28 | fun countAll(): Int
29 |
30 | @Insert(onConflict = OnConflictStrategy.REPLACE)
31 | fun insert(item: MovieEntity)
32 |
33 | @Query("SELECT * FROM movie WHERE id = :id LIMIT 1")
34 | fun loadMovie(id: Long): MovieEntity
35 |
36 | @Query("SELECT * FROM movie")
37 | fun allMovies(): List
38 |
39 | @Insert(onConflict = OnConflictStrategy.REPLACE)
40 | fun insertMovieGenres(items: List)
41 |
42 | @Query("SELECT * FROM movieGenre")
43 | fun allMovieGenres(): List
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/AppModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars
18 |
19 | import android.content.Context
20 | import android.content.res.AssetManager
21 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
22 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchersImpl
23 | import dagger.Module
24 | import dagger.Provides
25 | import dagger.hilt.InstallIn
26 | import dagger.hilt.android.qualifiers.ApplicationContext
27 | import dagger.hilt.components.SingletonComponent
28 | import javax.inject.Singleton
29 |
30 | @InstallIn(SingletonComponent::class)
31 | @Module
32 | class AppModule {
33 |
34 | @Singleton
35 | @Provides
36 | fun provideCoroutineDispatchers(): CoroutineDispatchers {
37 | return CoroutineDispatchersImpl()
38 | }
39 |
40 | @Provides
41 | fun assetManager(@ApplicationContext context: Context): AssetManager {
42 | return context.assets
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/LocalDateJsonAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data
18 |
19 | import com.squareup.moshi.FromJson
20 | import com.squareup.moshi.ToJson
21 | import timber.log.Timber
22 | import java.time.LocalDate
23 | import java.time.format.DateTimeFormatter
24 | import java.time.format.DateTimeParseException
25 |
26 | class LocalDateJsonAdapter {
27 | @ToJson
28 | fun toJson(localDate: LocalDate?): String? {
29 | return localDate?.format(FORMATTER)
30 | }
31 |
32 | @FromJson
33 | fun fromJson(json: String?): LocalDate? {
34 | if (json == null) return null
35 | return try {
36 | FORMATTER.parse(json, LocalDate::from)
37 | } catch (ex: DateTimeParseException) {
38 | Timber.e(ex)
39 | null
40 | }
41 | }
42 |
43 | companion object {
44 | private val FORMATTER = DateTimeFormatter.ISO_DATE
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_oscar_statue.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/watchlist/WatchlistDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.watchlist
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 | import kotlinx.coroutines.flow.Flow
24 |
25 | @Dao
26 | interface WatchlistDao {
27 |
28 | @Insert(onConflict = OnConflictStrategy.REPLACE)
29 | fun insert(watchlist: WatchlistEntity)
30 |
31 | @Query("SELECT * FROM watchlist WHERE movieId = :movieId LIMIT 1")
32 | fun loadWatchlistData(movieId: Long): Flow
33 |
34 | @Query("DELETE FROM watchlist WHERE id IN (:ids)")
35 | fun deleteAll(ids: Set)
36 |
37 | @Query("DELETE FROM watchlist WHERE id = :id")
38 | fun delete(id: Long)
39 |
40 | @Query("UPDATE watchlist SET hasWatched = 1 WHERE id IN (:ids)")
41 | fun setAllAsWatched(ids: Set)
42 |
43 | @Query("UPDATE watchlist SET hasWatched = 0 WHERE id IN (:ids)")
44 | fun setAllAsUnwatched(ids: Set)
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/AssetFileManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import android.content.res.AssetManager
20 | import com.squareup.moshi.Moshi
21 | import com.squareup.moshi.Types
22 | import okio.buffer
23 | import okio.source
24 | import java.io.InputStream
25 |
26 | interface AssetFileManager {
27 | fun openFile(fileName: String): InputStream
28 |
29 | companion object {
30 | fun AssetFileManager.openFileAsList(
31 | fileName: String,
32 | moshi: Moshi,
33 | itemType: Class,
34 | ): List {
35 | val type = Types.newParameterizedType(List::class.java, itemType)
36 | val adapter = moshi.adapter>(type)
37 | return adapter.fromJson(openFile(fileName).source().buffer())!!
38 | }
39 | }
40 | }
41 |
42 | class AndroidAssetFileManager(private val assetManager: AssetManager) : AssetFileManager {
43 | override fun openFile(fileName: String): InputStream = assetManager.open(fileName)
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/domain/InsertWatchlistDataUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.domain
18 |
19 | import com.chrisa.theoscars.core.data.db.watchlist.WatchlistEntity
20 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
21 | import com.chrisa.theoscars.features.movie.data.MovieDataRepository
22 | import kotlinx.coroutines.withContext
23 | import javax.inject.Inject
24 |
25 | class InsertWatchlistDataUseCase @Inject constructor(
26 | private val coroutineDispatchers: CoroutineDispatchers,
27 | private val movieDataRepository: MovieDataRepository,
28 | ) {
29 | suspend fun execute(watchListId: Long?, movieId: Long, hasWatched: Boolean) =
30 | withContext(coroutineDispatchers.io) {
31 | movieDataRepository.insertWatchlistData(
32 | WatchlistEntity(
33 | id = watchListId ?: 0L,
34 | movieId = movieId,
35 | hasWatched = hasWatched,
36 | ),
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/domain/LoadGenresUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.home.data.HomeDataRepository
21 | import com.chrisa.theoscars.features.home.domain.models.GenreModel
22 | import kotlinx.coroutines.withContext
23 | import javax.inject.Inject
24 |
25 | class LoadGenresUseCase @Inject constructor(
26 | private val coroutineDispatchers: CoroutineDispatchers,
27 | private val homeDataRepository: HomeDataRepository,
28 | ) {
29 | suspend fun execute(): List = withContext(coroutineDispatchers.io) {
30 | val allGenres = homeDataRepository.allGenres()
31 |
32 | val result = mutableListOf()
33 | result.add(GenreModel(name = "All", id = 0))
34 | result.addAll(
35 | allGenres.map {
36 | GenreModel(name = it.name, id = it.id)
37 | },
38 | )
39 |
40 | return@withContext result
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/watchlist/data/WatchlistDataRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.watchlist.data
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import com.chrisa.theoscars.core.data.db.nomination.MovieWatchlistSummary
21 | import kotlinx.coroutines.flow.Flow
22 | import javax.inject.Inject
23 |
24 | class WatchlistDataRepository @Inject constructor(
25 | private val appDatabase: AppDatabase,
26 | ) {
27 | fun watchlistMovies(): Flow> {
28 | val dao = appDatabase.nominationDao()
29 | return dao.watchlistMovies()
30 | }
31 |
32 | fun removeAllFromWatchList(ids: Set) {
33 | val dao = appDatabase.watchlistDao()
34 | return dao.deleteAll(ids)
35 | }
36 |
37 | fun setAllAsWatched(ids: Set) {
38 | val dao = appDatabase.watchlistDao()
39 | return dao.setAllAsWatched(ids)
40 | }
41 |
42 | fun setAllAsUnwatched(ids: Set) {
43 | val dao = appDatabase.watchlistDao()
44 | return dao.setAllAsUnwatched(ids)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/data/db/FakeAssetFileManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import com.chrisa.theoscars.core.data.db.category.CategorySeedData
20 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasSeedData
21 | import com.chrisa.theoscars.core.data.db.genre.GenreSeedData
22 | import com.chrisa.theoscars.core.data.db.movie.MovieSeedData
23 | import com.chrisa.theoscars.core.data.db.nomination.NominationSeedData
24 | import java.io.InputStream
25 |
26 | class FakeAssetFileManager : AssetFileManager {
27 | override fun openFile(fileName: String): InputStream {
28 | return when (fileName) {
29 | "movies.json" -> MovieSeedData.data.byteInputStream()
30 | "nominations.json" -> NominationSeedData.data.byteInputStream()
31 | "categoryAliases.json" -> CategoryAliasSeedData.data.byteInputStream()
32 | "categories.json" -> CategorySeedData.data.byteInputStream()
33 | "genres.json" -> GenreSeedData.data.byteInputStream()
34 | else -> "".byteInputStream()
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/domain/LoadCategoriesUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.home.data.HomeDataRepository
21 | import com.chrisa.theoscars.features.home.domain.models.CategoryModel
22 | import kotlinx.coroutines.withContext
23 | import javax.inject.Inject
24 |
25 | class LoadCategoriesUseCase @Inject constructor(
26 | private val coroutineDispatchers: CoroutineDispatchers,
27 | private val homeDataRepository: HomeDataRepository,
28 | ) {
29 | suspend fun execute(): List = withContext(coroutineDispatchers.io) {
30 | val categoryAliases = homeDataRepository.allCategoryAliases()
31 |
32 | val result = mutableListOf()
33 | result.add(CategoryModel(name = "All", id = 0))
34 | result.addAll(
35 | categoryAliases.map { c ->
36 | CategoryModel(name = c.name, id = c.id)
37 | },
38 | )
39 |
40 | return@withContext result
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/domain/LoadWatchlistDataUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.movie.data.MovieDataRepository
21 | import com.chrisa.theoscars.features.movie.domain.models.WatchlistDataModel
22 | import kotlinx.coroutines.flow.Flow
23 | import kotlinx.coroutines.flow.flowOn
24 | import kotlinx.coroutines.flow.map
25 | import javax.inject.Inject
26 |
27 | class LoadWatchlistDataUseCase @Inject constructor(
28 | private val coroutineDispatchers: CoroutineDispatchers,
29 | private val movieDataRepository: MovieDataRepository,
30 | ) {
31 | fun execute(movieId: Long): Flow =
32 | movieDataRepository.loadWatchlistData(movieId)
33 | .flowOn(coroutineDispatchers.io)
34 | .map {
35 | WatchlistDataModel(
36 | id = it?.id ?: 0L,
37 | movieId = it?.movieId ?: movieId,
38 | hasWatched = it?.hasWatched ?: false,
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationEntity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | import androidx.room.Entity
20 | import androidx.room.ForeignKey
21 | import androidx.room.Index
22 | import androidx.room.PrimaryKey
23 | import com.chrisa.theoscars.core.data.db.category.CategoryEntity
24 | import com.chrisa.theoscars.core.data.db.movie.MovieEntity
25 |
26 | @Entity(
27 | tableName = "nomination",
28 | foreignKeys = [
29 | ForeignKey(
30 | entity = CategoryEntity::class,
31 | parentColumns = ["id"],
32 | childColumns = ["categoryId"],
33 | ),
34 | ForeignKey(
35 | entity = MovieEntity::class,
36 | parentColumns = ["id"],
37 | childColumns = ["movieId"],
38 | ),
39 | ],
40 | indices = [
41 | Index("categoryId"),
42 | Index("movieId"),
43 | ],
44 | )
45 | data class NominationEntity(
46 | @PrimaryKey(autoGenerate = true)
47 | var id: Long = 0,
48 | var year: Int,
49 | var categoryId: Long,
50 | var movieId: Long,
51 | val content: String,
52 | val winner: Boolean,
53 | )
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/search/domain/SearchMoviesUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.search.data.SearchDataRepository
21 | import com.chrisa.theoscars.features.search.domain.models.SearchResultModel
22 | import kotlinx.coroutines.withContext
23 | import javax.inject.Inject
24 |
25 | class SearchMoviesUseCase @Inject constructor(
26 | private val coroutineDispatchers: CoroutineDispatchers,
27 | private val searchDataRepository: SearchDataRepository,
28 | ) {
29 | suspend fun execute(query: String): List =
30 | withContext(coroutineDispatchers.io) {
31 | if (query.isEmpty()) return@withContext emptyList()
32 | val movies = searchDataRepository.searchMovies("%$query%")
33 | return@withContext movies.map {
34 | SearchResultModel(
35 | movieId = it.id,
36 | title = it.title,
37 | posterImagePath = it.posterImagePath,
38 | year = it.year.toString(10),
39 | )
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/winner_badge.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/watchlist/domain/RemoveAllFromWatchlistUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.watchlist.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.watchlist.data.WatchlistDataRepository
21 | import kotlinx.coroutines.withContext
22 | import javax.inject.Inject
23 |
24 | class RemoveAllFromWatchlistUseCase @Inject constructor(
25 | private val coroutineDispatchers: CoroutineDispatchers,
26 | private val repository: WatchlistDataRepository,
27 | ) {
28 | suspend fun execute(ids: Set) = withContext(coroutineDispatchers.io) {
29 | repository.removeAllFromWatchList(ids)
30 | }
31 | }
32 |
33 | class SetAllAsWatchedUseCase @Inject constructor(
34 | private val coroutineDispatchers: CoroutineDispatchers,
35 | private val repository: WatchlistDataRepository,
36 | ) {
37 | suspend fun execute(ids: Set) = withContext(coroutineDispatchers.io) {
38 | repository.setAllAsWatched(ids)
39 | }
40 | }
41 |
42 | class SetAllAsUnwatchedUseCase @Inject constructor(
43 | private val coroutineDispatchers: CoroutineDispatchers,
44 | private val repository: WatchlistDataRepository,
45 | ) {
46 | suspend fun execute(ids: Set) = withContext(coroutineDispatchers.io) {
47 | repository.setAllAsUnwatched(ids)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/Bootstrapper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import com.chrisa.theoscars.core.data.db.category.CategoryHelper
20 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasHelper
21 | import com.chrisa.theoscars.core.data.db.genre.GenreHelper
22 | import com.chrisa.theoscars.core.data.db.movie.MovieHelper
23 | import com.chrisa.theoscars.core.data.db.nomination.NominationHelper
24 | import javax.inject.Inject
25 |
26 | interface Bootstrapper {
27 | fun insertData()
28 | }
29 |
30 | class DefaultBootstrapper @Inject constructor(
31 | private val appDatabase: AppDatabase,
32 | private val categoryAliasHelper: CategoryAliasHelper,
33 | private val categoryHelper: CategoryHelper,
34 | private val genreHelper: GenreHelper,
35 | private val nominationHelper: NominationHelper,
36 | private val movieHelper: MovieHelper,
37 | ) : Bootstrapper {
38 | override fun insertData() {
39 | appDatabase.beginTransaction()
40 | try {
41 | categoryAliasHelper.insertData()
42 | categoryHelper.insertData()
43 | genreHelper.insertData()
44 | movieHelper.insertData()
45 | nominationHelper.insertData()
46 | appDatabase.setTransactionSuccessful()
47 | } finally {
48 | appDatabase.endTransaction()
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/data/MovieDataRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.data
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import com.chrisa.theoscars.core.data.db.movie.MovieEntity
21 | import com.chrisa.theoscars.core.data.db.nomination.NominationCategory
22 | import com.chrisa.theoscars.core.data.db.watchlist.WatchlistEntity
23 | import kotlinx.coroutines.flow.Flow
24 | import javax.inject.Inject
25 |
26 | class MovieDataRepository @Inject constructor(
27 | private val appDatabase: AppDatabase,
28 | ) {
29 |
30 | fun loadMovie(id: Long): MovieEntity {
31 | val dao = appDatabase.movieDao()
32 | return dao.loadMovie(id)
33 | }
34 |
35 | fun loadNominations(movieId: Long): List {
36 | val dao = appDatabase.nominationDao()
37 | return dao.allNominationCategoriesForMovie(movieId)
38 | }
39 |
40 | fun loadWatchlistData(id: Long): Flow {
41 | val dao = appDatabase.watchlistDao()
42 | return dao.loadWatchlistData(id)
43 | }
44 |
45 | fun insertWatchlistData(watchlistEntity: WatchlistEntity) {
46 | val dao = appDatabase.watchlistDao()
47 | return dao.insert(watchlistEntity)
48 | }
49 |
50 | fun deleteWatchlistData(watchListId: Long) {
51 | val dao = appDatabase.watchlistDao()
52 | return dao.delete(watchListId)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/data/HomeDataRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.data
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import com.chrisa.theoscars.core.data.db.category.CategoryEntity
21 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasEntity
22 | import com.chrisa.theoscars.core.data.db.genre.GenreEntity
23 | import com.chrisa.theoscars.core.data.db.nomination.MovieSummary
24 | import kotlinx.coroutines.flow.Flow
25 | import javax.inject.Inject
26 |
27 | class HomeDataRepository @Inject constructor(
28 | private val appDatabase: AppDatabase,
29 | ) {
30 |
31 | fun allMoviesForCeremonyWithFilter(
32 | startYear: Int,
33 | endYear: Int,
34 | categoryAliasId: Long,
35 | genreId: Long,
36 | winner: Int,
37 | ): Flow> {
38 | val dao = appDatabase.nominationDao()
39 | return dao.allMoviesForCeremonyWithFilter(startYear, endYear, categoryAliasId, genreId, winner)
40 | }
41 |
42 | fun allGenres(): List {
43 | val dao = appDatabase.genreDao()
44 | return dao.all()
45 | }
46 |
47 | fun allCategories(): List {
48 | val dao = appDatabase.categoryDao()
49 | return dao.allCategories()
50 | }
51 |
52 | fun allCategoryAliases(): List {
53 | val dao = appDatabase.categoryAliasDao()
54 | return dao.allCategoryAliases()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/data/db/genre/GenreSeedData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.genre
18 |
19 | object GenreSeedData {
20 | val data = """
21 | [
22 | {
23 | "id": 28,
24 | "name": "Action"
25 | },
26 | {
27 | "id": 12,
28 | "name": "Adventure"
29 | },
30 | {
31 | "id": 16,
32 | "name": "Animation"
33 | },
34 | {
35 | "id": 35,
36 | "name": "Comedy"
37 | },
38 | {
39 | "id": 80,
40 | "name": "Crime"
41 | },
42 | {
43 | "id": 99,
44 | "name": "Documentary"
45 | },
46 | {
47 | "id": 18,
48 | "name": "Drama"
49 | },
50 | {
51 | "id": 10751,
52 | "name": "Family"
53 | },
54 | {
55 | "id": 14,
56 | "name": "Fantasy"
57 | },
58 | {
59 | "id": 36,
60 | "name": "History"
61 | },
62 | {
63 | "id": 27,
64 | "name": "Horror"
65 | },
66 | {
67 | "id": 10402,
68 | "name": "Music"
69 | },
70 | {
71 | "id": 9648,
72 | "name": "Mystery"
73 | },
74 | {
75 | "id": 10749,
76 | "name": "Romance"
77 | },
78 | {
79 | "id": 878,
80 | "name": "Science Fiction"
81 | },
82 | {
83 | "id": 10770,
84 | "name": "TV Movie"
85 | },
86 | {
87 | "id": 53,
88 | "name": "Thriller"
89 | },
90 | {
91 | "id": 10752,
92 | "name": "War"
93 | },
94 | {
95 | "id": 37,
96 | "name": "Western"
97 | }
98 | ]
99 | """.trim()
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/ui/common/RadioButtonWithLabel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.ui.common
18 |
19 | import androidx.compose.foundation.LocalIndication
20 | import androidx.compose.foundation.clickable
21 | import androidx.compose.foundation.interaction.MutableInteractionSource
22 | import androidx.compose.foundation.layout.Row
23 | import androidx.compose.foundation.layout.fillMaxWidth
24 | import androidx.compose.material3.RadioButton
25 | import androidx.compose.material3.Text
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.semantics.Role
31 |
32 | @Composable
33 | fun RadioButtonWithLabel(
34 | isSelected: Boolean,
35 | label: String,
36 | onSelected: () -> Unit,
37 | modifier: Modifier = Modifier,
38 | ) {
39 | val interactionSource = remember { MutableInteractionSource() }
40 |
41 | Row(
42 | verticalAlignment = Alignment.CenterVertically,
43 | modifier = modifier.clickable(
44 | interactionSource = interactionSource,
45 | indication = LocalIndication.current,
46 | role = Role.RadioButton,
47 | onClick = onSelected,
48 | ),
49 | ) {
50 | RadioButton(
51 | selected = isSelected,
52 | onClick = onSelected,
53 | )
54 | Text(
55 | text = label,
56 | modifier = Modifier.fillMaxWidth(),
57 | )
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import javax.inject.Inject
21 |
22 | class NominationHelper @Inject constructor(
23 | private val appDatabase: AppDatabase,
24 | private val dataSource: NominationDataSource,
25 | ) {
26 | private val dao = appDatabase.nominationDao()
27 |
28 | fun insertData() {
29 | val items = dao.countAll()
30 | if (items > 0) return
31 |
32 | val dataSourceItems = dataSource.getNominations()
33 | val categoryKeys = appDatabase.categoryDao().allCategories().associate { it.id to it.name }
34 | val movieKeys = appDatabase.movieDao().allMovies().associate { it.id to it.title }
35 |
36 | dataSourceItems.forEach {
37 | if (!categoryKeys.containsKey(it.categoryId)) {
38 | throw IllegalStateException("Category not found $it. ${categoryKeys.size}")
39 | }
40 |
41 | if (!movieKeys.containsKey(it.movieId)) {
42 | throw IllegalStateException("Movie not found $it. ${movieKeys.size}")
43 | }
44 |
45 | dao.insert(
46 | NominationEntity(
47 | year = it.ceremonyYear,
48 | categoryId = it.categoryId,
49 | movieId = it.movieId,
50 | content = it.content,
51 | winner = it.winner,
52 | ),
53 | )
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/movie/domain/LoadMovieDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.movie.domain
18 |
19 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
20 | import com.chrisa.theoscars.features.movie.data.MovieDataRepository
21 | import com.chrisa.theoscars.features.movie.domain.models.MovieDetailModel
22 | import com.chrisa.theoscars.features.movie.domain.models.NominationModel
23 | import kotlinx.coroutines.withContext
24 | import javax.inject.Inject
25 |
26 | class LoadMovieDetailUseCase @Inject constructor(
27 | private val coroutineDispatchers: CoroutineDispatchers,
28 | private val movieDataRepository: MovieDataRepository,
29 | ) {
30 | suspend fun execute(id: Long): MovieDetailModel = withContext(coroutineDispatchers.io) {
31 | val movie = movieDataRepository.loadMovie(id)
32 | val movieNominations = movieDataRepository.loadNominations(movie.id)
33 |
34 | val nominations = movieNominations.map {
35 | NominationModel(
36 | category = it.category,
37 | name = it.nomination,
38 | winner = it.winner,
39 | )
40 | }
41 | return@withContext MovieDetailModel(
42 | id = movie.id,
43 | backdropImagePath = movie.backdropImagePath,
44 | overview = movie.overview,
45 | title = movie.title,
46 | year = movieNominations.first().year.toString(10),
47 | youTubeVideoKey = movie.youTubeVideoKey,
48 | nominations = nominations,
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/watchlist/domain/WatchlistMoviesUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.watchlist.domain
18 |
19 | import com.chrisa.theoscars.core.data.db.nomination.MovieWatchlistSummary
20 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
21 | import com.chrisa.theoscars.features.watchlist.data.WatchlistDataRepository
22 | import com.chrisa.theoscars.features.watchlist.domain.models.WatchlistModel
23 | import com.chrisa.theoscars.features.watchlist.domain.models.WatchlistMovieModel
24 | import kotlinx.coroutines.flow.Flow
25 | import kotlinx.coroutines.flow.flowOn
26 | import kotlinx.coroutines.flow.map
27 | import javax.inject.Inject
28 |
29 | class WatchlistMoviesUseCase @Inject constructor(
30 | private val coroutineDispatchers: CoroutineDispatchers,
31 | private val repository: WatchlistDataRepository,
32 | ) {
33 | fun execute(): Flow =
34 | repository.watchlistMovies()
35 | .flowOn(coroutineDispatchers.io)
36 | .map { items ->
37 | WatchlistModel(
38 | moviesToWatch = items.filter { !it.hasWatched }.map(::mapWatchlistMovie),
39 | moviesWatched = items.filter { it.hasWatched }.map(::mapWatchlistMovie),
40 | )
41 | }
42 |
43 | private fun mapWatchlistMovie(it: MovieWatchlistSummary) =
44 | WatchlistMovieModel(
45 | id = it.id,
46 | movieId = it.movieId,
47 | title = it.title,
48 | posterImagePath = it.posterImagePath,
49 | year = it.year.toString(10),
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.ui.theme
18 | import androidx.compose.ui.graphics.Color
19 |
20 | val md_theme_dark_primary = Color(0xFFFFB875)
21 | val md_theme_dark_onPrimary = Color(0xFF4B2800)
22 | val md_theme_dark_primaryContainer = Color(0xFF6B3B00)
23 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFDCC0)
24 | val md_theme_dark_secondary = Color(0xFFE2C0A5)
25 | val md_theme_dark_onSecondary = Color(0xFF412C19)
26 | val md_theme_dark_secondaryContainer = Color(0xFF59422D)
27 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCC0)
28 | val md_theme_dark_tertiary = Color(0xFFC2CC98)
29 | val md_theme_dark_onTertiary = Color(0xFF2C340F)
30 | val md_theme_dark_tertiaryContainer = Color(0xFF424A23)
31 | val md_theme_dark_onTertiaryContainer = Color(0xFFDEE8B3)
32 | val md_theme_dark_error = Color(0xFFFFB4AB)
33 | val md_theme_dark_errorContainer = Color(0xFF93000A)
34 | val md_theme_dark_onError = Color(0xFF690005)
35 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
36 | val md_theme_dark_background = Color(0xFF201B17)
37 | val md_theme_dark_onBackground = Color(0xFFECE0D9)
38 | val md_theme_dark_surface = Color(0xFF201B17)
39 | val md_theme_dark_onSurface = Color(0xFFECE0D9)
40 | val md_theme_dark_surfaceVariant = Color(0xFF51443A)
41 | val md_theme_dark_onSurfaceVariant = Color(0xFFD5C3B6)
42 | val md_theme_dark_outline = Color(0xFF9E8E82)
43 | val md_theme_dark_inverseOnSurface = Color(0xFF201B17)
44 | val md_theme_dark_inverseSurface = Color(0xFFECE0D9)
45 | val md_theme_dark_inversePrimary = Color(0xFF8D4F00)
46 | val md_theme_dark_shadow = Color(0xFF000000)
47 | val md_theme_dark_surfaceTint = Color(0xFFFFB875)
48 | val md_theme_dark_outlineVariant = Color(0xFF51443A)
49 | val md_theme_dark_scrim = Color(0xFF000000)
50 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/unwatch.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/data/db/categoryalias/CategoryAliasSeedData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.categoryalias
18 |
19 | object CategoryAliasSeedData {
20 | val data = """
21 | [
22 | {
23 | "id": 1,
24 | "name": "Best Picture"
25 | },
26 | {
27 | "id": 2,
28 | "name": "International Feature Film"
29 | },
30 | {
31 | "id": 3,
32 | "name": "Animated Feature Film"
33 | },
34 | {
35 | "id": 4,
36 | "name": "Cinematography"
37 | },
38 | {
39 | "id": 5,
40 | "name": "Documentary Feature Film"
41 | },
42 | {
43 | "id": 6,
44 | "name": "Actor in a Leading Role"
45 | },
46 | {
47 | "id": 7,
48 | "name": "Actress in a Leading Role"
49 | },
50 | {
51 | "id": 8,
52 | "name": "Actor in a Supporting Role"
53 | },
54 | {
55 | "id": 9,
56 | "name": "Actress in a Supporting Role"
57 | },
58 | {
59 | "id": 10,
60 | "name": "Directing"
61 | },
62 | {
63 | "id": 11,
64 | "name": "Writing"
65 | },
66 | {
67 | "id": 12,
68 | "name": "Documentary Short Film"
69 | },
70 | {
71 | "id": 13,
72 | "name": "Short Film (Animated)"
73 | },
74 | {
75 | "id": 14,
76 | "name": "Short Film (Live Action)"
77 | },
78 | {
79 | "id": 15,
80 | "name": "Sound"
81 | },
82 | {
83 | "id": 16,
84 | "name": "Music (Original Score)"
85 | },
86 | {
87 | "id": 17,
88 | "name": "Music (Original Song)"
89 | },
90 | {
91 | "id": 18,
92 | "name": "Makeup and Hairstyling"
93 | },
94 | {
95 | "id": 19,
96 | "name": "Costume Design"
97 | },
98 | {
99 | "id": 20,
100 | "name": "Production Design"
101 | },
102 | {
103 | "id": 21,
104 | "name": "Visual Effects"
105 | },
106 | {
107 | "id": 22,
108 | "name": "Film Editing"
109 | }
110 | ]
111 | """.trim()
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/movie/MovieHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.movie
18 |
19 | import com.chrisa.theoscars.core.data.db.AppDatabase
20 | import com.chrisa.theoscars.core.data.db.LocalDateConverter
21 | import javax.inject.Inject
22 |
23 | class MovieHelper @Inject constructor(
24 | appDatabase: AppDatabase,
25 | private val dataSource: MovieDataSource,
26 | private val localDateConverter: LocalDateConverter,
27 | ) {
28 | private val movieDao = appDatabase.movieDao()
29 |
30 | fun insertData() {
31 | val items = movieDao.countAll()
32 | if (items > 0) return
33 |
34 | val dataSourceItems = dataSource.getMovies()
35 |
36 | dataSourceItems.forEach { movie ->
37 | movieDao.insert(
38 | MovieEntity(
39 | id = movie.id,
40 | backdropImagePath = movie.backdropImagePath,
41 | posterImagePath = movie.posterImagePath,
42 | overview = movie.overview,
43 | title = movie.title,
44 | releaseYear = movie.releaseYear,
45 | youTubeVideoKey = movie.youTubeVideoKey,
46 | imdbId = movie.imdbId,
47 | originalLanguage = movie.originalLanguage,
48 | spokenLanguages = movie.spokenLanguages,
49 | originalTitle = movie.originalTitle,
50 | displayTitle = movie.displayTitle,
51 | metadata = movie.metadata,
52 | runtime = movie.runtime,
53 | ),
54 | )
55 | val movieGenres = movie.genreIds.split(",")
56 | .filter { it.trim().isNotEmpty() }
57 | .map {
58 | MovieGenreEntity(movieId = movie.id, genreId = it.trim().toLong(10))
59 | }
60 | movieDao.insertMovieGenres(movieGenres)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/ui/common/ComingSoon.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.ui.common
18 |
19 | import androidx.compose.foundation.layout.Arrangement
20 | import androidx.compose.foundation.layout.Column
21 | import androidx.compose.foundation.layout.fillMaxSize
22 | import androidx.compose.foundation.layout.padding
23 | import androidx.compose.material3.MaterialTheme
24 | import androidx.compose.material3.Surface
25 | import androidx.compose.material3.Text
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.text.style.TextAlign
31 | import androidx.compose.ui.tooling.preview.Preview
32 | import androidx.compose.ui.unit.dp
33 | import com.chrisa.theoscars.R
34 | import com.chrisa.theoscars.core.ui.theme.OscarsTheme
35 |
36 | @Composable
37 | fun ComingSoon(
38 | modifier: Modifier = Modifier,
39 | ) {
40 | Column(
41 | modifier = modifier.fillMaxSize(),
42 | verticalArrangement = Arrangement.Center,
43 | horizontalAlignment = Alignment.CenterHorizontally,
44 | ) {
45 | Text(
46 | modifier = Modifier.padding(8.dp),
47 | text = stringResource(id = R.string.empty_screen_title),
48 | style = MaterialTheme.typography.titleLarge,
49 | textAlign = TextAlign.Center,
50 | color = MaterialTheme.colorScheme.primary,
51 | )
52 | Text(
53 | modifier = Modifier.padding(horizontal = 8.dp),
54 | text = stringResource(id = R.string.empty_screen_subtitle),
55 | style = MaterialTheme.typography.bodySmall,
56 | textAlign = TextAlign.Center,
57 | color = MaterialTheme.colorScheme.outline,
58 | )
59 | }
60 | }
61 |
62 | @Preview
63 | @Composable
64 | fun ComingSoonPreview() {
65 | OscarsTheme {
66 | Surface {
67 | ComingSoon()
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.ui.theme
18 |
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.darkColorScheme
21 | import androidx.compose.runtime.Composable
22 |
23 | private val DarkColors = darkColorScheme(
24 | primary = md_theme_dark_primary,
25 | onPrimary = md_theme_dark_onPrimary,
26 | primaryContainer = md_theme_dark_primaryContainer,
27 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
28 | secondary = md_theme_dark_secondary,
29 | onSecondary = md_theme_dark_onSecondary,
30 | secondaryContainer = md_theme_dark_secondaryContainer,
31 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
32 | tertiary = md_theme_dark_tertiary,
33 | onTertiary = md_theme_dark_onTertiary,
34 | tertiaryContainer = md_theme_dark_tertiaryContainer,
35 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
36 | error = md_theme_dark_error,
37 | errorContainer = md_theme_dark_errorContainer,
38 | onError = md_theme_dark_onError,
39 | onErrorContainer = md_theme_dark_onErrorContainer,
40 | background = md_theme_dark_background,
41 | onBackground = md_theme_dark_onBackground,
42 | surface = md_theme_dark_surface,
43 | onSurface = md_theme_dark_onSurface,
44 | surfaceVariant = md_theme_dark_surfaceVariant,
45 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
46 | outline = md_theme_dark_outline,
47 | inverseOnSurface = md_theme_dark_inverseOnSurface,
48 | inverseSurface = md_theme_dark_inverseSurface,
49 | inversePrimary = md_theme_dark_inversePrimary,
50 | surfaceTint = md_theme_dark_surfaceTint,
51 | outlineVariant = md_theme_dark_outlineVariant,
52 | scrim = md_theme_dark_scrim,
53 | )
54 |
55 | @Composable
56 | fun OscarsTheme(
57 | content: @Composable () -> Unit,
58 | ) {
59 | MaterialTheme(
60 | colorScheme = DarkColors,
61 | typography = typography,
62 | content = content,
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/search/presentation/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search.presentation
18 |
19 | import androidx.lifecycle.ViewModel
20 | import com.chrisa.theoscars.core.util.coroutines.CloseableCoroutineScope
21 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
22 | import com.chrisa.theoscars.features.search.domain.SearchMoviesUseCase
23 | import com.chrisa.theoscars.features.search.domain.models.SearchResultModel
24 | import dagger.hilt.android.lifecycle.HiltViewModel
25 | import kotlinx.coroutines.Job
26 | import kotlinx.coroutines.flow.MutableStateFlow
27 | import kotlinx.coroutines.flow.StateFlow
28 | import kotlinx.coroutines.flow.update
29 | import kotlinx.coroutines.launch
30 | import javax.inject.Inject
31 |
32 | @HiltViewModel
33 | class SearchViewModel @Inject constructor(
34 | private val dispatchers: CoroutineDispatchers,
35 | private val coroutineScope: CloseableCoroutineScope,
36 | private val searchMoviesUseCase: SearchMoviesUseCase,
37 | ) : ViewModel(coroutineScope) {
38 |
39 | private val _viewState = MutableStateFlow(ViewState.default())
40 | val viewState: StateFlow = _viewState
41 |
42 | private var queryJob: Job? = null
43 |
44 | fun updateQuery(searchQuery: String) {
45 | queryJob?.cancel()
46 | _viewState.update { vs -> vs.copy(searchQuery = searchQuery) }
47 | queryJob = coroutineScope.launch(dispatchers.io) {
48 | val searchResults = searchMoviesUseCase.execute(searchQuery)
49 | _viewState.update { vs -> vs.copy(searchResults = searchResults) }
50 | }
51 | }
52 |
53 | fun clearQuery() {
54 | queryJob?.cancel()
55 | _viewState.update { vs -> vs.copy(searchQuery = "", searchResults = emptyList()) }
56 | }
57 | }
58 |
59 | data class ViewState(
60 | val searchQuery: String,
61 | val searchResults: List,
62 | ) {
63 |
64 | companion object {
65 | fun default() = ViewState(
66 | searchQuery = "",
67 | searchResults = emptyList(),
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/core/data/db/BootstrapperBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import com.chrisa.theoscars.core.data.LocalDateJsonAdapter
20 | import com.chrisa.theoscars.core.data.LocalDateTimeJsonAdapter
21 | import com.chrisa.theoscars.core.data.db.category.CategoryAssetDataSource
22 | import com.chrisa.theoscars.core.data.db.category.CategoryHelper
23 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasAssetDataSource
24 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasHelper
25 | import com.chrisa.theoscars.core.data.db.genre.GenreAssetDataSource
26 | import com.chrisa.theoscars.core.data.db.genre.GenreHelper
27 | import com.chrisa.theoscars.core.data.db.movie.MovieAssetDataSource
28 | import com.chrisa.theoscars.core.data.db.movie.MovieHelper
29 | import com.chrisa.theoscars.core.data.db.nomination.NominationAssetDataSource
30 | import com.chrisa.theoscars.core.data.db.nomination.NominationHelper
31 | import com.squareup.moshi.Moshi
32 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
33 |
34 | class BootstrapperBuilder {
35 |
36 | fun build(appDatabase: AppDatabase, assetFileManager: AssetFileManager): Bootstrapper {
37 | val moshi = Moshi.Builder()
38 | .add(LocalDateJsonAdapter())
39 | .add(LocalDateTimeJsonAdapter())
40 | .addLast(KotlinJsonAdapterFactory())
41 | .build()
42 |
43 | return DefaultBootstrapper(
44 | appDatabase = appDatabase,
45 | categoryAliasHelper = CategoryAliasHelper(
46 | appDatabase,
47 | CategoryAliasAssetDataSource(moshi, assetFileManager),
48 | ),
49 | categoryHelper = CategoryHelper(
50 | appDatabase,
51 | CategoryAssetDataSource(moshi, assetFileManager),
52 | ),
53 | genreHelper = GenreHelper(
54 | appDatabase,
55 | GenreAssetDataSource(moshi, assetFileManager),
56 | ),
57 | nominationHelper = NominationHelper(
58 | appDatabase,
59 | NominationAssetDataSource(moshi, assetFileManager),
60 | ),
61 | movieHelper = MovieHelper(
62 | appDatabase,
63 | MovieAssetDataSource(moshi, assetFileManager),
64 | LocalDateConverter(),
65 | ),
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.ui.theme
18 |
19 | import androidx.compose.material3.Typography
20 | import androidx.compose.ui.text.TextStyle
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.unit.sp
23 |
24 | val typography = Typography(
25 | headlineLarge = TextStyle(
26 | fontWeight = FontWeight.SemiBold,
27 | fontSize = 32.sp,
28 | lineHeight = 40.sp,
29 | letterSpacing = 0.sp,
30 | ),
31 | headlineMedium = TextStyle(
32 | fontWeight = FontWeight.SemiBold,
33 | fontSize = 28.sp,
34 | lineHeight = 36.sp,
35 | letterSpacing = 0.sp,
36 | ),
37 | headlineSmall = TextStyle(
38 | fontWeight = FontWeight.SemiBold,
39 | fontSize = 24.sp,
40 | lineHeight = 32.sp,
41 | letterSpacing = 0.sp,
42 | ),
43 | titleLarge = TextStyle(
44 | fontWeight = FontWeight.SemiBold,
45 | fontSize = 22.sp,
46 | lineHeight = 28.sp,
47 | letterSpacing = 0.sp,
48 | ),
49 | titleMedium = TextStyle(
50 | fontWeight = FontWeight.SemiBold,
51 | fontSize = 16.sp,
52 | lineHeight = 24.sp,
53 | letterSpacing = 0.15.sp,
54 | ),
55 | titleSmall = TextStyle(
56 | fontWeight = FontWeight.Bold,
57 | fontSize = 14.sp,
58 | lineHeight = 20.sp,
59 | letterSpacing = 0.1.sp,
60 | ),
61 | bodyLarge = TextStyle(
62 | fontWeight = FontWeight.Normal,
63 | fontSize = 16.sp,
64 | lineHeight = 24.sp,
65 | letterSpacing = 0.15.sp,
66 | ),
67 | bodyMedium = TextStyle(
68 | fontWeight = FontWeight.Medium,
69 | fontSize = 14.sp,
70 | lineHeight = 20.sp,
71 | letterSpacing = 0.25.sp,
72 | ),
73 | bodySmall = TextStyle(
74 | fontWeight = FontWeight.Bold,
75 | fontSize = 12.sp,
76 | lineHeight = 16.sp,
77 | letterSpacing = 0.4.sp,
78 | ),
79 | labelLarge = TextStyle(
80 | fontWeight = FontWeight.SemiBold,
81 | fontSize = 14.sp,
82 | lineHeight = 20.sp,
83 | letterSpacing = 0.1.sp,
84 | ),
85 | labelMedium = TextStyle(
86 | fontWeight = FontWeight.SemiBold,
87 | fontSize = 12.sp,
88 | lineHeight = 16.sp,
89 | letterSpacing = 0.5.sp,
90 | ),
91 | labelSmall = TextStyle(
92 | fontWeight = FontWeight.SemiBold,
93 | fontSize = 11.sp,
94 | lineHeight = 16.sp,
95 | letterSpacing = 0.5.sp,
96 | ),
97 | )
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import android.content.Context
20 | import android.content.res.AssetManager
21 | import com.chrisa.theoscars.core.data.db.category.CategoryAssetDataSource
22 | import com.chrisa.theoscars.core.data.db.category.CategoryDataSource
23 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasAssetDataSource
24 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasDataSource
25 | import com.chrisa.theoscars.core.data.db.genre.GenreAssetDataSource
26 | import com.chrisa.theoscars.core.data.db.genre.GenreDataSource
27 | import com.chrisa.theoscars.core.data.db.movie.MovieAssetDataSource
28 | import com.chrisa.theoscars.core.data.db.movie.MovieDataSource
29 | import com.chrisa.theoscars.core.data.db.nomination.NominationAssetDataSource
30 | import com.chrisa.theoscars.core.data.db.nomination.NominationDataSource
31 | import dagger.Module
32 | import dagger.Provides
33 | import dagger.hilt.InstallIn
34 | import dagger.hilt.android.qualifiers.ApplicationContext
35 | import dagger.hilt.components.SingletonComponent
36 | import javax.inject.Singleton
37 |
38 | @InstallIn(SingletonComponent::class)
39 | @Module
40 | internal object DatabaseModule {
41 |
42 | @Singleton
43 | @Provides
44 | fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase =
45 | AndroidAppDatabase.buildDatabase(context)
46 |
47 | @Provides
48 | fun provideAssetFileManager(assetManager: AssetManager): AssetFileManager =
49 | AndroidAssetFileManager(assetManager)
50 |
51 | @Provides
52 | fun provideBootstrapper(bootstrapper: DefaultBootstrapper): Bootstrapper = bootstrapper
53 |
54 | @Provides
55 | fun provideMovieDataSource(movieAssetDataSource: MovieAssetDataSource): MovieDataSource =
56 | movieAssetDataSource
57 |
58 | @Provides
59 | fun provideNominationDataSource(nominationAssetDataSource: NominationAssetDataSource): NominationDataSource =
60 | nominationAssetDataSource
61 |
62 | @Provides
63 | fun provideCategoryAliasAssetDataSource(categoryAliasAssetDataSource: CategoryAliasAssetDataSource): CategoryAliasDataSource =
64 | categoryAliasAssetDataSource
65 |
66 | @Provides
67 | fun provideCategoryAssetDataSource(categoryAssetDataSource: CategoryAssetDataSource): CategoryDataSource =
68 | categoryAssetDataSource
69 |
70 | @Provides
71 | fun provideGenreAssetDataSource(genreAssetDataSource: GenreAssetDataSource): GenreDataSource =
72 | genreAssetDataSource
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chrisa/theoscars/core/data/db/TestDatabaseModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import android.content.Context
20 | import android.content.res.AssetManager
21 | import androidx.room.Room
22 | import com.chrisa.theoscars.core.data.db.category.CategoryAssetDataSource
23 | import com.chrisa.theoscars.core.data.db.category.CategoryDataSource
24 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasAssetDataSource
25 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasDataSource
26 | import com.chrisa.theoscars.core.data.db.genre.GenreAssetDataSource
27 | import com.chrisa.theoscars.core.data.db.genre.GenreDataSource
28 | import com.chrisa.theoscars.core.data.db.movie.MovieAssetDataSource
29 | import com.chrisa.theoscars.core.data.db.movie.MovieDataSource
30 | import com.chrisa.theoscars.core.data.db.nomination.NominationAssetDataSource
31 | import com.chrisa.theoscars.core.data.db.nomination.NominationDataSource
32 | import dagger.Module
33 | import dagger.Provides
34 | import dagger.hilt.android.qualifiers.ApplicationContext
35 | import dagger.hilt.components.SingletonComponent
36 | import dagger.hilt.testing.TestInstallIn
37 | import javax.inject.Singleton
38 |
39 | @Module
40 | @TestInstallIn(
41 | components = [SingletonComponent::class],
42 | replaces = [DatabaseModule::class],
43 | )
44 | internal object TestDatabaseModule {
45 |
46 | @Singleton
47 | @Provides
48 | fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase =
49 | Room.inMemoryDatabaseBuilder(context, AndroidAppDatabase::class.java)
50 | .allowMainThreadQueries()
51 | .build()
52 |
53 | @Provides
54 | fun provideAssetFileManager(assetManager: AssetManager): AssetFileManager =
55 | AndroidAssetFileManager(assetManager)
56 |
57 | @Provides
58 | fun provideBootstrapper(bootstrapper: DefaultBootstrapper): Bootstrapper = bootstrapper
59 |
60 | @Provides
61 | fun provideMovieDataSource(movieAssetDataSource: MovieAssetDataSource): MovieDataSource =
62 | movieAssetDataSource
63 |
64 | @Provides
65 | fun provideNominationDataSource(nominationAssetDataSource: NominationAssetDataSource): NominationDataSource =
66 | nominationAssetDataSource
67 |
68 | @Provides
69 | fun provideCategoryAliasAssetDataSource(categoryAliasAssetDataSource: CategoryAliasAssetDataSource): CategoryAliasDataSource =
70 | categoryAliasAssetDataSource
71 |
72 | @Provides
73 | fun provideCategoryAssetDataSource(categoryAssetDataSource: CategoryAssetDataSource): CategoryDataSource =
74 | categoryAssetDataSource
75 |
76 | @Provides
77 | fun provideGenreAssetDataSource(genreAssetDataSource: GenreAssetDataSource): GenreDataSource =
78 | genreAssetDataSource
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db
18 |
19 | import android.content.Context
20 | import androidx.room.Database
21 | import androidx.room.Room
22 | import androidx.room.RoomDatabase
23 | import com.chrisa.theoscars.core.data.db.category.CategoryDao
24 | import com.chrisa.theoscars.core.data.db.category.CategoryEntity
25 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasDao
26 | import com.chrisa.theoscars.core.data.db.categoryalias.CategoryAliasEntity
27 | import com.chrisa.theoscars.core.data.db.genre.GenreDao
28 | import com.chrisa.theoscars.core.data.db.genre.GenreEntity
29 | import com.chrisa.theoscars.core.data.db.movie.MovieDao
30 | import com.chrisa.theoscars.core.data.db.movie.MovieEntity
31 | import com.chrisa.theoscars.core.data.db.movie.MovieGenreEntity
32 | import com.chrisa.theoscars.core.data.db.nomination.NominationDao
33 | import com.chrisa.theoscars.core.data.db.nomination.NominationEntity
34 | import com.chrisa.theoscars.core.data.db.watchlist.WatchlistDao
35 | import com.chrisa.theoscars.core.data.db.watchlist.WatchlistEntity
36 |
37 | interface AppDatabase {
38 | fun nominationDao(): NominationDao
39 | fun movieDao(): MovieDao
40 | fun categoryAliasDao(): CategoryAliasDao
41 | fun categoryDao(): CategoryDao
42 | fun genreDao(): GenreDao
43 | fun watchlistDao(): WatchlistDao
44 |
45 | fun beginTransaction()
46 | fun setTransactionSuccessful()
47 | fun endTransaction()
48 | fun close()
49 | }
50 |
51 | @Database(
52 | entities = [
53 | CategoryAliasEntity::class,
54 | CategoryEntity::class,
55 | GenreEntity::class,
56 | NominationEntity::class,
57 | MovieEntity::class,
58 | MovieGenreEntity::class,
59 | WatchlistEntity::class,
60 | ],
61 | version = 1,
62 | exportSchema = true,
63 | )
64 | abstract class AndroidAppDatabase : RoomDatabase(), AppDatabase {
65 |
66 | abstract override fun nominationDao(): NominationDao
67 | abstract override fun movieDao(): MovieDao
68 | abstract override fun categoryAliasDao(): CategoryAliasDao
69 | abstract override fun categoryDao(): CategoryDao
70 | abstract override fun genreDao(): GenreDao
71 | abstract override fun watchlistDao(): WatchlistDao
72 |
73 | companion object {
74 | private const val databaseName = "the-oscars-db"
75 |
76 | fun buildDatabase(context: Context): AppDatabase {
77 | val seedDBExists = context.assets.list("")?.contains(databaseName) ?: false
78 | return Room.databaseBuilder(context, AndroidAppDatabase::class.java, databaseName)
79 | .apply {
80 | if (seedDBExists) {
81 | // createFromAsset(databaseName)
82 | }
83 | }
84 | .build()
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/AppNavGraph.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars
18 |
19 | import android.app.Activity
20 | import android.content.Intent
21 | import android.net.Uri
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 | import androidx.hilt.navigation.compose.hiltViewModel
25 | import androidx.navigation.NavHostController
26 | import androidx.navigation.NavType
27 | import androidx.navigation.compose.NavHost
28 | import androidx.navigation.compose.composable
29 | import androidx.navigation.compose.rememberNavController
30 | import androidx.navigation.navArgument
31 | import com.chrisa.theoscars.features.movie.presentation.MovieScreen
32 | import com.chrisa.theoscars.features.movie.presentation.MovieViewModel
33 | import com.chrisa.theoscars.features.search.presentation.SearchScreen
34 | import com.chrisa.theoscars.features.search.presentation.SearchViewModel
35 |
36 | @Composable
37 | fun AppNavGraph(
38 | activity: Activity,
39 | modifier: Modifier = Modifier,
40 | navController: NavHostController = rememberNavController(),
41 | ) {
42 | NavHost(
43 | navController = navController,
44 | startDestination = AppDestinations.MAIN,
45 | modifier = modifier,
46 | ) {
47 | composable(AppDestinations.MAIN) {
48 | MainScreen(
49 | onMovieClick = { movieId ->
50 | navController.navigate("movie/$movieId")
51 | },
52 | onSearchClick = {
53 | navController.navigate(AppDestinations.SEARCH)
54 | },
55 | )
56 | }
57 | composable(
58 | AppDestinations.MOVIE_DETAIL,
59 | arguments = listOf(navArgument("movieId") { type = NavType.LongType }),
60 | ) {
61 | val viewModel = hiltViewModel()
62 | MovieScreen(
63 | viewModel = viewModel,
64 | onClose = {
65 | navController.popBackStack()
66 | },
67 | onPlayClicked = { videoId ->
68 | activity.startActivity(
69 | Intent(
70 | Intent.ACTION_VIEW,
71 | Uri.parse("http://www.youtube.com/watch?v=$videoId"),
72 | ),
73 | )
74 | },
75 | )
76 | }
77 | composable(AppDestinations.SEARCH) {
78 | val viewModel = hiltViewModel()
79 | SearchScreen(
80 | viewModel,
81 | onMovieClick = { movieId ->
82 | navController.navigate("movie/$movieId")
83 | },
84 | onClose = {
85 | navController.popBackStack()
86 | },
87 | )
88 | }
89 | }
90 | }
91 |
92 | object AppDestinations {
93 | const val MAIN = "main"
94 | const val MOVIE_DETAIL = "movie/{movieId}"
95 | const val SEARCH = "search"
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/home/domain/FilterMoviesUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.home.domain
18 |
19 | import com.chrisa.theoscars.core.data.db.nomination.MovieSummary
20 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
21 | import com.chrisa.theoscars.features.home.data.HomeDataRepository
22 | import com.chrisa.theoscars.features.home.domain.models.CategoryModel
23 | import com.chrisa.theoscars.features.home.domain.models.GenreModel
24 | import com.chrisa.theoscars.features.home.domain.models.MovieSummaryModel
25 | import com.chrisa.theoscars.features.home.domain.models.SortDirection
26 | import com.chrisa.theoscars.features.home.domain.models.SortOrder
27 | import kotlinx.coroutines.flow.Flow
28 | import kotlinx.coroutines.flow.flowOn
29 | import kotlinx.coroutines.flow.map
30 | import javax.inject.Inject
31 |
32 | class FilterMoviesUseCase @Inject constructor(
33 | private val coroutineDispatchers: CoroutineDispatchers,
34 | private val homeDataRepository: HomeDataRepository,
35 | ) {
36 | fun execute(
37 | startYear: Int,
38 | endYear: Int,
39 | selectedCategory: CategoryModel,
40 | selectedGenre: GenreModel,
41 | winnersOnly: Boolean,
42 | sortOrder: SortOrder,
43 | sortDirection: SortDirection,
44 | ): Flow> =
45 | homeDataRepository.allMoviesForCeremonyWithFilter(
46 | startYear = startYear,
47 | endYear = endYear,
48 | categoryAliasId = selectedCategory.id,
49 | genreId = selectedGenre.id,
50 | winner = if (winnersOnly) 1 else -1,
51 | )
52 | .flowOn(coroutineDispatchers.io)
53 | .map { items ->
54 | items.applySortOrder(sortOrder, sortDirection)
55 | .map {
56 | MovieSummaryModel(
57 | id = it.id,
58 | backdropImagePath = it.backdropImagePath,
59 | title = it.title,
60 | overview = it.overview,
61 | year = it.year.toString(10),
62 | watchlistId = it.watchlistId,
63 | hasWatched = it.hasWatched ?: false,
64 | )
65 | }
66 | }
67 |
68 | private fun List.applySortOrder(sortOrder: SortOrder, sortDirection: SortDirection): List {
69 | return when {
70 | sortOrder == SortOrder.TITLE && sortDirection == SortDirection.ASCENDING -> this.sortedBy { it.title }
71 | sortOrder == SortOrder.TITLE && sortDirection == SortDirection.DESCENDING -> this.sortedByDescending { it.title }
72 | sortOrder == SortOrder.YEAR && sortDirection == SortDirection.ASCENDING -> this.sortedBy { it.year }
73 | sortOrder == SortOrder.YEAR && sortDirection == SortDirection.DESCENDING -> this.sortedByDescending { it.year }
74 | else -> this
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chrisa/theoscars/features/search/SearchScreenTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search
18 |
19 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
20 | import com.chrisa.theoscars.MainActivity
21 | import com.chrisa.theoscars.features.home.domain.InitializeDataUseCase
22 | import dagger.hilt.android.testing.HiltAndroidRule
23 | import dagger.hilt.android.testing.HiltAndroidTest
24 | import kotlinx.coroutines.runBlocking
25 | import org.junit.Before
26 | import org.junit.Rule
27 | import org.junit.Test
28 | import javax.inject.Inject
29 |
30 | @HiltAndroidTest
31 | class SearchScreenTest {
32 |
33 | @get:Rule(order = 1)
34 | var hiltRule = HiltAndroidRule(this)
35 |
36 | @get:Rule(order = 2)
37 | val composeTestRule = createAndroidComposeRule()
38 |
39 | @Inject
40 | lateinit var initializeDataUseCase: InitializeDataUseCase
41 |
42 | @Before
43 | fun setup() {
44 | hiltRule.inject()
45 | runBlocking {
46 | initializeDataUseCase.execute()
47 | }
48 | }
49 |
50 | @Test
51 | fun searchBarIsDisplayed() {
52 | SearchScreenRobot(composeTestRule)
53 | .setContent()
54 | .assertSearchBarIsFocused()
55 | }
56 |
57 | @Test
58 | fun searchBarHasPlaceholderText() {
59 | SearchScreenRobot(composeTestRule)
60 | .setContent()
61 | .assertSearchBarHasPlaceholderText()
62 | }
63 |
64 | @Test
65 | fun assertCloseAction() {
66 | SearchScreenRobot(composeTestRule)
67 | .setContent()
68 | .clickCloseAction()
69 | .assertCloseAction()
70 | }
71 |
72 | @Test
73 | fun assertEmptyMovieText() {
74 | SearchScreenRobot(composeTestRule)
75 | .setContent()
76 | .assertEmptyMovieTextDisplayed()
77 | }
78 |
79 | @Test
80 | fun assertSearchResult() {
81 | SearchScreenRobot(composeTestRule)
82 | .setContent()
83 | .clearSearchTerm()
84 | .enterSearchTerm("Everything Everywhere")
85 | .hideKeyboard()
86 | .assertMovieDisplayed(
87 | movieId = 545611L,
88 | title = "Everything Everywhere All at Once",
89 | year = "2023",
90 | )
91 | }
92 |
93 | @Test
94 | fun assertSearchResultCleared() {
95 | SearchScreenRobot(composeTestRule)
96 | .setContent()
97 | .clearSearchTerm()
98 | .enterSearchTerm("Every")
99 | .hideKeyboard()
100 | .clickClearSearchButton()
101 | .assertEmptyMovieTextDisplayed()
102 | }
103 |
104 | @Test
105 | fun assertMovieClickAction() {
106 | SearchScreenRobot(composeTestRule)
107 | .setContent()
108 | .clearSearchTerm()
109 | .enterSearchTerm("Everything Everywhere")
110 | .hideKeyboard()
111 | .clickMovie(movieId = 545611L)
112 | .assertMovieClickAction(movieId = 545611L)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/core/data/db/nomination/NominationDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.core.data.db.nomination
18 |
19 | import androidx.room.Dao
20 | import androidx.room.Insert
21 | import androidx.room.OnConflictStrategy
22 | import androidx.room.Query
23 | import kotlinx.coroutines.flow.Flow
24 |
25 | @Dao
26 | interface NominationDao {
27 |
28 | @Query("SELECT COUNT(year) FROM nomination")
29 | fun countAll(): Int
30 |
31 | @Insert(onConflict = OnConflictStrategy.REPLACE)
32 | fun insert(item: NominationEntity)
33 |
34 | @Query(
35 | "SELECT DISTINCT nomination.content as 'nomination', category.name as 'category', nomination.winner, nomination.year FROM nomination " +
36 | "INNER JOIN movie ON movie.id = nomination.movieId " +
37 | "INNER JOIN category ON category.id = nomination.categoryId " +
38 | "WHERE movieId = :movieId",
39 | )
40 | fun allNominationCategoriesForMovie(
41 | movieId: Long,
42 | ): List
43 |
44 | @Query(
45 | "SELECT DISTINCT movie.id, movie.backdropImagePath, movie.title, movie.overview, nomination.year, watchlist.id as watchlistId, watchlist.hasWatched FROM nomination " +
46 | "INNER JOIN movie ON movie.id = nomination.movieId " +
47 | "INNER JOIN category ON category.id = nomination.categoryId " +
48 | "INNER JOIN categoryAlias ON category.categoryAliasId = categoryAlias.id " +
49 | "LEFT OUTER JOIN movieGenre ON movieGenre.movieId = movie.id " +
50 | "LEFT OUTER JOIN watchlist ON watchlist.movieId = movie.id " +
51 | "WHERE (nomination.year >= :startYear AND nomination.year <= :endYear) AND (:categoryAliasId = 0 OR categoryAlias.id = :categoryAliasId) AND (:genreId = 0 OR movieGenre.genreId = :genreId) AND (:winner = -1 OR nomination.winner = :winner) " +
52 | "ORDER BY movie.title ASC",
53 | )
54 | fun allMoviesForCeremonyWithFilter(
55 | startYear: Int,
56 | endYear: Int,
57 | categoryAliasId: Long,
58 | genreId: Long,
59 | winner: Int,
60 | ): Flow>
61 |
62 | @Query(
63 | "SELECT DISTINCT movie.id, movie.posterImagePath, movie.title, movie.overview, nomination.year FROM nomination " +
64 | "INNER JOIN movie ON movie.id = nomination.movieId " +
65 | "WHERE movie.metadata LIKE :query",
66 | )
67 | fun searchMovies(
68 | query: String,
69 | ): List
70 |
71 | @Query(
72 | "SELECT DISTINCT watchlist.id, watchlist.movieId, movie.posterImagePath, movie.title, movie.overview, nomination.year, watchlist.hasWatched FROM nomination " +
73 | "INNER JOIN movie ON movie.id = nomination.movieId " +
74 | "INNER JOIN watchlist ON watchlist.movieId = movie.id ",
75 | )
76 | fun watchlistMovies(): Flow>
77 | }
78 |
79 | data class NominationCategory(
80 | val nomination: String,
81 | val category: String,
82 | val winner: Boolean,
83 | val year: Int,
84 | )
85 |
86 | data class MovieSummary(
87 | val id: Long,
88 | val backdropImagePath: String?,
89 | val title: String,
90 | val overview: String,
91 | val year: Int,
92 | val watchlistId: Long?,
93 | val hasWatched: Boolean?,
94 | )
95 |
96 | data class MovieSearchSummary(
97 | val id: Long,
98 | val posterImagePath: String?,
99 | val title: String,
100 | val year: Int,
101 | )
102 |
103 | data class MovieWatchlistSummary(
104 | val id: Long,
105 | val movieId: Long,
106 | val posterImagePath: String?,
107 | val title: String,
108 | val year: Int,
109 | val hasWatched: Boolean,
110 | )
111 |
--------------------------------------------------------------------------------
/.github/workflows/android-main.yml:
--------------------------------------------------------------------------------
1 | name: Android Main
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 |
8 | env:
9 | CACHE_VERSION: 1 # Increment this to invalidate the cache.
10 |
11 | jobs:
12 | build:
13 | name: Build
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: Copy CI gradle.properties
20 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
21 |
22 | - name: Set up JDK 11
23 | uses: actions/setup-java@v1
24 | with:
25 | java-version: 11
26 |
27 | - run: chmod u+x ./clear_gradle_cache.sh
28 | - run: ./clear_gradle_cache.sh
29 | - uses: actions/cache@v3
30 | with:
31 | path: |
32 | ~/.gradle/caches
33 | ~/.gradle/wrapper
34 | key: ${{ runner.os }}-gradle-${{ env.CACHE_VERSION }}
35 |
36 | - name: Check Gradle wrapper
37 | uses: gradle/wrapper-validation-action@v1
38 |
39 | - name: Compile
40 | run: bash ./gradlew compileDebugSources compileDebugUnitTestSources -PdisablePreDex --quiet
41 |
42 | - name: Lint
43 | run: bash ./gradlew app:lintDebug ktlintCheck -PdisablePreDex --quiet
44 |
45 | - name: Unit tests
46 | run: bash ./gradlew test --stacktrace
47 |
48 | - name: Upload build reports
49 | if: always()
50 | uses: actions/upload-artifact@v2
51 | with:
52 | name: build-reports
53 | path: app/build/reports
54 |
55 | ui-test:
56 | needs: build
57 | runs-on: macOS-11 # enables hardware acceleration in the virtual machine
58 | timeout-minutes: 60
59 | strategy:
60 | matrix:
61 | api-level: [26]
62 |
63 | steps:
64 | - name: Checkout
65 | uses: actions/checkout@v3
66 |
67 | - name: set up JDK 11
68 | uses: actions/setup-java@v1
69 | with:
70 | java-version: 11
71 |
72 | - name: Copy CI gradle.properties
73 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
74 |
75 | - run: chmod u+x ./clear_gradle_cache.sh
76 | - run: ./clear_gradle_cache.sh
77 | - uses: actions/cache@v3
78 | with:
79 | path: |
80 | ~/.gradle/caches
81 | ~/.gradle/wrapper
82 | key: ${{ runner.os }}-gradle-${{ env.CACHE_VERSION }}
83 |
84 | - name: Gradle cache
85 | uses: gradle/gradle-build-action@v2.4.2
86 |
87 | - name: AVD cache
88 | uses: actions/cache@v3
89 | id: avd-cache
90 | with:
91 | path: |
92 | ~/.android/avd/*
93 | ~/.android/adb*
94 | key: avd-${{ matrix.api-level }}
95 |
96 | - name: create AVD and generate snapshot for caching
97 | if: steps.avd-cache.outputs.cache-hit != 'true'
98 | uses: reactivecircus/android-emulator-runner@v2
99 | with:
100 | api-level: ${{ matrix.api-level }}
101 | arch: x86_64
102 | force-avd-creation: false
103 | profile: Nexus 6
104 | ram-size: 4096M
105 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
106 | disable-animations: false
107 | script: echo "Generated AVD snapshot for caching."
108 |
109 | - name: run tests
110 | uses: reactivecircus/android-emulator-runner@v2
111 | with:
112 | api-level: ${{ matrix.api-level }}
113 | arch: x86_64
114 | force-avd-creation: false
115 | profile: Nexus 6
116 | ram-size: 4096M
117 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
118 | disable-animations: true
119 | script: |
120 | adb logcat --clear || true
121 | adb logcat --clear || true
122 | adb logcat --clear || true
123 | adb logcat > logcat.txt &
124 | ./gradlew connectedCheck --stacktrace
125 |
126 | - name: Upload logcat output
127 | if: ${{ always() }}
128 | uses: actions/upload-artifact@v2
129 | with:
130 | name: logcat-${{ matrix.api-level }}
131 | path: logcat.txt
132 |
133 | - name: Upload test reports
134 | if: always()
135 | uses: actions/upload-artifact@v2
136 | with:
137 | name: test-reports
138 | path: app/build/reports
139 |
--------------------------------------------------------------------------------
/.github/workflows/android-feature.yml:
--------------------------------------------------------------------------------
1 | name: Android Feature
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | - '!main'
8 |
9 | env:
10 | CACHE_VERSION: 1 # Increment this to invalidate the cache.
11 |
12 | jobs:
13 | build:
14 | name: Build
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 |
20 | - name: Copy CI gradle.properties
21 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
22 |
23 | - name: Set up JDK 11
24 | uses: actions/setup-java@v1
25 | with:
26 | java-version: 11
27 |
28 | - run: chmod u+x ./clear_gradle_cache.sh
29 | - run: ./clear_gradle_cache.sh
30 | - uses: actions/cache@v3
31 | with:
32 | path: |
33 | ~/.gradle/caches
34 | ~/.gradle/wrapper
35 | key: ${{ runner.os }}-gradle-${{ env.CACHE_VERSION }}
36 |
37 | - name: Check Gradle wrapper
38 | uses: gradle/wrapper-validation-action@v1
39 |
40 | - name: Compile
41 | run: bash ./gradlew compileDebugSources compileDebugUnitTestSources -PdisablePreDex --quiet
42 |
43 | - name: Lint
44 | run: bash ./gradlew app:lintDebug ktlintCheck -PdisablePreDex --quiet
45 |
46 | - name: Unit tests
47 | run: bash ./gradlew test --stacktrace
48 |
49 | - name: Upload build reports
50 | if: always()
51 | uses: actions/upload-artifact@v2
52 | with:
53 | name: build-reports
54 | path: app/build/reports
55 |
56 | ui-test:
57 | needs: build
58 | runs-on: macOS-11 # enables hardware acceleration in the virtual machine
59 | timeout-minutes: 60
60 | strategy:
61 | matrix:
62 | api-level: [26]
63 |
64 | steps:
65 | - name: Checkout
66 | uses: actions/checkout@v3
67 |
68 | - name: set up JDK 11
69 | uses: actions/setup-java@v1
70 | with:
71 | java-version: 11
72 |
73 | - name: Copy CI gradle.properties
74 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
75 |
76 | - run: chmod u+x ./clear_gradle_cache.sh
77 | - run: ./clear_gradle_cache.sh
78 | - uses: actions/cache@v3
79 | with:
80 | path: |
81 | ~/.gradle/caches
82 | ~/.gradle/wrapper
83 | key: ${{ runner.os }}-gradle-${{ env.CACHE_VERSION }}
84 |
85 | - name: Gradle cache
86 | uses: gradle/gradle-build-action@v2.4.2
87 |
88 | - name: AVD cache
89 | uses: actions/cache@v3
90 | id: avd-cache
91 | with:
92 | path: |
93 | ~/.android/avd/*
94 | ~/.android/adb*
95 | key: avd-${{ matrix.api-level }}
96 |
97 | - name: create AVD and generate snapshot for caching
98 | if: steps.avd-cache.outputs.cache-hit != 'true'
99 | uses: reactivecircus/android-emulator-runner@v2
100 | with:
101 | api-level: ${{ matrix.api-level }}
102 | arch: x86_64
103 | force-avd-creation: false
104 | profile: Nexus 6
105 | ram-size: 4096M
106 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
107 | disable-animations: false
108 | script: echo "Generated AVD snapshot for caching."
109 |
110 | - name: run tests
111 | uses: reactivecircus/android-emulator-runner@v2
112 | with:
113 | api-level: ${{ matrix.api-level }}
114 | arch: x86_64
115 | force-avd-creation: false
116 | profile: Nexus 6
117 | ram-size: 4096M
118 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
119 | disable-animations: true
120 | script: |
121 | adb logcat --clear || true
122 | adb logcat --clear || true
123 | adb logcat --clear || true
124 | adb logcat > logcat.txt &
125 | ./gradlew connectedDebugAndroidTest --stacktrace
126 |
127 | - name: Upload logcat output
128 | if: ${{ always() }}
129 | uses: actions/upload-artifact@v2
130 | with:
131 | name: logcat-${{ matrix.api-level }}
132 | path: logcat.txt
133 |
134 | - name: Upload test reports
135 | if: always()
136 | uses: actions/upload-artifact@v2
137 | with:
138 | name: test-reports
139 | path: app/build/reports
140 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | The Oscars
3 |
4 |
5 | Coming Soon
6 | Under Construction
7 |
8 |
9 | Home
10 | Watchlist
11 |
12 |
13 | Nominations
14 | Review
15 | Watched
16 | Rating
17 | Notes
18 | Added to watchlist
19 | Removed from watchlist
20 | Marked as watched
21 | Marked as unwatched
22 |
23 |
24 | Filter
25 | Years
26 | Categories
27 | Genres
28 | Apply
29 | Clear All
30 | Select All
31 | From:
32 | To:
33 | Please enter a valid year format, e.g 1928 – 2023
34 | Winners Only
35 | No movies found
36 | Please try a different filter…
37 | Winners
38 |
39 |
40 | Sort Order
41 | Sort By
42 | Sort Direction
43 | Year
44 | Title
45 | Ascending
46 | Descending
47 |
48 |
49 | Search for movies
50 | No movies found
51 | Please try a different search query…
52 |
53 |
54 | To add movies you want to watch, tap the watchlist icon.
55 | To record movies as watched, tap the watch icon.
56 | %1$d Selected
57 | To Watch (%1$d)
58 | Watched (%1$d)
59 | %1$.1f%% Watched
60 |
61 |
62 | The Oscars App Logo
63 | Clear Search Query
64 | Close Button
65 | Filter Button
66 | %1$s default image
67 | %1$s image
68 | Play Button
69 | Search Button
70 | Winner
71 | Watchlist icon
72 | Watched icon
73 | Add to watchlist
74 | Remove from watchlist
75 | Remove from watched list
76 | Add to watched list
77 | Mark as watched
78 | Mark as unwatched
79 | %1$s Selected
80 | Change sort order
81 | Watched Progress Icon
82 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chrisa/theoscars/features/search/presentation/SearchViewModelTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.search.presentation
18 |
19 | import android.content.Context
20 | import androidx.room.Room
21 | import androidx.test.core.app.ApplicationProvider
22 | import com.chrisa.theoscars.core.data.db.AndroidAppDatabase
23 | import com.chrisa.theoscars.core.data.db.AppDatabase
24 | import com.chrisa.theoscars.core.data.db.Bootstrapper
25 | import com.chrisa.theoscars.core.data.db.BootstrapperBuilder
26 | import com.chrisa.theoscars.core.data.db.FakeAssetFileManager
27 | import com.chrisa.theoscars.core.util.coroutines.CloseableCoroutineScope
28 | import com.chrisa.theoscars.core.util.coroutines.TestCoroutineDispatchersImpl
29 | import com.chrisa.theoscars.core.util.coroutines.TestExecutor
30 | import com.chrisa.theoscars.features.search.data.SearchDataRepository
31 | import com.chrisa.theoscars.features.search.domain.SearchMoviesUseCase
32 | import com.chrisa.theoscars.features.search.domain.models.SearchResultModel
33 | import com.google.common.truth.Truth.assertThat
34 | import kotlinx.coroutines.Dispatchers
35 | import kotlinx.coroutines.ExperimentalCoroutinesApi
36 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
37 | import kotlinx.coroutines.test.resetMain
38 | import kotlinx.coroutines.test.setMain
39 | import org.junit.After
40 | import org.junit.Before
41 | import org.junit.Test
42 | import org.junit.runner.RunWith
43 | import org.robolectric.RobolectricTestRunner
44 | import org.robolectric.annotation.Config
45 |
46 | @OptIn(ExperimentalCoroutinesApi::class)
47 | @RunWith(RobolectricTestRunner::class)
48 | @Config(sdk = [27])
49 | class SearchViewModelTest {
50 | private val testDispatcher = UnconfinedTestDispatcher()
51 | private val dispatchers = TestCoroutineDispatchersImpl(testDispatcher)
52 |
53 | private lateinit var appDatabase: AppDatabase
54 | private lateinit var bootstrapper: Bootstrapper
55 |
56 | @Before
57 | fun setup() {
58 | Dispatchers.setMain(testDispatcher)
59 |
60 | val context = ApplicationProvider.getApplicationContext()
61 | this.appDatabase = Room.inMemoryDatabaseBuilder(context, AndroidAppDatabase::class.java)
62 | .setQueryExecutor(TestExecutor())
63 | .allowMainThreadQueries()
64 | .build()
65 |
66 | val assetManager = FakeAssetFileManager()
67 |
68 | this.bootstrapper = BootstrapperBuilder()
69 | .build(appDatabase, assetManager)
70 |
71 | bootstrapper.insertData()
72 | }
73 |
74 | @After
75 | fun tearDown() {
76 | this.appDatabase.close()
77 | Dispatchers.resetMain()
78 | }
79 |
80 | private fun searchViewModel(): SearchViewModel {
81 | return SearchViewModel(
82 | dispatchers,
83 | CloseableCoroutineScope(),
84 | SearchMoviesUseCase(
85 | dispatchers,
86 | SearchDataRepository(appDatabase),
87 | ),
88 | )
89 | }
90 |
91 | @Test
92 | fun `WHEN initialised THEN query is empty`() {
93 | val sut = searchViewModel()
94 |
95 | assertThat(sut.viewState.value.searchQuery).isEmpty()
96 | }
97 |
98 | @Test
99 | fun `WHEN initialised THEN results are empty`() {
100 | val sut = searchViewModel()
101 |
102 | assertThat(sut.viewState.value.searchResults).isEmpty()
103 | }
104 |
105 | @Test
106 | fun `WHEN query updated THEN matched results are returned`() {
107 | val sut = searchViewModel()
108 |
109 | sut.updateQuery("Everything Everywhere")
110 |
111 | assertThat(sut.viewState.value.searchResults).isEqualTo(
112 | listOf(
113 | SearchResultModel(
114 | movieId = 545611,
115 | title = "Everything Everywhere All at Once",
116 | posterImagePath = "/w3LxiVYdWWRvEVdn5RYq6jIqkb1.jpg",
117 | year = "2023",
118 | ),
119 | ),
120 | )
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chrisa/theoscars/features/watchlist/presentation/WatchlistViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.features.watchlist.presentation
18 |
19 | import androidx.lifecycle.ViewModel
20 | import com.chrisa.theoscars.core.util.coroutines.CloseableCoroutineScope
21 | import com.chrisa.theoscars.core.util.coroutines.CoroutineDispatchers
22 | import com.chrisa.theoscars.features.watchlist.domain.RemoveAllFromWatchlistUseCase
23 | import com.chrisa.theoscars.features.watchlist.domain.SetAllAsUnwatchedUseCase
24 | import com.chrisa.theoscars.features.watchlist.domain.SetAllAsWatchedUseCase
25 | import com.chrisa.theoscars.features.watchlist.domain.WatchlistMoviesUseCase
26 | import com.chrisa.theoscars.features.watchlist.domain.models.WatchlistModel
27 | import com.chrisa.theoscars.features.watchlist.domain.models.WatchlistMovieModel
28 | import dagger.hilt.android.lifecycle.HiltViewModel
29 | import kotlinx.coroutines.flow.MutableStateFlow
30 | import kotlinx.coroutines.flow.StateFlow
31 | import kotlinx.coroutines.flow.launchIn
32 | import kotlinx.coroutines.flow.onEach
33 | import kotlinx.coroutines.flow.update
34 | import kotlinx.coroutines.launch
35 | import javax.inject.Inject
36 |
37 | @HiltViewModel
38 | class WatchlistViewModel @Inject constructor(
39 | private val dispatchers: CoroutineDispatchers,
40 | private val coroutineScope: CloseableCoroutineScope,
41 | watchlistMoviesUseCase: WatchlistMoviesUseCase,
42 | private val removeAllFromWatchlistUseCase: RemoveAllFromWatchlistUseCase,
43 | private val setAllAsWatchedUseCase: SetAllAsWatchedUseCase,
44 | private val setAllAsUnwatchedUseCase: SetAllAsUnwatchedUseCase,
45 | ) : ViewModel(coroutineScope) {
46 |
47 | private val _viewState = MutableStateFlow(ViewState.default())
48 | val viewState: StateFlow = _viewState
49 |
50 | init {
51 | watchlistMoviesUseCase.execute()
52 | .onEach(::updateMovies)
53 | .launchIn(coroutineScope)
54 | }
55 |
56 | private fun updateMovies(watchlist: WatchlistModel) {
57 | _viewState.update {
58 | it.copy(
59 | moviesToWatch = watchlist.moviesToWatch,
60 | moviesWatched = watchlist.moviesWatched,
61 | )
62 | }
63 | }
64 |
65 | fun toggleItemSelection(id: Long) {
66 | val ids = _viewState.value.selectedIds.toMutableSet()
67 | if (ids.contains(id)) {
68 | ids.remove(id)
69 | } else {
70 | ids.add(id)
71 | }
72 | _viewState.update { it.copy(selectedIds = ids) }
73 | }
74 |
75 | fun clearItemSelection() {
76 | _viewState.update { it.copy(selectedIds = emptySet()) }
77 | }
78 |
79 | fun removeSelectionFromWatchlist() {
80 | coroutineScope.launch(dispatchers.io) {
81 | removeAllFromWatchlistUseCase.execute(_viewState.value.selectedIds)
82 | _viewState.update { it.copy(selectedIds = emptySet()) }
83 | }
84 | }
85 |
86 | fun addSelectionToWatchedList() {
87 | coroutineScope.launch(dispatchers.io) {
88 | setAllAsWatchedUseCase.execute(_viewState.value.selectedIds)
89 | _viewState.update { it.copy(selectedIds = emptySet()) }
90 | }
91 | }
92 |
93 | fun removeSelectionFromWatchedList() {
94 | coroutineScope.launch(dispatchers.io) {
95 | setAllAsUnwatchedUseCase.execute(_viewState.value.selectedIds)
96 | _viewState.update { it.copy(selectedIds = emptySet()) }
97 | }
98 | }
99 | }
100 |
101 | data class ViewState(
102 | val moviesToWatch: List,
103 | val moviesWatched: List,
104 | val selectedIds: Set,
105 | ) {
106 | val hasSelectedIds = selectedIds.isNotEmpty()
107 | val selectedIdCount = selectedIds.size
108 |
109 | companion object {
110 | fun default() = ViewState(
111 | moviesToWatch = emptyList(),
112 | moviesWatched = emptyList(),
113 | selectedIds = emptySet(),
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chrisa/theoscars/util/AndroidComposeTestRuleExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Chris Anderson.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chrisa.theoscars.util
18 |
19 | import androidx.activity.ComponentActivity
20 | import androidx.annotation.StringRes
21 | import androidx.compose.ui.test.SemanticsMatcher
22 | import androidx.compose.ui.test.SemanticsNodeInteraction
23 | import androidx.compose.ui.test.SemanticsNodeInteractionCollection
24 | import androidx.compose.ui.test.junit4.AndroidComposeTestRule
25 | import androidx.compose.ui.test.onAllNodesWithContentDescription
26 | import androidx.compose.ui.test.onAllNodesWithTag
27 | import androidx.compose.ui.test.onAllNodesWithText
28 | import androidx.compose.ui.test.onNodeWithText
29 | import androidx.test.ext.junit.rules.ActivityScenarioRule
30 |
31 | fun AndroidComposeTestRule, A>.onAllNodesWithStringResId(
32 | @StringRes id: Int,
33 | ): SemanticsNodeInteractionCollection = onAllNodesWithText(activity.getString(id))
34 |
35 | fun AndroidComposeTestRule, A>.onNodeWithStringResId(
36 | @StringRes id: Int,
37 | ): SemanticsNodeInteraction = onNodeWithText(activity.getString(id))
38 |
39 | fun AndroidComposeTestRule, A>.getString(
40 | @StringRes id: Int,
41 | vararg args: Any,
42 | ): String {
43 | return activity.getString(id, *args)
44 | }
45 |
46 | private const val defaultTimeoutMillis = 5000L
47 |
48 | fun AndroidComposeTestRule, A>.waitOnAllNodesWithText(
49 | text: String,
50 | useUnmergedTree: Boolean = false,
51 | timeoutMillis: Long = defaultTimeoutMillis,
52 | ) =
53 | this.waitUntil(timeoutMillis = timeoutMillis) {
54 | this.onAllNodesWithText(text = text, useUnmergedTree = useUnmergedTree)
55 | .fetchSemanticsNodes()
56 | .isNotEmpty()
57 | }
58 |
59 | fun AndroidComposeTestRule, A>.waitOnAllNodesWithTag(
60 | tag: String,
61 | useUnmergedTree: Boolean = false,
62 | timeoutMillis: Long = defaultTimeoutMillis,
63 | ) =
64 | this.waitUntil(timeoutMillis = timeoutMillis) {
65 | this.onAllNodesWithTag(testTag = tag, useUnmergedTree = useUnmergedTree)
66 | .fetchSemanticsNodes()
67 | .isNotEmpty()
68 | }
69 |
70 | fun AndroidComposeTestRule, A>.waitOnAllNodesWithStringResId(
71 | @StringRes id: Int,
72 | useUnmergedTree: Boolean = false,
73 | timeoutMillis: Long = defaultTimeoutMillis,
74 | ) =
75 | this.waitUntil(timeoutMillis = timeoutMillis) {
76 | this.onAllNodesWithText(text = activity.getString(id), useUnmergedTree = useUnmergedTree)
77 | .fetchSemanticsNodes()
78 | .isNotEmpty()
79 | }
80 |
81 | fun AndroidComposeTestRule, A>.waitOnAllNodesWithContentDescription(
82 | contentDescription: String,
83 | useUnmergedTree: Boolean = false,
84 | timeoutMillis: Long = defaultTimeoutMillis,
85 | ) =
86 | this.waitUntil(timeoutMillis = timeoutMillis) {
87 | this.onAllNodesWithContentDescription(label = contentDescription, useUnmergedTree = useUnmergedTree)
88 | .fetchSemanticsNodes()
89 | .isNotEmpty()
90 | }
91 |
92 | fun AndroidComposeTestRule, A>.assertNodeWithStringResIdDoesNotExist(
93 | @StringRes id: Int,
94 | useUnmergedTree: Boolean = false,
95 | timeoutMillis: Long = defaultTimeoutMillis,
96 | ) =
97 | this.waitUntil(timeoutMillis = timeoutMillis) {
98 | this.onAllNodesWithText(text = activity.getString(id), useUnmergedTree = useUnmergedTree)
99 | .fetchSemanticsNodes()
100 | .isEmpty()
101 | }
102 |
103 | fun AndroidComposeTestRule, A>.waitOnAllNodesWithMatcher(
104 | matcher: SemanticsMatcher,
105 | useUnmergedTree: Boolean = false,
106 | timeoutMillis: Long = defaultTimeoutMillis,
107 | ) =
108 | this.waitUntil(timeoutMillis = timeoutMillis) {
109 | this.onAllNodes(matcher = matcher, useUnmergedTree = useUnmergedTree)
110 | .fetchSemanticsNodes()
111 | .isNotEmpty()
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------