feat: recommendation mode filter setting (#840)

* feat: Add filter options for view count and duration in settings

* chore: update styles && rename func.ts to useFilter.ts

* feat: Add scrollbar check to determine if the viewport has a scrollbar

* feat: app recommendation filters

* chore: update

* feat: i18n support

---------

Co-authored-by: pengyunfei <pengyunfei@360.cn>
This commit is contained in:
Hakadao
2024-07-01 01:05:18 +08:00
committed by GitHub
parent b7d7c4bab5
commit 06382e10e2
11 changed files with 218 additions and 11 deletions

View File

@@ -49,6 +49,9 @@ settings:
group_logo: LOGO
group_search_bar: 搜索栏
group_recommendation_mode: 推荐模式
group_recommendation_filters: 推荐过滤
group_recommendation_filters_desc: |
请勿将限制值设置得太大,不然可能会增加加载时间!
group_search_page_mode: 搜索页模式
group_home_tabs: 首页标签栏
@@ -137,6 +140,10 @@ settings:
授权使用后能在首页推送 bilibili App 端算法的推荐视频,授权 key 有效期约为一个月,过后记得重新授权获取 access key。
authorize_app_more_info_access_key: 关于 access key
scan_qrcode_desc: 使用 Bilibili 移动应用程序扫描此二维码,以授权获取 access key。
filter_by_view_count: 按播放数筛选
filter_by_view_count_unit:
filter_by_duration: 按时长筛选
filter_by_duration_unit:
use_search_page_mode: 首页使用搜索页模式
settings_shared_with_the_search_page: 与搜索页共用的配置
settings_shared_with_the_search_page_desc: 这些设置都与搜索页共用

View File

@@ -50,6 +50,9 @@ settings:
group_search_bar: 搜尋欄
group_recommendation_mode: 推薦模式
group_search_page_mode: 搜尋頁模式
group_recommendation_filters: 推薦過濾
group_recommendation_filters_desc: |
限制的數值不要設太大,不然載入時間可能會增加!
group_home_tabs: 首頁標籤欄
# Settings buttons
@@ -140,6 +143,10 @@ settings:
key。
authorize_app_more_info_access_key: 關於 access key
scan_qrcode_desc: 使用 Bilibili 手機應用程式掃描這個QR碼以授權取得 access key。
filter_by_view_count: 按觀看次數篩選
filter_by_view_count_unit:
filter_by_duration: 按影片長度篩選
filter_by_duration_unit:
use_search_page_mode: 首頁使用搜尋頁模式
settings_shared_with_the_search_page: 與搜尋頁共用的設定
settings_shared_with_the_search_page_desc: 這些設定都與搜尋頁共用

View File

@@ -49,6 +49,9 @@ settings:
group_logo: LOGO
group_search_bar: Search Bar
group_recommendation_mode: Recommendation Mode
group_recommendation_filters: Recommendation Filters
group_recommendation_filters_desc: |
Do not set the restricted value too large, as this may increase loading time!
group_search_page_mode: Search Page Mode
group_home_tabs: Home Tabs
@@ -138,6 +141,10 @@ settings:
The access key is valid for about one month. After that, remember to re-authorize the BewlyBewly to use the access key.
authorize_app_more_info_access_key: More information about the access key
scan_qrcode_desc: Use the Bilibili mobile app to scan this QR code in order to authorize the access key.
filter_by_view_count: Filter by view count
filter_by_view_count_unit: views
filter_by_duration: Filter by duration
filter_by_duration_unit: sec
use_search_page_mode: Use search page mode on homepage
settings_shared_with_the_search_page: Settings shared with the search page
settings_shared_with_the_search_page_desc: Those settings are used in common with the search page

View File

@@ -49,6 +49,9 @@ settings:
group_logo: LOGO
group_search_bar: 搜尋欄
group_recommendation_mode: 推介模式
group_recommendation_filters: 推介篩選
group_recommendation_filters_desc: |
限制嘅數值唔好設太大,唔係嘅話會影響載入時間!
group_search_page_mode: 搵嘢頁模式
group_home_tabs: 主頁分頁欄
@@ -140,6 +143,10 @@ settings:
access key。
authorize_app_more_info_access_key: 關於 access key
scan_qrcode_desc: 用 Bilibili 流動應用程式去 scan 呢個 QR code 愛嚟授權取得 access key。
filter_by_view_count: 根據觀看次數篩選
filter_by_view_count_unit:
filter_by_duration: 根據條片長度篩選
filter_by_duration_unit:
use_search_page_mode: 主頁使用搵嘢頁模式
settings_shared_with_the_search_page: 同搵嘢頁共用嘅設定
settings_shared_with_the_search_page_desc: 呢啲設定都同搵嘢頁共用

View File

@@ -3,6 +3,9 @@ type Size = 'small' | 'medium' | 'large'
interface Props {
modelValue: string
size?: Size
type?: 'text' | 'password' | 'email' | 'number'
min?: number
max?: number
}
const props = withDefaults(defineProps<Props>(), { size: 'medium' })
@@ -16,15 +19,21 @@ onMounted(() => {
</script>
<template>
<input
v-model="modelValue" type="text" class="b-input"
<div
focus-within:ring="2px $bew-theme-color"
p="x-4 y-2"
rounded="$bew-radius" outline-none transition-all duration-300
bg="$bew-fill-1"
focus:shadow focus:ring="2px $bew-theme-color"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@keydown.enter="$emit('enter')"
rounded="$bew-radius" transition-all duration-300
bg="$bew-fill-1" flex="~ gap-2"
>
<slot name="prefix" />
<input
v-model="modelValue" :type="type" :min="min" :max="max"
outline-none flex-1
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@keydown.enter="$emit('enter')"
>
<slot name="suffix" />
</div>
</template>
<style lang="scss" scoped>

View File

@@ -5,6 +5,7 @@ import { useToast } from 'vue-toastification'
import draggable from 'vuedraggable'
import Button from '~/components/Button.vue'
import Input from '~/components/Input.vue'
import Radio from '~/components/Radio.vue'
import { accessKey, settings } from '~/logic'
import { useMainStore } from '~/stores/mainStore'
@@ -219,6 +220,41 @@ function handleToggleHomeTab(tab: any) {
</div>
</ChildSettingsDialog>
</SettingsItemGroup>
<SettingsItemGroup
:title="$t('settings.group_recommendation_filters')"
:desc="$t('settings.group_recommendation_filters_desc')"
>
<SettingsItem :title="$t('settings.filter_by_view_count')">
<div flex="~ justify-end" w-full>
<Input
v-if="settings.enableFilterByViewCount"
v-model="settings.filterByViewCount" type="number" :min="1" :max="1000000"
flex-1
>
<template #suffix>
{{ $t('settings.filter_by_view_count_unit') }}
</template>
</Input>
<Radio v-model="settings.enableFilterByViewCount" />
</div>
</SettingsItem>
<SettingsItem :title="$t('settings.filter_by_duration')">
<div flex="~ justify-end" w-full>
<Input
v-if="settings.enableFilterByDuration"
v-model="settings.filterByDuration" type="number" :min="1" :max="1000000"
flex-1
>
<template #suffix>
{{ $t('settings.filter_by_duration_unit') }}
</template>
</Input>
<Radio v-model="settings.enableFilterByDuration" />
</div>
</SettingsItem>
</SettingsItemGroup>
<SettingsItemGroup
:title="$t('settings.group_home_tabs')"
>

View File

@@ -10,6 +10,7 @@ export interface BewlyAppProvider {
handleReachBottom: Ref<(() => void) | undefined>
handlePageRefresh: Ref<(() => void) | undefined>
handleBackToTop: (targetScrollTop?: number) => void
haveScrollbar: () => boolean
}
export function useBewlyApp(): BewlyAppProvider {

View File

@@ -0,0 +1,95 @@
import { settings } from '~/logic'
const get = (obj: any, path: string[]) => path.reduce((acc, part) => acc && acc[part], obj)
function compareNumber(item: any, keyPath: string[], filterValue: number) {
return get(item, keyPath) > filterValue
}
function compareNumberString(item: any, keyPath: string[], filterValue: number) {
const value = get(item, keyPath)
// for example: `1.2万`, `1.2萬`, `-` (indicates no data)
if (typeof value === 'string' && (value.includes('万') || value.includes('萬'))) {
const processedValue = value.replace(/万|萬/g, '')
return Number(processedValue) * 10000 > filterValue
}
const numericValue = Number(value)
return !Number.isNaN(numericValue) && numericValue > filterValue
}
export enum FilterType {
viewCount,
viewCountStr,
duration,
}
const funcMap = {
[FilterType.viewCount]: {
func: compareNumber,
enabledKey: 'enableFilterByViewCount',
valueKey: 'filterByViewCount',
},
[FilterType.duration]: {
func: compareNumber,
enabledKey: 'enableFilterByDuration',
valueKey: 'filterByDuration',
},
[FilterType.viewCountStr]: {
func: compareNumberString,
enabledKey: 'enableFilterByViewCount',
valueKey: 'filterByViewCount',
},
}
type KeyPath = Array<string>[]
export function factoryFilter(filterOpt: FilterType[], keyList: KeyPath): Function {
const funcs: {
keyPath: string[]
func: Function
value: number | string
}[] = []
filterOpt.forEach((type, index) => {
const { func, enabledKey, valueKey } = funcMap[type]
if ((settings.value as { [key: string]: any })[enabledKey]) {
funcs.push({
keyPath: keyList[index],
func,
value: (settings.value as { [key: string]: any })[valueKey],
})
}
})
return (item: object): boolean => {
const result = funcs.every(({ keyPath, func, value }) => {
// const check = func(item, keyPath, value)
// if (!check) {
// console.log('当前项目被拦截! 原因: ', '目标路径值 :>> ', keyPath, '大于', value, 'currentValue :>> ', get(item, keyPath))
// }
return func(item, keyPath, value)
})
return result
}
}
export function useFilter(filterOpt: FilterType[], keyList: KeyPath) {
const filter = ref<Function | null>(null)
watch(() => [
settings.value.enableFilterByDuration,
settings.value.enableFilterByViewCount,
settings.value.filterByDuration,
settings.value.filterByViewCount,
], ([isD, isV]) => {
if (!isD && !isV) {
filter.value = null
return
}
filter.value = factoryFilter(filterOpt, keyList)
}, { immediate: true })
return filter
}

View File

@@ -279,6 +279,17 @@ function handleReduceFrostedGlassBlur() {
// window.open(`https://www.bilibili.com/video/${bvid}`, '_self')
// }
/**
* Checks if the current viewport has a scrollbar.
* @returns {boolean} Returns true if the viewport has a scrollbar, false otherwise.
*/
function haveScrollbar() {
const osInstance = scrollbarRef.value?.osInstance()
const { viewport } = osInstance.elements()
const { scrollHeight } = viewport // get scroll offset
return scrollHeight > window.innerHeight
}
provide<BewlyAppProvider>('BEWLY_APP', {
activatedPage,
mainAppRef,
@@ -287,6 +298,7 @@ provide<BewlyAppProvider>('BEWLY_APP', {
handleBackToTop,
handlePageRefresh,
handleReachBottom,
haveScrollbar,
})
</script>

View File

@@ -11,6 +11,7 @@ import Empty from '~/components/Empty.vue'
import VideoCard from '~/components/VideoCard/VideoCard.vue'
import { useApiClient } from '~/composables/api'
import { useBewlyApp } from '~/composables/useAppProvider'
import { FilterType, useFilter } from '~/composables/useFilter'
import { LanguageType } from '~/enums/appEnums'
import type { GridLayout } from '~/logic'
import { accessKey, settings } from '~/logic'
@@ -29,6 +30,9 @@ const emit = defineEmits<{
(e: 'afterLoading'): void
}>()
const filterFunc = useFilter([FilterType.duration, FilterType.viewCount], [['duration'], ['stat', 'view']])
const appFilterFunc = useFilter([FilterType.duration, FilterType.viewCountStr], [['player_args', 'duration'], ['cover_left_text_1']])
const { t } = useI18n()
// https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L16
@@ -59,7 +63,7 @@ const needToLoginFirst = ref<boolean>(false)
const containerRef = ref<HTMLElement>() as Ref<HTMLElement>
const refreshIdx = ref<number>(1)
const noMoreContent = ref<boolean>(false)
const { handleReachBottom, handlePageRefresh, scrollbarRef } = useBewlyApp()
const { handleReachBottom, handlePageRefresh, scrollbarRef, haveScrollbar } = useBewlyApp()
const showVideoOptions = ref<boolean>(false)
const appVideoOptions = ref<ThreePointV2[] | undefined>([])
const videoOptions = reactive<{ id: number, name: string }[]>([
@@ -189,7 +193,10 @@ async function getRecommendVideos() {
const resData = [] as VideoItem[]
response.data.item.forEach((item: VideoItem) => {
resData.push(item)
if (!filterFunc.value || filterFunc.value(item))
resData.push(item)
// resData.push(item)
})
// when videoList has length property, it means it is the first time to load
@@ -204,12 +211,16 @@ async function getRecommendVideos() {
}
})
}
if (!haveScrollbar()) {
getRecommendVideos()
}
}
else if (response.code === 62011) {
needToLoginFirst.value = true
}
}
catch {
finally {
videoList.value = videoList.value.filter(video => video.item)
}
}
@@ -236,7 +247,7 @@ async function getAppRecommendVideos() {
response.data.items.forEach((item: AppVideoItem) => {
// Remove banner & ad cards
if (!item.card_type.includes('banner') && item.card_type !== 'cm_v1')
if (!item.card_type.includes('banner') && item.card_type !== 'cm_v1' && (!appFilterFunc.value || appFilterFunc.value(item)))
resData.push(item)
})
@@ -252,6 +263,10 @@ async function getAppRecommendVideos() {
}
})
}
if (!haveScrollbar()) {
getAppRecommendVideos()
}
}
else if (response.code === 62011) {
needToLoginFirst.value = true

View File

@@ -50,6 +50,11 @@ export interface Settings {
searchPageWallpaperBlurIntensity: number
recommendationMode: 'web' | 'app'
// filter setting
enableFilterByViewCount: boolean
filterByViewCount: number
enableFilterByDuration: boolean
filterByDuration: number
homePageTabVisibilityList: { page: HomeSubPage, visible: boolean }[]
alwaysShowTabsOnHomePage: boolean
useSearchPageModeOnHomePage: boolean
@@ -102,6 +107,12 @@ export const settings = useStorageLocal('settings', ref<Settings>({
searchPageWallpaperBlurIntensity: 0,
recommendationMode: 'web',
// filter setting
enableFilterByViewCount: false,
filterByViewCount: 10000,
enableFilterByDuration: false,
filterByDuration: 3600,
homePageTabVisibilityList: [],
alwaysShowTabsOnHomePage: false,
useSearchPageModeOnHomePage: false,