├── 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 | --------------------------------------------------------------------------------