Skip to content

Commit 692036f

Browse files
committed
[feature] Support searching media library files
1 parent 3caa379 commit 692036f

24 files changed

+572
-50
lines changed

app/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ android {
2222
minSdk = 24
2323
targetSdk = 35
2424
versionCode = 26
25-
versionName = "3.1-beta04"
25+
versionName = "3.1-beta05"
2626

2727
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2828

app/src/main/java/com/skyd/anivu/ext/StringExt.kt

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ fun String.toDecodedUrl(): String {
2020

2121
fun String.toRemoveHtml(): String = parseAsHtml().toString()
2222

23+
fun CharSequence.splitByBlank(limit: Int = 0): List<String> = trim().split("\\s+".toRegex(), limit)
24+
2325
fun String.readable(): String =
2426
Readability4JExtended("", this).parse().textContent?.trim().orEmpty()
2527

app/src/main/java/com/skyd/anivu/model/db/dao/GroupDao.kt

-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.skyd.anivu.model.db.dao
22

33
import androidx.paging.PagingSource
4-
import androidx.room.ColumnInfo
54
import androidx.room.Dao
65
import androidx.room.Insert
76
import androidx.room.OnConflictStrategy
@@ -140,13 +139,6 @@ interface GroupDao {
140139
)
141140
fun queryGroupIdByName(name: String): String
142141

143-
data class TypeToId(
144-
@ColumnInfo("type")
145-
val type: String,
146-
@ColumnInfo("_id")
147-
val id: String?,
148-
)
149-
150142
@Transaction
151143
@Query(
152144
"WITH default_group_order(`order`) AS (SELECT COALESCE(MIN(`${GroupBean.ORDER_POSITION_COLUMN}`), 0) - $ORDER_DELTA FROM `$GROUP_TABLE_NAME`) " +

app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt

+85-19
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import androidx.compose.ui.util.fastFirstOrNull
55
import com.skyd.anivu.appContext
66
import com.skyd.anivu.base.BaseRepository
77
import com.skyd.anivu.ext.dataStore
8+
import com.skyd.anivu.ext.splitByBlank
89
import com.skyd.anivu.ext.validateFileName
910
import com.skyd.anivu.model.bean.MediaBean
1011
import com.skyd.anivu.model.bean.MediaGroupBean
1112
import com.skyd.anivu.model.bean.MediaGroupBean.Companion.isDefaultGroup
13+
import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean
14+
import com.skyd.anivu.model.bean.feed.FeedBean
1215
import com.skyd.anivu.model.db.dao.ArticleDao
1316
import com.skyd.anivu.model.db.dao.FeedDao
1417
import com.skyd.anivu.model.preference.appearance.media.MediaFileFilterPreference
@@ -20,10 +23,14 @@ import com.skyd.anivu.model.preference.behavior.media.MediaSubListSortByPreferen
2023
import kotlinx.coroutines.Dispatchers
2124
import kotlinx.coroutines.flow.Flow
2225
import kotlinx.coroutines.flow.MutableSharedFlow
26+
import kotlinx.coroutines.flow.asFlow
27+
import kotlinx.coroutines.flow.collect
2328
import kotlinx.coroutines.flow.combine
29+
import kotlinx.coroutines.flow.debounce
2430
import kotlinx.coroutines.flow.distinctUntilChanged
2531
import kotlinx.coroutines.flow.filter
2632
import kotlinx.coroutines.flow.first
33+
import kotlinx.coroutines.flow.flatMapLatest
2734
import kotlinx.coroutines.flow.flow
2835
import kotlinx.coroutines.flow.flowOf
2936
import kotlinx.coroutines.flow.flowOn
@@ -49,7 +56,7 @@ class MediaRepository @Inject constructor(
4956
const val OLD_MEDIA_LIB_JSON_NAME = "group.json"
5057
const val MEDIA_LIB_JSON_NAME = "MediaLib.json"
5158

52-
private val mediaLibJsons = LruCache<String, MediaLibJson>(maxSize = 5)
59+
private val mediaLibJsons = LruCache<String, MediaLibJson>(maxSize = 10)
5360

5461
private val refreshPath = MutableSharedFlow<String>(extraBufferCapacity = Int.MAX_VALUE)
5562
}
@@ -169,6 +176,31 @@ class MediaRepository @Inject constructor(
169176
}
170177
}
171178

179+
private fun FileJson.toMediaBean(
180+
path: String,
181+
articleWithEnclosure: ArticleWithEnclosureBean?,
182+
feedBean: FeedBean?,
183+
): MediaBean? {
184+
val file = File(path, fileName)
185+
if (!file.exists()) return null
186+
val fileCount = if (file.isDirectory) {
187+
runCatching { file.list()?.size }.getOrNull()?.run {
188+
this - listOf(
189+
File(file, MEDIA_LIB_JSON_NAME).exists(),
190+
File(file, FOLDER_INFO_JSON_NAME).exists(),
191+
).count { it }
192+
} ?: 0
193+
} else 0
194+
195+
return MediaBean(
196+
displayName = displayName,
197+
file = file,
198+
fileCount = fileCount,
199+
articleWithEnclosure = articleWithEnclosure,
200+
feedBean = feedBean,
201+
)
202+
}
203+
172204
fun requestGroups(path: String): Flow<List<MediaGroupBean>> = merge(
173205
flowOf(path), refreshFiles, refreshPath
174206
).filter { it == path }.map {
@@ -205,21 +237,8 @@ class MediaRepository @Inject constructor(
205237
).associateBy { it.feed.url }
206238

207239
jsons.mapNotNull { fileJson ->
208-
val file = File(path, fileJson.fileName)
209-
if (!file.exists()) return@mapNotNull null
210-
val fileCount = if (file.isDirectory) {
211-
runCatching { file.list()?.size }.getOrNull()?.run {
212-
this - listOf(
213-
File(file, MEDIA_LIB_JSON_NAME).exists(),
214-
File(file, FOLDER_INFO_JSON_NAME).exists(),
215-
).count { it }
216-
} ?: 0
217-
} else 0
218-
219-
MediaBean(
220-
displayName = fileJson.displayName,
221-
file = file,
222-
fileCount = fileCount,
240+
fileJson.toMediaBean(
241+
path = path,
223242
articleWithEnclosure = articleMap[fileJson.articleId]?.articleWithEnclosure,
224243
feedBean = feedMap[fileJson.feedUrl]?.feed
225244
?: articleMap[fileJson.articleId]?.feed,
@@ -267,6 +286,51 @@ class MediaRepository @Inject constructor(
267286
if (sortAsc) list else list.reversed()
268287
}.flowOn(Dispatchers.IO)
269288

289+
fun search(
290+
path: String,
291+
query: String,
292+
recursive: Boolean = false,
293+
): Flow<List<MediaBean>> = flowOf(
294+
query.trim() to recursive
295+
).flatMapLatest { (query, recursive) ->
296+
merge(
297+
flowOf(path), refreshFiles, refreshPath
298+
).debounce(70).filter { it == path }.map {
299+
val queries = query.splitByBlank()
300+
301+
val fileJsons = mutableListOf<FileJson>()
302+
File(path).walkBottomUp().onEnter { dir ->
303+
dir.path == path || recursive
304+
}.filter { it.isDirectory }.asFlow().map {
305+
val mediaLibJson = getOrReadMediaLibJson(it.path)
306+
fileJsons += mediaLibJson.files
307+
}.collect()
308+
309+
val articleMap = articleDao.getArticleListByIds(
310+
fileJsons.mapNotNull { it.articleId }
311+
).associateBy { it.articleWithEnclosure.article.articleId }
312+
val feedMap = feedDao.getFeedsIn(
313+
fileJsons.mapNotNull { it.feedUrl }
314+
).associateBy { it.feed.url }
315+
316+
fileJsons.filter { file ->
317+
queries.any {
318+
it in file.fileName ||
319+
it in file.displayName.orEmpty() ||
320+
it in file.feedUrl.orEmpty() ||
321+
it in file.articleLink.orEmpty()
322+
}
323+
}.mapNotNull { fileJson ->
324+
fileJson.toMediaBean(
325+
path = path,
326+
articleWithEnclosure = articleMap[fileJson.articleId]?.articleWithEnclosure,
327+
feedBean = feedMap[fileJson.feedUrl]?.feed
328+
?: articleMap[fileJson.articleId]?.feed,
329+
)
330+
}
331+
}
332+
}.flowOn(Dispatchers.IO)
333+
270334
fun deleteFile(file: File): Flow<Boolean> {
271335
return flow {
272336
val path = file.parentFile!!.path
@@ -322,11 +386,12 @@ class MediaRepository @Inject constructor(
322386

323387
val realGroupName = if (group.isDefaultGroup()) null else group.name
324388
val realArticleId = articleId.takeIf { !it.isNullOrBlank() }
325-
val realDisplayName = displayName.takeIf { !it.isNullOrBlank() }
326-
val article = articleId?.let { articleDao.getArticleWithFeed(it).first() }
389+
val article = realArticleId?.let { articleDao.getArticleWithFeed(it).first() }
327390
val articleLink = article?.articleWithEnclosure?.article?.link
328391
val articleGuid = article?.articleWithEnclosure?.article?.guid
329392
val feedUrl = article?.feed?.url
393+
val realDisplayName = displayName.takeIf { !it.isNullOrBlank() }
394+
?: article?.articleWithEnclosure?.article?.title
330395

331396
val path = file.parentFile!!.path
332397
var mediaLibJson = getOrReadMediaLibJson(path = path)
@@ -403,7 +468,8 @@ class MediaRepository @Inject constructor(
403468

404469
val realGroupName = if (group.isDefaultGroup()) null else group.name
405470
val realFeedUrl = feedUrl.takeIf { !it.isNullOrBlank() }
406-
val realDisplayName = displayName.takeIf { !it.isNullOrBlank() }
471+
val feed = realFeedUrl?.let { feedDao.getFeed(it) }
472+
val realDisplayName = displayName.takeIf { !it.isNullOrBlank() } ?: feed?.feed?.title
407473

408474
val path = file.parentFile!!.path
409475
var mediaLibJson = getOrReadMediaLibJson(path = path)

app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.skyd.anivu.base.BaseRepository
1010
import com.skyd.anivu.config.allSearchDomain
1111
import com.skyd.anivu.ext.dataStore
1212
import com.skyd.anivu.ext.getOrDefault
13+
import com.skyd.anivu.ext.splitByBlank
1314
import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME
1415
import com.skyd.anivu.model.bean.article.ArticleBean
1516
import com.skyd.anivu.model.bean.article.ArticleWithFeed
@@ -66,7 +67,8 @@ class SearchRepository @Inject constructor(
6667
): Flow<PagingData<ArticleWithFeed>> {
6768
return searchQuery.debounce(70).flatMapLatest { query ->
6869
Pager(pagingConfig) {
69-
articleDao.getArticlePagingSource(genSql(
70+
articleDao.getArticlePagingSource(
71+
genSql(
7072
tableName = ARTICLE_TABLE_NAME,
7173
k = query,
7274
leadingFilter = buildString {
@@ -130,7 +132,7 @@ class SearchRepository @Inject constructor(
130132
val sql = buildString {
131133
if (intersectSearchBySpace) {
132134
// 以多个连续的空格/制表符/换行符分割
133-
val keywords = k.trim().split("\\s+".toRegex()).toSet()
135+
val keywords = k.splitByBlank().toSet()
134136

135137
keywords.forEachIndexed { i, s ->
136138
if (i > 0) append("INTERSECT \n")

app/src/main/java/com/skyd/anivu/ui/activity/MainActivity.kt

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ import com.skyd.anivu.ui.screen.filepicker.PATH_KEY
9898
import com.skyd.anivu.ui.screen.filepicker.PICK_FOLDER_KEY
9999
import com.skyd.anivu.ui.screen.history.HISTORY_SCREEN_ROUTE
100100
import com.skyd.anivu.ui.screen.history.HistoryScreen
101+
import com.skyd.anivu.ui.screen.media.search.MEDIA_SEARCH_SCREEN_ROUTE
102+
import com.skyd.anivu.ui.screen.media.search.MediaSearchScreen
103+
import com.skyd.anivu.ui.screen.media.search.SEARCH_PATH_KEY
101104
import com.skyd.anivu.ui.screen.media.sub.SUB_MEDIA_SCREEN_MEDIA_KEY
102105
import com.skyd.anivu.ui.screen.media.sub.SUB_MEDIA_SCREEN_ROUTE
103106
import com.skyd.anivu.ui.screen.media.sub.SubMediaScreenRoute
@@ -377,6 +380,9 @@ private fun MainNavHost() {
377380
} ?: SearchDomain.Feed
378381
SearchScreen(searchDomain = searchDomain)
379382
}
383+
composable(route = MEDIA_SEARCH_SCREEN_ROUTE) {
384+
MediaSearchScreen(path = it.arguments?.getString(SEARCH_PATH_KEY)!!)
385+
}
380386
composable(route = SUB_MEDIA_SCREEN_ROUTE) {
381387
SubMediaScreenRoute(
382388
media = it.arguments?.let { arguments ->

app/src/main/java/com/skyd/anivu/ui/mpv/port/MediaArea.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Box
55
import androidx.compose.foundation.layout.aspectRatio
66
import androidx.compose.foundation.layout.fillMaxSize
77
import androidx.compose.foundation.layout.fillMaxWidth
8-
import androidx.compose.foundation.layout.height
98
import androidx.compose.foundation.layout.padding
109
import androidx.compose.foundation.shape.RoundedCornerShape
1110
import androidx.compose.material3.Card
@@ -41,7 +40,7 @@ internal fun MediaArea(
4140
.animateContentSize(),
4241
) {
4342
if (isVideo) {
44-
Box(modifier = Modifier.height(200.dp)) { playerContent() }
43+
Box(modifier = Modifier.aspectRatio(1f)) { playerContent() }
4544
} else {
4645
Thumbnail(playState.thumbnail ?: playState.mediaThumbnail ?: playState.thumbnailAny)
4746
}

app/src/main/java/com/skyd/anivu/ui/screen/feed/FeedScreen.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,6 @@ private fun FeedList(
221221
style = PodAuraTopBarStyle.Small,
222222
title = { Text(text = stringResource(id = R.string.feed_screen_name)) },
223223
actions = {
224-
PodAuraIconButton(
225-
onClick = { onShowArticleListByFeedUrls(emptyList()) },
226-
imageVector = Icons.AutoMirrored.Outlined.Article,
227-
contentDescription = stringResource(id = R.string.feed_screen_all_articles),
228-
)
229224
PodAuraIconButton(
230225
onClick = {
231226
openSearchScreen(
@@ -236,6 +231,11 @@ private fun FeedList(
236231
imageVector = Icons.Outlined.Search,
237232
contentDescription = stringResource(id = R.string.feed_screen_search_feed),
238233
)
234+
PodAuraIconButton(
235+
onClick = { onShowArticleListByFeedUrls(emptyList()) },
236+
imageVector = Icons.AutoMirrored.Outlined.Article,
237+
contentDescription = stringResource(id = R.string.feed_screen_all_articles),
238+
)
239239
PodAuraIconButton(
240240
onClick = { openMoreMenu = true },
241241
imageVector = Icons.Outlined.MoreVert,

app/src/main/java/com/skyd/anivu/ui/screen/media/MediaScreen.kt

+19-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.outlined.MoreVert
2222
import androidx.compose.material.icons.outlined.MyLocation
2323
import androidx.compose.material.icons.outlined.Palette
2424
import androidx.compose.material.icons.outlined.Refresh
25+
import androidx.compose.material.icons.outlined.Search
2526
import androidx.compose.material.icons.outlined.Workspaces
2627
import androidx.compose.material3.DropdownMenu
2728
import androidx.compose.material3.DropdownMenuItem
@@ -82,6 +83,7 @@ import com.skyd.anivu.ui.screen.filepicker.ListenToFilePicker
8283
import com.skyd.anivu.ui.screen.filepicker.openFilePicker
8384
import com.skyd.anivu.ui.screen.media.list.GroupInfo
8485
import com.skyd.anivu.ui.screen.media.list.MediaList
86+
import com.skyd.anivu.ui.screen.media.search.openMediaSearchScreen
8587
import com.skyd.anivu.ui.screen.settings.appearance.media.MEDIA_STYLE_SCREEN_ROUTE
8688
import kotlinx.coroutines.launch
8789
import java.io.File
@@ -161,10 +163,10 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) {
161163
actions = {
162164
PodAuraIconButton(
163165
onClick = {
164-
openFilePicker(navController = navController, path = path)
166+
openMediaSearchScreen(navController = navController, path = path)
165167
},
166-
imageVector = Icons.Outlined.MyLocation,
167-
contentDescription = stringResource(id = R.string.data_screen_media_lib_location),
168+
imageVector = Icons.Outlined.Search,
169+
contentDescription = stringResource(id = R.string.media_screen_search_hint),
168170
)
169171
PodAuraIconButton(
170172
onClick = { showSortMediaDialog = true },
@@ -180,6 +182,9 @@ fun MediaScreen(path: String, viewModel: MediaViewModel = hiltViewModel()) {
180182
expanded = openMoreMenu,
181183
onDismissRequest = { openMoreMenu = false },
182184
onRefresh = { dispatch(MediaIntent.RefreshGroup(path)) },
185+
onChangeLibLocation = {
186+
openFilePicker(navController = navController, path = path)
187+
}
183188
)
184189
}
185190
)
@@ -390,9 +395,20 @@ private fun MoreMenu(
390395
expanded: Boolean,
391396
onDismissRequest: () -> Unit,
392397
onRefresh: () -> Unit,
398+
onChangeLibLocation: () -> Unit,
393399
) {
394400
val navController = LocalNavController.current
395401
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
402+
DropdownMenuItem(
403+
text = { Text(text = stringResource(R.string.data_screen_change_lib_location)) },
404+
leadingIcon = {
405+
Icon(imageVector = Icons.Outlined.MyLocation, contentDescription = null)
406+
},
407+
onClick = {
408+
onChangeLibLocation()
409+
onDismissRequest()
410+
},
411+
)
396412
DropdownMenuItem(
397413
text = { Text(text = stringResource(R.string.media_screen_refresh_group)) },
398414
leadingIcon = {

app/src/main/java/com/skyd/anivu/ui/screen/media/list/Media1Item.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ fun Media1Item(
9898
}
9999
},
100100
)
101-
.padding(horizontal = 16.dp, vertical = 10.dp),
101+
.padding(horizontal = 13.dp, vertical = 10.dp),
102102
verticalAlignment = Alignment.CenterVertically,
103103
) {
104104
Box(
@@ -145,7 +145,7 @@ fun Media1Item(
145145
fileIcon()
146146
}
147147
}
148-
Spacer(modifier = Modifier.width(10.dp))
148+
Spacer(modifier = Modifier.width(11.dp))
149149
Column {
150150
Row(verticalAlignment = Alignment.CenterVertically) {
151151
Text(
@@ -164,7 +164,7 @@ fun Media1Item(
164164
Row(verticalAlignment = Alignment.CenterVertically) {
165165
if (fileExtension.isNotBlank()) {
166166
TagText(text = remember(fileExtension) { fileExtension.uppercase(Locale.getDefault()) })
167-
Spacer(modifier = Modifier.width(12.dp))
167+
Spacer(modifier = Modifier.width(6.dp))
168168
} else if (data.isDir) {
169169
TagText(text = stringResource(id = R.string.folder))
170170
}

app/src/main/java/com/skyd/anivu/ui/screen/media/list/MediaList.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ internal fun MediaList(
164164
}
165165

166166
@Composable
167-
private fun MediaList(
167+
internal fun MediaList(
168168
modifier: Modifier = Modifier,
169169
list: List<MediaBean>,
170170
groups: List<MediaGroupBean>,

0 commit comments

Comments
 (0)