Skip to content

Commit

Permalink
新搜索以及导航 (#1069)
Browse files Browse the repository at this point in the history
* Add SubjectItemLayout

* SubjectSearchRepository: Don't expose database entity

* new search page (WIP)

* Rename AdaptiveSearchBar file

* AniListDetailPaneScaffold: add back handler

* Fix ExplorationPage.android.kt filename

* wip

* Mark createTestSearchPageState with `@TestOnly`

* extract state

* Fix calculating ListDetailLayoutParameters for COMPACT devices

* Add general Search implementation

* subject search page

* fix search

* fix subject item layout

* improve PopupSearchBar corner size animation

* Add FAB in navigation rail

* fix layout

* fix tests

* CI: Automatically rerun "Clean and download dependencies"

* Move search to MainScene

* Use typesafe navigation

* fix navigation

* fix navigation animation and color

* fix settings page shape

* use ExplorationPageViewModel directly

* Remove search bar from exploration page

* fix search bar layout on phone

* make SubjectCollectionsColumn dummy items not empty to fix animation problems

* Align SubjectDetailsPage background color with design

* fix search page back

* 将设置移动到头像里

* 在收藏页也添加设置按钮

* fix settings page window insets on android

* Fix settings page default tab on android

* 大屏设备条目详情页采用 fade

* 搜索页删除设置按钮

* Remove unused UpdateCheckerItem

* fixup! 大屏设备条目详情页采用 fade

* fix search bar expand on desktop

* improve TrendingSubjectsCarousel item size
  • Loading branch information
Him188 authored Oct 19, 2024
1 parent 2a64cd4 commit 3f69af9
Show file tree
Hide file tree
Showing 55 changed files with 2,651 additions and 2,070 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ jobs:
run: chmod -R 777 *

- name: Clean and download dependencies
run: ./gradlew clean ${{ env.gradleArgs }}
if: ${{ matrix.run_tests }}
uses: nick-fields/retry@v2
with:
max_attempts: 3
timeout_minutes: 60
command: ./gradlew clean ${{ env.gradleArgs }}

- name: Update version name
run: ./gradlew updateDevVersionNameFromGit ${{ env.gradleArgs }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ data class SubjectAiringInfo(
}
}

// Mainly for SubjectAiringLabel
@Stable
fun SubjectAiringInfo.computeTotalEpisodeText(): String? {
return if (kind == SubjectAiringKind.UPCOMING && episodeCount == 0) {
// 剧集还未知
null
} else {
when (kind) {
SubjectAiringKind.COMPLETED -> "$episodeCount"

SubjectAiringKind.ON_AIR,
SubjectAiringKind.UPCOMING,
-> "预定全 $episodeCount"
}
}
}

/**
* 正在播出 (第一集还未开播, 将在未来开播)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/*
* 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.data.models.subject

import androidx.compose.runtime.Immutable
Expand Down Expand Up @@ -45,6 +54,7 @@ data class SubjectInfo(
) {
/**
* 放送开始
* @sample me.him188.ani.app.ui.subject.renderSubjectSeason
*/
val airDate: PackedDate = if (airDateString == null) PackedDate.Invalid else PackedDate.parseFromDate(airDateString)

Expand Down Expand Up @@ -110,13 +120,6 @@ val SubjectInfo.totalEpisodesOrEps: Int
}


@Serializable
@Immutable
class Tag(
val name: String,
val count: Int,
)

