danger-dangermattic 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile.lock +95 -57
  5. data/lib/dangermattic/gem_version.rb +1 -1
  6. data/lib/dangermattic/plugins/android_unit_test_checker.rb +7 -4
  7. data/lib/dangermattic/plugins/manifest_pr_checker.rb +25 -8
  8. data/lib/dangermattic/plugins/podfile_checker.rb +2 -0
  9. data/lib/dangermattic/plugins/pr_size_checker.rb +10 -12
  10. data/rakelib/console.rake +1 -1
  11. data/rakelib/git_helpers.rake +1 -1
  12. data/spec/android_unit_test_checker_spec.rb +93 -3
  13. data/spec/fixtures/android_unit_test_checker/MixDataPlusNormalClass.kt +14 -0
  14. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/ApiResult.kt +69 -0
  15. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/WebRecordApi.kt +49 -0
  16. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/domain/drive/UiStates.kt +13 -0
  17. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/main/settings/integrations/connect/AppIntegrationModule.kt +29 -0
  18. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/main/streaks/StreaksViewModel.kt +377 -0
  19. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/mediastorage/MediaStorageConfiguration.kt +31 -0
  20. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/utils/AccountType.kt +15 -0
  21. data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/utils/usecase/SelectPhotoUseCase.kt +76 -0
  22. data/spec/pr_size_checker_spec.rb +27 -26
  23. data/spec/spec_helper.rb +8 -2
  24. data/spec/view_changes_checker_spec.rb +15 -15
  25. metadata +11 -2
