diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 596fffb0..96972995 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -238,6 +238,7 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.RELEASE_TAG }} + target_commitish: ${{ github.sha }} name: ${{ env.RELEASE_TAG }} body_path: release-notes/${{ env.RELEASE_TAG }}.md files: | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c4517a6d..7d481b93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "dev.typetype.android" minSdk = 23 targetSdk = 37 - versionCode = 10819 - versionName = "1.8.0-beta.19" + versionCode = 10821 + versionName = "1.8.0-beta.20" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resValue("string", "app_name", "TypeType") } diff --git a/app/src/androidTest/java/dev/typetype/android/feature/player/components/PlayerControlsLayoutTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/player/components/PlayerControlsLayoutTest.kt index 28b97533..10bfd1da 100644 --- a/app/src/androidTest/java/dev/typetype/android/feature/player/components/PlayerControlsLayoutTest.kt +++ b/app/src/androidTest/java/dev/typetype/android/feature/player/components/PlayerControlsLayoutTest.kt @@ -110,6 +110,7 @@ class PlayerControlsLayoutTest { .padding(top = 200.dp), ) } + } composeRule.onNodeWithContentDescription( composeRule.activity.getString(R.string.player_timeline), diff --git a/app/src/androidTest/java/dev/typetype/android/feature/search/SearchScreenStateTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/search/SearchScreenStateTest.kt index a10cff62..df58d2ca 100644 --- a/app/src/androidTest/java/dev/typetype/android/feature/search/SearchScreenStateTest.kt +++ b/app/src/androidTest/java/dev/typetype/android/feature/search/SearchScreenStateTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import dev.typetype.android.core.ui.components.LocalAnimatedStatePlayback import dev.typetype.android.core.ui.theme.TypeTypeTheme +import dev.typetype.android.domain.search.SearchFilterOption import dev.typetype.android.feature.menu.VideoMenuScope import org.junit.Rule import org.junit.Test @@ -61,6 +62,32 @@ class SearchScreenStateTest { composeRule.onAllNodesWithText("Compose").assertCountEquals(2) } + @Test + fun filtersStayHiddenUntilSearchFinishesSubmitting() { + show(searchState()) + + composeRule.onNodeWithContentDescription("Content type").assertDoesNotExist() + } + + @Test + fun filtersAppearOnlyWithResultsState() { + composeRule.setContent { + TypeTypeTheme { + SearchFilterBar( + contentFilters = searchState().contentFilters, + filterGroups = emptyList(), + selectedContent = null, + selectedFilters = emptyList(), + onContentSelect = {}, + onFilterToggle = { _, _ -> }, + onResetFilters = {}, + ) + } + } + + composeRule.onNodeWithContentDescription("Content type").assertIsDisplayed() + } + @Test fun completedSearchHasAnExplicitEmptyState() { composeRule.setContent { @@ -106,4 +133,11 @@ class SearchScreenStateTest { watchLater = emptySet(), onAction = { _, _ -> }, ) + + private fun searchState() = SearchState( + query = "kotlin", + contentFilters = listOf( + SearchFilterOption(value = "video", label = "video", isDefault = false), + ), + ) } diff --git a/app/src/androidTest/java/dev/typetype/android/feature/search/SearchServiceSelectorTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/search/SearchServiceSelectorTest.kt new file mode 100644 index 00000000..1062420c --- /dev/null +++ b/app/src/androidTest/java/dev/typetype/android/feature/search/SearchServiceSelectorTest.kt @@ -0,0 +1,30 @@ +package dev.typetype.android.feature.search + +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import dev.typetype.android.core.ui.theme.TypeTypeTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class SearchServiceSelectorTest { + @get:Rule + val composeRule = createComposeRule() + + @Test + fun selectingAServiceReportsItsAccountId() { + var selectedService = -1 + composeRule.setContent { + TypeTypeTheme { + SearchServiceSelector(service = 0) { service -> + selectedService = service + } + } + } + + composeRule.onNodeWithText("NicoNico").performClick() + + assertEquals(6, selectedService) + } +} diff --git a/app/src/main/java/dev/typetype/android/MainViewModel.kt b/app/src/main/java/dev/typetype/android/MainViewModel.kt index 2fe55708..bfaeaaf7 100644 --- a/app/src/main/java/dev/typetype/android/MainViewModel.kt +++ b/app/src/main/java/dev/typetype/android/MainViewModel.kt @@ -27,6 +27,7 @@ import dev.typetype.android.domain.subscriptions.SubscriptionsRepository import dev.typetype.android.domain.usersettings.UserSettings import dev.typetype.android.domain.usersettings.UserSettingsRepository import dev.typetype.android.feature.player.host.PlayerHostController +import dev.typetype.android.services.push.PushRegistrationManager import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -63,6 +64,7 @@ class MainViewModel @Inject constructor( private val subscriptionsRepository: SubscriptionsRepository, private val libraryRepository: LibraryRepository, private val activeAccountScope: ActiveAccountScope, + private val pushRegistrationManager: PushRegistrationManager, private val startupLandingStore: StartupLandingStore, private val playbackResumeRepository: PlaybackResumeRepository, private val playbackQueueRepository: PlaybackQueueRepository, @@ -182,6 +184,7 @@ class MainViewModel @Inject constructor( launch { profileRepository.refresh() } launch { subscriptionsRepository.refresh() } launch { libraryRepository.resumePendingWrites() } + launch { pushRegistrationManager.reconcileRegistration() } } } @@ -221,6 +224,7 @@ class MainViewModel @Inject constructor( launch { profileRepository.refresh() } launch { subscriptionsRepository.refresh() } launch { libraryRepository.resumePendingWrites() } + launch { pushRegistrationManager.reconcileRegistration() } launch { restorePlaybackUnlessExternalRequestArrives() } diff --git a/app/src/main/java/dev/typetype/android/feature/player/components/PlayerGestureLayer.kt b/app/src/main/java/dev/typetype/android/feature/player/components/PlayerGestureLayer.kt index d60da04d..5e073802 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/components/PlayerGestureLayer.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/components/PlayerGestureLayer.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource diff --git a/app/src/main/java/dev/typetype/android/feature/search/SearchAction.kt b/app/src/main/java/dev/typetype/android/feature/search/SearchAction.kt index 256d68dc..81c33919 100644 --- a/app/src/main/java/dev/typetype/android/feature/search/SearchAction.kt +++ b/app/src/main/java/dev/typetype/android/feature/search/SearchAction.kt @@ -5,6 +5,7 @@ sealed interface SearchAction { data object OnSearch : SearchAction data class OnSuggestionClick(val query: String) : SearchAction data object OnClearQuery : SearchAction + data class OnServiceSelect(val service: Int) : SearchAction data class OnContentFilterSelect(val value: String?) : SearchAction data class OnFilterToggle(val groupKey: String, val optionValue: String) : SearchAction data object OnResetFilters : SearchAction diff --git a/app/src/main/java/dev/typetype/android/feature/search/SearchScreen.kt b/app/src/main/java/dev/typetype/android/feature/search/SearchScreen.kt index ef1b10e2..08ce4aa5 100644 --- a/app/src/main/java/dev/typetype/android/feature/search/SearchScreen.kt +++ b/app/src/main/java/dev/typetype/android/feature/search/SearchScreen.kt @@ -106,17 +106,23 @@ fun SearchScreen( onNavigateBack = onNavigateBack, focusRequester = focusRequester, ) - SearchFilterBar( - contentFilters = state.contentFilters, - filterGroups = state.filterGroups, - selectedContent = state.selectedContentFilter, - selectedFilters = state.selectedFilters, - onContentSelect = { onAction(SearchAction.OnContentFilterSelect(it)) }, - onFilterToggle = { groupKey, value -> - onAction(SearchAction.OnFilterToggle(groupKey, value)) - }, - onResetFilters = { onAction(SearchAction.OnResetFilters) }, + SearchServiceSelector( + service = state.service, + onServiceSelect = { onAction(SearchAction.OnServiceSelect(it)) }, ) + if (state.hasSearched) { + SearchFilterBar( + contentFilters = state.contentFilters, + filterGroups = state.filterGroups, + selectedContent = state.selectedContentFilter, + selectedFilters = state.selectedFilters, + onContentSelect = { onAction(SearchAction.OnContentFilterSelect(it)) }, + onFilterToggle = { groupKey, value -> + onAction(SearchAction.OnFilterToggle(groupKey, value)) + }, + onResetFilters = { onAction(SearchAction.OnResetFilters) }, + ) + } when { state.isLoading -> Box( diff --git a/app/src/main/java/dev/typetype/android/feature/search/SearchServiceSelector.kt b/app/src/main/java/dev/typetype/android/feature/search/SearchServiceSelector.kt new file mode 100644 index 00000000..4f892ba0 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/search/SearchServiceSelector.kt @@ -0,0 +1,80 @@ +package dev.typetype.android.feature.search + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.typetype.android.R + +@Immutable +private data class SearchService( + val id: Int, + val labelRes: Int, + val iconRes: Int, + val brandColor: Color, +) + +private val SEARCH_SERVICES = listOf( + SearchService( + id = 0, + labelRes = R.string.settings_default_service_youtube, + iconRes = R.drawable.ic_service_youtube, + brandColor = Color(0xFFFF0000), + ), + SearchService( + id = 6, + labelRes = R.string.settings_default_service_niconico, + iconRes = R.drawable.ic_service_niconico, + brandColor = Color(0xFF231815), + ), + SearchService( + id = 5, + labelRes = R.string.settings_default_service_bilibili, + iconRes = R.drawable.ic_service_bilibili, + brandColor = Color(0xFF00A1D6), + ), +) + +@Composable +internal fun SearchServiceSelector( + service: Int, + onServiceSelect: (Int) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 12.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SEARCH_SERVICES.forEach { candidate -> + val label = stringResource(candidate.labelRes) + FilterChip( + selected = service == candidate.id, + onClick = { onServiceSelect(candidate.id) }, + leadingIcon = { + Icon( + painter = painterResource(candidate.iconRes), + contentDescription = null, + tint = candidate.brandColor, + modifier = Modifier.size(18.dp), + ) + }, + label = { Text(label) }, + ) + } + } +} diff --git a/app/src/main/java/dev/typetype/android/feature/search/SearchViewModel.kt b/app/src/main/java/dev/typetype/android/feature/search/SearchViewModel.kt index 2ee7fcab..643d32f3 100644 --- a/app/src/main/java/dev/typetype/android/feature/search/SearchViewModel.kt +++ b/app/src/main/java/dev/typetype/android/feature/search/SearchViewModel.kt @@ -86,6 +86,7 @@ class SearchViewModel @Inject constructor( loadMoreError = false, ) } + is SearchAction.OnServiceSelect -> selectService(action.service) is SearchAction.OnContentFilterSelect -> selectContentFilter(action.value) is SearchAction.OnFilterToggle -> toggleFilter(action.groupKey, action.optionValue) SearchAction.OnResetFilters -> resetFilters() @@ -98,6 +99,12 @@ class SearchViewModel @Inject constructor( } } + private fun selectService(service: Int) { + viewModelScope.launch { + userSettingsRepository.update { it.copy(defaultService = service) } + } + } + private fun observeSuggestions() { viewModelScope.launch { _state diff --git a/app/src/main/java/dev/typetype/android/feature/subscriptions/SubscriptionsFeedContinuity.kt b/app/src/main/java/dev/typetype/android/feature/subscriptions/SubscriptionsFeedContinuity.kt new file mode 100644 index 00000000..e9c00dc0 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/subscriptions/SubscriptionsFeedContinuity.kt @@ -0,0 +1,12 @@ +package dev.typetype.android.feature.subscriptions + +import dev.typetype.android.domain.feed.Video + +internal fun mergeSubscriptionFirstPage( + loadedVideos: List