Skip to content

Commit

Permalink
➕ Refactored code and added the first search option: regex search mode
Browse files Browse the repository at this point in the history
  • Loading branch information
CXwudi committed Dec 1, 2023
1 parent c4dac33 commit 0534fef
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package mikufan.cx.songfinder.backend.controller.mainpage

import androidx.compose.runtime.State
import mikufan.cx.songfinder.backend.statemodel.SearchOptionsStateModel
import mikufan.cx.songfinder.backend.statemodel.SearchRegexOption
import org.springframework.stereotype.Controller

@Controller
class RegexMatchOptionController(
private val searchOptionsStateModel: SearchOptionsStateModel,
private val songSearchIntermediateController: SongSearchIntermediateController
) {

val currentRegexOptionState: State<SearchRegexOption> = searchOptionsStateModel.searchRegexOptionState

suspend fun setRegexOption(newOption: SearchRegexOption) {
searchOptionsStateModel.searchRegexOptionState.value = newOption
songSearchIntermediateController.triggerSearch(100)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package mikufan.cx.songfinder.backend.controller.mainpage

import androidx.compose.runtime.State
import mikufan.cx.inlinelogging.KInlineLogging
import mikufan.cx.songfinder.backend.service.SongSearchService
import mikufan.cx.songfinder.backend.statemodel.SearchInputStateModel
import mikufan.cx.songfinder.backend.statemodel.SearchResultStateModel
import mikufan.cx.songfinder.backend.statemodel.SearchStatus
Expand All @@ -11,8 +9,8 @@ import org.springframework.stereotype.Controller
@Controller
class SearchBarController(
private val searchInputStateModel: SearchInputStateModel,
private val searchResultStateModel: SearchResultStateModel,
private val songSearchService: SongSearchService,
searchResultStateModel: SearchResultStateModel,
private val songSearchIntermediateController: SongSearchIntermediateController
) {

val currentInputState: State<String> get() = searchInputStateModel.currentInputState
Expand All @@ -23,19 +21,6 @@ class SearchBarController(
}

suspend fun search() {
try {
val title = searchInputStateModel.currentInputState.value
searchResultStateModel.setAsSearching()
val results = if (title.isNotEmpty()) {
songSearchService.search(title)
} else {
emptyList()
}
searchResultStateModel.setAsDoneWith(results)
} catch (e: Exception) {
log.warn(e) { "Exception happened during search, what is that?" }
}
songSearchIntermediateController.triggerSearch()
}
}

private val log = KInlineLogging.logger()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package mikufan.cx.songfinder.backend.controller.mainpage

import kotlinx.coroutines.*
import mikufan.cx.inlinelogging.KInlineLogging
import mikufan.cx.songfinder.backend.service.SongSearchService
import mikufan.cx.songfinder.backend.statemodel.SearchInputStateModel
import mikufan.cx.songfinder.backend.statemodel.SearchOptionsStateModel
import mikufan.cx.songfinder.backend.statemodel.SearchResultStateModel
import org.springframework.stereotype.Controller

/**
* Intermediate controller for song search functionality.
* This class handles triggering the search and updating the search state.
* @property searchInputStateModel The state model for search input.
* @property searchOptionsStateModel The state model for search options.
* @property searchResultStateModel The state model for search results.
* @property songSearchService The service used for song search.
*/
@Controller
class SongSearchIntermediateController(
private val searchInputStateModel: SearchInputStateModel,
private val searchOptionsStateModel: SearchOptionsStateModel,
private val searchResultStateModel: SearchResultStateModel,
private val songSearchService: SongSearchService,
) {

/**
* The current search job.
*/
var searchJob: Job? = null

/**
* Triggers a search operation with an optional delay.
*
* @param wait The optional delay in milliseconds before starting the search.
*/
suspend fun triggerSearch(wait: Long = 500) = coroutineScope {
searchJob?.cancel()
searchJob = launch {
try {
delay(wait) // do a small delay waiting for any rapid user input
doSearch()
} catch (e: CancellationException) {
log.info { "Previous job is cancelled" }
} catch (e: Exception) {
log.warn(e) { "Exception happened during search, what is that?" }
}
}
}

/**
* Performs a search using the current input state and search options.
* Sets the search result state model accordingly.
*
* @throws Exception if an error occurs during the search process.
*/
private suspend fun doSearch() {
searchResultStateModel.setAsSearching()
val title = searchInputStateModel.currentInputState.value
val regexOption = searchOptionsStateModel.searchRegexOptionState.value
val results = if (title.isNotEmpty()) {
songSearchService.search(title, regexOption)
} else {
emptyList()
}
searchResultStateModel.setAsDoneWith(results)
}
}

private val log = KInlineLogging.logger()
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ interface SongRepository : VocaDbRepository<Song, Long> {
FROM songs s
JOIN song_names sn ON s.id = sn.song_id
WHERE
s.japanese_name LIKE concat('%', :title, '%')
OR s.english_name LIKE concat('%', :title, '%')
OR s.romaji_name LIKE concat('%', :title, '%')
OR sn.value LIKE concat('%', :title, '%')
s.japanese_name REGEXP :title
OR s.english_name REGEXP :title
OR s.romaji_name REGEXP :title
OR sn.value REGEXP :title
""")
fun findByAllPossibleNamesContain(title: String): List<Song>
fun findByAllPossibleNames(title: String): List<Song>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import mikufan.cx.songfinder.backend.db.projection.ArtistInSong
import mikufan.cx.songfinder.backend.db.repository.*
import mikufan.cx.songfinder.backend.model.PVInfo
import mikufan.cx.songfinder.backend.model.SongSearchResult
import mikufan.cx.songfinder.backend.statemodel.SearchRegexOption
import org.springframework.stereotype.Service

/**
Expand Down Expand Up @@ -50,16 +51,19 @@ class SongSearchService(
* @param title The title to search for.
* @return A list of SongSearchResult objects matching the search criteria.
*/
suspend fun search(title: String): List<SongSearchResult> {
log.info { "Searching '$title'" }
suspend fun search(title: String, regexOption: SearchRegexOption): List<SongSearchResult> {
val regexOptionDescription = regexOption.description
val pattern = regexOption.pattern
log.info { "Searching '$title' with $regexOptionDescription" }
// search steps:
// 1. search songs by title
val formattedTitle = pattern.format(title)
val songs = withContext(ioDispatcher) {
songRepo.findByAllPossibleNamesContain(title)
songRepo.findByAllPossibleNames(formattedTitle)
}
log.debug { "found ${songs.size} entries" }
if (songs.isEmpty()) {
log.info { "No song found for '$title'" }
log.info { "No song found for '$formattedTitle'" }
return emptyList()
}
val songIds = songs.map { it.id }
Expand Down Expand Up @@ -122,7 +126,7 @@ class SongSearchService(
.sortedBy { it.publishDate }
}

log.info { "Found ${results.size} results for '$title'" }
log.info { "Found ${results.size} results for '$title' with $regexOptionDescription" }
return results
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@ import androidx.compose.runtime.mutableStateOf
class SearchInputStateModel(
initialInput: String
) {
/**
* To record the current line of input from file
*/
// var currentInputFromFile = initialInput

/**
* To record the current input content from the search bar
*/
var currentInputState: MutableState<String> = mutableStateOf(initialInput)

fun update(newInput: String) {
currentInputState.value = newInput
}

// fun setToNext(newInput: String) {
// currentInputState.value = newInput
// currentInputFromFile = newInput
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package mikufan.cx.songfinder.backend.statemodel

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import org.springframework.stereotype.Component


/**
* This class represents the state model for search options.
*
* @property searchRegexOptionState The state of the search regex option.
*/
@Component
class SearchOptionsStateModel {

/**
* Mutable state variable representing the search regex option state.
*
* This variable holds the current value of the search regex option state, which determines how the search is performed.
* It is declared with a `MutableState` type, allowing it to be changed and observed by other parts of the codebase.
*
* @property searchRegexOptionState The mutable state object holding the search regex option.
*
* @see SearchRegexOption
*
*/
val searchRegexOptionState: MutableState<SearchRegexOption> = mutableStateOf(SearchRegexOption.Contains)
}

/**
* Enum class representing different search options for regular expressions.
*
* @property pattern The regular expression pattern for the search option.
* @property description The description of the search option.
* @property displayName The formatted display name of the search option.
*/
enum class SearchRegexOption(val pattern: String, val description: String) {
Exact("^%s$", "Exact Match"),
StartWith("^%s.*", "Start With"),
Contains(".*%s.*", "Contains");

val displayName: String = "$description (${pattern.format("title")})"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import mikufan.cx.songfinder.backend.controller.MainScreenController
import mikufan.cx.songfinder.backend.db.entity.PvService
import mikufan.cx.songfinder.backend.db.entity.PvType
import mikufan.cx.songfinder.backend.db.entity.SongType
import mikufan.cx.songfinder.backend.model.PVInfo
import mikufan.cx.songfinder.backend.model.SongSearchResult
import mikufan.cx.songfinder.backend.statemodel.SearchRegexOption
import mikufan.cx.songfinder.backend.statemodel.SearchStatus
import mikufan.cx.songfinder.getSpringBean
import mikufan.cx.songfinder.ui.common.ColumnCentralizedWithSpacing
Expand All @@ -28,11 +30,14 @@ fun MainScreen() {


@Composable
fun RealMainScreen(isAllFinished: State<Boolean>) = ColumnCentralizedWithSpacing {
fun RealMainScreen(isAllFinished: State<Boolean>) = ColumnCentralizedWithSpacing(
horizontalAlignment = Alignment.Start
) {
ProgressBar()
Divider()
RestOfPart(isAllFinished.value, { FinishMessagePanel() }, {
SearchBar()
RegexMatchOption()
ResultPanel()
})
}
Expand Down Expand Up @@ -62,6 +67,7 @@ fun PreviewMainScreen() {
{},
)
)
RealRegexMatchOption(SearchRegexOption.Exact, {})
RealResultPanel(
listOf(
SongSearchResult(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package mikufan.cx.songfinder.ui.component.mainpage

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.onClick
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch
import mikufan.cx.songfinder.backend.controller.mainpage.RegexMatchOptionController
import mikufan.cx.songfinder.backend.statemodel.SearchRegexOption
import mikufan.cx.songfinder.getSpringBean
import mikufan.cx.songfinder.ui.common.RowCentralizedWithSpacing
import mikufan.cx.songfinder.ui.theme.spacing

/**
* Composable function for handling regex match options.
*
*/
@Composable
fun RegexMatchOption() {
val controller = getSpringBean<RegexMatchOptionController>()
val option by controller.currentRegexOptionState
RealRegexMatchOption(option, controller::setRegexOption)
}


/**
* Composable function to render a row of regex match options.
*
* @param option The currently selected search regex option.
* @param onOptionSet The callback function called when a regex option is selected.
*/
@Composable
fun RealRegexMatchOption(option: SearchRegexOption, onOptionSet: suspend (SearchRegexOption) -> Unit) = RowCentralizedWithSpacing(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.spacing)
) {
Text("Regex Match Option: ")
RegexMatchOptionButton(SearchRegexOption.Exact, option, onOptionSet)
RegexMatchOptionButton(SearchRegexOption.StartWith, option, onOptionSet)
RegexMatchOptionButton(SearchRegexOption.Contains, option, onOptionSet)
}

/**
* Composable function to render a single regex match option.
*
* @param renderedOption The regex option to render.
* @param selectedOption The currently selected regex option.
* @param onOptionSet The callback function called when a regex option is selected.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RegexMatchOptionButton(
renderedOption: SearchRegexOption,
selectedOption: SearchRegexOption,
onOptionSet: suspend (SearchRegexOption) -> Unit
) = Row(
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically
) {
val scope = rememberCoroutineScope()
RadioButton(
selected = renderedOption == selectedOption,
onClick = { scope.launch { onOptionSet(renderedOption) } }
)
Text(
text = renderedOption.displayName,
modifier = Modifier.onClick { scope.launch { onOptionSet(renderedOption) } }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ fun ResultPanelGrid(
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(256.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.spacingSmaller),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.spacingSmaller),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.spacingSmall),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.spacingSmall),
// modifier = modifier.padding(end = MaterialTheme.spacing.paddingLarge),
state = gridState,
) {
Expand Down
Loading

0 comments on commit 0534fef

Please sign in to comment.