@@ -0,0 +1,69 @@
1
+ package com.dayoneapp.dayone.api
2
+
3
+ import com.dayoneapp.dayone.api.ApiResult.Failure
4
+ import com.dayoneapp.dayone.api.ApiResult.FailureType.GENERIC
5
+ import com.dayoneapp.dayone.api.ApiResult.FailureType.SERVER_ERROR
6
+ import com.dayoneapp.dayone.api.ApiResult.FailureType.TIMEOUT
7
+ import com.dayoneapp.dayone.api.ApiResult.FailureType.UNAUTHORIZED
8
+ import com.dayoneapp.syncservice.NetworkErrorType
9
+ import com.google.api.client.http.HttpStatusCodes
10
+ import retrofit2.Response
11
+ import java.io.IOException
12
+
13
+ sealed class ApiResult<T> {
14
+ data class Success<T>(
15
+ val data: T,
16
+ ) : ApiResult<T>()
17
+
18
+ class Empty<T> : ApiResult<T>()
19
+
20
+ data class Failure<T>(
21
+ val type: FailureType,
22
+ val errorCode: Int? = null,
23
+ val errorMessage: String? = null,
24
+ ) : ApiResult<T>()
25
+
26
+ enum class FailureType {
27
+ SERVER_ERROR,
28
+ UNAUTHORIZED,
29
+ TIMEOUT,
30
+ GENERIC,
31
+ NOT_CONNECTED,
32
+ ;
33
+
34
+ companion object {
35
+ fun fromNetworkErrorType(networkErrorType: NetworkErrorType): FailureType =
36
+ when (networkErrorType) {
37
+ NetworkErrorType.SERVER_ERROR -> SERVER_ERROR
38
+ NetworkErrorType.UNAUTHORIZED -> UNAUTHORIZED
39
+ NetworkErrorType.TIMEOUT -> TIMEOUT
40
+ NetworkErrorType.GENERIC -> GENERIC
41
+ else -> {
42
+ GENERIC
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ suspend fun <T> apiCall(apiCall: suspend () -> Response<T>): ApiResult<T> =
50
+ try {
51
+ with(apiCall()) {
52
+ if (this.isSuccessful) {
53
+ this.body()?.let {
54
+ ApiResult.Success(it)
55
+ } ?: ApiResult.Empty()
56
+ } else {
57
+ val type =
58
+ when (this.code()) {
59
+ HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, HttpStatusCodes.STATUS_CODE_FORBIDDEN -> UNAUTHORIZED
60
+ HttpStatusCodes.STATUS_CODE_SERVER_ERROR -> SERVER_ERROR
61
+ else -> GENERIC
62
+ }
63
+ Failure(type, this.code(), this.message())
64
+ }
65
+ }
66
+ } catch (e: IOException) {
67
+ Failure(TIMEOUT)
68
+ }
69
+
@@ -0,0 +1,49 @@
1
+ package com.dayoneapp.dayone.api
2
+
3
+ import com.dayoneapp.dayone.domain.sync.WebRecordRemote
4
+ import com.google.gson.annotations.SerializedName
5
+ import retrofit2.Response
6
+ import retrofit2.http.*
7
+
8
+ @JvmInline
9
+ value class CursorTime(
10
+ val time: String,
11
+ )
12
+
13
+ val ZeroCursorTime = CursorTime("0")
14
+
15
+ data class WebRecordChanges(
16
+ @SerializedName("changes")
17
+ val changes: List<WebRecordRemote>,
18
+ @SerializedName("cursor")
19
+ val cursor: String,
20
+ )
21
+
22
+ interface WebRecordApi {
23
+ @POST("$SYNC/named/{name}")
24
+ suspend fun createRecord(
25
+ @Path("name") name: String,
26
+ @Body record: WebRecordRemote,
27
+ ): Response<WebRecordRemote>
28
+
29
+ @PUT("$SYNC/{syncId}")
30
+ suspend fun updateRecord(
31
+ @Path("syncId") syncId: String,
32
+ @Body record: WebRecordRemote,
33
+ ): Response<WebRecordRemote>
34
+
35
+ @GET("$SYNC/named/{name}")
36
+ suspend fun fetchRecord(
37
+ @Path("name") name: String,
38
+ ): Response<WebRecordRemote>
39
+
40
+ @GET("$SYNC/changes/{kind}")
41
+ suspend fun fetchRecordChanges(
42
+ @Path("kind") kind: String,
43
+ @Query("cursor") cursor: CursorTime,
44
+ ): Response<WebRecordChanges>
45
+
46
+ companion object {
47
+ const val SYNC = "/api/v4/sync"
48
+ }
49
+ }
@@ -0,0 +1,13 @@
1
+ package com.dayoneapp.dayone.domain.drive
2
+
3
+ sealed class LoadKeyUiState(
4
+ val buttonEnabled: Boolean,
5
+ ) {
6
+ object Init : LoadKeyUiState(buttonEnabled = true)
7
+
8
+ object Loading : LoadKeyUiState(buttonEnabled = false)
9
+
10
+ object KeyLoaded : LoadKeyUiState(buttonEnabled = false)
11
+
12
+ object KeyNotFound : LoadKeyUiState(buttonEnabled = true)
13
+ }
@@ -0,0 +1,29 @@
1
+ package com.dayoneapp.dayone.main.settings.integrations.connect
2
+
3
+ import com.dayoneapp.dayone.main.settings.integrations.IntegrationAppId
4
+ import com.dayoneapp.dayone.main.settings.integrations.strava.StravaAppIntegrationHandler
5
+ import dagger.Module
6
+ import dagger.Provides
7
+ import dagger.hilt.InstallIn
8
+ import dagger.hilt.components.SingletonComponent
9
+ import javax.inject.Qualifier
10
+ import javax.inject.Singleton
11
+ import kotlin.jvm.JvmWildcard
12
+
13
+ @Qualifier
14
+ @Retention(AnnotationRetention.BINARY)
15
+ annotation class AppIntegrationHandlers
16
+
17
+ @Module
18
+ @InstallIn(SingletonComponent::class)
19
+ internal object AppIntegrationModule {
20
+ @Provides
21
+ @Singleton
22
+ @AppIntegrationHandlers
23
+ fun provideAppIntegrationHandlers(
24
+ stravaAppIntegrationHandler: StravaAppIntegrationHandler,
25
+ ): Map<IntegrationAppId, @JvmWildcard AppIntegrationHandler> =
26
+ mapOf(
27
+ IntegrationAppId.STRAVA to stravaAppIntegrationHandler,
28
+ )
29
+ }
@@ -0,0 +1,377 @@
1
+ package com.dayoneapp.dayone.main.streaks
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import androidx.annotation.ColorRes
6
+ import androidx.annotation.VisibleForTesting
7
+ import androidx.lifecycle.ViewModel
8
+ import androidx.lifecycle.viewModelScope
9
+ import com.dayoneapp.dayone.di.DatabaseThreadDispatcher
10
+ import com.dayoneapp.dayone.domain.JournalRepository
11
+ import com.dayoneapp.dayone.domain.SelectedJournalsProvider
12
+ import com.dayoneapp.dayone.domain.StreakRepository
13
+ import com.dayoneapp.dayone.domain.UserRepository
14
+ import com.dayoneapp.dayone.domain.analytics.AnalyticsTracker
15
+ import com.dayoneapp.dayone.domain.analytics.InitialContent
16
+ import com.dayoneapp.dayone.domain.entry.EntryRepository
17
+ import com.dayoneapp.dayone.main.editor.EditorLauncher
18
+ import com.dayoneapp.dayone.main.editor.MainActivityLauncher
19
+ import com.dayoneapp.dayone.main.navigation.ActivityEventHandler
20
+ import com.dayoneapp.dayone.main.navigation.Navigator
21
+ import com.dayoneapp.dayone.main.sharedjournals.events.OpenJournalOnTimeline
22
+ import com.dayoneapp.dayone.utils.DOStreakCalculator
23
+ import com.dayoneapp.dayone.utils.TimeProvider
24
+ import com.dayoneapp.dayone.utils.UtilsWrapper
25
+ import dagger.hilt.android.lifecycle.HiltViewModel
26
+ import kotlinx.coroutines.CoroutineDispatcher
27
+ import kotlinx.coroutines.flow.MutableStateFlow
28
+ import kotlinx.coroutines.flow.SharingStarted
29
+ import kotlinx.coroutines.flow.asStateFlow
30
+ import kotlinx.coroutines.flow.combine
31
+ import kotlinx.coroutines.flow.distinctUntilChanged
32
+ import kotlinx.coroutines.flow.map
33
+ import kotlinx.coroutines.flow.stateIn
34
+ import kotlinx.coroutines.launch
35
+ import java.io.File
36
+ import java.time.DayOfWeek
37
+ import java.time.LocalDate
38
+ import java.util.Calendar
39
+ import javax.inject.Inject
40
+
41
+ @HiltViewModel
42
+ class StreaksViewModel
43
+ @Inject
44
+ constructor(
45
+ @DatabaseThreadDispatcher private val databaseDispatcher: CoroutineDispatcher,
46
+ private val entryRepository: EntryRepository,
47
+ private val journalRepository: JournalRepository,
48
+ private val streakRepository: StreakRepository,
49
+ private val streaksCalculator: DOStreakCalculator,
50
+ private val timeProvider: TimeProvider,
51
+ private val userRepository: UserRepository,
52
+ private val navigator: Navigator,
53
+ private val selectedJournalsProvider: SelectedJournalsProvider,
54
+ private val utilsWrapper: UtilsWrapper,
55
+ private val analyticsTracker: AnalyticsTracker,
56
+ private val mainActivityLauncher: MainActivityLauncher,
57
+ private val editorLauncher: EditorLauncher,
58
+ private val activityEventHandler: ActivityEventHandler,
59
+ ) : ViewModel() {
60
+ val uiState =
61
+ combine(
62
+ entryRepository.liveEntryCount().distinctUntilChanged(),
63
+ entryRepository.liveEntryCountForDate().distinctUntilChanged(),
64
+ journalRepository
65
+ .getAllJournalsLive(false)
66
+ .map { journals ->
67
+ journals.map { journal -> journal.isHideStreaksEnabled }
68
+ }.distinctUntilChanged(),
69
+ ) { _, _, _ ->
70
+ val streakDays = streaksCalculator.calculateStreakWithSharedJournals()
71
+ val journals = getStreakJournals()
72
+ val streakWeekDays = getStreakWeekDays()
73
+
74
+ UiState.Streaks(
75
+ streakDays = streakDays,
76
+ streakWeekDays = streakWeekDays,
77
+ journals = journals,
78
+ )
79
+ }.stateIn(viewModelScope, SharingStarted.Lazily, UiState.Loading)
80
+
81
+ private val _journalOptionsState = MutableStateFlow<JournalOptionsState?>(null)
82
+ val journalOptionsState = _journalOptionsState.asStateFlow()
83
+
84
+ private val dayOfWeekList: DaysOfWeekList = DaysOfWeekList(timeProvider.getFirstDayOfWeek())
85
+
86
+ fun shareStreaks(
87
+ bitmap: Bitmap,
88
+ context: Context,
89
+ ) {
90
+ viewModelScope.launch(databaseDispatcher) {
91
+ analyticsTracker.trackButtonTapped("streakView_share")
92
+ val streaksFile: File = utilsWrapper.saveBitmapToFile(context, bitmap, "streaks_file")
93
+ utilsWrapper.shareImage(context, streaksFile.absolutePath)
94
+ }
95
+ }
96
+
97
+ fun close() {
98
+ viewModelScope.launch {
99
+ analyticsTracker.trackButtonTapped("streakView_close")
100
+ navigator.goBack()
101
+ }
102
+ }
103
+
104
+ fun showJournalOptions(
105
+ journalId: Int,
106
+ journalName: String,
107
+ ) {
108
+ viewModelScope.launch {
109
+ analyticsTracker.trackButtonTapped("streakView_journal")
110
+ _journalOptionsState.value = JournalOptionsState(journalId, journalName)
111
+ }
112
+ }
113
+
114
+ fun openJournal(journalId: Int) {
115
+ viewModelScope.launch {
116
+ _journalOptionsState.value = null
117
+
118
+ analyticsTracker.trackButtonTapped("streakView_journalMenu_openJournal")
119
+
120
+ activityEventHandler.sendEvent(
121
+ OpenJournalOnTimeline(
122
+ journalId = journalId,
123
+ selectedJournalsProvider = selectedJournalsProvider,
124
+ mainActivityLauncher = mainActivityLauncher,
125
+ ),
126
+ )
127
+ }
128
+ }
129
+
130
+ fun createEntry(journalId: Int) {
131
+ viewModelScope.launch {
132
+ _journalOptionsState.value = null
133
+
134
+ analyticsTracker.trackButtonTapped("streakView_journalMenu_createEntry")
135
+
136
+ val newEntry =
137
+ entryRepository.createNewEntry(
138
+ journalId = journalId,
139
+ )
140
+ val newEntryInformation =
141
+ EditorLauncher.NewEntryInformation(
142
+ AnalyticsTracker.NewEntrySource.STREAKS,
143
+ InitialContent.BLANK,
144
+ )
145
+ editorLauncher.editNewEntry(
146
+ entry = newEntry,
147
+ newEntryInformation = newEntryInformation,
148
+ navigateTo = navigator::navigateTo,
149
+ )
150
+ }
151
+ }
152
+
153
+ fun closeJournalOptions() {
154
+ _journalOptionsState.value = null
155
+ }
156
+
157
+ private suspend fun getStreakWeekDays(): List<StreakWeekDay> {
158
+ val week = mutableListOf<StreakWeekDay>()
159
+ // First day of the week can vary based on the user's locale. Generally, SUNDAY is the default in countries
160
+ // influenced by the US, MONDAY is standard in most of Europe and Asia, and SATURDAY can be common in some
161
+ // Middle Eastern countries due to the weekend structure.
162
+ val firstDayOfWeek = timeProvider.getFirstDayOfWeek()
163
+ if (firstDayOfWeek == Calendar.SATURDAY) {
164
+ week.add(getStreakWeekDay(DayOfWeek.SATURDAY))
165
+ }
166
+ if (firstDayOfWeek == Calendar.SUNDAY || firstDayOfWeek == Calendar.SATURDAY) {
167
+ week.add(getStreakWeekDay(DayOfWeek.SUNDAY))
168
+ }
169
+ week.add(getStreakWeekDay(DayOfWeek.MONDAY))
170
+ week.add(getStreakWeekDay(DayOfWeek.TUESDAY))
171
+ week.add(getStreakWeekDay(DayOfWeek.WEDNESDAY))
172
+ week.add(getStreakWeekDay(DayOfWeek.THURSDAY))
173
+ week.add(getStreakWeekDay(DayOfWeek.FRIDAY))
174
+ if (firstDayOfWeek != Calendar.SATURDAY) {
175
+ week.add(getStreakWeekDay(DayOfWeek.SATURDAY))
176
+ }
177
+ if (firstDayOfWeek != Calendar.SUNDAY && firstDayOfWeek != Calendar.SATURDAY) {
178
+ week.add(getStreakWeekDay(DayOfWeek.SUNDAY))
179
+ }
180
+
181
+ return week
182
+ }
183
+
184
+ @VisibleForTesting
185
+ suspend fun getStreakWeekDay(dayOfWeek: DayOfWeek): StreakWeekDay {
186
+ val currentDate = timeProvider.currentLocalDate()
187
+ val daysToDayOfWeek =
188
+ dayOfWeekList.daysToPreviousDay(
189
+ currentDay = currentDate.dayOfWeek,
190
+ previousDay = dayOfWeek,
191
+ )
192
+ // Go to the previous date based on [dateOfWeek] and current date.
193
+ val currentDateForDayOfWeek = currentDate.minusDays(daysToDayOfWeek.daysToPreviousDay.toLong())
194
+ // From that date, go back DAYS_MINUS_WEEK * 7 days to cover DAYS_MINUS_WEEK (18) instances of the [dayOfWeek].
195
+ // For example,if [dayOfWeek] is MONDAY and current date is 2024-10-8 (a TUESDAY), then [daysToDayOfWeek] is 1
196
+ // and [currentDateForDayOfWeek] is 2024-10-7 and since becomes 2024-6-7 which represents the 18th MONDAY from
197
+ // the current date.
198
+ val previousWeeks = if (daysToDayOfWeek.wasInPreviousWeek) DAYS_MINUS_WEEK - 1 else DAYS_MINUS_WEEK
199
+ val since = currentDateForDayOfWeek.minusDays(previousWeeks * 7)
200
+ // Generates a sequences of dates that represents the [dayOfWeek] for the previous 18 weeks. For the previous
201
+ // example, represent dates for each MONDAY from 2024-6-7 to 2024-10-7.
202
+ val dateRange: Sequence<LocalDate> =
203
+ generateSequence(since) { date ->
204
+ if (date < currentDateForDayOfWeek) date.plusDays(7) else null
205
+ }
206
+ // We query all the dates with entries since 2024-6-7 and check if the [dayOfWeek] for each week has an entry.
207
+ val userId = userRepository.getUser()?.id
208
+ val datesWithEntries = streakRepository.getDatesThatHaveEntriesSince(since, userId)
209
+ val days =
210
+ dateRange
211
+ .map { date ->
212
+ DayJournaled(datesWithEntries.contains(date))
213
+ }.toMutableList()
214
+ .also {
215
+ if (daysToDayOfWeek.wasInPreviousWeek) {
216
+ it.add(DayJournaled(null))
217
+ }
218
+ }
219
+
220
+ return StreakWeekDay(dayOfWeek = dayOfWeek, days = days)
221
+ }
222
+
223
+ @VisibleForTesting
224
+ suspend fun getStreakJournals(): List<StreakJournal> {
225
+ val currentDate = timeProvider.currentLocalDate()
226
+ val since = currentDate.minusDays(DAYS_MINUS_JOURNAL)
227
+ val dateRange: Sequence<LocalDate> =
228
+ generateSequence(since) { date ->
229
+ if (date < currentDate) date.plusDays(1) else null
230
+ }
231
+ return journalRepository
232
+ .getAllJournals(includeHidden = true)
233
+ .filter {
234
+ !it.isHideFromStreakEnabledNonNull() && !it.isPlaceholderForEncryptedJournalNonNull()
235
+ }.sortedBy { it.sortOrder ?: 0 }
236
+ .map { journal ->
237
+ val datesWithEntries =
238
+ if (journal.isShared == true) {
239
+ streakRepository.getDatesThatHaveEntriesSinceForSharedJournal(
240
+ journalId = journal.id,
241
+ since = since,
242
+ userId = userRepository.getUser()?.id ?: "",
243
+ )
244
+ } else {
245
+ streakRepository.getDatesThatHaveEntriesSinceForJournal(journalId = journal.id, since = since)
246
+ }
247
+ val days =
248
+ dateRange
249
+ .map { date ->
250
+ DayJournaled(datesWithEntries.contains(date))
251
+ }.toList()
252
+ val entriesToday =
253
+ if (journal.isShared == true) {
254
+ streakRepository.getNumEntriesForSharedJournalOnDate(
255
+ journalId = journal.id,
256
+ date = currentDate,
257
+ userId = userRepository.getUser()?.id ?: "",
258
+ )
259
+ } else {
260
+ streakRepository.getNumEntriesOnDate(journalId = journal.id, date = currentDate)
261
+ }
262
+
263
+ StreakJournal(
264
+ journalId = journal.id,
265
+ journalName = journal.name ?: "",
266
+ entriesToday = entriesToday,
267
+ color = journal.journalColor.backgroundColorRes,
268
+ days = days,
269
+ )
270
+ }
271
+ }
272
+
273
+ companion object {
274
+ private const val DAYS_MINUS_JOURNAL = 18L
275
+ private const val DAYS_MINUS_WEEK = 17L
276
+ }
277
+
278
+ sealed interface UiState {
279
+ data object Loading : UiState
280
+
281
+ data class Streaks(
282
+ val streakDays: Int,
283
+ val streakWeekDays: List<StreakWeekDay>,
284
+ val journals: List<StreakJournal>,
285
+ ) : UiState
286
+ }
287
+
288
+ class JournalOptionsState(
289
+ val journalId: Int,
290
+ val journalName: String,
291
+ )
292
+ }
293
+
294
+ data class StreakWeekDay(
295
+ val dayOfWeek: DayOfWeek,
296
+ val days: List<DayJournaled>,
297
+ )
298
+
299
+ @JvmInline
300
+ value class DayJournaled(
301
+ val isFilled: Boolean?,
302
+ )
303
+
304
+ data class StreakJournal(
305
+ val journalId: Int,
306
+ val journalName: String,
307
+ val entriesToday: Int,
308
+ @ColorRes
309
+ val color: Int,
310
+ val days: List<DayJournaled>,
311
+ )
312
+
313
+ private class DaysOfWeekList(
314
+ private val firstDayOfWeek: Int,
315
+ ) {
316
+ private val days =
317
+ buildList {
318
+ if (firstDayOfWeek == Calendar.SATURDAY) {
319
+ add(DayOfWeek.SATURDAY)
320
+ }
321
+ if (firstDayOfWeek == Calendar.SUNDAY || firstDayOfWeek == Calendar.SATURDAY) {
322
+ add(DayOfWeek.SUNDAY)
323
+ }
324
+ add(DayOfWeek.MONDAY)
325
+ add(DayOfWeek.TUESDAY)
326
+ add(DayOfWeek.WEDNESDAY)
327
+ add(DayOfWeek.THURSDAY)
328
+ add(DayOfWeek.FRIDAY)
329
+ if (firstDayOfWeek != Calendar.SATURDAY) {
330
+ add(DayOfWeek.SATURDAY)
331
+ }
332
+ if (firstDayOfWeek != Calendar.SUNDAY && firstDayOfWeek != Calendar.SATURDAY) {
333
+ add(DayOfWeek.SUNDAY)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Return the number of days between the current day of the week and the previous day of the week.
339
+ *
340
+ * Example: If the current day of the week is MONDAY and the previous day of the week is FRIDAY, then it would
341
+ * return 3 because we have to go through SUNDAY, SATURDAY to FRIDAY.
342
+ *
343
+ * @param currentDay The current day of the week.
344
+ * @param previousDay The previous day of the week.
345
+ * @return Number of days between the current day of the week and the previous day of the week.
346
+ */
347
+ fun daysToPreviousDay(
348
+ currentDay: DayOfWeek,
349
+ previousDay: DayOfWeek,
350
+ ): PreviousDays {
351
+ if (currentDay == previousDay) {
352
+ return PreviousDays(0, false)
353
+ }
354
+ var indexCurrentDay = days.indexOf(currentDay)
355
+ var daysToPreviousDay = 0
356
+ var wasInPreviousWeek = false
357
+ while (true) {
358
+ indexCurrentDay -= 1
359
+ if (indexCurrentDay < 0) {
360
+ wasInPreviousWeek = true
361
+ indexCurrentDay = days.size - 1
362
+ }
363
+ daysToPreviousDay += 1
364
+
365
+ if (days[indexCurrentDay] == previousDay) {
366
+ break
367
+ }
368
+ }
369
+
370
+ return PreviousDays(daysToPreviousDay, wasInPreviousWeek)
371
+ }
372
+
373
+ class PreviousDays(
374
+ val daysToPreviousDay: Int,
375
+ val wasInPreviousWeek: Boolean,
376
+ )
377
+ }
@@ -0,0 +1,31 @@
1
+ package com.dayoneapp.mediastorage
2
+
3
+ import android.graphics.Bitmap
4
+
5
+ @JvmInline
6
+ value class CompressQuality(
7
+ val value: Int,
8
+ ) {
9
+ init {
10
+ require(value in 1..100) { }
11
+ }
12
+ }
13
+
14
+ class MediaStorageConfiguration(
15
+ val mediaPath: String,
16
+ val externalImagesPath: String,
17
+ val externalVideosPath: String,
18
+ val externalAudiosPath: String,
19
+ val externalDocumentsPath: String,
20
+ val avatarsPath: String,
21
+ val thumbnailsConfiguration: ThumbnailsConfiguration,
22
+ )
23
+
24
+ class ThumbnailsConfiguration(
25
+ val thumbnailsPath: String,
26
+ val height: Int,
27
+ val width: Int,
28
+ val extension: String,
29
+ val compression: Bitmap.CompressFormat,
30
+ val quality: CompressQuality,
31
+ )
@@ -0,0 +1,15 @@
1
+ package com.dayoneapp.dayone.utils
2
+
3
+ enum class AccountType(
4
+ val key: String,
5
+ ) {
6
+ DAY_ONE("Day One"),
7
+ GOOGLE("Google"),
8
+ APPLE("Apple ID"),
9
+ ;
10
+
11
+ companion object {
12
+ @JvmStatic
13
+ fun fromString(key: String): AccountType = values().find { it.key == key }!!
14
+ }
15
+ }
@@ -0,0 +1,76 @@
1
+ package com.dayoneapp.dayone.utils.usecase
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import androidx.activity.result.ActivityResultLauncher
6
+ import androidx.activity.result.contract.ActivityResultContracts.GetContent
7
+ import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents
8
+ import androidx.fragment.app.Fragment
9
+ import com.dayoneapp.dayone.utils.UriWrapper
10
+ import kotlinx.coroutines.flow.MutableSharedFlow
11
+ import kotlinx.coroutines.flow.asSharedFlow
12
+ import javax.inject.Inject
13
+
14
+ class SelectPhotoUseCase
15
+ @Inject
16
+ constructor() {
17
+ private lateinit var getContent: ActivityResultLauncher<String>
18
+ private val _mediaUris = MutableSharedFlow<List<UriWrapper>>(extraBufferCapacity = 5)
19
+ val mediaUris = _mediaUris.asSharedFlow()
20
+
21
+ fun onCreate(
22
+ fragment: Fragment,
23
+ singlePhoto: Boolean = false,
24
+ ) {
25
+ getContent =
26
+ if (singlePhoto) {
27
+ fragment.registerForActivityResult(GetSingleImage()) { uri ->
28
+ uri?.let {
29
+ _mediaUris.tryEmit(listOf(UriWrapper(it)))
30
+ }
31
+ }
32
+ } else {
33
+ fragment.registerForActivityResult(GetMultipleImages()) { uris ->
34
+ _mediaUris.tryEmit(uris.map { UriWrapper(it) })
35
+ }
36
+ }
37
+ }
38
+
39
+ fun selectPhotos() {
40
+ getContent.launch("image/*")
41
+ }
42
+
43
+ private class GetMultipleImages : GetMultipleContents() {
44
+ // Required to remove a lint error due to this method no longer calling super.createIntent()
45
+ @Suppress("MissingSuperCall")
46
+ override fun createIntent(
47
+ context: Context,
48
+ input: String,
49
+ ): Intent =
50
+ Intent(
51
+ Intent.ACTION_PICK,
52
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
53
+ ).setType(input)
54
+ .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
55
+ .apply {
56
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/jpeg", "image/png", "image/jpg"))
57
+ }
58
+ }
59
+
60
+ private class GetSingleImage : GetContent() {
61
+ // Required to remove a lint error due to this method no longer calling super.createIntent()
62
+ @Suppress("MissingSuperCall")
63
+ override fun createIntent(
64
+ context: Context,
65
+ input: String,
66
+ ): Intent =
67
+ Intent(
68
+ Intent.ACTION_PICK,
69
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
70
+ ).setType(input)
71
+ .apply {
72
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/jpeg", "image/png", "image/jpg"))
73
+ }
74
+ }
75
+ }
76
+