├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── improvement.md │ └── refactoring.md └── pull_request_template.md ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── boostcamp │ │ └── dailyfilm │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── calendar_floating_button.json │ │ ├── lottie_loading.json │ │ └── sound_lottie.json │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── boostcamp │ │ │ └── dailyfilm │ │ │ ├── DailyFilmApplication.kt │ │ │ ├── data │ │ │ ├── DailyFilmDB.kt │ │ │ ├── calendar │ │ │ │ ├── CalendarDao.kt │ │ │ │ ├── CalendarDataSource.kt │ │ │ │ └── CalendarRepository.kt │ │ │ ├── dataStore │ │ │ │ ├── PreferencesKeys.kt │ │ │ │ └── UserPreferencesRepository.kt │ │ │ ├── delete │ │ │ │ ├── DeleteFilmDataSource.kt │ │ │ │ ├── DeleteFilmRepository.kt │ │ │ │ ├── local │ │ │ │ │ └── DeleteFilmLocalDataSource.kt │ │ │ │ └── remote │ │ │ │ │ └── DeleteFilmRemoteDataSource.kt │ │ │ ├── login │ │ │ │ └── LoginRepository.kt │ │ │ ├── model │ │ │ │ ├── CachedVideoEntity.kt │ │ │ │ ├── DailyFilmItem.kt │ │ │ │ ├── FilmEntity.kt │ │ │ │ ├── Result.kt │ │ │ │ └── VideoItem.kt │ │ │ ├── playfilm │ │ │ │ ├── PlayFilmDataSource.kt │ │ │ │ ├── PlayFilmRepository.kt │ │ │ │ ├── local │ │ │ │ │ └── PlayFilmLocalDataSource.kt │ │ │ │ └── remote │ │ │ │ │ └── PlayFilmRemoteDataSource.kt │ │ │ ├── selectvideo │ │ │ │ ├── GalleryPagingSource.kt │ │ │ │ └── GalleryVideoRepository.kt │ │ │ ├── settings │ │ │ │ ├── SettingsDao.kt │ │ │ │ ├── SettingsDataSource.kt │ │ │ │ └── SettingsRepository.kt │ │ │ ├── sync │ │ │ │ ├── SyncDataSource.kt │ │ │ │ └── SyncRepository.kt │ │ │ └── uploadfilm │ │ │ │ ├── UploadFilmDataSource.kt │ │ │ │ ├── UploadFilmRepository.kt │ │ │ │ ├── local │ │ │ │ ├── LocalUriDao.kt │ │ │ │ └── UploadFilmLocalDataSource.kt │ │ │ │ └── remote │ │ │ │ └── UploadFilmRemoteDataSource.kt │ │ │ ├── di │ │ │ ├── CalendarModule.kt │ │ │ ├── ContentResolverModule.kt │ │ │ ├── DailyFilmDBModule.kt │ │ │ ├── FirebaseModule.kt │ │ │ ├── LoginModule.kt │ │ │ ├── PlayFilmModule.kt │ │ │ ├── PreferenceModule.kt │ │ │ ├── SelectVideoModule.kt │ │ │ ├── SettingsModule.kt │ │ │ ├── SyncModule.kt │ │ │ └── UploadFilmModule.kt │ │ │ └── presentation │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── calendar │ │ │ ├── CalendarActivity.kt │ │ │ ├── CalendarBindingAdapter.kt │ │ │ ├── CalendarViewModel.kt │ │ │ ├── DateBindingAdapter.kt │ │ │ ├── DateFragment.kt │ │ │ ├── DatePickerDialog.kt │ │ │ ├── DateViewModel.kt │ │ │ ├── adpater │ │ │ │ └── CalendarPagerAdapter.kt │ │ │ ├── custom │ │ │ │ ├── CalendarView.kt │ │ │ │ ├── DateImgView.kt │ │ │ │ └── DateTextView.kt │ │ │ └── model │ │ │ │ ├── DateModel.kt │ │ │ │ └── DateState.kt │ │ │ ├── login │ │ │ ├── LoginActivity.kt │ │ │ └── LoginViewModel.kt │ │ │ ├── playfilm │ │ │ ├── PlayFilmActivity.kt │ │ │ ├── PlayFilmActivityBindingAdapter.kt │ │ │ ├── PlayFilmActivityViewModel.kt │ │ │ ├── PlayFilmBottomSheetBindingAdapter.kt │ │ │ ├── PlayFilmBottomSheetDialog.kt │ │ │ ├── PlayFilmFragment.kt │ │ │ ├── PlayFilmFragmentBindingAdapter.kt │ │ │ ├── PlayFilmViewModel.kt │ │ │ ├── adapter │ │ │ │ ├── PlayFilmBottomSheetAdapter.kt │ │ │ │ └── PlayFilmPageAdapter.kt │ │ │ └── model │ │ │ │ ├── BottomSheetModel.kt │ │ │ │ ├── EditState.kt │ │ │ │ └── SpeedState.kt │ │ │ ├── searchfilm │ │ │ ├── SearchFilmActivity.kt │ │ │ ├── SearchFilmBindingAdapter.kt │ │ │ ├── SearchFilmViewModel.kt │ │ │ └── adapter │ │ │ │ └── SearchFilmAdapter.kt │ │ │ ├── selectvideo │ │ │ ├── SelectVideoActivity.kt │ │ │ ├── SelectVideoBindingAdapter.kt │ │ │ ├── SelectVideoViewModel.kt │ │ │ └── adapter │ │ │ │ ├── SelectVideoAdapter.kt │ │ │ │ ├── SelectVideoViewHolder.kt │ │ │ │ ├── VideoLoadStateAdapter.kt │ │ │ │ ├── VideoLoadStateViewHolder.kt │ │ │ │ └── VideoSelectListener.kt │ │ │ ├── settings │ │ │ ├── SettingsActivity.kt │ │ │ └── SettingsViewModel.kt │ │ │ ├── totalfilm │ │ │ ├── TotalFilmActivity.kt │ │ │ ├── TotalFilmViewModel.kt │ │ │ └── TotalfilmBindingAdapter.kt │ │ │ ├── trimvideo │ │ │ ├── TrimVideoActivity.kt │ │ │ └── TrimVideoViewModel.kt │ │ │ ├── uploadfilm │ │ │ ├── UploadFilmActivity.kt │ │ │ ├── UploadFilmBindingAdapter.kt │ │ │ ├── UploadFilmViewModel.kt │ │ │ └── model │ │ │ │ └── DateAndVideoModel.kt │ │ │ └── util │ │ │ ├── CalendarUtil.kt │ │ │ ├── Event.kt │ │ │ ├── LottieDialogFragment.kt │ │ │ ├── PlayState.kt │ │ │ ├── RoundedBackgroundSpan.kt │ │ │ ├── UiState.kt │ │ │ ├── bindingadapter │ │ │ └── ImageView.kt │ │ │ └── network │ │ │ ├── NetworkAlertDialog.kt │ │ │ ├── NetworkManager.kt │ │ │ └── NetworkState.kt │ └── res │ │ ├── anim │ │ ├── anim_camera_open.xml │ │ ├── anim_gallery_close.xml │ │ ├── anim_gallery_open.xml │ │ └── camera_gallery_close.xml │ │ ├── drawable-night │ │ ├── ic_datepicker_month.xml │ │ ├── ic_double_arrow_left.xml │ │ ├── ic_double_arrow_right.xml │ │ ├── ic_drawer_menu.xml │ │ └── ic_play_circle.xml │ │ ├── drawable-v24 │ │ ├── ic_done.xml │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── background_rounded.xml │ │ ├── baseline_close_24.xml │ │ ├── baseline_photo_camera_24.xml │ │ ├── baseline_picture_in_picture_24.xml │ │ ├── bg_focused_date.xml │ │ ├── bg_rounded_solid.xml │ │ ├── div_calendar_week.xml │ │ ├── ic_add.xml │ │ ├── ic_back.xml │ │ ├── ic_back_button.xml │ │ ├── ic_back_primary.xml │ │ ├── ic_baseline_keyboard_backspace_24.xml │ │ ├── ic_close_button.xml │ │ ├── ic_datepicker_month.xml │ │ ├── ic_delete.xml │ │ ├── ic_double_arrow_left.xml │ │ ├── ic_double_arrow_right.xml │ │ ├── ic_drawer_menu.xml │ │ ├── ic_edit_text.xml │ │ ├── ic_fast.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu.xml │ │ ├── ic_play_circle.xml │ │ ├── ic_re_upload.xml │ │ ├── ic_search.xml │ │ ├── ic_settings.xml │ │ ├── ic_text_button_ripple.xml │ │ ├── ic_text_button_ripple_2.xml │ │ ├── ic_text_gradient_36.xml │ │ ├── ic_text_gradient_36_2.xml │ │ └── pb_custom.xml │ │ ├── layout │ │ ├── activity_calendar.xml │ │ ├── activity_login.xml │ │ ├── activity_play_film.xml │ │ ├── activity_search_film.xml │ │ ├── activity_select_video.xml │ │ ├── activity_settings.xml │ │ ├── activity_total_film.xml │ │ ├── activity_trim_viedo.xml │ │ ├── activity_upload_film.xml │ │ ├── dialog_bottom_sheet.xml │ │ ├── dialog_datepicker.xml │ │ ├── dialog_lottie.xml │ │ ├── fragment_date.xml │ │ ├── fragment_play_film.xml │ │ ├── item_bottom_sheet.xml │ │ ├── item_date.xml │ │ ├── item_search_result.xml │ │ ├── item_select_video.xml │ │ └── item_video_load_state.xml │ │ ├── menu │ │ ├── menu_calendar_drawer.xml │ │ └── menu_search.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── img_logo.png │ │ ├── mipmap-night │ │ └── img_logo.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── lottie_loading.json │ │ ├── lottie_textstate.json │ │ └── lottie_writing.json │ │ ├── values-en │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── boostcamp │ └── dailyfilm │ ├── CalendarUtilTest.kt │ ├── DateViewModelUnitTest.kt │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 고쳐야 할 기능 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 진행 상황 11 | 12 | - [ ] 체크 포인트 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 구현해야 될 기능 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 진행 상황 11 | 12 | - [ ] 체크 포인트 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement 3 | about: 기능 개선 4 | title: '' 5 | labels: fix, improving 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactoring.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactoring 3 | about: 리팩토링 관련 4 | title: '' 5 | labels: fix 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # PR 내용 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,kotlin 3 | 4 | ### Kotlin ### 5 | # Compiled class file 6 | *.class 7 | 8 | # Log file 9 | *.log 10 | 11 | # BlueJ files 12 | *.ctxt 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | replay_pid* 29 | 30 | ### AndroidStudio ### 31 | # Covers files to be ignored for android development using Android Studio. 32 | 33 | # Built application files 34 | *.apk 35 | *.ap_ 36 | *.aab 37 | 38 | # Files for the ART/Dalvik VM 39 | *.dex 40 | 41 | # Java class files 42 | 43 | # Generated files 44 | bin/ 45 | gen/ 46 | out/ 47 | 48 | # Gradle files 49 | .gradle 50 | .gradle/ 51 | build/ 52 | 53 | # Signing files 54 | .signing/ 55 | 56 | # Local configuration file (sdk path, etc) 57 | local.properties 58 | 59 | # Proguard folder generated by Eclipse 60 | proguard/ 61 | 62 | # Log Files 63 | 64 | # Android Studio 65 | /*/build/ 66 | /*/local.properties 67 | /*/out 68 | /*/*/build 69 | /*/*/production 70 | captures/ 71 | .navigation/ 72 | *.ipr 73 | *~ 74 | *.swp 75 | 76 | # Keystore files 77 | *.jks 78 | *.keystore 79 | 80 | # Google Services (e.g. APIs or Firebase) 81 | # google-services.json 82 | 83 | # Android Patch 84 | gen-external-apklibs 85 | 86 | # External native build folder generated in Android Studio 2.2 and later 87 | .externalNativeBuild 88 | 89 | # NDK 90 | obj/ 91 | 92 | # IntelliJ IDEA 93 | *.iml 94 | *.iws 95 | /out/ 96 | 97 | # User-specific configurations 98 | .idea/ 99 | 100 | # Legacy Eclipse project files 101 | .classpath 102 | .project 103 | .cproject 104 | .settings/ 105 | 106 | # Mobile Tools for Java (J2ME) 107 | 108 | # Package Files # 109 | 110 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 111 | 112 | ## Plugin-specific files: 113 | 114 | # mpeltonen/sbt-idea plugin 115 | .idea_modules/ 116 | 117 | # JIRA plugin 118 | atlassian-ide-plugin.xml 119 | 120 | # Mongo Explorer plugin 121 | .idea/mongoSettings.xml 122 | 123 | # Crashlytics plugin (for Android Studio and IntelliJ) 124 | com_crashlytics_export_strings.xml 125 | crashlytics.properties 126 | crashlytics-build.properties 127 | fabric.properties 128 | 129 | ### AndroidStudio Patch ### 130 | 131 | !/gradle/wrapper/gradle-wrapper.jar 132 | 133 | # Google-services 134 | google-services.json 135 | 136 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin 137 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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/androidTest/java/com/boostcamp/dailyfilm/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.boostcamp.dailyfilm", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/DailyFilmApplication.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import android.util.Log 7 | import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager 8 | import dagger.hilt.android.HiltAndroidApp 9 | 10 | @HiltAndroidApp 11 | class DailyFilmApplication : Application() { 12 | 13 | private val TAG: String = DailyFilmApplication::class.java.name 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | 18 | initNetwork() 19 | 20 | registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { 21 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 22 | Log.d(TAG, "onActivityCreated: ${activity.localClassName}") 23 | } 24 | 25 | override fun onActivityStarted(activity: Activity) { 26 | Log.d(TAG, "onActivityStarted: ${activity.localClassName}") 27 | } 28 | 29 | override fun onActivityResumed(activity: Activity) { 30 | Log.d(TAG, "onActivityResumed: ${activity.localClassName}") 31 | } 32 | 33 | override fun onActivityPaused(activity: Activity) { 34 | Log.d(TAG, "onActivityPaused: ${activity.localClassName}") 35 | } 36 | 37 | override fun onActivityStopped(activity: Activity) { 38 | Log.d(TAG, "onActivityStopped: ${activity.localClassName}") 39 | } 40 | 41 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { 42 | Log.d(TAG, "onActivitySaveInstanceState: ${activity.localClassName}") 43 | } 44 | 45 | override fun onActivityDestroyed(activity: Activity) { 46 | Log.d(TAG, "onActivityDestroyed: ${activity.localClassName}") 47 | } 48 | }) 49 | } 50 | 51 | private fun initNetwork() { 52 | NetworkManager.initNetwork(applicationContext) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/DailyFilmDB.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.boostcamp.dailyfilm.data.calendar.CalendarDao 8 | import com.boostcamp.dailyfilm.data.model.CachedVideoEntity 9 | import com.boostcamp.dailyfilm.data.model.FilmEntity 10 | import com.boostcamp.dailyfilm.data.settings.SettingsDao 11 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 12 | 13 | @Database( 14 | entities = [FilmEntity::class, CachedVideoEntity::class], 15 | version = 2, 16 | exportSchema = false 17 | ) 18 | 19 | abstract class DailyFilmDB : RoomDatabase() { 20 | companion object { 21 | fun create(context: Context): DailyFilmDB { 22 | val databaseBuilder = 23 | Room.databaseBuilder(context, DailyFilmDB::class.java, "dailyfilm.db") 24 | return databaseBuilder.fallbackToDestructiveMigration().build() 25 | } 26 | } 27 | 28 | abstract fun calendarDao(): CalendarDao 29 | 30 | abstract fun localUriDao(): LocalUriDao 31 | 32 | abstract fun settingsDao(): SettingsDao 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDao.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.calendar 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.boostcamp.dailyfilm.data.model.FilmEntity 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface CalendarDao { 12 | @Query( 13 | "SELECT * FROM film_entity " + 14 | "WHERE updateDate BETWEEN :startAt AND :endAt " 15 | ) 16 | fun loadFilmFlow(startAt: Int, endAt: Int): Flow> 17 | 18 | @Query( 19 | "SELECT * FROM film_entity " + 20 | "WHERE updateDate BETWEEN :startAt AND :endAt " 21 | ) 22 | suspend fun loadFilm(startAt: Int, endAt: Int): List 23 | 24 | @Insert(onConflict = OnConflictStrategy.REPLACE) 25 | suspend fun insertAll(filmEntityList: List) 26 | 27 | @Insert(onConflict = OnConflictStrategy.REPLACE) 28 | suspend fun insert(filmEntity: FilmEntity) 29 | 30 | @Query("DELETE FROM film_entity") 31 | suspend fun deleteAll() 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.calendar 2 | 3 | import com.boostcamp.dailyfilm.data.model.FilmEntity 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.launch 9 | import kotlin.coroutines.resume 10 | import kotlin.coroutines.suspendCoroutine 11 | 12 | interface CalendarDataSource { 13 | fun loadFilmFlow(startAt: Int, endAt: Int): Flow> 14 | 15 | suspend fun loadFilm(startAt: Int, endAt: Int): List 16 | 17 | suspend fun insertFilm(film: FilmEntity) 18 | 19 | suspend fun insertAllFilm(filmList: List) 20 | 21 | suspend fun deleteAllData(): Result 22 | } 23 | 24 | class CalendarLocalDataSource( 25 | private val calendarDao: CalendarDao 26 | ) : CalendarDataSource { 27 | 28 | override fun loadFilmFlow(startAt: Int, endAt: Int): Flow> = 29 | calendarDao.loadFilmFlow(startAt, endAt) 30 | 31 | override suspend fun loadFilm(startAt: Int, endAt: Int): List = 32 | calendarDao.loadFilm(startAt, endAt) 33 | 34 | override suspend fun insertFilm(film: FilmEntity) { 35 | calendarDao.insert(film) 36 | } 37 | 38 | override suspend fun insertAllFilm(filmList: List) { 39 | calendarDao.insertAll(filmList) 40 | } 41 | 42 | override suspend fun deleteAllData() = suspendCoroutine { continuation -> 43 | CoroutineScope(Dispatchers.IO).launch { 44 | runCatching { 45 | calendarDao.deleteAll() 46 | }.onSuccess { 47 | continuation.resume(Result.Success(Unit)) 48 | }.onFailure { exception -> 49 | continuation.resume(Result.Error(exception)) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.calendar 2 | 3 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.map 7 | 8 | interface CalendarRepository { 9 | fun loadFilmInfo(startAt: String, endAt: String): Flow> 10 | 11 | suspend fun loadFilm(startAt: String, endAt: String): List 12 | 13 | suspend fun deleteAllData(): Result 14 | } 15 | 16 | class CalendarRepositoryImpl( 17 | private val calendarLocalDataSource: CalendarDataSource 18 | ) : CalendarRepository { 19 | 20 | override fun loadFilmInfo(startAt: String, endAt: String): Flow> = 21 | calendarLocalDataSource.loadFilmFlow(startAt.toInt(), endAt.toInt()).map { filmList -> 22 | filmList.map { filmEntity -> 23 | filmEntity?.mapToDailyFilmItem() 24 | } 25 | } 26 | 27 | override suspend fun loadFilm(startAt: String, endAt: String): List = 28 | calendarLocalDataSource.loadFilm(startAt.toInt(), endAt.toInt()).map { filmEntity -> 29 | filmEntity?.mapToDailyFilmItem() 30 | } 31 | 32 | override suspend fun deleteAllData(): Result = 33 | calendarLocalDataSource.deleteAllData() 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/dataStore/PreferencesKeys.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.dataStore 2 | 3 | import androidx.datastore.preferences.core.intPreferencesKey 4 | import androidx.datastore.preferences.core.stringSetPreferencesKey 5 | 6 | object PreferencesKeys { 7 | 8 | private const val SPEED_INDEX = "speed" 9 | private const val CACHED_YEAR = "year" 10 | 11 | val SPEED_INDEX_KEY = intPreferencesKey(SPEED_INDEX) 12 | val CACHED_YEAR_KEY = stringSetPreferencesKey(CACHED_YEAR) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/dataStore/UserPreferencesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.dataStore 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import com.boostcamp.dailyfilm.data.dataStore.PreferencesKeys.SPEED_INDEX_KEY 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | 10 | class UserPreferencesRepository( 11 | private val dataStore: DataStore 12 | ) { 13 | val userFastFlow: Flow = dataStore.data.map { 14 | it[SPEED_INDEX_KEY] 15 | } 16 | 17 | suspend fun editFast(index: Int) { 18 | dataStore.edit { 19 | it[SPEED_INDEX_KEY] = index 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/delete/DeleteFilmDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.delete 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | 6 | interface DeleteFilmDataSource { 7 | suspend fun deleteVideo(uploadDate: String, videoUri: Uri): Result 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/delete/DeleteFilmRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.delete 2 | 3 | import android.net.Uri 4 | import androidx.core.net.toUri 5 | import com.boostcamp.dailyfilm.data.delete.remote.DeleteFilmRemoteDataSource 6 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 7 | import com.boostcamp.dailyfilm.data.model.Result 8 | 9 | interface DeleteFilmRepository { 10 | suspend fun delete(deleteDate: String): Result 11 | suspend fun deleteVideo(uploadDate: String, videoUri: Uri): Result 12 | suspend fun deleteFilmInfo(deleteDate: String): Result 13 | } 14 | 15 | class DeleteFilmRepositoryImpl( 16 | private val deleteFilmLocalDataSource: DeleteFilmDataSource, 17 | private val deleteFilmRemoteDataSource: DeleteFilmDataSource, 18 | ) : DeleteFilmRepository { 19 | override suspend fun delete(deleteDate: String): Result { 20 | when (val result = deleteFilmInfo(deleteDate)) { 21 | is Result.Success -> { 22 | val item = result.data 23 | ?: return Result.Error(Exception("There is a failure in delete process")) 24 | return deleteVideo(deleteDate, item.videoUrl.toUri()) 25 | } 26 | is Result.Error -> { 27 | return Result.Error(result.exception) 28 | } 29 | } 30 | } 31 | 32 | override suspend fun deleteVideo(uploadDate: String, videoUri: Uri): Result { 33 | val localResult = deleteFilmLocalDataSource.deleteVideo(uploadDate, videoUri) 34 | val remoteResult = deleteFilmRemoteDataSource.deleteVideo(uploadDate, videoUri) 35 | 36 | return if (localResult is Result.Success && remoteResult is Result.Success) { 37 | Result.Success(Unit) 38 | } else { 39 | Result.Error(Exception("There is a failure in delete process")) 40 | } 41 | } 42 | 43 | override suspend fun deleteFilmInfo(deleteDate: String) = 44 | (deleteFilmRemoteDataSource as DeleteFilmRemoteDataSource).deleteFilm(deleteDate) 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/delete/local/DeleteFilmLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.delete.local 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.delete.DeleteFilmDataSource 5 | import com.boostcamp.dailyfilm.data.model.Result 6 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 7 | 8 | class DeleteFilmLocalDataSource( 9 | private val localUriDao: LocalUriDao 10 | ) : DeleteFilmDataSource { 11 | override suspend fun deleteVideo(uploadDate: String, videoUri: Uri): Result { 12 | runCatching { 13 | localUriDao.deleteFilm(uploadDate.toInt()) 14 | localUriDao.deleteVideoFilm(uploadDate.toInt()) 15 | }.onSuccess { 16 | return Result.Success(Unit) 17 | }.onFailure { exception -> 18 | return Result.Error(exception) 19 | } 20 | return Result.Error(Error()) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/delete/remote/DeleteFilmRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.delete.remote 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.BuildConfig 5 | import com.boostcamp.dailyfilm.data.delete.DeleteFilmDataSource 6 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 7 | import com.boostcamp.dailyfilm.data.model.Result 8 | import com.google.firebase.auth.FirebaseAuth 9 | import com.google.firebase.database.ktx.database 10 | import com.google.firebase.ktx.Firebase 11 | import com.google.firebase.storage.ktx.storage 12 | import kotlin.coroutines.resume 13 | import kotlin.coroutines.suspendCoroutine 14 | 15 | class DeleteFilmRemoteDataSource : DeleteFilmDataSource { 16 | override suspend fun deleteVideo(uploadDate: String, videoUri: Uri): Result = 17 | suspendCoroutine { continuation -> 18 | val reference = storage.reference 19 | val videoRef = reference.child("${videoUri.lastPathSegment}") 20 | 21 | videoRef.delete() 22 | .addOnSuccessListener { 23 | continuation.resume(Result.Success(Unit)) 24 | }.addOnFailureListener { exception -> 25 | continuation.resume(Result.Error(exception)) 26 | } 27 | } 28 | 29 | suspend fun deleteFilm(uploadDate: String) = 30 | suspendCoroutine { continuation -> 31 | userId?.let { id -> 32 | val reference = database.reference 33 | .child(DIRECTORY_USER) 34 | .child(id) 35 | .child(uploadDate) 36 | 37 | reference.get() 38 | .addOnSuccessListener { snapshot -> 39 | reference.removeValue() 40 | .addOnSuccessListener { 41 | continuation.resume(Result.Success(snapshot.getValue(DailyFilmItem::class.java))) 42 | } 43 | .addOnFailureListener { exception -> 44 | continuation.resume(Result.Error(exception)) 45 | } 46 | }.addOnFailureListener { exception -> 47 | continuation.resume(Result.Error(exception)) 48 | } 49 | } 50 | } 51 | 52 | companion object { 53 | val userId = FirebaseAuth.getInstance().currentUser?.uid 54 | val storage = Firebase.storage 55 | 56 | // BuildConfig.BUILD_TYPE 57 | val database = Firebase.database(BuildConfig.DATABASE_URL) 58 | const val DIRECTORY_USER = "users" 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/login/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.login 2 | 3 | import android.util.Log 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import com.google.firebase.FirebaseNetworkException 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.google.firebase.auth.FirebaseAuthException 8 | import com.google.firebase.auth.FirebaseUser 9 | import com.google.firebase.auth.GoogleAuthProvider 10 | import kotlinx.coroutines.channels.awaitClose 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.callbackFlow 13 | import javax.inject.Inject 14 | import javax.security.auth.login.LoginException 15 | 16 | interface LoginRepository { 17 | fun requestLogin(idToken: String): Flow> 18 | } 19 | 20 | class LoginRepositoryImpl @Inject constructor() : LoginRepository { 21 | private val firebaseAuth = FirebaseAuth.getInstance() 22 | 23 | override fun requestLogin(idToken: String): Flow> = callbackFlow { 24 | val credential = GoogleAuthProvider.getCredential(idToken, null) 25 | firebaseAuth.signInWithCredential(credential).addOnCompleteListener { task -> 26 | if (task.isSuccessful) { 27 | trySend(Result.Success(firebaseAuth.currentUser)) 28 | } 29 | }.addOnFailureListener { exception -> 30 | when(exception){ 31 | is FirebaseNetworkException ->{ 32 | Log.d("errorCodeCheck" ,"FirebaseNetworkException ${exception.message}") 33 | } 34 | is FirebaseAuthException ->{ 35 | Log.d("errorCodeCheck" ,"FirebaseAuthException ${exception.errorCode}") 36 | } 37 | } 38 | trySend(Result.Error(exception)) 39 | } 40 | awaitClose() 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/model/CachedVideoEntity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "cached_video_entity") 7 | data class CachedVideoEntity( 8 | val localUri: String, 9 | @PrimaryKey val updateDate: Int = 0 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/model/DailyFilmItem.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.model 2 | 3 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 4 | 5 | data class DailyFilmItem( 6 | val videoUrl: String = "", 7 | val text: String = "", 8 | val updateDate: String = "" 9 | ) { 10 | fun mapToFilmEntity(): FilmEntity = 11 | FilmEntity( 12 | videoUrl, 13 | text, 14 | updateDate.toInt() 15 | ) 16 | 17 | fun toDateModel(): DateModel = 18 | DateModel( 19 | updateDate.substring(0, 4), 20 | updateDate.substring(4, 6), 21 | updateDate.substring(6), 22 | text, 23 | videoUrl 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/model/FilmEntity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "film_entity") 7 | data class FilmEntity( 8 | val videoUrl: String = "", 9 | val text: String = "", 10 | @PrimaryKey val updateDate: Int = 0 11 | ) { 12 | fun mapToDailyFilmItem(): DailyFilmItem = 13 | DailyFilmItem( 14 | videoUrl, 15 | text, 16 | updateDate.toString() 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/model/Result.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.model 2 | 3 | import java.io.IOException 4 | 5 | 6 | sealed class Result { 7 | data class Success(val data: T) : Result() 8 | data class Error(val exception: Throwable) : Result() { 9 | val isNetworkError = exception is IOException 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/model/VideoItem.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class VideoItem( 9 | val uri: Uri, 10 | val name: String, 11 | val duration: Int, 12 | val size: Int 13 | ) : Parcelable 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/playfilm/PlayFilmDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.playfilm 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface PlayFilmDataSource { 8 | 9 | fun loadVideo(uploadDate: String): Flow> 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/playfilm/PlayFilmRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.playfilm 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import com.boostcamp.dailyfilm.data.playfilm.local.PlayFilmLocalDataSource 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface PlayFilmRepository { 9 | 10 | fun checkVideo(uploadDate: String): Flow> 11 | 12 | fun downloadVideo(uploadDate: String): Flow> 13 | 14 | fun insertVideo(uploadDate: String, localUri: String): Flow> 15 | } 16 | 17 | class PlayFilmRepositoryImpl( 18 | private val playFilmLocalDataSource: PlayFilmDataSource, 19 | private val playFilmRemoteDataSource: PlayFilmDataSource 20 | ): PlayFilmRepository { 21 | 22 | override fun checkVideo(uploadDate: String): Flow> = 23 | playFilmLocalDataSource.loadVideo(uploadDate) 24 | 25 | override fun downloadVideo(uploadDate: String): Flow> = 26 | playFilmRemoteDataSource.loadVideo(uploadDate) // load url 27 | 28 | override fun insertVideo(uploadDate: String, localUri: String): Flow> = 29 | (playFilmLocalDataSource as PlayFilmLocalDataSource).insertVideo(uploadDate, localUri) 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/playfilm/local/PlayFilmLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.playfilm.local 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import com.boostcamp.dailyfilm.data.model.CachedVideoEntity 6 | import com.boostcamp.dailyfilm.data.model.Result 7 | import com.boostcamp.dailyfilm.data.playfilm.PlayFilmDataSource 8 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 9 | import kotlinx.coroutines.channels.awaitClose 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.callbackFlow 12 | 13 | class PlayFilmLocalDataSource( 14 | private val localUriDao: LocalUriDao 15 | ) : PlayFilmDataSource { 16 | 17 | override fun loadVideo(uploadDate: String): Flow> = callbackFlow { 18 | 19 | runCatching { 20 | localUriDao.loadFilm(uploadDate.toInt()) 21 | }.onSuccess { 22 | if (it == null) { 23 | trySend(Result.Success(null)) 24 | } else { 25 | trySend(Result.Success(Uri.parse(it.localUri))) 26 | } 27 | }.onFailure { exception -> 28 | trySend(Result.Error(exception)) 29 | } 30 | 31 | awaitClose() 32 | } 33 | 34 | fun insertVideo(uploadDate: String, localUri: String): Flow> = callbackFlow { 35 | 36 | runCatching { 37 | localUriDao.insert(CachedVideoEntity(localUri, uploadDate.toInt())) 38 | }.onSuccess { 39 | trySend(Result.Success(Unit)) 40 | }.onFailure { exception -> 41 | trySend(Result.Error(exception)) 42 | } 43 | 44 | awaitClose() 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/playfilm/remote/PlayFilmRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.playfilm.remote 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.util.Log 6 | import androidx.core.net.toUri 7 | import com.boostcamp.dailyfilm.data.model.Result 8 | import com.boostcamp.dailyfilm.data.playfilm.PlayFilmDataSource 9 | import com.boostcamp.dailyfilm.data.uploadfilm.remote.UploadFilmRemoteDataSource 10 | import com.google.firebase.auth.FirebaseAuth 11 | import com.google.firebase.ktx.Firebase 12 | import com.google.firebase.storage.ktx.storage 13 | import dagger.hilt.android.qualifiers.ApplicationContext 14 | import kotlinx.coroutines.channels.awaitClose 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.callbackFlow 17 | import java.io.File 18 | import javax.inject.Inject 19 | 20 | class PlayFilmRemoteDataSource @Inject constructor( 21 | @ApplicationContext private val context: Context 22 | ) : PlayFilmDataSource { 23 | 24 | override fun loadVideo(uploadDate: String): Flow> = callbackFlow { 25 | userId?.let { id -> 26 | val urlRef = UploadFilmRemoteDataSource.database.reference 27 | .child(DIRECTORY_USER) 28 | .child(id) 29 | .child(uploadDate) 30 | .child(VIDEO_URL) 31 | 32 | urlRef.get() 33 | .addOnSuccessListener { snapshot -> 34 | snapshot.value ?: return@addOnSuccessListener 35 | 36 | val videoUrl = snapshot.value.toString() 37 | val storageReference = storage.getReferenceFromUrl(videoUrl) 38 | val file = File(context.filesDir, "$uploadDate.mp4") 39 | Log.d("LoadVideo", "absolutePath : ${file.absolutePath}") 40 | 41 | storageReference.getFile(file) 42 | .addOnSuccessListener { 43 | trySend(Result.Success(file.toUri())) 44 | }.addOnFailureListener { exception -> 45 | trySend(Result.Error(exception)) 46 | } 47 | } 48 | .addOnFailureListener { exception -> 49 | trySend(Result.Error(exception)) 50 | } 51 | } 52 | 53 | awaitClose() 54 | } 55 | 56 | companion object { 57 | val userId = FirebaseAuth.getInstance().currentUser?.uid 58 | val storage = Firebase.storage 59 | const val DIRECTORY_USER = "users" 60 | const val VIDEO_URL = "videoUrl" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/selectvideo/GalleryVideoRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.selectvideo 2 | 3 | import android.content.ContentResolver 4 | import androidx.paging.* 5 | import com.boostcamp.dailyfilm.data.model.VideoItem 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | 9 | interface GalleryVideoRepository { 10 | fun loadVideo(): Flow> 11 | } 12 | 13 | class GalleryVideoRepositoryImpl @Inject constructor( 14 | private val contentResolver: ContentResolver 15 | ) : GalleryVideoRepository { 16 | override fun loadVideo(): Flow> { 17 | return Pager(config = PagingConfig(pageSize = GalleryPagingSource.PAGING_SIZE)) { 18 | GalleryPagingSource(contentResolver) 19 | }.flow 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/settings/SettingsDao.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.settings 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | 6 | @Dao 7 | interface SettingsDao { 8 | 9 | @Query("DELETE FROM film_entity") 10 | suspend fun deleteAll() 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/settings/SettingsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.settings 2 | 3 | 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import kotlinx.coroutines.channels.awaitClose 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.callbackFlow 8 | 9 | interface SettingsDataSource { 10 | 11 | fun deleteAllData(): Flow> 12 | 13 | } 14 | 15 | class SettingsLocalDataSource( 16 | private val settingsDao: SettingsDao 17 | ) : SettingsDataSource { 18 | 19 | override fun deleteAllData(): Flow> = callbackFlow { 20 | runCatching { 21 | settingsDao.deleteAll() 22 | }.onSuccess { 23 | trySend(Result.Success(Unit)) 24 | }.onFailure { exception -> 25 | trySend(Result.Error(exception)) 26 | } 27 | 28 | awaitClose() 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/settings/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.settings 2 | 3 | 4 | import com.boostcamp.dailyfilm.data.model.Result 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface SettingsRepository { 8 | fun deleteAllData(): Flow> 9 | } 10 | 11 | class SettingsRepositoryImpl( 12 | private val settingsLocalDataSource: SettingsDataSource 13 | ) : SettingsRepository { 14 | 15 | override fun deleteAllData(): Flow> = 16 | settingsLocalDataSource.deleteAllData() 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/sync/SyncDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.sync 2 | 3 | import com.boostcamp.dailyfilm.BuildConfig 4 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 5 | import com.google.firebase.database.ktx.database 6 | import com.google.firebase.ktx.Firebase 7 | import kotlin.coroutines.resume 8 | import kotlin.coroutines.suspendCoroutine 9 | 10 | interface SyncDataSource { 11 | suspend fun loadFilmInfo(userId: String, startAt: String, endAt: String): List? 12 | } 13 | 14 | class SyncRemoteDataSource : SyncDataSource { 15 | 16 | override suspend fun loadFilmInfo( 17 | userId: String, 18 | startAt: String, 19 | endAt: String 20 | ): List? = suspendCoroutine { continuation -> 21 | database.reference 22 | .child(DIRECTORY_USER) 23 | .child(userId) 24 | .orderByKey() 25 | .startAt(startAt) 26 | .endAt(endAt) 27 | .get() 28 | .addOnSuccessListener { snapshot -> 29 | continuation.resume( 30 | snapshot.children.map { 31 | it.getValue(DailyFilmItem::class.java) 32 | } 33 | ) 34 | } 35 | .addOnFailureListener { 36 | continuation.resume(null) 37 | } 38 | } 39 | 40 | companion object { 41 | val database = Firebase.database(BuildConfig.DATABASE_URL) 42 | const val DIRECTORY_USER = "users" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/sync/SyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.sync 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import com.boostcamp.dailyfilm.data.calendar.CalendarDataSource 7 | import com.boostcamp.dailyfilm.data.dataStore.PreferencesKeys.CACHED_YEAR_KEY 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.collectLatest 11 | import kotlinx.coroutines.flow.map 12 | import kotlinx.coroutines.launch 13 | 14 | interface SyncRepository { 15 | suspend fun startSync(userId: String, startAt: String, endAt: String) 16 | 17 | fun isSynced(year: Int): Boolean 18 | 19 | suspend fun addSyncedYear(year: Int) 20 | 21 | suspend fun saveSyncedYear() 22 | 23 | suspend fun clearSyncedYear() 24 | } 25 | 26 | class SyncRepositoryImpl( 27 | private val syncDataSource: SyncDataSource, 28 | private val calendarDataSource: CalendarDataSource, 29 | private val dataStore: DataStore 30 | ) : SyncRepository { 31 | 32 | private val syncedYearSet = mutableSetOf() 33 | 34 | init { 35 | CoroutineScope(Dispatchers.IO).launch { 36 | dataStore.data.map { 37 | it[CACHED_YEAR_KEY] ?: setOf() 38 | }.collectLatest { 39 | syncedYearSet.addAll(it) 40 | } 41 | } 42 | } 43 | 44 | override suspend fun startSync(userId: String, startAt: String, endAt: String) { 45 | val filmItemList = syncDataSource.loadFilmInfo(userId, startAt, endAt) ?: return 46 | calendarDataSource.insertAllFilm( 47 | filmItemList.filterNotNull().map { filmItem -> 48 | filmItem.mapToFilmEntity() 49 | } 50 | ) 51 | } 52 | 53 | override fun isSynced(year: Int): Boolean = syncedYearSet.contains(year.toString()) 54 | 55 | override suspend fun addSyncedYear(year: Int) { 56 | syncedYearSet.add(year.toString()) 57 | } 58 | 59 | override suspend fun saveSyncedYear() { 60 | dataStore.edit { it[CACHED_YEAR_KEY] = syncedYearSet.toSet() } 61 | } 62 | 63 | override suspend fun clearSyncedYear() { 64 | syncedYearSet.clear() 65 | dataStore.edit { it[CACHED_YEAR_KEY] = emptySet() } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/uploadfilm/UploadFilmDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.uploadfilm 2 | 3 | 4 | import android.net.Uri 5 | import com.boostcamp.dailyfilm.data.model.Result 6 | 7 | interface UploadFilmDataSource { 8 | suspend fun uploadVideo(uploadDate: String, videoUri: Uri): Result 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/uploadfilm/UploadFilmRepository.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.uploadfilm 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.calendar.CalendarDataSource 5 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 6 | import com.boostcamp.dailyfilm.data.model.Result 7 | import com.boostcamp.dailyfilm.data.uploadfilm.remote.UploadFilmRemoteDataSource 8 | import javax.inject.Inject 9 | 10 | interface UploadFilmRepository { 11 | suspend fun uploadVideo(uploadDate: String, videoUri: Uri): Result 12 | suspend fun uploadFilmInfo(uploadDate: String, filmInfo: DailyFilmItem): Result 13 | suspend fun uploadEditVideo(uploadDate: String, item: DailyFilmItem): Result 14 | suspend fun insertFilmEntity(filmInfo: DailyFilmItem) 15 | } 16 | 17 | class UploadFilmRepositoryImpl @Inject constructor( 18 | private val uploadFilmLocalDataSource: UploadFilmDataSource, 19 | private val uploadFilmRemoteDataSource: UploadFilmDataSource, 20 | private val calendarDataSource: CalendarDataSource 21 | ) : UploadFilmRepository { 22 | 23 | override suspend fun uploadVideo(uploadDate: String, videoUri: Uri): Result { 24 | val localResult = uploadFilmLocalDataSource.uploadVideo(uploadDate, videoUri) 25 | val remoteResult = uploadFilmRemoteDataSource.uploadVideo(uploadDate, videoUri) 26 | 27 | return if (localResult is Result.Success && remoteResult is Result.Success) { 28 | Result.Success(remoteResult.data) 29 | } else { 30 | Result.Error(Exception("There is a failure in upload process")) 31 | } 32 | } 33 | 34 | override suspend fun uploadEditVideo(uploadDate: String, item: DailyFilmItem): Result { 35 | val remoteResult = uploadFilmInfo(uploadDate, item) 36 | 37 | return if (remoteResult is Result.Success) { 38 | insertFilmEntity(item) 39 | Result.Success(remoteResult.data) 40 | } else { 41 | Result.Error(Exception("There is a failure in upload process")) 42 | } 43 | } 44 | 45 | override suspend fun uploadFilmInfo(uploadDate: String, filmInfo: DailyFilmItem) = 46 | (uploadFilmRemoteDataSource as UploadFilmRemoteDataSource).uploadFilmInfo( 47 | uploadDate, 48 | filmInfo 49 | ) 50 | 51 | override suspend fun insertFilmEntity(filmInfo: DailyFilmItem) { 52 | calendarDataSource.insertFilm(filmInfo.mapToFilmEntity()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/uploadfilm/local/LocalUriDao.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.uploadfilm.local 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.boostcamp.dailyfilm.data.model.CachedVideoEntity 8 | 9 | @Dao 10 | interface LocalUriDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | suspend fun insert(cachedVideoEntity: CachedVideoEntity) 14 | 15 | @Query("SELECT * FROM cached_video_entity WHERE updateDate = :updateDate LIMIT 1") 16 | suspend fun loadFilm(updateDate: Int): CachedVideoEntity? 17 | 18 | @Query("DELETE FROM cached_video_entity WHERE updateDate = :updateDate") 19 | suspend fun deleteVideoFilm(updateDate: Int) 20 | 21 | @Query("DELETE FROM film_entity WHERE updateDate = :updateDate") 22 | suspend fun deleteFilm(updateDate: Int) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/uploadfilm/local/UploadFilmLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.uploadfilm.local 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.data.model.CachedVideoEntity 5 | import com.boostcamp.dailyfilm.data.model.Result 6 | import com.boostcamp.dailyfilm.data.uploadfilm.UploadFilmDataSource 7 | 8 | class UploadFilmLocalDataSource( 9 | private val localUriDao: LocalUriDao 10 | ) : UploadFilmDataSource { 11 | 12 | override suspend fun uploadVideo(uploadDate: String, videoUri: Uri): Result { 13 | runCatching { 14 | localUriDao.insert(CachedVideoEntity(videoUri.toString(), uploadDate.toInt())) 15 | }.onSuccess { 16 | return Result.Success(videoUri) 17 | }.onFailure { exception -> 18 | return Result.Error(exception) 19 | } 20 | return Result.Error(Error()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/data/uploadfilm/remote/UploadFilmRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.data.uploadfilm.remote 2 | 3 | import android.net.Uri 4 | import com.boostcamp.dailyfilm.BuildConfig 5 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 6 | import com.boostcamp.dailyfilm.data.model.Result 7 | import com.boostcamp.dailyfilm.data.uploadfilm.UploadFilmDataSource 8 | import com.google.firebase.auth.FirebaseAuth 9 | import com.google.firebase.database.ktx.database 10 | import com.google.firebase.ktx.Firebase 11 | import com.google.firebase.storage.ktx.storage 12 | import com.google.firebase.storage.ktx.storageMetadata 13 | import kotlin.coroutines.resume 14 | import kotlin.coroutines.suspendCoroutine 15 | 16 | class UploadFilmRemoteDataSource : UploadFilmDataSource { 17 | override suspend fun uploadVideo(uploadDate: String, videoUri: Uri): Result = 18 | suspendCoroutine { continuation -> 19 | val reference = storage.reference // File Pointer 20 | val videoRef = 21 | reference.child("user_videos/${videoUri.lastPathSegment}") // TODO 선택한 날짜값을 이름으로 재구성할 것 22 | val metadata = storageMetadata { 23 | contentType = "video/mp4" 24 | } 25 | 26 | videoRef.putFile(videoUri, metadata) 27 | .continueWithTask { 28 | videoRef.downloadUrl 29 | } 30 | .addOnSuccessListener { uri -> 31 | continuation.resume(Result.Success(uri)) 32 | }.addOnFailureListener { exception -> 33 | // (exception as StorageException).errorCode 34 | continuation.resume(Result.Error(exception)) 35 | } 36 | } 37 | 38 | suspend fun uploadFilmInfo(uploadDate: String, filmInfo: DailyFilmItem) = 39 | suspendCoroutine { continuation -> 40 | userId?.let { id -> 41 | val reference = database.reference 42 | .child(DIRECTORY_USER) 43 | .child(id) 44 | .child(uploadDate) 45 | 46 | reference.setValue(filmInfo) 47 | .addOnSuccessListener { 48 | continuation.resume(Result.Success(Unit)) 49 | } 50 | .addOnFailureListener { exception -> 51 | continuation.resume(Result.Error(exception)) 52 | } 53 | } 54 | } 55 | 56 | companion object { 57 | val userId = FirebaseAuth.getInstance().currentUser?.uid 58 | val storage = Firebase.storage 59 | 60 | // BuildConfig.BUILD_TYPE 61 | val database = Firebase.database(BuildConfig.DATABASE_URL) 62 | const val DIRECTORY_USER = "users" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/CalendarModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import com.boostcamp.dailyfilm.data.calendar.CalendarDao 4 | import com.boostcamp.dailyfilm.data.calendar.CalendarDataSource 5 | import com.boostcamp.dailyfilm.data.calendar.CalendarLocalDataSource 6 | import com.boostcamp.dailyfilm.data.calendar.CalendarRepository 7 | import com.boostcamp.dailyfilm.data.calendar.CalendarRepositoryImpl 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @InstallIn(SingletonComponent::class) 15 | @Module 16 | object CalendarModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideCalenderDataSource(calendarDao: CalendarDao): CalendarDataSource = CalendarLocalDataSource(calendarDao) 21 | 22 | @Singleton 23 | @Provides 24 | fun provideCalenderRepository( 25 | calendarLocalDataSource: CalendarDataSource 26 | ): CalendarRepository = 27 | CalendarRepositoryImpl(calendarLocalDataSource) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/ContentResolverModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @InstallIn(SingletonComponent::class) 13 | @Module 14 | object ContentResolverModule { 15 | 16 | @Singleton 17 | @Provides 18 | fun provideContentResolver(@ApplicationContext context:Context):ContentResolver = 19 | context.contentResolver 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/DailyFilmDBModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import android.content.Context 4 | import com.boostcamp.dailyfilm.data.DailyFilmDB 5 | import com.boostcamp.dailyfilm.data.calendar.CalendarDao 6 | import com.boostcamp.dailyfilm.data.settings.SettingsDao 7 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DailyFilmDBModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideDB( 22 | @ApplicationContext context: Context 23 | ): DailyFilmDB = DailyFilmDB.create(context) 24 | 25 | @Provides 26 | @Singleton 27 | fun provideCalendarDao( 28 | dailyFilmDB: DailyFilmDB 29 | ): CalendarDao = dailyFilmDB.calendarDao() 30 | 31 | @Provides 32 | @Singleton 33 | fun provideLocalUriDao( 34 | dailyFilmDB: DailyFilmDB 35 | ): LocalUriDao = dailyFilmDB.localUriDao() 36 | 37 | @Provides 38 | @Singleton 39 | fun provideSettingsDao( 40 | dailyFilmDB: DailyFilmDB 41 | ): SettingsDao = dailyFilmDB.settingsDao() 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object FirebaseModule { 13 | 14 | @Provides 15 | @Singleton 16 | fun provideFirebaseUid(): String = 17 | FirebaseAuth.getInstance().currentUser?.uid ?: error("Unknown User") 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import com.boostcamp.dailyfilm.data.login.LoginRepository 4 | import com.boostcamp.dailyfilm.data.login.LoginRepositoryImpl 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object LoginModule { 15 | @Provides 16 | @Singleton 17 | fun provideLoginRepository(): LoginRepository = 18 | LoginRepositoryImpl() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/PlayFilmModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import android.content.Context 4 | import com.boostcamp.dailyfilm.data.delete.DeleteFilmDataSource 5 | import com.boostcamp.dailyfilm.data.delete.DeleteFilmRepository 6 | import com.boostcamp.dailyfilm.data.delete.DeleteFilmRepositoryImpl 7 | import com.boostcamp.dailyfilm.data.delete.local.DeleteFilmLocalDataSource 8 | import com.boostcamp.dailyfilm.data.delete.remote.DeleteFilmRemoteDataSource 9 | import com.boostcamp.dailyfilm.data.playfilm.PlayFilmDataSource 10 | import com.boostcamp.dailyfilm.data.playfilm.PlayFilmRepository 11 | import com.boostcamp.dailyfilm.data.playfilm.PlayFilmRepositoryImpl 12 | import com.boostcamp.dailyfilm.data.playfilm.local.PlayFilmLocalDataSource 13 | import com.boostcamp.dailyfilm.data.playfilm.remote.PlayFilmRemoteDataSource 14 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.android.qualifiers.ApplicationContext 19 | import dagger.hilt.components.SingletonComponent 20 | import javax.inject.Qualifier 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object PlayFilmModule { 26 | @Qualifier 27 | @Retention(AnnotationRetention.RUNTIME) 28 | annotation class LocalPlayFilmDataSource 29 | 30 | @Qualifier 31 | @Retention(AnnotationRetention.RUNTIME) 32 | annotation class RemotePlayFilmDataSource 33 | 34 | @Qualifier 35 | @Retention(AnnotationRetention.RUNTIME) 36 | annotation class LocalDeleteFilmDataSource 37 | 38 | @Qualifier 39 | @Retention(AnnotationRetention.RUNTIME) 40 | annotation class RemoteDeleteFilmDataSource 41 | 42 | @Singleton 43 | @LocalPlayFilmDataSource 44 | @Provides 45 | fun providePlayFilmLocalDataSource(localUriDao: LocalUriDao): PlayFilmDataSource = 46 | PlayFilmLocalDataSource(localUriDao) 47 | 48 | @Singleton 49 | @RemotePlayFilmDataSource 50 | @Provides 51 | fun providePlayFilmRemoteDataSource(@ApplicationContext context: Context): PlayFilmDataSource = 52 | PlayFilmRemoteDataSource(context) 53 | 54 | @Singleton 55 | @LocalDeleteFilmDataSource 56 | @Provides 57 | fun provideDeleteFilmLocalDataSource(localUriDao: LocalUriDao): DeleteFilmDataSource = 58 | DeleteFilmLocalDataSource(localUriDao) 59 | 60 | @Singleton 61 | @RemoteDeleteFilmDataSource 62 | @Provides 63 | fun provideDeleteFilmRemoteDataSource(): DeleteFilmDataSource = 64 | DeleteFilmRemoteDataSource() 65 | 66 | @Singleton 67 | @Provides 68 | fun providePlayFilmRepository( 69 | @LocalPlayFilmDataSource playFilmLocalDataSource: PlayFilmDataSource, 70 | @RemotePlayFilmDataSource playFilmRemoteDataSource: PlayFilmDataSource 71 | ): PlayFilmRepository = 72 | PlayFilmRepositoryImpl(playFilmLocalDataSource, playFilmRemoteDataSource) 73 | 74 | @Singleton 75 | @Provides 76 | fun provideDeleteFilmRepository( 77 | @LocalDeleteFilmDataSource deleteFilmLocalDataSource: DeleteFilmDataSource, 78 | @RemoteDeleteFilmDataSource deleteFilmRemoteDataSource: DeleteFilmDataSource 79 | ): DeleteFilmRepository = 80 | DeleteFilmRepositoryImpl(deleteFilmLocalDataSource, deleteFilmRemoteDataSource) 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/PreferenceModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.preferencesDataStore 7 | import com.boostcamp.dailyfilm.data.dataStore.UserPreferencesRepository 8 | import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_SPEED 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object PreferenceModule { 19 | 20 | private val Context.dataStore by preferencesDataStore(KEY_SPEED) 21 | 22 | @Provides 23 | @Singleton 24 | fun provideUserPreferenceRepository(dataStore: DataStore) = 25 | UserPreferencesRepository(dataStore) 26 | 27 | @Provides 28 | @Singleton 29 | fun provideDataStore(@ApplicationContext context: Context) = context.dataStore 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/SelectVideoModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import android.content.ContentResolver 4 | import com.boostcamp.dailyfilm.data.selectvideo.GalleryVideoRepository 5 | import com.boostcamp.dailyfilm.data.selectvideo.GalleryVideoRepositoryImpl 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object SelectVideoModule { 15 | @Provides 16 | @Singleton 17 | fun provideGalleryVideoRepository(contentResolver: ContentResolver): GalleryVideoRepository = 18 | GalleryVideoRepositoryImpl(contentResolver) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/SettingsModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import com.boostcamp.dailyfilm.data.settings.* 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object SettingsModule { 13 | 14 | @Singleton 15 | @Provides 16 | fun provideSettingsDataSource(settingsDao: SettingsDao): SettingsDataSource = 17 | SettingsLocalDataSource(settingsDao) 18 | 19 | @Singleton 20 | @Provides 21 | fun provideSettingsRepository( 22 | settingsLocalDataSource: SettingsDataSource 23 | ): SettingsRepository = SettingsRepositoryImpl(settingsLocalDataSource) 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/SyncModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import com.boostcamp.dailyfilm.data.calendar.CalendarDataSource 6 | import com.boostcamp.dailyfilm.data.sync.SyncDataSource 7 | import com.boostcamp.dailyfilm.data.sync.SyncRemoteDataSource 8 | import com.boostcamp.dailyfilm.data.sync.SyncRepository 9 | import com.boostcamp.dailyfilm.data.sync.SyncRepositoryImpl 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object SyncModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun provideSyncRepository( 23 | syncDataSource: SyncDataSource, 24 | calendarDataSource: CalendarDataSource, 25 | dataStore: DataStore 26 | ): SyncRepository = SyncRepositoryImpl(syncDataSource, calendarDataSource, dataStore) 27 | 28 | @Provides 29 | @Singleton 30 | fun provideSyncDataSource(): SyncDataSource = SyncRemoteDataSource() 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/di/UploadFilmModule.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.di 2 | 3 | import com.boostcamp.dailyfilm.data.calendar.CalendarDataSource 4 | import com.boostcamp.dailyfilm.data.uploadfilm.UploadFilmDataSource 5 | import com.boostcamp.dailyfilm.data.uploadfilm.UploadFilmRepository 6 | import com.boostcamp.dailyfilm.data.uploadfilm.UploadFilmRepositoryImpl 7 | import com.boostcamp.dailyfilm.data.uploadfilm.local.LocalUriDao 8 | import com.boostcamp.dailyfilm.data.uploadfilm.local.UploadFilmLocalDataSource 9 | import com.boostcamp.dailyfilm.data.uploadfilm.remote.UploadFilmRemoteDataSource 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Qualifier 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object UploadFilmModule { 20 | 21 | @Qualifier 22 | @Retention(AnnotationRetention.RUNTIME) 23 | annotation class LocalUploadDataSource 24 | 25 | @Qualifier 26 | @Retention(AnnotationRetention.RUNTIME) 27 | annotation class RemoteUploadDataSource 28 | 29 | @Singleton 30 | @LocalUploadDataSource 31 | @Provides 32 | fun provideUploadLocalDataSource(localUriDao: LocalUriDao): UploadFilmDataSource = 33 | UploadFilmLocalDataSource(localUriDao) 34 | 35 | @Singleton 36 | @RemoteUploadDataSource 37 | @Provides 38 | fun provideUploadRemoteDataSource(): UploadFilmDataSource = 39 | UploadFilmRemoteDataSource() 40 | 41 | @Provides 42 | @Singleton 43 | fun provideUploadRepository( 44 | @LocalUploadDataSource uploadFilmLocalDataSource: UploadFilmDataSource, 45 | @RemoteUploadDataSource uploadFilmRemoteDataSource: UploadFilmDataSource, 46 | calendarDataSource: CalendarDataSource 47 | ): UploadFilmRepository = 48 | UploadFilmRepositoryImpl(uploadFilmLocalDataSource, uploadFilmRemoteDataSource, calendarDataSource) 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation 2 | 3 | import android.content.pm.ActivityInfo 4 | import android.os.Bundle 5 | import androidx.annotation.LayoutRes 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | 10 | abstract class BaseActivity(@LayoutRes private val layoutResId: Int) : 11 | AppCompatActivity() { 12 | 13 | protected lateinit var binding: B 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | binding = DataBindingUtil.setContentView(this, layoutResId) 18 | requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 19 | overridePendingTransition(android.R.anim.fade_in, 0) 20 | 21 | binding.lifecycleOwner = this 22 | 23 | initView() 24 | } 25 | 26 | abstract fun initView() 27 | 28 | override fun finish() { 29 | super.finish() 30 | overridePendingTransition(0, android.R.anim.fade_out) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import androidx.databinding.DataBindingUtil 9 | import androidx.databinding.ViewDataBinding 10 | import androidx.fragment.app.Fragment 11 | 12 | abstract class BaseFragment( 13 | @LayoutRes val layoutId: Int 14 | ) : Fragment() { 15 | 16 | private var _binding: B? = null 17 | protected val binding get() = _binding ?: error("Binding is null") 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View? { 24 | _binding = DataBindingUtil.inflate(inflater, layoutId, container, false) 25 | binding.lifecycleOwner = viewLifecycleOwner 26 | return binding.root 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | initView() 33 | } 34 | 35 | override fun onDestroyView() { 36 | _binding = null 37 | super.onDestroyView() 38 | } 39 | 40 | abstract fun initView() 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/CalendarBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.viewpager2.widget.ViewPager2 5 | import com.boostcamp.dailyfilm.presentation.calendar.adpater.CalendarPagerAdapter 6 | import java.util.* 7 | 8 | @BindingAdapter( 9 | value = ["setAdapter", "setViewModel"] 10 | ) 11 | fun ViewPager2.initViewPager( 12 | calendarPagerAdapter: CalendarPagerAdapter, 13 | viewModel: CalendarViewModel 14 | ) { 15 | if (adapter == null) { 16 | adapter = calendarPagerAdapter 17 | isSaveEnabled = false 18 | setCurrentItem(CalendarPagerAdapter.START_POSITION, false) 19 | offscreenPageLimit = 2 20 | 21 | val todayCalendar = Calendar.getInstance(Locale.getDefault()) 22 | val todayYear = todayCalendar.get(Calendar.YEAR) 23 | val todayMonth = todayCalendar.get(Calendar.MONTH) 24 | 25 | val datePickerDialog = DatePickerDialog(viewModel.calendar) { year, month -> 26 | val position = (year * 12 + month) - (todayYear * 12 + todayMonth) 27 | currentItem = CalendarPagerAdapter.START_POSITION + position 28 | } 29 | 30 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { 31 | override fun onPageSelected(position: Int) { 32 | super.onPageSelected(position) 33 | viewModel.getViewPagerPosition(position) 34 | viewModel.changeSelectedItem(null, null) 35 | 36 | val calendar = Calendar.getInstance(Locale.getDefault()).apply { 37 | add(Calendar.MONTH, position - CalendarPagerAdapter.START_POSITION) 38 | } 39 | datePickerDialog.setCalendar(calendar) 40 | } 41 | }) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DateBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.MotionEvent 5 | import androidx.databinding.BindingAdapter 6 | import com.boostcamp.dailyfilm.presentation.calendar.custom.CalendarView 7 | import com.bumptech.glide.Glide 8 | 9 | @SuppressLint("ClickableViewAccessibility") 10 | @BindingAdapter( 11 | value = ["setDateFragment", "setActivityViewModel", "setViewModel"] 12 | ) 13 | fun CalendarView.initCalendarView( 14 | fragment: DateFragment, 15 | activityViewModel: CalendarViewModel, 16 | viewModel: DateViewModel 17 | ) { 18 | initCalendar( 19 | Glide.with(fragment), 20 | viewModel.initialDateList(), 21 | viewModel.calendar 22 | ) 23 | 24 | setOnTouchListener { _, event -> 25 | when (event.action) { 26 | MotionEvent.ACTION_DOWN -> true 27 | MotionEvent.ACTION_UP -> { 28 | val x = event.x.toInt() / tmpHorizontal // child horizontal Index 29 | val y = event.y.toInt() / tmpVertical // child vertical Index 30 | 31 | val index = (y * 7 + x) * 2 32 | 33 | if (childCount <= index) return@setOnTouchListener false 34 | 35 | setSelected(index) { 36 | activityViewModel.changeSelectedItem(index / 2, it) 37 | } 38 | true 39 | } 40 | else -> false 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DatePickerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.fragment.app.DialogFragment 9 | import com.boostcamp.dailyfilm.R 10 | import com.boostcamp.dailyfilm.databinding.DialogDatepickerBinding 11 | import java.util.* 12 | 13 | class DatePickerDialog(private var calendar: Calendar, private val callback: (Int, Int) -> Unit) : DialogFragment() { 14 | 15 | private var _binding: DialogDatepickerBinding? = null 16 | private val binding get() = _binding ?: error("Binding is null") 17 | 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 19 | _binding = DataBindingUtil.inflate(inflater, R.layout.dialog_datepicker, container, false) 20 | binding.lifecycleOwner = viewLifecycleOwner 21 | return binding.root 22 | } 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | super.onViewCreated(view, savedInstanceState) 26 | 27 | binding.calendar = calendar 28 | 29 | binding.btnCancel.setOnClickListener { 30 | dismiss() 31 | } 32 | 33 | binding.btnSet.setOnClickListener { 34 | callback( 35 | binding.datePickerSpinner.year, 36 | binding.datePickerSpinner.month 37 | ) 38 | dismiss() 39 | } 40 | } 41 | fun setCalendar(calendar: Calendar) { 42 | this.calendar = calendar 43 | } 44 | 45 | override fun onDestroyView() { 46 | _binding = null 47 | super.onDestroyView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/adpater/CalendarPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar.adpater 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.boostcamp.dailyfilm.presentation.calendar.DateFragment 6 | import java.util.* 7 | 8 | class CalendarPagerAdapter( 9 | fragmentActivity: FragmentActivity 10 | ) : FragmentStateAdapter(fragmentActivity) { 11 | 12 | override fun getItemCount(): Int = Int.MAX_VALUE 13 | 14 | override fun createFragment(position: Int): DateFragment { 15 | val calendar = Calendar.getInstance(Locale.getDefault()) 16 | calendar.add(Calendar.MONTH, getItemId(position).toInt()) 17 | if (getItemId(position).toInt() != 0) { 18 | calendar.set(Calendar.DAY_OF_MONTH, 1) 19 | } 20 | return DateFragment.newInstance(calendar) 21 | } 22 | 23 | override fun getItemId(position: Int): Long = (position - START_POSITION).toLong() 24 | 25 | companion object { 26 | const val START_POSITION = Int.MAX_VALUE / 2 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/custom/DateImgView.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar.custom 2 | 3 | import android.content.Context 4 | import androidx.appcompat.widget.AppCompatImageView 5 | import com.boostcamp.dailyfilm.R 6 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 7 | import com.bumptech.glide.RequestManager 8 | import com.bumptech.glide.load.resource.bitmap.CenterCrop 9 | import com.bumptech.glide.load.resource.bitmap.RoundedCorners 10 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 11 | import com.bumptech.glide.request.transition.DrawableCrossFadeFactory 12 | 13 | class DateImgView constructor( 14 | context: Context, 15 | var dateModel: DateModel, 16 | private val requestManager: RequestManager, 17 | private val staticWidth: Int, 18 | private val staticHeight: Int 19 | ) : AppCompatImageView(context) { 20 | 21 | private val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build() 22 | 23 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 24 | setMeasuredDimension(staticWidth, staticHeight) 25 | } 26 | 27 | fun setVideoUrl(dateModel: DateModel) { 28 | this.dateModel = dateModel 29 | load(dateModel.videoUrl) 30 | } 31 | 32 | private fun load(imageUrl: String?) { 33 | imageUrl ?: return 34 | requestManager.load(imageUrl) 35 | .transition(DrawableTransitionOptions.withCrossFade(factory)) 36 | .placeholder(R.color.gray) 37 | .transform(CenterCrop(), RoundedCorners(10)) 38 | .into(this) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/custom/DateTextView.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar.custom 2 | 3 | import android.content.Context 4 | import androidx.appcompat.widget.AppCompatTextView 5 | 6 | class DateTextView constructor( 7 | context: Context, 8 | private val staticWidth: Int, 9 | private val staticHeight: Int, 10 | ) : AppCompatTextView(context) { 11 | 12 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 13 | setMeasuredDimension(staticWidth, staticHeight) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/model/DateModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar.model 2 | 3 | import android.os.Parcelable 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Parcelize 9 | @Entity(tableName = "dateModel") 10 | data class DateModel( 11 | @PrimaryKey val year: String, 12 | val month: String, 13 | val day: String, 14 | val text: String? = null, 15 | val videoUrl: String? = null 16 | ) : Parcelable { 17 | 18 | fun getDate() = 19 | year + month.padStart(2, '0') + day.padStart(2, '0') 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/model/DateState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.calendar.model 2 | 3 | enum class DateState { 4 | BEFORE, TODAY, AFTER 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.login 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.boostcamp.dailyfilm.data.login.LoginRepository 6 | import com.boostcamp.dailyfilm.data.model.Result 7 | import com.boostcamp.dailyfilm.presentation.util.UiState 8 | import com.google.firebase.auth.FirebaseUser 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.* 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class LoginViewModel @Inject constructor( 15 | private val loginRepository: LoginRepository 16 | ) : ViewModel() { 17 | 18 | private val _uiState = MutableStateFlow>(UiState.Uninitialized) 19 | val uiState = _uiState.asStateFlow() 20 | 21 | fun requestLogin(idToken: String) { 22 | _uiState.value = UiState.Loading 23 | loginRepository.requestLogin(idToken).onEach { result -> 24 | when (result) { 25 | is Result.Success -> { 26 | _uiState.value = UiState.Success(result.data) 27 | } 28 | is Result.Error -> { 29 | _uiState.value = UiState.Failure(result.exception) 30 | } 31 | } 32 | }.launchIn(viewModelScope) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmActivity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm 2 | 3 | import androidx.activity.viewModels 4 | import com.boostcamp.dailyfilm.R 5 | import com.boostcamp.dailyfilm.databinding.ActivityPlayFilmBinding 6 | import com.boostcamp.dailyfilm.presentation.BaseActivity 7 | import com.boostcamp.dailyfilm.presentation.playfilm.adapter.PlayFilmPageAdapter 8 | import dagger.hilt.android.AndroidEntryPoint 9 | 10 | @AndroidEntryPoint 11 | class PlayFilmActivity : BaseActivity(R.layout.activity_play_film) { 12 | 13 | private val viewModel: PlayFilmActivityViewModel by viewModels() 14 | 15 | override fun initView() { 16 | initBinding() 17 | } 18 | 19 | private fun initBinding() { 20 | binding.viewModel = viewModel 21 | binding.adapter = PlayFilmPageAdapter( 22 | viewModel.filmArray ?: arrayListOf(), 23 | this 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmActivityBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.viewpager2.widget.ViewPager2 5 | import com.boostcamp.dailyfilm.presentation.playfilm.adapter.PlayFilmPageAdapter 6 | 7 | 8 | @BindingAdapter("setAdapter", "setViewModel") 9 | fun ViewPager2.initPlayFilmViewPager( 10 | playFilmPageAdapter: PlayFilmPageAdapter, 11 | viewModel: PlayFilmActivityViewModel 12 | ) { 13 | adapter = playFilmPageAdapter 14 | setCurrentItem(viewModel.dateModelIndex ?: 0, false) 15 | offscreenPageLimit = 2 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_FILM_ARRAY 6 | import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_CALENDAR_INDEX 7 | import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_DATE_MODEL_INDEX 8 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class PlayFilmActivityViewModel @Inject constructor( 14 | savedStateHandle: SavedStateHandle 15 | ) : ViewModel() { 16 | val dateModelIndex = savedStateHandle.get(KEY_DATE_MODEL_INDEX) 17 | val calendarIndex = savedStateHandle.get(KEY_CALENDAR_INDEX) 18 | val filmArray = savedStateHandle.get>(KEY_FILM_ARRAY) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmBottomSheetBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm 2 | 3 | import android.widget.ImageView 4 | import androidx.annotation.DrawableRes 5 | import androidx.databinding.BindingAdapter 6 | 7 | @BindingAdapter("setImg") 8 | fun ImageView.setImg( 9 | @DrawableRes id: Int 10 | ) { 11 | setImageResource(id) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmBottomSheetAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.boostcamp.dailyfilm.databinding.ItemBottomSheetBinding 9 | import com.boostcamp.dailyfilm.presentation.playfilm.model.BottomSheetModel 10 | 11 | class PlayFilmBottomSheetAdapter( 12 | val onClick: (Int) -> (Unit) 13 | ) : 14 | ListAdapter( 15 | diffUtil 16 | ) { 17 | 18 | override fun onCreateViewHolder( 19 | parent: ViewGroup, 20 | viewType: Int 21 | ): PlayFilmBottomSheetItemViewHolder { 22 | return PlayFilmBottomSheetItemViewHolder( 23 | ItemBottomSheetBinding.inflate(LayoutInflater.from(parent.context), parent, false) 24 | ) 25 | } 26 | 27 | override fun onBindViewHolder(holder: PlayFilmBottomSheetItemViewHolder, position: Int) { 28 | getItem(position)?.let { 29 | holder.setItem(getItem(position)) 30 | } 31 | } 32 | 33 | inner class PlayFilmBottomSheetItemViewHolder(private val bind: ItemBottomSheetBinding) : 34 | RecyclerView.ViewHolder(bind.root) { 35 | fun setItem(item: BottomSheetModel) { 36 | bind.item = item 37 | bind.root.setOnClickListener { onClick(item.title) } 38 | } 39 | } 40 | 41 | companion object { 42 | val diffUtil = object : DiffUtil.ItemCallback() { 43 | override fun areItemsTheSame( 44 | oldItem: BottomSheetModel, 45 | newItem: BottomSheetModel 46 | ): Boolean { 47 | return oldItem == newItem 48 | } 49 | 50 | override fun areContentsTheSame( 51 | oldItem: BottomSheetModel, 52 | newItem: BottomSheetModel 53 | ): Boolean { 54 | return oldItem == newItem 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmPageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm.adapter 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 6 | import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmFragment 7 | 8 | class PlayFilmPageAdapter( 9 | private val dateList: ArrayList, 10 | fragmentActivity: FragmentActivity 11 | ) : 12 | FragmentStateAdapter(fragmentActivity) { 13 | 14 | override fun getItemCount(): Int = dateList.size 15 | 16 | override fun createFragment(position: Int): PlayFilmFragment { 17 | return PlayFilmFragment.newInstance(dateList[position]) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/model/BottomSheetModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm.model 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | 6 | data class BottomSheetModel( 7 | @DrawableRes val icon: Int, 8 | @StringRes val title: Int 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/model/EditState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm.model 2 | 3 | enum class EditState { 4 | NEW_UPLOAD, RE_UPLOAD, EDIT_CONTENT 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/model/SpeedState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.playfilm.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | enum class SpeedState(val speed: Float) : Parcelable { 8 | NORMAL(1f), 9 | FAST_1_5(1.5f), 10 | FAST_2(2f), 11 | FAST_3(3f); 12 | 13 | override fun toString(): String { 14 | return when (this) { 15 | NORMAL -> "x1.0" 16 | FAST_1_5 -> "x1.5" 17 | FAST_2 -> "x2.0" 18 | FAST_3 -> "x3.0" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.searchfilm 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.TextView 5 | import androidx.databinding.BindingAdapter 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.findViewTreeLifecycleOwner 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.lifecycle.repeatOnLifecycle 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 12 | import com.boostcamp.dailyfilm.presentation.searchfilm.adapter.SearchFilmAdapter 13 | import kotlinx.coroutines.launch 14 | 15 | @BindingAdapter("itemList", "viewModel", requireAll = true) 16 | fun RecyclerView.updateAdapter(itemList: List, viewModel: SearchFilmViewModel) { 17 | if (adapter == null) { 18 | adapter = SearchFilmAdapter { index -> 19 | viewModel.onClickItem(index) 20 | } 21 | } 22 | 23 | findViewTreeLifecycleOwner()?.lifecycleScope?.launch { 24 | findViewTreeLifecycleOwner()?.repeatOnLifecycle(Lifecycle.State.STARTED) { 25 | (adapter as SearchFilmAdapter).submitList(itemList) 26 | } 27 | } 28 | } 29 | 30 | @SuppressLint("SetTextI18n") 31 | @BindingAdapter("startDate", "endDate", requireAll = true) 32 | fun TextView.setSearchRange(startDate: String?, endDate: String?) { 33 | if (startDate != null && endDate != null) { 34 | text = "$startDate ~ $endDate" 35 | } 36 | } 37 | 38 | @SuppressLint("SetTextI18n") 39 | @BindingAdapter("setDate", requireAll = true) 40 | fun TextView.setDate(date: String) { 41 | text = "${date.substring(0, 4)}년 ${date.substring(4, 6)}월 ${date.substring(6)}일" 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.searchfilm 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.boostcamp.dailyfilm.data.calendar.CalendarRepository 6 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 7 | import com.boostcamp.dailyfilm.data.sync.SyncRepository 8 | import com.google.firebase.auth.FirebaseAuth 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.SharedFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.asSharedFlow 15 | import kotlinx.coroutines.flow.asStateFlow 16 | import kotlinx.coroutines.launch 17 | import java.text.SimpleDateFormat 18 | import java.util.* 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class SearchFilmViewModel @Inject constructor( 23 | private val calendarRepository: CalendarRepository, 24 | private val syncRepository: SyncRepository 25 | ) : ViewModel() { 26 | 27 | private val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) 28 | private val dottedDateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()) 29 | var startAt: Long? = null 30 | private set 31 | var endAt: Long? = null 32 | private set 33 | private val userId = FirebaseAuth.getInstance().currentUser?.uid ?: error("Unknown User") 34 | 35 | private val _itemListFlow = MutableStateFlow>(emptyList()) 36 | val itemListFlow: StateFlow> = _itemListFlow.asStateFlow() 37 | 38 | private val _eventFlow = MutableSharedFlow() 39 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 40 | 41 | private val _startDateFlow = MutableStateFlow(null) 42 | val startDateFlow: StateFlow = _startDateFlow.asStateFlow() 43 | 44 | private val _endDateFlow = MutableStateFlow(null) 45 | val endDateFlow: StateFlow = _endDateFlow.asStateFlow() 46 | 47 | fun searchDateRange(startAt: Long, endAt: Long) { 48 | this.startAt = startAt 49 | this.endAt = endAt 50 | 51 | viewModelScope.launch { 52 | _startDateFlow.tryEmit(dottedDateFormat.format(startAt)) 53 | _endDateFlow.tryEmit(dottedDateFormat.format(endAt)) 54 | 55 | val start = dateFormat.format(startAt) 56 | val end = dateFormat.format(endAt) 57 | val startYear = start.substring(0, 4).toInt() 58 | val endYear = end.substring(0, 4).toInt() 59 | 60 | for (year in startYear..endYear) { 61 | if (syncRepository.isSynced(year).not()) { 62 | val startDate = "${year}0101" 63 | val endDate = "${year}1231" 64 | syncRepository.addSyncedYear(year) 65 | syncRepository.startSync(userId, startDate, endDate) 66 | } 67 | } 68 | 69 | _itemListFlow.emit(calendarRepository.loadFilm(start, end)) 70 | } 71 | } 72 | 73 | fun searchKeyword(query: String) { 74 | viewModelScope.launch { 75 | if (startAt != null && endAt != null) { 76 | _itemListFlow.emit( 77 | calendarRepository.loadFilm(dateFormat.format(startAt), dateFormat.format(endAt)) 78 | .filter { it?.text?.contains(query) ?: false } 79 | ) 80 | } 81 | } 82 | } 83 | 84 | fun onClickItem(index: Int) { 85 | event(SearchEvent.ItemClickEvent(index)) 86 | } 87 | 88 | private fun event(event: SearchEvent) { 89 | viewModelScope.launch { 90 | _eventFlow.emit(event) 91 | } 92 | } 93 | } 94 | 95 | sealed class SearchEvent { 96 | data class ItemClickEvent(val index: Int) : SearchEvent() 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/adapter/SearchFilmAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.searchfilm.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.boostcamp.dailyfilm.R 10 | import com.boostcamp.dailyfilm.data.model.DailyFilmItem 11 | import com.boostcamp.dailyfilm.databinding.ItemSearchResultBinding 12 | import com.bumptech.glide.Glide 13 | 14 | class SearchFilmAdapter(private val onClick: (Int) -> Unit) : ListAdapter(diffUtil) { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchFilmViewHolder { 17 | return SearchFilmViewHolder( 18 | DataBindingUtil.inflate( 19 | LayoutInflater.from(parent.context), 20 | R.layout.item_search_result, 21 | parent, 22 | false 23 | ) 24 | ) 25 | } 26 | 27 | override fun onBindViewHolder(holder: SearchFilmViewHolder, position: Int) { 28 | holder.bind(getItem(position)) 29 | } 30 | 31 | inner class SearchFilmViewHolder(private val binding: ItemSearchResultBinding) : RecyclerView.ViewHolder(binding.root) { 32 | init { 33 | itemView.setOnClickListener { 34 | onClick(absoluteAdapterPosition) 35 | } 36 | } 37 | 38 | fun bind(item: DailyFilmItem) { 39 | binding.item = item 40 | binding.requestManager = Glide.with(itemView) 41 | } 42 | } 43 | 44 | companion object { 45 | private val diffUtil = object : DiffUtil.ItemCallback() { 46 | override fun areItemsTheSame(oldItem: DailyFilmItem, newItem: DailyFilmItem): Boolean { 47 | return oldItem == newItem 48 | } 49 | 50 | override fun areContentsTheSame(oldItem: DailyFilmItem, newItem: DailyFilmItem): Boolean { 51 | return oldItem.videoUrl == newItem.videoUrl 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.PagingData 7 | import androidx.paging.cachedIn 8 | import com.boostcamp.dailyfilm.data.model.VideoItem 9 | import com.boostcamp.dailyfilm.data.selectvideo.GalleryVideoRepository 10 | import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity 11 | import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_EDIT_STATE 12 | import com.boostcamp.dailyfilm.presentation.calendar.DateFragment 13 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 14 | import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState 15 | import com.boostcamp.dailyfilm.presentation.selectvideo.adapter.VideoSelectListener 16 | import com.boostcamp.dailyfilm.presentation.uploadfilm.model.DateAndVideoModel 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.CoroutineScope 19 | import kotlinx.coroutines.flow.* 20 | import kotlinx.coroutines.launch 21 | import javax.inject.Inject 22 | 23 | @HiltViewModel 24 | class SelectVideoViewModel @Inject constructor( 25 | private val selectVideoRepository: GalleryVideoRepository, 26 | savedStateHandle: SavedStateHandle 27 | ) : ViewModel(), VideoSelectListener { 28 | 29 | val dateModel = savedStateHandle.get(CalendarActivity.KEY_DATE_MODEL) 30 | val calendarIndex = savedStateHandle.get(DateFragment.KEY_CALENDAR_INDEX) 31 | val editState = savedStateHandle.get(KEY_EDIT_STATE) 32 | 33 | override val viewTreeLifecycleScope: CoroutineScope 34 | get() = viewModelScope 35 | 36 | private val _videosState = MutableStateFlow>(PagingData.empty()) 37 | val videosState: StateFlow> get() = _videosState 38 | 39 | private val _selectedVideo = MutableStateFlow(null) 40 | override val selectedVideo = _selectedVideo.asStateFlow() 41 | 42 | private var clickSound = false 43 | 44 | private val _eventFlow = MutableSharedFlow() 45 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 46 | 47 | fun navigateToUpload() { 48 | viewModelScope.launch { 49 | selectedVideo.value?.let { selectedVideoItem -> 50 | if (dateModel != null) { 51 | event( 52 | SelectVideoEvent.NextButtonResult( 53 | DateAndVideoModel( 54 | selectedVideoItem.uri, 55 | dateModel.getDate() 56 | ) 57 | ) 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | 64 | fun controlSound() { 65 | clickSound = !clickSound 66 | event(SelectVideoEvent.ControlSoundResult(clickSound)) 67 | } 68 | 69 | fun backToMain() { 70 | event(SelectVideoEvent.BackButtonResult(true)) 71 | } 72 | 73 | fun loadVideo() { 74 | selectVideoRepository.loadVideo().cachedIn(viewModelScope).onEach { pagingData -> 75 | _videosState.value = pagingData 76 | }.launchIn(viewModelScope) 77 | } 78 | 79 | private fun event(event: SelectVideoEvent) { 80 | viewModelScope.launch { 81 | _eventFlow.emit(event) 82 | } 83 | } 84 | 85 | override fun chooseVideo(videoItem: VideoItem?) { 86 | viewModelScope.launch { 87 | _selectedVideo.emit(videoItem) 88 | } 89 | } 90 | 91 | } 92 | 93 | sealed class SelectVideoEvent { 94 | data class NextButtonResult(val dateAndVideoModelItem: DateAndVideoModel) : SelectVideoEvent() 95 | data class BackButtonResult(val result: Boolean) : SelectVideoEvent() 96 | data class ControlSoundResult(val result: Boolean) : SelectVideoEvent() 97 | } 98 | 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.paging.PagingDataAdapter 8 | import com.boostcamp.dailyfilm.R 9 | import com.boostcamp.dailyfilm.data.model.VideoItem 10 | 11 | class SelectVideoAdapter( 12 | private val videoSelectListener: VideoSelectListener 13 | ) : PagingDataAdapter(diffUtil) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectVideoViewHolder { 16 | return SelectVideoViewHolder( 17 | DataBindingUtil.inflate( 18 | LayoutInflater.from(parent.context), 19 | R.layout.item_select_video, 20 | parent, 21 | false 22 | ), 23 | videoSelectListener 24 | ) 25 | } 26 | 27 | override fun onBindViewHolder(holder: SelectVideoViewHolder, position: Int) { 28 | holder.bind(getItem(position) ?: return) 29 | } 30 | 31 | companion object { 32 | 33 | val diffUtil = object : DiffUtil.ItemCallback() { 34 | override fun areItemsTheSame(oldItem: VideoItem, newItem: VideoItem): Boolean { 35 | return oldItem.uri == newItem.uri 36 | } 37 | 38 | override fun areContentsTheSame(oldItem: VideoItem, newItem: VideoItem): Boolean { 39 | return oldItem == newItem 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo.adapter 2 | 3 | import androidx.core.view.doOnAttach 4 | import androidx.core.view.doOnDetach 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.findViewTreeLifecycleOwner 7 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 8 | import com.boostcamp.dailyfilm.databinding.ItemSelectVideoBinding 9 | import com.boostcamp.dailyfilm.data.model.VideoItem 10 | 11 | class SelectVideoViewHolder( 12 | private val binding:ItemSelectVideoBinding, 13 | private val videoSelectListener: VideoSelectListener 14 | ):ViewHolder(binding.root) { 15 | 16 | private var lifecycleOwner: LifecycleOwner? = null 17 | 18 | init { 19 | 20 | itemView.doOnAttach { 21 | lifecycleOwner = itemView.findViewTreeLifecycleOwner() 22 | } 23 | 24 | itemView.doOnDetach { 25 | lifecycleOwner = null 26 | binding.item = null 27 | binding.clickListener = null 28 | } 29 | 30 | } 31 | 32 | fun bind(item:VideoItem){ 33 | binding.item = item 34 | binding.lifecycleOwner = lifecycleOwner 35 | binding.clickListener = videoSelectListener 36 | binding.executePendingBindings() 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.paging.LoadState 5 | import androidx.paging.LoadStateAdapter 6 | 7 | class VideoLoadStateAdapter( 8 | private val retry: () -> Unit 9 | ) : LoadStateAdapter() { 10 | override fun onCreateViewHolder( 11 | parent: ViewGroup, 12 | loadState: LoadState 13 | ) = VideoLoadStateViewHolder(parent, retry) 14 | 15 | override fun onBindViewHolder( 16 | holder: VideoLoadStateViewHolder, 17 | loadState: LoadState 18 | ) = holder.bind(loadState) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import android.widget.Button 6 | import android.widget.ProgressBar 7 | import android.widget.TextView 8 | import androidx.core.view.isVisible 9 | import androidx.paging.LoadState 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.boostcamp.dailyfilm.R 12 | import com.boostcamp.dailyfilm.databinding.ItemVideoLoadStateBinding 13 | 14 | class VideoLoadStateViewHolder( 15 | parent: ViewGroup, 16 | retry: () -> Unit 17 | ) : RecyclerView.ViewHolder( 18 | LayoutInflater.from(parent.context) 19 | .inflate(R.layout.item_video_load_state, parent, false) 20 | ) { 21 | private val binding = ItemVideoLoadStateBinding.bind(itemView) 22 | private val progressBar: ProgressBar = binding.loadStateProgress 23 | private val errorMsg: TextView = binding.loadStateErrorMessage 24 | private val retry: Button = binding.loadStateRetry 25 | .also { 26 | it.setOnClickListener { retry() } 27 | } 28 | 29 | fun bind(loadState: LoadState) { 30 | if (loadState is LoadState.Error) { 31 | errorMsg.text = loadState.error.localizedMessage 32 | } 33 | progressBar.isVisible = loadState is LoadState.Loading 34 | retry.isVisible = loadState is LoadState.Error 35 | errorMsg.isVisible = loadState is LoadState.Error 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoSelectListener.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.selectvideo.adapter 2 | 3 | import com.boostcamp.dailyfilm.data.model.VideoItem 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.StateFlow 6 | 7 | interface VideoSelectListener { 8 | 9 | val selectedVideo: StateFlow 10 | 11 | val viewTreeLifecycleScope: CoroutineScope 12 | 13 | fun chooseVideo(videoItem: VideoItem?) 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.settings 2 | 3 | import android.content.Intent 4 | import androidx.activity.viewModels 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.lifecycleScope 7 | import androidx.lifecycle.repeatOnLifecycle 8 | import com.boostcamp.dailyfilm.R 9 | import com.boostcamp.dailyfilm.databinding.ActivitySettingsBinding 10 | import com.boostcamp.dailyfilm.presentation.BaseActivity 11 | import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity 12 | import com.boostcamp.dailyfilm.presentation.login.LoginActivity 13 | import com.google.android.gms.auth.api.signin.GoogleSignIn 14 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 15 | import com.google.firebase.auth.FirebaseAuth 16 | import dagger.hilt.android.AndroidEntryPoint 17 | import kotlinx.coroutines.launch 18 | 19 | @AndroidEntryPoint 20 | class SettingsActivity : BaseActivity(R.layout.activity_settings) { 21 | 22 | private val viewModel: SettingsViewModel by viewModels() 23 | override fun initView() { 24 | initViewModel() 25 | collectFlow() 26 | initListener() 27 | } 28 | 29 | private fun initListener() { 30 | binding.toolbar.setNavigationOnClickListener { 31 | viewModel.backToPrevious() 32 | } 33 | } 34 | 35 | private fun initViewModel() { 36 | binding.viewModel = viewModel 37 | } 38 | 39 | private fun collectFlow() { 40 | lifecycleScope.launch { 41 | repeatOnLifecycle(Lifecycle.State.STARTED) { 42 | launch { 43 | viewModel.settingsEventFlow.collect { event -> 44 | when (event) { 45 | is SettingsEvent.Logout, SettingsEvent.DeleteUser -> logout() 46 | is SettingsEvent.Back -> backToPrevious() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | private fun logout() { 55 | FirebaseAuth.getInstance().signOut() 56 | GoogleSignIn.getClient( 57 | this, GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 58 | .requestEmail() 59 | .build() 60 | ).signOut().addOnCompleteListener { 61 | navigateToLogin() 62 | } 63 | } 64 | 65 | private fun navigateToLogin() { 66 | startActivity( 67 | Intent( 68 | this, 69 | LoginActivity::class.java 70 | ).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) }) 71 | } 72 | 73 | private fun backToPrevious() { 74 | finish() 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.settings 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.boostcamp.dailyfilm.data.model.Result 7 | import com.boostcamp.dailyfilm.data.settings.SettingsRepository 8 | import com.boostcamp.dailyfilm.data.sync.SyncRepository 9 | import com.google.firebase.auth.FirebaseAuth 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.MutableSharedFlow 12 | import kotlinx.coroutines.flow.SharedFlow 13 | import kotlinx.coroutines.flow.asSharedFlow 14 | import kotlinx.coroutines.flow.collectLatest 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class SettingsViewModel @Inject constructor( 20 | savedStateHandle: SavedStateHandle, 21 | private val settingsRepository: SettingsRepository, 22 | private val syncRepository: SyncRepository 23 | ) : ViewModel() { 24 | 25 | private val _settingsEventFlow = MutableSharedFlow() 26 | val settingsEventFlow: SharedFlow = _settingsEventFlow.asSharedFlow() 27 | 28 | fun backToPrevious() = event(SettingsEvent.Back) 29 | 30 | fun logout() { 31 | event(SettingsEvent.Logout) 32 | 33 | viewModelScope.launch { 34 | settingsRepository.deleteAllData().collectLatest { result -> 35 | when (result) { 36 | is Result.Success -> { 37 | syncRepository.clearSyncedYear() 38 | event(SettingsEvent.Logout) 39 | } 40 | else -> {} 41 | } 42 | } 43 | } 44 | } 45 | 46 | fun deleteUser() { 47 | FirebaseAuth.getInstance().currentUser?.delete()?.addOnCompleteListener { task -> 48 | if (task.isSuccessful) { 49 | viewModelScope.launch { 50 | settingsRepository.deleteAllData().collectLatest { result -> 51 | when (result) { 52 | is Result.Success -> { 53 | syncRepository.clearSyncedYear() 54 | event(SettingsEvent.DeleteUser) 55 | } 56 | else -> {} 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | private fun event(settingsEvent: SettingsEvent) = 65 | viewModelScope.launch { _settingsEventFlow.emit(settingsEvent) } 66 | } 67 | 68 | sealed class SettingsEvent { 69 | object Back : SettingsEvent() 70 | object Logout : SettingsEvent() 71 | object DeleteUser : SettingsEvent() 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmActivity.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.totalfilm 2 | 3 | import androidx.activity.viewModels 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.lifecycle.repeatOnLifecycle 7 | import com.boostcamp.dailyfilm.R 8 | import com.boostcamp.dailyfilm.databinding.ActivityTotalFilmBinding 9 | import com.boostcamp.dailyfilm.presentation.BaseActivity 10 | import dagger.hilt.android.AndroidEntryPoint 11 | import kotlinx.coroutines.flow.collectLatest 12 | import kotlinx.coroutines.launch 13 | 14 | @AndroidEntryPoint 15 | class TotalFilmActivity : BaseActivity(R.layout.activity_total_film) { 16 | 17 | private val viewModel: TotalFilmViewModel by viewModels() 18 | 19 | override fun initView() { 20 | binding.viewModel = viewModel 21 | setObserveVideoEnded() 22 | } 23 | 24 | private fun setObserveVideoEnded() { 25 | lifecycleScope.launch { 26 | repeatOnLifecycle(Lifecycle.State.STARTED) { 27 | viewModel.isEnded.collectLatest { isEnded -> 28 | if (isEnded) { 29 | finish() 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | override fun onResume() { 37 | super.onResume() 38 | binding.backgroundPlayer.player?.let { player -> 39 | if (player.isPlaying.not()) { 40 | player.play() 41 | } 42 | } 43 | } 44 | 45 | override fun onStop() { 46 | binding.backgroundPlayer.player?.let { player -> 47 | if (player.isPlaying) { 48 | player.pause() 49 | } 50 | } 51 | super.onStop() 52 | } 53 | 54 | override fun onDestroy() { 55 | binding.backgroundPlayer.player?.release() 56 | binding.backgroundPlayer.player = null 57 | super.onDestroy() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalfilmBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.totalfilm 2 | 3 | import android.animation.ValueAnimator 4 | import android.net.Uri 5 | import androidx.databinding.BindingAdapter 6 | import androidx.lifecycle.* 7 | import com.airbnb.lottie.LottieAnimationView 8 | import com.boostcamp.dailyfilm.presentation.playfilm.model.SpeedState 9 | import com.google.android.exoplayer2.ExoPlayer 10 | import com.google.android.exoplayer2.MediaItem 11 | import com.google.android.exoplayer2.Player 12 | import com.google.android.exoplayer2.ui.StyledPlayerView 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.launch 15 | 16 | @BindingAdapter("setViewModel") 17 | fun StyledPlayerView.playTotalVideo(viewModel: TotalFilmViewModel) { 18 | if (player == null) { 19 | player = ExoPlayer.Builder(context).build().apply { 20 | volume = 0.5f 21 | setPlaybackSpeed(viewModel.isSpeed.value?.speed ?: SpeedState.FAST_2.speed) 22 | } 23 | } 24 | 25 | player?.apply { 26 | viewModel.viewModelScope.launch { 27 | viewModel.downloadedVideoUri.collectLatest { uri -> 28 | uri?.let { 29 | if (uri == Uri.EMPTY) { 30 | return@collectLatest 31 | } else { 32 | addMediaItem(MediaItem.fromUri(it)) 33 | prepare() 34 | } 35 | } 36 | } 37 | } 38 | 39 | playWhenReady = true 40 | 41 | setOnClickListener { 42 | if (isPlaying) { 43 | pause() 44 | } else { 45 | play() 46 | } 47 | } 48 | 49 | addListener(object : Player.Listener { 50 | override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { 51 | super.onMediaItemTransition(mediaItem, reason) 52 | player?.let { player -> 53 | viewModel.filmArray?.get(player.currentMediaItemIndex)?.let { model -> 54 | viewModel.setCurrentDateItem(model) 55 | } 56 | } 57 | } 58 | 59 | override fun onPlaybackStateChanged(playbackState: Int) { 60 | if (playbackState == ExoPlayer.STATE_ENDED) { 61 | viewModel.changeEndState() 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | 68 | @BindingAdapter("changeVolume") 69 | fun StyledPlayerView.changeVolume(isMuted: Boolean) { 70 | player?.volume = 71 | when (isMuted) { 72 | true -> { 73 | 0.0f 74 | } 75 | false -> { 76 | 0.5f 77 | } 78 | } 79 | } 80 | 81 | @BindingAdapter("changeSpeed") 82 | fun StyledPlayerView.changeSpeed(speed: SpeedState) { 83 | player?.setPlaybackSpeed(speed.speed) 84 | } 85 | 86 | @BindingAdapter("syncMuteIcon") 87 | fun LottieAnimationView.syncMuteIcon(isMuted: Boolean) { 88 | val animator: ValueAnimator = 89 | when (isMuted) { 90 | true -> { 91 | ValueAnimator.ofFloat(0.0f, 0.5f).apply { 92 | duration = 500 93 | } 94 | } 95 | false -> { 96 | ValueAnimator.ofFloat(0.5f, 1.0f).apply { 97 | duration = 500 98 | } 99 | } 100 | }.apply { 101 | addUpdateListener { 102 | progress = it.animatedValue as Float 103 | } 104 | }.also { 105 | it.start() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.uploadfilm 2 | 3 | import android.animation.ValueAnimator 4 | import android.net.Uri 5 | import android.widget.EditText 6 | import androidx.databinding.BindingAdapter 7 | import com.airbnb.lottie.LottieAnimationView 8 | import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState 9 | import com.google.android.exoplayer2.ExoPlayer 10 | import com.google.android.exoplayer2.MediaItem 11 | import com.google.android.exoplayer2.Player 12 | import com.google.android.exoplayer2.ui.StyledPlayerView 13 | import kotlinx.coroutines.* 14 | 15 | @BindingAdapter(value = ["updateAnimation", "inputText", "showKeyboard"], requireAll = false) 16 | fun LottieAnimationView.updateAnimation( 17 | isWriting: Boolean, 18 | text: EditText, 19 | activity: UploadFilmActivity 20 | ) { 21 | lateinit var animator: ValueAnimator 22 | 23 | when (isWriting) { 24 | true -> { 25 | animator = ValueAnimator.ofFloat(0.0f, 0.5f).apply { 26 | duration = 500 27 | } 28 | text.requestFocus() 29 | activity.showKeyboard() 30 | } 31 | false -> { 32 | animator = ValueAnimator.ofFloat(0.5f, 1.0f).apply { 33 | duration = 500 34 | } 35 | text.clearFocus() 36 | activity.hideKeyboard() 37 | } 38 | } 39 | 40 | animator.addUpdateListener { 41 | progress = it.animatedValue as Float 42 | } 43 | animator.start() 44 | } 45 | 46 | @BindingAdapter(value = ["originVideo", "resultVideo", "videoStartTime", "editState"], requireAll = false) 47 | fun StyledPlayerView.playVideoAt(origin: Uri?, result: Uri?, startTime: Long, editState: EditState) { 48 | if (player == null) { 49 | player = ExoPlayer.Builder(context).build().apply { 50 | volume = 0.5f 51 | repeatMode = Player.REPEAT_MODE_ONE 52 | } 53 | } 54 | 55 | lateinit var mediaItem: MediaItem 56 | when(editState){ 57 | EditState.EDIT_CONTENT -> { // 내용 수정 모드 -> 로컬 저장 비디오로 미리보기 58 | result?.let { 59 | mediaItem = MediaItem.fromUri(it) 60 | } 61 | } 62 | EditState.NEW_UPLOAD, EditState.RE_UPLOAD -> { // 업로드 혹은 재업로드 -> 원본 영상 + 구간 데이터로 미리보기 63 | origin?.let { 64 | mediaItem = MediaItem.fromUri(it) 65 | 66 | CoroutineScope(Dispatchers.Main).launch { 67 | while (true){ 68 | player?.seekTo(startTime) // 다시 시작지점으로 69 | delay(10000) // 10초 만큼 진행하고 70 | if (player == null) // 메모리 누수 방지 71 | break 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | player?.setMediaItem(mediaItem) 79 | player?.prepare() 80 | player?.play() 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/model/DateAndVideoModel.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.uploadfilm.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Parcelize 9 | data class DateAndVideoModel( 10 | val uri: Uri, 11 | val uploadDate: String 12 | ) : Parcelable { 13 | fun getDateModel() = DateModel( 14 | uploadDate.substring(0, 4), 15 | uploadDate.substring(4, 6), 16 | uploadDate.substring(6, 8) 17 | ) 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/CalendarUtil.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | private val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) 7 | 8 | // get 9 | fun Calendar.month() = this.get(Calendar.MONTH) + 1 10 | fun Calendar.day() = this.get(Calendar.DAY_OF_MONTH) 11 | fun Calendar.year() = this.get(Calendar.YEAR) 12 | fun Calendar.maximum() = this.getActualMaximum(Calendar.DAY_OF_MONTH) 13 | fun Calendar.dayOfWeek() = this.get(Calendar.DAY_OF_WEEK) 14 | 15 | // add 16 | fun Calendar.addDay(amount: Int) = this.apply { add(Calendar.DAY_OF_MONTH, amount) } 17 | fun Calendar.addMonth(amount: Int) = this.apply { add(Calendar.MONTH, amount) } 18 | 19 | // set 20 | fun Calendar.setYear(year: Int) = this.apply { set(Calendar.YEAR, year) } 21 | fun Calendar.setMonth(month: Int) = this.apply { set(Calendar.MONTH, month - 1) } 22 | fun Calendar.setDay(day: Int) = this.apply { set(Calendar.DAY_OF_MONTH, day) } 23 | fun Calendar.setDate(year: Int = this.year(), month: Int = this.month(), day: Int = this.day()) = 24 | this.apply { 25 | setYear(year) 26 | setMonth(month) 27 | setDay(day) 28 | } 29 | 30 | // create 31 | fun createCalendar(): Calendar = Calendar.getInstance() 32 | fun createCalendar(locale: Locale): Calendar = Calendar.getInstance(locale) 33 | fun createCalendar(time: Date) = createCalendar().apply { this.time = time } 34 | fun createCalendar(year: Int, month: Int, day: Int) = createCalendar().apply { 35 | setDate(year, month, day) 36 | } 37 | 38 | fun createCalendar( 39 | calendar: Calendar, 40 | year: Int = calendar.year(), 41 | month: Int = calendar.month(), 42 | day: Int = calendar.day() 43 | ): Calendar = createCalendar().apply { 44 | timeInMillis = calendar.timeInMillis 45 | setDate(year, month, day) 46 | } 47 | 48 | // DateFormat 49 | fun getStartAt(startCalendar: Calendar): String = dateFormat.format(startCalendar.time) 50 | fun getEndAt(currentMonth: Int, startCalendar: Calendar): String = with(startCalendar) { 51 | addDay(34) 52 | if (currentMonth != month()) { 53 | if (currentMonth == month() && maximum() != day()) { 54 | addDay(7) 55 | } 56 | } 57 | dateFormat.format(time) 58 | } 59 | 60 | fun getStartCalendar( 61 | prevCalendar: Calendar, 62 | prevMaxDay: Int, 63 | dayOfWeek: Int 64 | ): Calendar { 65 | return createCalendar().apply { 66 | timeInMillis = prevCalendar.timeInMillis 67 | val day = if (dayOfWeek == 1) { 68 | addMonth(1) 69 | 1 70 | } else { 71 | prevMaxDay - (dayOfWeek - 2) 72 | } 73 | setDay(day) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/Event.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util 2 | 3 | open class Event(private val content: T) { 4 | 5 | var hasBeenHandled = false 6 | private set 7 | 8 | fun getContentIfNotHandled(): T? { 9 | return if (hasBeenHandled) { 10 | null 11 | } else { 12 | hasBeenHandled = true 13 | content 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/LottieDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.ColorDrawable 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.DialogFragment 10 | import androidx.fragment.app.FragmentManager 11 | import com.boostcamp.dailyfilm.R 12 | 13 | 14 | class LottieDialogFragment : DialogFragment() { 15 | 16 | override fun onCreateView( 17 | inflater: LayoutInflater, 18 | container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ): View { 21 | return inflater.inflate(R.layout.dialog_lottie, container, false) 22 | } 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | super.onViewCreated(view, savedInstanceState) 26 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 27 | dialog?.setCanceledOnTouchOutside(false) 28 | setStyle(STYLE_NO_FRAME, R.style.Theme_DailyFilm) 29 | } 30 | 31 | fun showProgressDialog(sp : FragmentManager) { 32 | if (this.isAdded.not()) { 33 | this.show(sp, "loader") 34 | } 35 | } 36 | 37 | fun hideProgressDialog() { 38 | if (this.isAdded) { 39 | this.dismissAllowingStateLoss() 40 | } 41 | } 42 | companion object { 43 | 44 | fun newInstance(): LottieDialogFragment { 45 | val args = Bundle() 46 | val fragment = LottieDialogFragment() 47 | fragment.arguments = args 48 | fragment.isCancelable=false 49 | return fragment 50 | 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/PlayState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util 2 | 3 | import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel 4 | 5 | sealed class PlayState { 6 | object Uninitialized : PlayState() 7 | object Loading : PlayState() 8 | object Playing : PlayState() 9 | data class Deleted(val dateModel: DateModel) : PlayState() 10 | data class Failure(val throwable: Throwable) : PlayState() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/UiState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util 2 | 3 | 4 | sealed class UiState { 5 | object Uninitialized : UiState() 6 | object Loading : UiState() 7 | data class Success(val item: T) : UiState() 8 | data class Failure(val throwable: Throwable) : UiState() 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/bindingadapter/ImageView.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util.bindingadapter 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import com.boostcamp.dailyfilm.R 6 | import com.bumptech.glide.RequestManager 7 | import com.bumptech.glide.load.resource.bitmap.CenterCrop 8 | import com.bumptech.glide.load.resource.bitmap.RoundedCorners 9 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 10 | import com.bumptech.glide.request.transition.DrawableCrossFadeFactory 11 | 12 | private val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build() 13 | 14 | @BindingAdapter(value = ["glide", "loadUrl", "cornerRadius"], requireAll = false) 15 | fun ImageView.loadImage(glide: RequestManager, imageUrl: String?, cornerRadius: Int? = null) { 16 | imageUrl ?: return 17 | glide.load(imageUrl) 18 | .transition(DrawableTransitionOptions.withCrossFade(factory)) 19 | .placeholder(R.color.gray) 20 | .let { builder -> 21 | if (cornerRadius != null) { 22 | builder.transform(CenterCrop(), RoundedCorners(cornerRadius)) 23 | } else { 24 | builder.transform(CenterCrop()) 25 | } 26 | } 27 | .into(this) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/network/NetworkAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util.network 2 | 3 | import android.content.res.Resources 4 | import com.boostcamp.dailyfilm.R 5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 6 | 7 | fun MaterialAlertDialogBuilder.networkAlertDialog(resources: Resources) = 8 | MaterialAlertDialogBuilder(context) 9 | .setTitle(resources.getString(R.string.connect_network)) 10 | .setNegativeButton(resources.getString(R.string.yes)) { dialog, _ -> 11 | dialog.dismiss() 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/network/NetworkManager.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util.network 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.ConnectivityManager.NetworkCallback 6 | import android.net.Network 7 | import android.net.NetworkCapabilities 8 | import android.net.NetworkRequest 9 | 10 | object NetworkManager { 11 | private lateinit var connectivityManager: ConnectivityManager 12 | private lateinit var networkRequest: NetworkRequest 13 | private lateinit var network: Network 14 | lateinit var actNetwork: NetworkCapabilities 15 | 16 | fun initNetwork(context: Context?) { 17 | context ?: return 18 | connectivityManager = context.getSystemService(ConnectivityManager::class.java) 19 | networkRequest = NetworkRequest.Builder() 20 | .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 21 | .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) 22 | .build() 23 | } 24 | 25 | fun checkNetwork(): NetworkState { 26 | network = connectivityManager.activeNetwork ?: return NetworkState.LOST 27 | actNetwork = connectivityManager.getNetworkCapabilities(network) ?: return NetworkState.LOST 28 | 29 | return actNetwork.let { network -> 30 | when { 31 | network.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkState.AVAILABLE 32 | network.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkState.AVAILABLE 33 | else -> NetworkState.LOST 34 | } 35 | } 36 | } 37 | 38 | fun registerNetworkCallback(networkCallback: NetworkCallback) { 39 | connectivityManager.registerNetworkCallback(networkRequest, networkCallback) 40 | } 41 | 42 | fun terminateNetworkCallback(networkCallback: NetworkCallback) { 43 | connectivityManager.unregisterNetworkCallback(networkCallback) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/boostcamp/dailyfilm/presentation/util/network/NetworkState.kt: -------------------------------------------------------------------------------- 1 | package com.boostcamp.dailyfilm.presentation.util.network 2 | 3 | enum class NetworkState(val value: Boolean) { 4 | AVAILABLE(true), LOST(false) 5 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_camera_open.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_gallery_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_gallery_open.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/anim/camera_gallery_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_datepicker_month.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_double_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_double_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_drawer_menu.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_play_circle.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_done.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_photo_camera_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_picture_in_picture_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_focused_date.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_solid.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/div_calendar_week.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_primary.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_keyboard_backspace_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_datepicker_month.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_double_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_double_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drawer_menu.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_text.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fast.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_re_upload.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_button_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_button_ripple_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_gradient_36.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_gradient_36_2.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pb_custom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 25 | 26 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_play_film.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search_film.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 26 | 27 | 36 | 37 | 38 | 39 | 57 | 58 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 32 | 33 | 48 | 49 | 59 | 60 | 72 | 73 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_trim_viedo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_datepicker.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 29 | 30 |