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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +95 -57
- data/lib/dangermattic/gem_version.rb +1 -1
- data/lib/dangermattic/plugins/android_unit_test_checker.rb +7 -4
- data/lib/dangermattic/plugins/manifest_pr_checker.rb +25 -8
- data/lib/dangermattic/plugins/podfile_checker.rb +2 -0
- data/lib/dangermattic/plugins/pr_size_checker.rb +10 -12
- data/rakelib/console.rake +1 -1
- data/rakelib/git_helpers.rake +1 -1
- data/spec/android_unit_test_checker_spec.rb +93 -3
- data/spec/fixtures/android_unit_test_checker/MixDataPlusNormalClass.kt +14 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/ApiResult.kt +69 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/WebRecordApi.kt +49 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/domain/drive/UiStates.kt +13 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/main/settings/integrations/connect/AppIntegrationModule.kt +29 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/main/streaks/StreaksViewModel.kt +377 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/mediastorage/MediaStorageConfiguration.kt +31 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/utils/AccountType.kt +15 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/utils/usecase/SelectPhotoUseCase.kt +76 -0
- data/spec/pr_size_checker_spec.rb +27 -26
- data/spec/spec_helper.rb +8 -2
- data/spec/view_changes_checker_spec.rb +15 -15
- metadata +11 -2
data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/ApiResult.kt
ADDED
@@ -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
|
+
|
data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/api/WebRecordApi.kt
ADDED
@@ -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
|
+
)
|
data/spec/fixtures/android_unit_test_checker/src/main/java/com/dayoneapp/dayone/utils/AccountType.kt
ADDED
@@ -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
|
+
|