@Serializable
@Immutable
class InfoboxItem(
Expand Down
176 changes: 176 additions & 0 deletions app/shared/app-data/src/commonMain/kotlin/data/models/subject/Tag.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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.data.models.subject

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlinx.serialization.Serializable

@Serializable
@Immutable
class Tag(
val name: String,
val count: Int,
)

/**
* 是否为公共标签.
*/
@Stable
val Tag.isCanonical: Boolean
get() = CanonicalTagKind.matchOrNull(name) != null

/**
* 获取标签的种类, 如果不是公共标签则返回 `null`.
*/
@Stable
val Tag.kind: CanonicalTagKind?
get() = CanonicalTagKind.matchOrNull(name)

/**
* Bangumi 定义的公共标签种类.
*/
// https://bgm.tv/wiki/tag/list
// Generated by GPT-4o
// Use list because we want to keep the order
@Immutable
sealed class CanonicalTagKind(val values: List<String>) {
/**
* 分类
*/
@Immutable
data object Category : CanonicalTagKind(
listOf("短片", "剧场版", "TV", "OVA", "MV", "CM", "WEB", "PV", "动态漫画"),
)

/**
* 来源
*/
@Immutable
data object Source : CanonicalTagKind(
listOf("原创", "漫画改", "游戏改", "小说改"),
)

/**
* 类型
*/
@Immutable
data object Genre : CanonicalTagKind(
listOf(
"科幻", "喜剧", "百合", "校园", "惊悚", "后宫", "机战", "悬疑", "恋爱", "奇幻",
"推理", "运动", "耽美", "音乐", "战斗", "冒险", "萌系", "穿越", "玄幻", "乙女",
"恐怖", "历史", "日常", "剧情", "武侠", "美食", "职场",
),
)

/**
* 地区
*/
@Immutable
data object Region : CanonicalTagKind(
listOf(
"欧美", "日本", "美国", "中国", "法国", "韩国", "俄罗斯", "英国",
"苏联", "香港", "捷克", "台湾",
),
)

/**
* 分级
*/
@Immutable
data object Rating : CanonicalTagKind(
listOf("R18"),
)

/**
* 受众
*/
@Immutable
data object Audience : CanonicalTagKind(
listOf("BL", "GL", "子供向", "女性向", "少女向", "少年向", "青年向"),
)

/**
* 设定
*/
@Immutable
data object Setting : CanonicalTagKind(
listOf(
"魔法少女", "超能力", "偶像", "网游", "末世", "乐队",
"赛博朋克", "宫廷", "都市", "异世界", "性转", "龙傲天", "凤傲天",
),
)

/**
* 角色
*/
@Immutable
data object Character : CanonicalTagKind(
listOf(
"制服", "兽耳", "伪娘", "吸血鬼", "妹控", "萝莉", "傲娇", "女仆", "巨乳", "电波",
"动物", "正太", "兄控", "僵尸", "群像", "美少女", "美少年",
),
)

/**
* 情绪
*/
@Immutable
data object Emotion : CanonicalTagKind(
listOf("热血", "治愈", "温情", "催泪", "纯爱", "友情", "致郁"),
)

/**
* 技术
*/
@Immutable
data object Technology : CanonicalTagKind(
listOf("黑白", "3D", "水墨", "定格", "粘土", "剪纸", "转描", "三渲二"),
)

/**
* 系列
*/
@Immutable
data object Series : CanonicalTagKind(
listOf(
"高达", "东方", "Fate", "空之境界", "柯南", "光之美少女", "哆啦A梦",
"物语系列", "刀剑神域", "进击的巨人",
),
)

@Stable
companion object {
@Stable
val entries by lazy {
listOf(
Category, Source, Genre, Region, Rating, Audience,
Setting, Character, Emotion, Technology, Series,
)
}

// A map to directly associate tags with their corresponding CanonicalTagKind for fast querying
private val tagMap: Map<String, CanonicalTagKind> by lazy {
val entries = entries
buildMap {
for (kind in entries) {
for (value in kind.values) {
put(value, kind)
}
}
}
}

/**
* 获取匹配的 [CanonicalTagKind], 如果没有匹配则返回 `null`.
*/
fun matchOrNull(tag: String): CanonicalTagKind? = tagMap[tag]
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
/*
* 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.data.repository

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.him188.ani.app.data.persistent.database.dao.SearchHistoryDao
import me.him188.ani.app.data.persistent.database.dao.SearchTagDao
import me.him188.ani.app.data.persistent.database.eneity.SearchHistoryEntity
Expand All @@ -9,11 +19,11 @@ import org.koin.core.component.KoinComponent

interface SubjectSearchRepository {
suspend fun addHistory(history: SearchHistoryEntity)
fun getHistoryFlow(): Flow<List<SearchHistoryEntity>>
fun getHistoryFlow(): Flow<List<String>>
suspend fun deleteHistoryBySeq(seq: Int)

suspend fun addTag(tag: SearchTagEntity)
fun getTagFlow(): Flow<List<SearchTagEntity>>
fun getTagFlow(): Flow<List<String>>
suspend fun deleteTagByName(content: String)
suspend fun increaseCountByName(content: String)
suspend fun deleteTagById(id: Int)
Expand All @@ -28,8 +38,8 @@ class SubjectSearchRepositoryImpl(
searchHistory.insert(history)
}

override fun getHistoryFlow(): Flow<List<SearchHistoryEntity>> {
return searchHistory.getFlow()
override fun getHistoryFlow(): Flow<List<String>> {
return searchHistory.getFlow().map { list -> list.map { it.content } }
}

override suspend fun deleteHistoryBySeq(seq: Int) {
Expand All @@ -40,8 +50,8 @@ class SubjectSearchRepositoryImpl(
searchTag.insert(tag)
}

override fun getTagFlow(): Flow<List<SearchTagEntity>> {
return searchTag.getFlow()
override fun getTagFlow(): Flow<List<String>> {
return searchTag.getFlow().map { list -> list.map { it.content } }
}

override suspend fun deleteTagByName(content: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,40 @@

package me.him188.ani.app.domain.search

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import me.him188.ani.app.data.models.ApiResponse
import me.him188.ani.app.data.models.subject.SubjectInfo
import me.him188.ani.app.tools.ldc.LazyDataCache
import me.him188.ani.app.ui.foundation.BackgroundScope
import me.him188.ani.app.ui.foundation.HasBackgroundScope
import kotlin.coroutines.CoroutineContext

class SubjectSearcher(
interface SubjectSearcher {
/**
* 唯一 ID, 每次调用 [search] 时增加
*/
val searchId: StateFlow<Int>

val list: Flow<List<SubjectInfo>>
val hasMore: Flow<Boolean>

suspend fun requestMore(): Boolean?
fun clear()
fun search(query: SubjectSearchQuery)
}

class SubjectSearcherImpl(
private val subjectProvider: SubjectProvider,
parentCoroutineContext: CoroutineContext,
) : HasBackgroundScope by BackgroundScope(parentCoroutineContext) {
) : SubjectSearcher, HasBackgroundScope by BackgroundScope(parentCoroutineContext) {
private val currentQuery: MutableStateFlow<SubjectSearchQuery?> = MutableStateFlow(null)

private val ldc = currentQuery.map { query ->
Expand All @@ -35,18 +53,20 @@ class SubjectSearcher(
debugName = "SubjectSearcher.ldc",
)
}.shareInBackground(started = SharingStarted.Lazily)
override val searchId: MutableStateFlow<Int> = MutableStateFlow(0)

val list = ldc.flatMapLatest { it?.cachedDataFlow ?: flowOf(emptyList()) }
val hasMore = ldc.flatMapLatest { it?.isCompleted ?: flowOf(true) }
override val list: Flow<List<SubjectInfo>> = ldc.flatMapLatest { it?.cachedDataFlow ?: flowOf(emptyList()) }
override val hasMore: Flow<Boolean> = ldc.flatMapLatest { it?.isCompleted ?: flowOf(true) }
.map { !it }

suspend fun requestMore() = ldc.first()?.requestMore()
override suspend fun requestMore() = ldc.first()?.requestMore()

fun clear() {
override fun clear() {
currentQuery.value = null
}

fun search(query: SubjectSearchQuery) {
override fun search(query: SubjectSearchQuery) {
searchId.update { it + 1 }
currentQuery.value = query
}
}
Loading

0 comments on commit 3f69af9

Please sign in to comment.