├── .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 |
41 |
42 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_lottie.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_date.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
13 |
14 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
16 |
17 |
26 |
27 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_date.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
23 |
24 |
37 |
38 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_search_result.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
24 |
25 |
29 |
30 |
43 |
44 |
56 |
57 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_select_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
24 |
25 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_video_load_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
26 |
27 |
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_calendar_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/img_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-mdpi/img_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-night/img_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-night/img_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Daily Film
3 | sun
4 | mon
5 | tus
6 | wed
7 | thu
8 | fri
9 | sat
10 | next
11 | complete
12 | Cancel
13 | accept
14 | %sY %sM %sD
15 | date
16 | contents
17 | before
18 | close
19 | Guide to Required Permissions
20 | Permissions are required for the following reasons.\n\n1. Video access rights \n -Required for video registration.
21 | request again
22 | change settings
23 | Please select a video
24 | To use the app smoothly, please set permission permission.
25 | retry
26 | Logout
27 | Delete Account
28 | delete
29 | reUpload
30 | Edit Text
31 | Are you sure you want to delete it?
32 | yes
33 | no
34 | Minimum recording time is 10 seconds
35 | Please select a date
36 | Video does not exist
37 | Your current device does not have a camera function
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Daily Film
3 | 日
4 | 月
5 | 火
6 | 水
7 | 木
8 | 金
9 | 土
10 | 次
11 | 完了
12 | キャンセル
13 | 受け入れる
14 | %s年 %s月 %s日
15 | 日にち
16 | コンテンツ
17 | 前
18 | 近い
19 | 必要なアクセス許可のガイド
20 |
21 | 次の理由により、アクセス許可が必要です。\n\n1.ビデオ アクセス権 \n - ビデオの登録に必要です。\n -Required for video registration.
22 | 再度リクエストする
23 | 設定を変更する
24 | 動画を選択してください
25 | アプリをスムーズにご利用いただくために、パーミッションパーミッションの設定をお願いいたします。
26 | リトライ
27 | ログアウト
28 | アカウントを削除する
29 | 消去
30 | 再アップロード
31 | テキストの編集
32 | 削除してもよろしいですか?
33 | はい
34 | いいえ
35 | 最短録音時間は10秒
36 | 日付を選択してください
37 | ビデオが存在しません
38 | 現在お使いのデバイスにはカメラ機能がありません。
39 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #4D000000
5 | #FFFFFFFF
6 | #FF0000
7 | #0022FF
8 | #CFCFCF
9 | #37363B
10 |
11 | #FFFFFF
12 | #202022
13 | #03DAC6
14 | #018786
15 | #202022
16 | #121212
17 | #CF6679
18 | #FFFFFF
19 | #FFFFFF
20 | #FFFFFF
21 | #FFFFFF
22 | #000000
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Daily Film
3 | 日
4 | 月
5 | 火
6 | 水
7 | 木
8 | 金
9 | 土
10 | 次
11 | 完成
12 | 取消
13 | 接受
14 | %s年 %s月 %s日
15 | 日期
16 | 内容
17 | 前
18 | 靠近
19 | 所需权限指南
20 | 出于以下原因需要权限: \n\n1.视频访问权限\n - 视频注册需要。 \n - 视频注册需要。
21 | 再次请求
22 | 更改设置
23 | 请选择视频
24 | 为了顺利使用应用,请设置权限权限。
25 | 重试
26 | 登出
27 | 删除帐户
28 | 删除
29 | 重新上传
30 | 编辑文字
31 | 你确定你要删除吗?
32 | 是的
33 | 不
34 | 最短录音时间为 10 秒
35 | 请选择日期
36 | 视频不存在
37 | 您当前的设备没有相机功能
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #4D000000
5 | #FFFFFFFF
6 | #FF0000
7 | #0022FF
8 | #CFCFCF
9 | #E1E1E1
10 | #646464
11 | #BBBBBB
12 |
13 | #202022
14 | #202022
15 | #03DAC6
16 | #018786
17 | #FFFFFF
18 | #FFFFFF
19 | #B00020
20 | #FFFFFF
21 | #000000
22 | #FFFFFF
23 | #000000
24 | #FFFFFF
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2dp
5 | 4dp
6 | 8dp
7 |
8 | 16dp
9 | 20dp
10 | 24dp
11 | 28dp
12 |
13 | 32dp
14 | 40dp
15 | 48dp
16 | 56dp
17 | 64dp
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Daily Film
3 | 일
4 | 월
5 | 화
6 | 수
7 | 목
8 | 금
9 | 토
10 | 다음
11 | 완료
12 | 취소
13 | 완료
14 | %s년 %s월 %s일
15 | 날짜
16 | 내용
17 | 이전
18 | 닫기
19 | 필수권한 안내
20 | 아래와 같은 이유로 권한 허용이 필요합니다.\n\n1.동영상 접근 권한 \n -영상등록을 위하여 필요합니다.
21 | 재요청
22 | 설정변경
23 | 비디오를 선택해주세요.
24 | 앱을 원활히 사용하시려면 권한 허용을 설정 부탁드립니다.
25 | 재시도
26 | 로그아웃
27 | 탈퇴하기
28 | 삭제하기
29 | 재 업로드
30 | 텍스트 편집
31 | 정말로 삭제 하시겠습니까?
32 | 예
33 | 아니오
34 | 최소 촬영시간은 10초입니다.
35 | 날짜를 선택 해주세요
36 | 영상이 존재 하지 않습니다.
37 | 현재 디바이스에 카메라 기능이 없습니다.
38 | 네트워크 연결을 확인해주세요.
39 | 검색
40 | 썸네일
41 | 날짜로 이동
42 | 한 달 전체 재생
43 | 검색 범위 선택
44 | 검색 범위를 설정하세요.
45 | 설정
46 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/boostcamp/dailyfilm/CalendarUtilTest.kt:
--------------------------------------------------------------------------------
1 | package com.boostcamp.dailyfilm
2 |
3 | import com.boostcamp.dailyfilm.presentation.util.*
4 | import junit.framework.Assert.assertEquals
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.junit.MockitoJUnitRunner
9 | import java.util.*
10 |
11 |
12 | @RunWith(MockitoJUnitRunner::class)
13 | class CalendarUtilTest {
14 |
15 | lateinit var calendar: Calendar
16 |
17 | @Before
18 | fun init() {
19 | calendar = Calendar.getInstance().apply {
20 | set(Calendar.YEAR, 2023)
21 | set(Calendar.MONTH, 0)
22 | set(Calendar.DAY_OF_MONTH, 15)
23 | }
24 | }
25 |
26 | @Test
27 | fun getTest() {
28 | // year
29 | assertEquals(calendar.year(), 2023)
30 | // month
31 | assertEquals(calendar.month(), 1)
32 | // day
33 | assertEquals(calendar.day(), 15)
34 | }
35 |
36 | @Test
37 | fun createTest() {
38 | val calendar = createCalendar(calendar, day = 2)
39 |
40 | // month
41 | assertEquals(calendar.month(), 1)
42 | // day
43 | assertEquals(calendar.day(), 2)
44 | }
45 |
46 | @Test
47 | fun getStartCalendarTest() {
48 | val prevMaxDay = 31
49 | val dayOfWeek = 4
50 |
51 | val testCalendar = getStartCalendar(calendar, prevMaxDay, dayOfWeek)
52 | // month
53 | assertEquals(calendar.month(), 1)
54 | // month
55 | assertEquals(testCalendar.month(), 1)
56 | // day
57 | assertEquals(testCalendar.day(), 29)
58 | }
59 |
60 | @Test
61 | fun startEndAtTest() {
62 | val calendar1 = createCalendar(2023, 2, 26)
63 | // startAt
64 | assertEquals(getStartAt(calendar1), "20230226")
65 | // endAt
66 | assertEquals(getEndAt(3, calendar1), "20230401")
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/boostcamp/dailyfilm/DateViewModelUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.boostcamp.dailyfilm
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.boostcamp.dailyfilm.data.calendar.CalendarRepository
5 | import com.boostcamp.dailyfilm.data.sync.SyncRepository
6 | import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_CALENDAR
7 | import com.boostcamp.dailyfilm.presentation.calendar.DateViewModel
8 | import com.boostcamp.dailyfilm.presentation.util.*
9 | import org.junit.Before
10 | import org.junit.runner.RunWith
11 | import org.mockito.Mockito
12 | import org.mockito.junit.MockitoJUnitRunner
13 | import java.text.SimpleDateFormat
14 | import java.util.*
15 |
16 | @RunWith(MockitoJUnitRunner::class)
17 | class DateViewModelUnitTest {
18 |
19 | private lateinit var dateViewModel: DateViewModel
20 | private val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
21 |
22 | @Before
23 | fun init() {
24 | val calendarRepository = Mockito.mock(CalendarRepository::class.java)
25 | val syncRepository = Mockito.mock(SyncRepository::class.java)
26 | val savedStateHandle =
27 | SavedStateHandle().apply {
28 | set(
29 | KEY_CALENDAR,
30 | Calendar.getInstance(Locale.getDefault())
31 | )
32 | }
33 | dateViewModel = DateViewModel(calendarRepository, syncRepository, "", savedStateHandle)
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/boostcamp/dailyfilm/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.boostcamp.dailyfilm
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | dependencies {
7 | def nav_version = "2.5.3"
8 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
9 | classpath 'com.google.gms:google-services:4.3.14'
10 | }
11 | }
12 |
13 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
14 | plugins {
15 | id 'com.android.application' version '7.3.1' apply false
16 | id 'com.android.library' version '7.3.1' apply false
17 | id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
18 | id 'com.google.dagger.hilt.android' version '2.44' apply false
19 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2022/android02-DailyFilm/a1fea5a0ae439af38bf1befff6deb39ce0e0c90e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 08 15:14:55 KST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven { url 'https://jitpack.io' }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url 'https://jitpack.io' }
15 | }
16 | }
17 | rootProject.name = "DailyFilm"
18 | include ':app'
19 |
--------------------------------------------------------------------------------