From 6936080865add10af775db8db5948ae29ece49bb Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 24 Sep 2024 13:58:16 +0100 Subject: [PATCH] =?UTF-8?q?SelectorMediaSource:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E6=A0=87=E7=AD=BE=E5=8C=B9=E9=85=8D=E6=9D=A1=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/source/web/SelectorMediaSource.kt | 2 +- .../media/source/web/SelectorSearchConfig.kt | 3 + .../media/source/web/WebSearchSubjectInfo.kt | 3 +- .../web/format/SelectorSubjectFormat.kt | 83 +++++++++++- ...ectorChannelConfigurationColumn.android.kt | 6 +- .../selector/test/SelectorTestPane.android.kt | 8 +- ...lumn.kt => SelectorChannelFormatColumn.kt} | 35 ++--- ...nDefaults.kt => SelectorConfigDefaults.kt} | 0 ...igurationPane.kt => SelectorConfigPane.kt} | 80 +++++++++--- .../selector/edit/SelectorConfigState.kt | 38 ++++++ .../edit/SelectorSubjectFormatColumn.kt | 120 ++++++++++++++++++ .../selector/test/SelectorTestPane.kt | 46 ++++++- .../test/SelectorTestSearchSubjectResult.kt | 7 +- ...kt => SelectorTestSubjectResultLazyRow.kt} | 9 +- 14 files changed, 386 insertions(+), 54 deletions(-) rename app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/{SelectorChannelConfigurationColumn.kt => SelectorChannelFormatColumn.kt} (86%) rename app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/{SelectorConfigurationDefaults.kt => SelectorConfigDefaults.kt} (100%) rename app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/{SelectorConfigurationPane.kt => SelectorConfigPane.kt} (84%) create mode 100644 app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorSubjectFormatColumn.kt rename app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/{SubjectResultLazyRow.kt => SelectorTestSubjectResultLazyRow.kt} (94%) diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorMediaSource.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorMediaSource.kt index 4e4689eeab..f90378d4b2 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorMediaSource.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorMediaSource.kt @@ -153,7 +153,7 @@ class SelectorMediaSource( } } .mapNotNull { subject -> - doHttpGet(subject.subjectDetailsPageUrl) + doHttpGet(subject.fullUrl) .getOrNull() } .asSequence() diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorSearchConfig.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorSearchConfig.kt index 45c7132c5d..0d006e871d 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorSearchConfig.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/SelectorSearchConfig.kt @@ -20,6 +20,7 @@ import me.him188.ani.app.data.source.media.source.web.format.SelectorFormatConfi import me.him188.ani.app.data.source.media.source.web.format.SelectorFormatId import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormat import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatA +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatIndexed import me.him188.ani.app.data.source.media.source.web.format.parseOrNull import org.intellij.lang.annotations.Language @@ -33,6 +34,7 @@ data class SelectorSearchConfig( // Phase 2, for search result, select subjects val subjectFormatId: SelectorFormatId = SelectorSubjectFormatA.id, val selectorSubjectFormatA: SelectorSubjectFormatA.Config = SelectorSubjectFormatA.Config(), + val selectorSubjectFormatIndexed: SelectorSubjectFormatIndexed.Config = SelectorSubjectFormatIndexed.Config(), // Phase 3, for each subject, select channels val channelFormatId: SelectorFormatId = SelectorChannelFormatNoChannel.id, val selectorChannelFormatFlattened: SelectorChannelFormatFlattened.Config = SelectorChannelFormatFlattened.Config(), @@ -128,6 +130,7 @@ fun SelectorSearchConfig.getFormatConfig(format: Sele @Suppress("UNCHECKED_CAST") return when (format) { SelectorSubjectFormatA -> selectorSubjectFormatA as C + SelectorSubjectFormatIndexed -> selectorSubjectFormatIndexed as C } } diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/WebSearchSubjectInfo.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/WebSearchSubjectInfo.kt index 07abfdb31b..88a0d5ad86 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/WebSearchSubjectInfo.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/WebSearchSubjectInfo.kt @@ -15,7 +15,8 @@ import me.him188.ani.utils.xml.Element data class WebSearchSubjectInfo( val internalId: String, val name: String, - val subjectDetailsPageUrl: String, + val fullUrl: String, + val partialUrl: String, val origin: Element?, ) diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/format/SelectorSubjectFormat.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/format/SelectorSubjectFormat.kt index 7bf241b415..32d49a4c3e 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/format/SelectorSubjectFormat.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/source/web/format/SelectorSubjectFormat.kt @@ -10,12 +10,14 @@ package me.him188.ani.app.data.source.media.source.web.format import androidx.compose.runtime.Immutable +import androidx.compose.ui.util.fastMapNotNull import kotlinx.serialization.Serializable import me.him188.ani.app.data.source.media.source.web.WebSearchSubjectInfo import me.him188.ani.utils.xml.Element import me.him188.ani.utils.xml.QueryParser import me.him188.ani.utils.xml.parseSelectorOrNull import org.intellij.lang.annotations.Language +import kotlin.contracts.contract /** * 决定如何匹配条目 @@ -34,7 +36,10 @@ sealed class SelectorSubjectFormat(override va companion object { val entries by lazy { // 必须 lazy, 否则可能获取到 null - listOf(checkNotNull(SelectorSubjectFormatA)) // checkNotNull is needed to be fail-fast + listOf( + checkNotNull(SelectorSubjectFormatA), + checkNotNull(SelectorSubjectFormatIndexed), + ) // checkNotNull is needed to be fail-fast } fun findById(id: SelectorFormatId): SelectorSubjectFormat<*>? { @@ -74,7 +79,8 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat`, 按顺序对应 + */ +data object SelectorSubjectFormatIndexed : + SelectorSubjectFormat(SelectorFormatId("indexed")) { + @Immutable + @Serializable + data class Config( + @Language("css") + val selectNames: String = ".search-box .thumb-content > .thumb-txt", + @Language("css") + val selectLinks: String = ".search-box .thumb-menu > a", + val preferShorterName: Boolean = true, + ) : SelectorFormatConfig { + override fun isValid(): Boolean { + return selectNames.isNotBlank() + } + } + + override fun select( + document: Element, + baseUrl: String, + config: Config, + ): List? { + val selectNames = QueryParser.parseSelectorOrNull(config.selectNames) ?: return null + val selectLinks = QueryParser.parseSelectorOrNull(config.selectLinks) ?: return null + + + val names: List = document.select(selectNames).fastMapNotNull { a -> + a.text().takeIf { it.isNotBlank() } + } + + val links = document.select(selectLinks).fastMapNotNull { a -> + val href = a.attr("href") + href.takeIf { it.isNotBlank() } + } + + return names.fastZipNotNullToMutable(links) { name, href -> + val id = href.substringBeforeLast(".html").substringAfterLast("/") + WebSearchSubjectInfo( + internalId = id, + name = name, + fullUrl = SelectorHelpers.computeAbsoluteUrl(baseUrl, href), + partialUrl = href, + origin = null, + ) + }.apply { + if (config.preferShorterName) { + sortBy { info -> + info.name.length + } + } + } + } +} + +private inline fun List.fastZipNotNullToMutable( + other: List, + transform: (a: T, b: R) -> V? +): MutableList { + contract { callsInPlace(transform) } + val minSize = minOf(size, other.size) + val target = ArrayList(minSize) + for (i in 0 until minSize) { + val res = transform(get(i), other[i]) + if (res != null) { + target += res + } + } + return target +} diff --git a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.android.kt b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.android.kt index 273b789b25..c46878cda5 100644 --- a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.android.kt +++ b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.android.kt @@ -41,7 +41,7 @@ fun rememberTestSelectorConfigurationState( @Preview private fun PreviewSelectorChannelConfigurationColumnNotFound() = ProvideFoundationCompositionLocalsForPreview { Surface { - SelectorChannelConfigurationColumn(SelectorFormatId("dummy"), rememberTestSelectorConfigurationState()) + SelectorChannelFormatColumn(SelectorFormatId("dummy"), rememberTestSelectorConfigurationState()) } } @@ -49,7 +49,7 @@ private fun PreviewSelectorChannelConfigurationColumnNotFound() = ProvideFoundat @Preview private fun PreviewSelectorChannelConfigurationColumnFlattened() = ProvideFoundationCompositionLocalsForPreview { Surface { - SelectorChannelConfigurationColumn(SelectorChannelFormatFlattened.id, rememberTestSelectorConfigurationState()) + SelectorChannelFormatColumn(SelectorChannelFormatFlattened.id, rememberTestSelectorConfigurationState()) } } @@ -57,6 +57,6 @@ private fun PreviewSelectorChannelConfigurationColumnFlattened() = ProvideFounda @Preview private fun PreviewSelectorChannelConfigurationColumnNoChannel() = ProvideFoundationCompositionLocalsForPreview { Surface { - SelectorChannelConfigurationColumn(SelectorChannelFormatNoChannel.id, rememberTestSelectorConfigurationState()) + SelectorChannelFormatColumn(SelectorChannelFormatNoChannel.id, rememberTestSelectorConfigurationState()) } } diff --git a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.android.kt b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.android.kt index aca4af2553..0c3a1c404e 100644 --- a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.android.kt +++ b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.android.kt @@ -74,10 +74,10 @@ class TestSelectorMediaSourceEngine : SelectorMediaSourceEngine() { override fun selectSubjects(document: Element, config: SelectorSearchConfig): List { return listOf( - WebSearchSubjectInfo("a", "Test Subject", "https://example.com", null), - WebSearchSubjectInfo("a", "Test Subject", "https://example.com", null), - WebSearchSubjectInfo("a", "Test Subject", "https://example.com", null), - WebSearchSubjectInfo("a", "Test Subject", "https://example.com", null), + WebSearchSubjectInfo("a", "Test Subject", "https://example.com/1.html", "1.html", null), + WebSearchSubjectInfo("a", "Test Subject", "https://example.com/2.html", "2.html", null), + WebSearchSubjectInfo("a", "Test Subject", "https://example.com/3.html", "3.html", null), + WebSearchSubjectInfo("a", "Test Subject", "https://example.com/4.html", "4.html", null), ) } diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelFormatColumn.kt similarity index 86% rename from app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.kt rename to app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelFormatColumn.kt index 468a9b606c..8f8448cc8c 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelConfigurationColumn.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorChannelFormatColumn.kt @@ -39,7 +39,7 @@ import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor import me.him188.ani.app.ui.settings.mediasource.MediaSourceConfigurationDefaults @Composable -internal fun SelectorChannelConfigurationColumn( +internal fun SelectorChannelFormatColumn( formatId: SelectorFormatId, state: SelectorConfigState, modifier: Modifier = Modifier, @@ -105,23 +105,26 @@ internal fun SelectorChannelConfigurationColumn( } null -> { - Column( - Modifier.align(Alignment.CenterHorizontally), - ) { - ProvideTextStyleContentColor(MaterialTheme.typography.bodyLarge, MaterialTheme.colorScheme.error) { - Icon( - Icons.Rounded.Error, null, - Modifier.align(Alignment.CenterHorizontally).size(48.dp), - ) - Text( - "当前版本不支持该配置类型:${formatId.value}\n\n这可能是导入了一个在更高版本编辑的配置导致的\n可升级 Ani 或切换到其他配置类型", - Modifier.padding(top = 24.dp), - textAlign = TextAlign.Center, - ) - } - } + UnsupportedFormatIdHint(formatId, Modifier.align(Alignment.CenterHorizontally)) } } } } +@Composable +internal fun UnsupportedFormatIdHint(formatId: SelectorFormatId, modifier: Modifier = Modifier) { + Column(modifier) { + ProvideTextStyleContentColor(MaterialTheme.typography.bodyLarge, MaterialTheme.colorScheme.error) { + Icon( + Icons.Rounded.Error, null, + Modifier.align(Alignment.CenterHorizontally).size(48.dp), + ) + Text( + "当前版本不支持该配置类型:${formatId.value}\n\n这可能是导入了一个在更高版本编辑的配置导致的\n可升级 Ani 或切换到其他配置类型", + Modifier.padding(top = 24.dp), + textAlign = TextAlign.Center, + ) + } + } +} + diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigurationDefaults.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigDefaults.kt similarity index 100% rename from app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigurationDefaults.kt rename to app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigDefaults.kt diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigurationPane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt similarity index 84% rename from app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigurationPane.kt rename to app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt index 8b1622ace4..229fa67009 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigurationPane.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt @@ -44,6 +44,9 @@ import androidx.compose.ui.unit.dp import me.him188.ani.app.data.source.media.source.web.format.SelectorChannelFormatFlattened import me.him188.ani.app.data.source.media.source.web.format.SelectorChannelFormatNoChannel import me.him188.ani.app.data.source.media.source.web.format.SelectorFormatId +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormat +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatA +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatIndexed import me.him188.ani.app.ui.foundation.animation.StandardEasing import me.him188.ani.app.ui.foundation.effects.moveFocusOnEnter import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor @@ -141,27 +144,25 @@ internal fun SelectorConfigurationPane( }, colors = listItemColors, ) + } - val conf = state.subjectFormatA - OutlinedTextField( - conf.selectLists, { conf.selectLists = it }, - Modifier.fillMaxWidth().moveFocusOnEnter().padding(top = verticalSpacing), - label = { Text("提取条目列表") }, - supportingText = { Text("CSS Selector 表达式。期望返回一些 ,每个对应一个条目,将会读取其 href 属性和 text") }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - shape = textFieldShape, - isError = conf.selectListsIsError, - ) - ListItem( - headlineContent = { Text("选择最短标题") }, - Modifier - .padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp)) - .clickable { conf.preferShorterName = !conf.preferShorterName }, - supportingContent = { Text("选择满足匹配的标题最短的条目。可避免为第一季匹配到第二季") }, - trailingContent = { - Switch(conf.preferShorterName, { conf.preferShorterName = it }) - }, - colors = listItemColors, + SelectorSubjectFormatSelectionButtonRow( + state, + Modifier.fillMaxWidth().padding(bottom = 4.dp), + ) + + AnimatedContent( + SelectorSubjectFormat.findById(state.subjectFormatId), + Modifier + .padding(vertical = 16.dp) + .fillMaxWidth() + .animateContentSize(tween(EasingDurations.standard, easing = StandardEasing)), + transitionSpec = AniThemeDefaults.standardAnimatedContentTransition, + ) { format -> + SelectorSubjectConfigurationColumn( + format, state, + textFieldShape, verticalSpacing, listItemColors, + Modifier.fillMaxWidth(), ) } @@ -187,7 +188,7 @@ internal fun SelectorConfigurationPane( .animateContentSize(tween(EasingDurations.standard, easing = StandardEasing)), transitionSpec = AniThemeDefaults.standardAnimatedContentTransition, ) { formatId -> - SelectorChannelConfigurationColumn(formatId, state, Modifier.fillMaxWidth()) + SelectorChannelFormatColumn(formatId, state, Modifier.fillMaxWidth()) } Row(Modifier.padding(top = verticalSpacing, bottom = 12.dp)) { @@ -276,6 +277,43 @@ internal fun SelectorConfigurationPane( } } + +@Composable +private fun SelectorSubjectFormatSelectionButtonRow( + state: SelectorConfigState, + modifier: Modifier = Modifier, +) { + SingleChoiceSegmentedButtonRow(modifier) { + @Composable + fun Btn( + id: SelectorFormatId, index: Int, + enabled: Boolean = true, + label: @Composable () -> Unit, + ) { + SegmentedButton( + state.subjectFormatId == id, + { state.subjectFormatId = id }, + SegmentedButtonDefaults.itemShape(index, state.allSubjectFormats.size), + icon = { SegmentedButtonDefaults.Icon(state.subjectFormatId == id) }, + label = label, + enabled = enabled, + ) + } + + for ((index, format) in state.allSubjectFormats.withIndex()) { + Btn(format.id, index) { + Text( + when (format) { // type-safe to handle all formats + SelectorSubjectFormatA -> "单标签" + SelectorSubjectFormatIndexed -> "多标签" + }, + softWrap = false, + ) + } + } + } +} + @Composable private fun SelectorChannelSelectionButtonRow( state: SelectorConfigState, diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigState.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigState.kt index 188d9649e5..65915dd8ab 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigState.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigState.kt @@ -17,7 +17,9 @@ import me.him188.ani.app.data.source.media.source.web.SelectorSearchConfig import me.him188.ani.app.data.source.media.source.web.format.SelectorChannelFormat import me.him188.ani.app.data.source.media.source.web.format.SelectorChannelFormatFlattened import me.him188.ani.app.data.source.media.source.web.format.SelectorChannelFormatNoChannel +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormat import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatA +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatIndexed import me.him188.ani.app.ui.settings.danmaku.isValidRegex import me.him188.ani.app.ui.settings.mediasource.rss.SaveableStorage import me.him188.ani.utils.xml.QueryParser @@ -60,6 +62,11 @@ class SelectorConfigState( // region SubjectFormat + val allSubjectFormats get() = SelectorSubjectFormat.entries + var subjectFormatId by argumentsStorage.prop( + { it.searchConfig.subjectFormatId }, { copy(searchConfig = searchConfig.copy(subjectFormatId = it)) }, + SelectorMediaSourceArguments.Default.searchConfig.subjectFormatId, + ) val subjectFormatA = SubjectFormatAConfig() @Stable @@ -86,6 +93,37 @@ class SelectorConfigState( var preferShorterName by prop({ it.preferShorterName }, { copy(preferShorterName = it) }) } + val subjectFormatIndex = SubjectFormatIndexedConfig() + + inner class SubjectFormatIndexedConfig { + private fun prop( + get: (SelectorSubjectFormatIndexed.Config) -> T, + set: SelectorSubjectFormatIndexed.Config.(T) -> SelectorSubjectFormatIndexed.Config, + ) = argumentsStorage.prop( + { it.searchConfig.selectorSubjectFormatIndexed.let(get) }, + { + copy( + searchConfig = searchConfig.copy( + selectorSubjectFormatIndexed = searchConfig.selectorSubjectFormatIndexed.set(it), + ), + ) + }, + SelectorMediaSourceArguments.Default.searchConfig.selectorSubjectFormatIndexed.let(get), + ) + + var selectNames by prop({ it.selectNames }, { copy(selectNames = it) }) + val selectNamesIsError by derivedStateOf { + QueryParser.parseSelectorOrNull(selectNames) == null + } + + var selectLinks by prop({ it.selectLinks }, { copy(selectLinks = it) }) + val selectLinksIsError by derivedStateOf { + QueryParser.parseSelectorOrNull(selectLinks) == null + } + + var preferShorterName by prop({ it.preferShorterName }, { copy(preferShorterName = it) }) + } + // endregion // region ChannelFormat diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorSubjectFormatColumn.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorSubjectFormatColumn.kt new file mode 100644 index 0000000000..7809adbca6 --- /dev/null +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorSubjectFormatColumn.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * + * https://github.com/open-ani/ani/blob/main/LICENSE + */ + +package me.him188.ani.app.ui.settings.mediasource.selector.edit + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormat +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatA +import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatIndexed +import me.him188.ani.app.ui.foundation.effects.moveFocusOnEnter + +@Composable +internal fun SelectorSubjectConfigurationColumn( + format: SelectorSubjectFormat<*>?, + state: SelectorConfigState, + textFieldShape: Shape, + verticalSpacing: Dp, + listItemColors: ListItemColors, + modifier: Modifier = Modifier, +) { + when (format) { + SelectorSubjectFormatA -> Column(modifier) { + Text( + "单个表达式,选取一些 ,根据其 title 属性或 text 确定名称,href 属性确定链接", + Modifier, + style = MaterialTheme.typography.bodyLarge, + ) + + val conf = state.subjectFormatA + OutlinedTextField( + conf.selectLists, { conf.selectLists = it }, + Modifier.fillMaxWidth().moveFocusOnEnter().padding(top = verticalSpacing), + label = { Text("提取条目列表") }, + supportingText = { Text("CSS Selector 表达式。期望返回一些 ,每个对应一个条目,将会读取其 href 属性和 text") }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + shape = textFieldShape, + isError = conf.selectListsIsError, + ) + ListItem( + headlineContent = { Text("选择最短标题") }, + Modifier + .padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp)) + .clickable { conf.preferShorterName = !conf.preferShorterName }, + supportingContent = { Text("选择满足匹配的标题最短的条目。可避免为第一季匹配到第二季") }, + trailingContent = { + Switch(conf.preferShorterName, { conf.preferShorterName = it }) + }, + colors = listItemColors, + ) + } + + SelectorSubjectFormatIndexed -> Column(modifier) { + Text( + "两个表达式,分别选取条目名称列表和链接列表,按顺序一一对应", + Modifier, + style = MaterialTheme.typography.bodyLarge, + ) + val conf = state.subjectFormatIndex + OutlinedTextField( + conf.selectNames, { conf.selectNames = it }, + Modifier.fillMaxWidth().moveFocusOnEnter().padding(top = verticalSpacing), + label = { Text("提取条目列表") }, + supportingText = { Text("CSS Selector 表达式。选取条目名称列表") }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + shape = textFieldShape, + isError = conf.selectNamesIsError, + ) + OutlinedTextField( + conf.selectLinks, { conf.selectLinks = it }, + Modifier.fillMaxWidth().moveFocusOnEnter().padding(top = verticalSpacing), + label = { Text("提取条目列表") }, + supportingText = { Text("CSS Selector 表达式。选取链接列表") }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + shape = textFieldShape, + isError = conf.selectLinksIsError, + ) + ListItem( + headlineContent = { Text("选择最短标题") }, + Modifier + .padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp)) + .clickable { conf.preferShorterName = !conf.preferShorterName }, + supportingContent = { Text("选择满足匹配的标题最短的条目。可避免为第一季匹配到第二季") }, + trailingContent = { + Switch(conf.preferShorterName, { conf.preferShorterName = it }) + }, + colors = listItemColors, + ) + } + + null -> Column(modifier) { + UnsupportedFormatIdHint( + state.subjectFormatId, + Modifier.align(Alignment.CenterHorizontally), + ) + } + } +} diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.kt index 73d220f210..0f1211f4f8 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestPane.kt @@ -12,14 +12,22 @@ package me.him188.ani.app.ui.settings.mediasource.selector.test import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowOutward +import androidx.compose.material.icons.rounded.Link import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -28,6 +36,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import me.him188.ani.app.ui.foundation.interaction.nestedScrollWorkaround import me.him188.ani.app.ui.foundation.layout.ConnectedScrollState @@ -38,6 +49,7 @@ import me.him188.ani.app.ui.foundation.layout.only import me.him188.ani.app.ui.foundation.layout.rememberConnectedScrollState import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults import me.him188.ani.app.ui.foundation.widgets.FastLinearProgressIndicator +import me.him188.ani.app.ui.foundation.widgets.LocalToaster import me.him188.ani.app.ui.settings.mediasource.EditMediaSourceTestDataCardDefaults import me.him188.ani.app.ui.settings.mediasource.RefreshIndicatedHeadlineRow import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigurationDefaults @@ -112,8 +124,8 @@ fun SharedTransitionScope.SelectorTestPane( state.selectedSubject, Modifier.padding(contentPadding.only(PaddingValuesSides.Horizontal)), transitionSpec = AniThemeDefaults.standardAnimatedContentTransition, - ) { selectedSubjectIndex -> - if (selectedSubjectIndex != null) { + ) { selectedSubject -> + if (selectedSubject != null) { Column { RefreshIndicatedHeadlineRow( headline = { Text(SelectorConfigurationDefaults.STEP_NAME_2) }, @@ -122,6 +134,36 @@ fun SharedTransitionScope.SelectorTestPane( Modifier.padding(top = verticalSpacing), ) + val url = state.selectedSubject?.subjectDetailsPageUrl ?: "" + val clipboard = LocalClipboardManager.current + val toaster = LocalToaster.current + Row( + Modifier.fillMaxWidth() + .clickable(onClickLabel = "复制条目链接") { + clipboard.setText(AnnotatedString(url)) + toaster.toast("已复制") + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Rounded.Link, + contentDescription = null, + Modifier.padding(end = 16.dp).size(24.dp), + ) + Text( + url, + Modifier.weight(1f), + style = MaterialTheme.typography.bodyLarge, + ) + val uriHandler = LocalUriHandler.current + IconButton({ uriHandler.openUri(url) }, Modifier.padding(start = 8.dp)) { + Icon( + Icons.Rounded.ArrowOutward, + contentDescription = "打开条目页面", + ) + } + } + Box(Modifier.height(12.dp), contentAlignment = Alignment.Center) { FastLinearProgressIndicator( state.episodeListSearcher.isSearching, diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSearchSubjectResult.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSearchSubjectResult.kt index 6f54bfc5ae..70dc8b854e 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSearchSubjectResult.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSearchSubjectResult.kt @@ -62,7 +62,7 @@ data class SelectorTestSubjectPresentation( val tags = computeTags(info, query, config) return SelectorTestSubjectPresentation( name = info.name, - subjectDetailsPageUrl = info.subjectDetailsPageUrl, + subjectDetailsPageUrl = info.fullUrl, origin = origin, tags = tags, ) @@ -82,6 +82,11 @@ data class SelectorTestSubjectPresentation( emit("标题", isMatch = true) } } + if (info.fullUrl.isBlank()) { + emit("链接", isMissing = true) + } else { + emit(info.partialUrl, isMatch = true) + } } } } diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SubjectResultLazyRow.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSubjectResultLazyRow.kt similarity index 94% rename from app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SubjectResultLazyRow.kt rename to app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSubjectResultLazyRow.kt index 6703053e99..b5e8a5b214 100644 --- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SubjectResultLazyRow.kt +++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/test/SelectorTestSubjectResultLazyRow.kt @@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRowScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -50,7 +50,10 @@ internal fun SelectorTestSubjectResultLazyRow( title = { Text("1\n2") }, isSelected = false, onClick = {}, - tags = { Tag { Text("Dummy") } }, + tags = { + Tag { Text("Dummy") } + Tag { Text("https://example.com/example/example") } + }, ) } val itemSize = measurable.measure(constraints) @@ -61,7 +64,7 @@ internal fun SelectorTestSubjectResultLazyRow( item, selectedItemIndex == index, onClick = { onSelect(index, item) }, - Modifier.height(itemSize.height.toDp()), // 使用固定高度 + Modifier.heightIn(min = itemSize.height.toDp()), ) } }