mirror of
https://github.com/BewlyBewly/BewlyBewly.git
synced 2025-04-14 13:15:29 +00:00
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:
@@ -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: 这些设置都与搜索页共用
|
||||
|
||||
@@ -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: 這些設定都與搜尋頁共用
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 呢啲設定都同搵嘢頁共用
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')"
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
95
src/composables/useFilter.ts
Normal file
95
src/composables/useFilter.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user