mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Search page improvements (#709)
## Description Improves how search results are displayed: - Reorders search categories to Movies → TV Shows → Episodes → Collections, surfacing the most commonly searched content types at the top. - Adds a new "Combined search results" preference in Advanced settings → Search. When enabled, shows all results in a single list instead of separate categories. - When Seerr is active, adds Library/Discover tabs to switch between local library results and Seerr discovery results. - Hides empty result categories instead of showing "No results" placeholders. ### Screenshots <img width="1935" height="1100" alt="Screenshot_20260116_113656" src="https://github.com/user-attachments/assets/1e29aa3a-3ba2-4f86-ab61-fed683490474" /> <img width="1934" height="1095" alt="image" src="https://github.com/user-attachments/assets/8b93fa38-d4c9-4e72-a997-caa82b4949f0" /> <img width="1921" height="1094" alt="image" src="https://github.com/user-attachments/assets/82aed1a5-810c-413f-a266-6d78490a1534" /> ### AI/LLM usage Code auditing and PR description drafting with Claude Code. Also used Claude Code for the regex.
This commit is contained in:
parent
f364ef9708
commit
98ffaf51ac
6 changed files with 798 additions and 274 deletions
|
|
@ -66,7 +66,8 @@ class AppPreferencesSerializer
|
||||||
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
||||||
mediaExtensionsEnabled =
|
mediaExtensionsEnabled =
|
||||||
AppPreference.FfmpegPreference.defaultValue
|
AppPreference.FfmpegPreference.defaultValue
|
||||||
assPlaybackMode = AppPreference.AssSubtitleMode.defaultValue
|
assPlaybackMode =
|
||||||
|
AppPreference.AssSubtitleMode.defaultValue
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
mpvOptions =
|
mpvOptions =
|
||||||
|
|
@ -99,6 +100,13 @@ class AppPreferencesSerializer
|
||||||
backdropStyle = AppPreference.BackdropStylePref.defaultValue
|
backdropStyle = AppPreference.BackdropStylePref.defaultValue
|
||||||
showLogos = AppPreference.ShowLogos.defaultValue
|
showLogos = AppPreference.ShowLogos.defaultValue
|
||||||
|
|
||||||
|
searchPreferences =
|
||||||
|
SearchPreferences
|
||||||
|
.newBuilder()
|
||||||
|
.apply {
|
||||||
|
combinedSearchResults = false
|
||||||
|
}.build()
|
||||||
|
|
||||||
subtitlesPreferences =
|
subtitlesPreferences =
|
||||||
SubtitlePreferences
|
SubtitlePreferences
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
|
|
@ -207,6 +215,11 @@ inline fun AppPreferences.updateInterfacePreferences(block: InterfacePreferences
|
||||||
interfacePreferences = interfacePreferences.toBuilder().apply(block).build()
|
interfacePreferences = interfacePreferences.toBuilder().apply(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun AppPreferences.updateSearchPreferences(block: SearchPreferences.Builder.() -> Unit): AppPreferences =
|
||||||
|
updateInterfacePreferences {
|
||||||
|
searchPreferences = searchPreferences.toBuilder().apply(block).build()
|
||||||
|
}
|
||||||
|
|
||||||
inline fun AppPreferences.updateSubtitlePreferences(block: SubtitlePreferences.Builder.() -> Unit): AppPreferences =
|
inline fun AppPreferences.updateSubtitlePreferences(block: SubtitlePreferences.Builder.() -> Unit): AppPreferences =
|
||||||
updateInterfacePreferences {
|
updateInterfacePreferences {
|
||||||
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
|
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ fun <T : CardGridItem> CardGrid(
|
||||||
val cardWidth =
|
val cardWidth =
|
||||||
ceil((width - (spacingPx * (columns - 1))) / columns)
|
ceil((width - (spacingPx * (columns - 1))) / columns)
|
||||||
cardWidthPx = cardWidth.toInt()
|
cardWidthPx = cardWidth.toInt()
|
||||||
Timber.v("cardWidthPx=%s", cardWidthPx)
|
// Timber.v("cardWidthPx=%s", cardWidthPx)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
items(pager.size) { index ->
|
items(pager.size) { index ->
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object SearchRelevance {
|
||||||
|
private val REGEX_PATTERN = Regex("^/(.+)/(i)?$")
|
||||||
|
|
||||||
|
fun score(
|
||||||
|
item: BaseItem,
|
||||||
|
query: String,
|
||||||
|
): Int {
|
||||||
|
val name = item.name?.lowercase(Locale.getDefault()) ?: return Int.MAX_VALUE
|
||||||
|
val q = query.lowercase(Locale.getDefault())
|
||||||
|
|
||||||
|
REGEX_PATTERN.find(query)?.let { match ->
|
||||||
|
val pattern = match.groupValues[1]
|
||||||
|
val caseInsensitive = match.groupValues[2] == "i"
|
||||||
|
return regexScore(item.name ?: "", pattern, caseInsensitive) + typeBonus(item.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
name == q -> 0
|
||||||
|
name.startsWith(q) -> 100
|
||||||
|
name.contains(" $q") -> 200
|
||||||
|
name.contains(q) -> 300
|
||||||
|
else -> fuzzyScore(name, q)
|
||||||
|
} + typeBonus(item.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun regexScore(
|
||||||
|
name: String,
|
||||||
|
pattern: String,
|
||||||
|
caseInsensitive: Boolean,
|
||||||
|
): Int =
|
||||||
|
try {
|
||||||
|
val options = if (caseInsensitive) setOf(RegexOption.IGNORE_CASE) else emptySet()
|
||||||
|
if (Regex(pattern, options).containsMatchIn(name)) 250 else 400
|
||||||
|
} catch (e: Exception) {
|
||||||
|
400
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fuzzyScore(
|
||||||
|
name: String,
|
||||||
|
query: String,
|
||||||
|
): Int {
|
||||||
|
val distance = levenshteinDistance(name, query)
|
||||||
|
val maxLen = maxOf(name.length, query.length)
|
||||||
|
val similarity = 1.0 - (distance.toDouble() / maxLen)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
similarity >= 0.8 -> 350
|
||||||
|
similarity >= 0.6 -> 375
|
||||||
|
else -> 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun levenshteinDistance(
|
||||||
|
s1: String,
|
||||||
|
s2: String,
|
||||||
|
): Int {
|
||||||
|
val m = s1.length
|
||||||
|
val n = s2.length
|
||||||
|
val dp = Array(m + 1) { IntArray(n + 1) }
|
||||||
|
for (i in 0..m) dp[i][0] = i
|
||||||
|
for (j in 0..n) dp[0][j] = j
|
||||||
|
for (i in 1..m) {
|
||||||
|
for (j in 1..n) {
|
||||||
|
dp[i][j] =
|
||||||
|
if (s1[i - 1] == s2[j - 1]) {
|
||||||
|
dp[i - 1][j - 1]
|
||||||
|
} else {
|
||||||
|
1 + minOf(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dp[m][n]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeBonus(type: BaseItemKind): Int =
|
||||||
|
when (type) {
|
||||||
|
BaseItemKind.SERIES -> 0
|
||||||
|
BaseItemKind.MOVIE -> 1
|
||||||
|
BaseItemKind.BOX_SET -> 2
|
||||||
|
BaseItemKind.EPISODE -> 10
|
||||||
|
else -> 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -165,6 +165,10 @@ message ScreensaverPreferences{
|
||||||
bool show_clock = 7;
|
bool show_clock = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SearchPreferences {
|
||||||
|
bool combined_search_results = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message InterfacePreferences {
|
message InterfacePreferences {
|
||||||
ThemeSongVolume play_theme_songs = 1;
|
ThemeSongVolume play_theme_songs = 1;
|
||||||
bool remember_selected_tab = 2;
|
bool remember_selected_tab = 2;
|
||||||
|
|
@ -179,6 +183,7 @@ message InterfacePreferences {
|
||||||
ScreensaverPreferences screensaver_preference = 11;
|
ScreensaverPreferences screensaver_preference = 11;
|
||||||
bool enable_media_management = 12;
|
bool enable_media_management = 12;
|
||||||
bool show_logos = 13;
|
bool show_logos = 13;
|
||||||
|
SearchPreferences search_preferences = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvancedPreferences {
|
message AdvancedPreferences {
|
||||||
|
|
|
||||||
|
|
@ -445,6 +445,10 @@
|
||||||
<string name="color_code_programs">Color-code programs</string>
|
<string name="color_code_programs">Color-code programs</string>
|
||||||
<string name="subtitle_delay">Subtitle delay</string>
|
<string name="subtitle_delay">Subtitle delay</string>
|
||||||
<string name="backdrop_display">Backdrop style</string>
|
<string name="backdrop_display">Backdrop style</string>
|
||||||
|
<string name="combined_search_results">Combined search results</string>
|
||||||
|
<string name="combined_search_results_on">Show all results in a single list</string>
|
||||||
|
<string name="combined_search_results_off">Show results by category</string>
|
||||||
|
<string name="results">Results</string>
|
||||||
<string name="resolution_switching">Resolution switching</string>
|
<string name="resolution_switching">Resolution switching</string>
|
||||||
<string name="local">Local</string>
|
<string name="local">Local</string>
|
||||||
<string name="play_trailer">Play trailer</string>
|
<string name="play_trailer">Play trailer</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue