Merge pull request #241 from hakadao/refactor-lazy-components

Refactor lazy components
This commit is contained in:
Hakadao
2024-01-30 17:41:53 +08:00
committed by GitHub
29 changed files with 788 additions and 496 deletions

View File

@@ -14,6 +14,7 @@ module.exports = antfu({
},
},
],
'no-alert': 'off',
},
eslint: {
ignorePatterns: [

View File

@@ -3,7 +3,7 @@
"displayName": "BewlyBewly",
"version": "0.14.3",
"private": true,
"packageManager": "pnpm@8.14.1",
"packageManager": "pnpm@8.15.0",
"description": "Just make a few small changes to your Bilibili homepage.",
"homepage": "https://github.com/hakadao/BewlyBewly",
"scripts": {
@@ -37,36 +37,36 @@
"dplayer": "^1.27.1",
"md5": "^2.3.0",
"mitt": "^3.0.1",
"overlayscrollbars": "^2.4.5",
"overlayscrollbars-vue": "^0.5.6",
"overlayscrollbars": "^2.4.7",
"overlayscrollbars-vue": "^0.5.7",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.1",
"vue-i18n": "^9.7.0",
"vue-i18n": "^9.9.0",
"vue-toastification": "2.0.0-rc.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.1",
"@ffflorian/jszip-cli": "^3.5.1",
"@iconify/json": "^2.2.143",
"@antfu/eslint-config": "^2.6.3",
"@ffflorian/jszip-cli": "^3.6.2",
"@iconify/json": "^2.2.176",
"@iconify/vue": "^4.1.1",
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@rollup/plugin-replace": "^5.0.5",
"@types/dplayer": "^1.25.5",
"@types/fs-extra": "^9.0.13",
"@types/node": "^18.18.10",
"@types/node": "^18.19.10",
"@types/webextension-polyfill": "^0.9.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@unocss/reset": "^0.54.3",
"@vitejs/plugin-vue": "^3.2.0",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.4.2",
"@vueuse/core": "^10.6.1",
"@vue/compiler-sfc": "^3.4.15",
"@vue/test-utils": "^2.4.4",
"@vueuse/core": "^10.7.2",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
"crx": "^5.0.1",
"eslint": "^8.54.0",
"eslint": "^8.56.0",
"esno": "^4.0.0",
"fs-extra": "^10.1.0",
"jsdom": "^20.0.3",
@@ -74,9 +74,9 @@
"lint-staged": "^15.2.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"sass": "^1.69.5",
"sass": "^1.70.0",
"simple-git-hooks": "^2.9.0",
"terser": "^5.24.0",
"terser": "^5.27.0",
"tsup": "^6.7.0",
"typescript": "^4.9.5",
"unocss": "^0.54.3",
@@ -85,9 +85,9 @@
"unplugin-vue-components": "^0.22.12",
"vite": "^3.2.8",
"vitest": "^0.24.5",
"vue": "^3.3.8",
"vue": "^3.4.15",
"vue-demi": "^0.13.11",
"web-ext": "^7.8.0",
"web-ext": "^7.11.0",
"webext-bridge": "^5.0.5",
"webextension-polyfill": "^0.10.0"
},

523
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ common:
disable: 禁用
refresh: 刷新
reset: 重置
no_more_content: 没有更多内容了
settings:
title: 设置

View File

@@ -23,6 +23,7 @@ common:
disable: 停用
refresh: 重新整理
reset: 重置
no_more_content: 沒有更多內容了
settings:
title: 設定

View File

@@ -23,6 +23,7 @@ common:
disable: Disable
refresh: Refresh
reset: Reset
no_more_content: No more content now, owari da
settings:
title: Settings

View File

@@ -23,6 +23,7 @@ common:
disable: 閂埋
refresh: 重新整理
reset: 重置
no_more_content: 唔使睇喇,冇嘢喇
settings:
title: 設定

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Icon } from '@iconify/vue'
import type { Ref } from 'vue'
import type { CurrentDockItem, HoveringDockItem } from './types'
import type { AppPage } from '~/enums/appEnums'
import { settings } from '~/logic'
@@ -11,8 +10,7 @@ import { useMainStore } from '~/stores/mainStore'
defineProps<{ activatedPage: AppPage }>()
const emit = defineEmits(['change-page', 'settings-visibility-change'])
const mainAppRef = inject('mainAppRef') as Ref<HTMLDivElement>
const { mainAppRef } = useBewlyApp()
const mainStore = useMainStore()
const { t } = useI18n()

View File

@@ -0,0 +1,6 @@
import 'overlayscrollbars/overlayscrollbars.css'
export default defineAsyncComponent(async () => {
const { OverlayScrollbarsComponent } = await import('overlayscrollbars-vue')
return OverlayScrollbarsComponent
})

View File

@@ -1,11 +1,9 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { HoveringDockItem } from './types'
import { settings } from '~/logic'
const emit = defineEmits(['settings-visibility-change'])
const mainAppRef = inject('mainAppRef') as Ref<HTMLDivElement>
const { mainAppRef } = useBewlyApp()
const hoveringDockItem = reactive<HoveringDockItem>({
themeMode: false,

View File

@@ -1,12 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Icon } from '@iconify/vue'
import General from './components/General.vue'
import Appearance from './components/Appearance.vue'
import SearchPage from './components/SearchPage.vue'
import Home from './components/Home.vue'
import Compatibility from './components/Compatibility.vue'
import About from './components/About.vue'
import type { MenuItem } from './types'
import { MenuType } from './types'
import { settings } from '~/logic'
@@ -15,7 +10,14 @@ const emit = defineEmits(['close'])
const { t } = useI18n()
const settingsMenu = { General, Appearance, SearchPage, Home, Compatibility, About }
const settingsMenu = {
[MenuType.General]: defineAsyncComponent(() => import('./components/General.vue')),
[MenuType.Appearance]: defineAsyncComponent(() => import('./components/Appearance.vue')),
[MenuType.SearchPage]: defineAsyncComponent(() => import('./components/SearchPage.vue')),
[MenuType.Home]: defineAsyncComponent(() => import('./components/Home.vue')),
[MenuType.Compatibility]: defineAsyncComponent(() => import('./components/Compatibility.vue')),
[MenuType.About]: defineAsyncComponent(() => import('./components/About.vue')),
}
const activatedMenuItem = ref<MenuType>(MenuType.General)
const title = ref<string>(t('settings.title'))
const preventCloseSettings = ref<boolean>(false)

View File

@@ -10,7 +10,6 @@ import HistoryPop from './components/HistoryPop.vue'
import { getUserID, isHomePage } from '~/utils/main'
import { settings } from '~/logic'
import emitter from '~/utils/mitt'
import type { AppPage } from '~/enums/appEnums'
// import { useTopBarStore } from '~/stores/topBarStore'
@@ -33,8 +32,7 @@ interface Props {
// return topBarStore.topBarItems
// })
const activatedPage = inject('activatedPage') as Ref<AppPage>
const scrollbarRef = inject('scrollbarRef') as Ref
const { activatedPage, scrollbarRef } = useBewlyApp()
const mid = getUserID() || ''
const userInfo = reactive<UserInfo | NonNullable<unknown>>({}) as UnwrapNestedRefs<UserInfo>

View File

@@ -0,0 +1,20 @@
import type { Ref } from 'vue'
import type { AppPage } from '~/enums/appEnums'
export interface BewlyAppProvider {
activatedPage: Ref<AppPage>
scrollbarRef: Ref<any>
mainAppRef: Ref<HTMLElement>
handleReachBottom: Ref<(() => void) | undefined>
handlePageRefresh: Ref<(() => void) | undefined>
handleBackToTop: (targetScrollTop: number) => void
}
export function useBewlyApp(): BewlyAppProvider {
const provider = inject<BewlyAppProvider>('BEWLY_APP')
if (import.meta.env.DEV && !provider)
throw new Error('AppProvider is not injected')
return provider!
}

View File

@@ -2,7 +2,6 @@
import AnimeTimeTable from './components/AnimeTimeTable.vue'
import { getUserID, openLinkToNewTab } from '~/utils/main'
import { numFormatter } from '~/utils/dataFormatter'
import emitter from '~/utils/mitt'
import type { List as WatchListItem, WatchListResult } from '~/models/anime/watchList'
import type { List as PopularAnimeItem, PopularAnimeResult } from '~/models/anime/popular'
import type { ItemSubItem as RecommendationItem, RecommendationResult } from '~/models/anime/recommendation'
@@ -15,22 +14,47 @@ const isLoadingAnimeWatchList = ref<boolean>()
const isLoadingPopularAnime = ref<boolean>()
const isLoadingRecommendAnime = ref<boolean>()
const activatedSeasonId = ref<number>()
const noMoreContent = ref<boolean>()
const animeTimeTableRef = ref()
const { handleReachBottom, handlePageRefresh } = useBewlyApp()
const isLoading = computed(() => {
return isLoadingAnimeWatchList.value || isLoadingPopularAnime.value || isLoadingRecommendAnime.value
})
onMounted(() => {
getAnimeWatchList()
getPopularAnimeList()
getRecommendAnimeList()
emitter.off('reachBottom')
emitter.on('reachBottom', () => {
if (!isLoadingRecommendAnime.value)
getRecommendAnimeList()
})
initPageAction()
})
onUnmounted(() => {
emitter.off('reachBottom')
})
function initPageAction() {
handleReachBottom.value = () => {
if (isLoadingRecommendAnime.value)
return
if (noMoreContent.value)
return
getRecommendAnimeList()
}
handlePageRefresh.value = () => {
if (isLoading.value)
return
animeWatchList.length = 0
recommendAnimeList.length = 0
popularAnimeList.length = 0
cursor.value = 0
noMoreContent.value = false
getAnimeWatchList()
getPopularAnimeList()
getRecommendAnimeList()
animeTimeTableRef.value?.refreshAnimeTimeTable()
}
}
function getAnimeWatchList() {
isLoadingAnimeWatchList.value = true
@@ -70,10 +94,13 @@ function getRecommendAnimeList() {
if (code === 0 && has_next) {
if (recommendAnimeList.length === 0)
Object.assign(recommendAnimeList, items[0].sub_items as RecommendationItem[])
else recommendAnimeList.push(...items[0].sub_items)
else
recommendAnimeList.push(...items[0].sub_items)
cursor.value = coursor
}
if (code === 0 && !has_next)
noMoreContent.value = true
})
.finally(() => {
isLoadingRecommendAnime.value = false
@@ -213,7 +240,7 @@ function getPopularAnimeList() {
</h3>
</div>
<AnimeTimeTable w="[calc(100%+1.5rem)]" />
<AnimeTimeTable ref="animeTimeTableRef" w="[calc(100%+1.5rem)]" />
</section>
<!-- Recommended for you -->
@@ -246,6 +273,10 @@ function getPopularAnimeList() {
</div>
</section>
</div>
<!-- no more content -->
<Empty v-if="noMoreContent" class="pb-4" :description="$t('common.no_more_content')" />
<!-- loading -->
<loading v-if="isLoadingRecommendAnime && recommendAnimeList.length !== 0" m="-t-4" />
</div>

View File

@@ -2,13 +2,12 @@
import type { Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import browser from 'webextension-polyfill'
import type { AnimeTimeTableItem } from '../types'
import { removeHttpFromUrl } from '~/utils/main'
import type { Result as TimetableItem, TimetableResult } from '~/models/apiModels/anime/timetable'
import type { Result as TimetableItem, TimetableResult } from '~/models/anime/timeTable'
const { t } = useI18n()
const animeTimeTable = reactive<AnimeTimeTableItem[]>([])
const animeTimeTable = reactive<TimetableItem[]>([])
const animeTimeTableWrap = ref<HTMLElement>() as Ref<HTMLElement>
const daysOfTheWeekList = computed(() => {
@@ -27,6 +26,11 @@ onMounted(() => {
getAnimeTimeTable()
})
function refreshAnimeTimeTable() {
animeTimeTable.length = 0
getAnimeTimeTable()
}
function getAnimeTimeTable() {
browser.runtime
.sendMessage({
@@ -38,6 +42,8 @@ function getAnimeTimeTable() {
Object.assign(animeTimeTable, result as TimetableItem[])
})
}
defineExpose({ refreshAnimeTimeTable })
</script>
<template>

View File

@@ -1,28 +1,32 @@
<script setup lang="ts">
import { useToggle } from '@vueuse/core'
import { useThrottleFn, useToggle } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import browser from 'webextension-polyfill'
import type { Ref } from 'vue'
import Home from './Home/Home.vue'
import Search from './Search/Search.vue'
import Anime from './Anime/Anime.vue'
import History from './History/History.vue'
import WatchLater from './WatchLater/WatchLater.vue'
import Favorites from './Favorites/Favorites.vue'
import { accessKey, settings } from '~/logic'
import { AppPage, LanguageType } from '~/enums/appEnums'
import { getUserID, hexToRGBA, isHomePage, smoothScrollToTop } from '~/utils/main'
import emitter from '~/utils/mitt'
import type { BewlyAppProvider } from '~/composables/useAppProvider'
const activatedPage = ref<AppPage>(settings.value.dockItemVisibilityList.find(e => e.visible === true)?.page ?? AppPage.Home)
const { locale } = useI18n()
const [showSettings, toggleSettings] = useToggle(false)
const pages = { Home, Search, Anime, History, WatchLater, Favorites }
const pages = {
[AppPage.Home]: defineAsyncComponent(() => import('./Home/Home.vue')),
[AppPage.Search]: defineAsyncComponent(() => import('./Search/Search.vue')),
[AppPage.Anime]: defineAsyncComponent(() => import('./Anime/Anime.vue')),
[AppPage.History]: defineAsyncComponent(() => import('./History/History.vue')),
[AppPage.WatchLater]: defineAsyncComponent(() => import('./WatchLater/WatchLater.vue')),
[AppPage.Favorites]: defineAsyncComponent(() => import('./Favorites/Favorites.vue')),
}
const mainAppRef = ref<HTMLElement>() as Ref<HTMLElement>
const scrollbarRef = ref()
const showTopBarMask = ref<boolean>(false)
const dynamicComponentKey = ref<string>(`dynamicComponent${Number(new Date())}`)
const handlePageRefresh = ref<() => void>()
const handleReachBottom = ref<() => void>()
const handleThrottledPageRefresh = useThrottleFn(() => handlePageRefresh.value?.(), 500)
const handleThrottledReachBottom = useThrottleFn(() => handleReachBottom.value?.(), 500)
const topBarRef = ref()
const isVideoPage = computed(() => {
@@ -127,7 +131,7 @@ function changeActivatePage(pageName: AppPage) {
if (activatedPage.value === pageName) {
if (activatedPage.value !== AppPage.Search) {
if (scrollTop === 0)
handleRefresh()
handleThrottledPageRefresh()
else
handleBackToTop()
}
@@ -202,12 +206,6 @@ function setAppThemeColor() {
document.documentElement.style.setProperty(`--bew-theme-color-${i + 1}0`, hexToRGBA(settings.value.themeColor, i * 0.1 + 0.1))
}
function handleRefresh() {
emitter.emit('pageRefresh')
if (activatedPage.value === AppPage.Anime)
dynamicComponentKey.value = `dynamicComponent${Number(new Date())}`
}
function handleBackToTop(targetScrollTop = 0 as number) {
const osInstance = scrollbarRef.value?.osInstance()
@@ -233,15 +231,15 @@ function handleOsScroll() {
showTopBarMask.value = true
if (clientHeight + scrollTop >= scrollHeight - 20)
emitter.emit('reachBottom')
handleThrottledReachBottom()
if (isHomePage())
topBarRef.value?.handleScroll()
}
// // fix #166 https://github.com/hakadao/BewlyBewly/issues/166
// fix #166 https://github.com/hakadao/BewlyBewly/issues/166
// function openVideoPageIfBvidExists() {
// // Assume the URL is https://www.bilibili.com/?bvid=BV1be41127ft&spm_id_from=333.788.seo.out
// Assume the URL is https://www.bilibili.com/?bvid=BV1be41127ft&spm_id_from=333.788.seo.out
// // Get the current URL's query string
// const queryString = window.location.search
@@ -253,11 +251,14 @@ function handleOsScroll() {
// window.open(`https://www.bilibili.com/video/${bvid}`, '_self')
// }
provide('handleBackToTop', handleBackToTop)
provide('handleRefresh', handleRefresh)
provide('activatedPage', activatedPage)
provide('scrollbarRef', scrollbarRef)
provide('mainAppRef', mainAppRef)
provide<BewlyAppProvider>('BEWLY_APP', {
activatedPage,
mainAppRef,
scrollbarRef,
handleBackToTop,
handlePageRefresh,
handleReachBottom,
})
</script>
<template>
@@ -330,12 +331,12 @@ provide('mainAppRef', mainAppRef)
<!-- control button group -->
<BackToTopAndRefreshButtons
v-if="activatedPage !== AppPage.Search" :show-refresh-button="!showTopBarMask"
@refresh="handleRefresh"
@refresh="handleThrottledPageRefresh"
@back-to-top="handleBackToTop"
/>
<Transition name="page-fade">
<Component :is="pages[activatedPage]" :key="dynamicComponentKey" />
<Component :is="pages[activatedPage]" />
</Transition>
</div>
</main>

View File

@@ -19,7 +19,7 @@ const activatedCategoryCover = ref<string>('')
const shouldMoveCtrlBarUp = ref<boolean>(false)
const currentPageNum = ref<number>(1)
const keyword = ref<string>('')
const { handlePageRefresh, handleReachBottom } = useBewlyApp()
const isLoading = ref<boolean>(true)
const isFullPageLoading = ref<boolean>(false)
const noMoreContent = ref<boolean>()
@@ -28,20 +28,8 @@ onMounted(async () => {
await getFavoriteCategories()
changeCategory(favoriteCategories[0])
emitter.off('reachBottom')
emitter.on('reachBottom', () => {
if (isLoading.value)
return
initPageAction()
if (!noMoreContent.value)
getFavoriteResources(selectedCategory.value!.id, ++currentPageNum.value, keyword.value)
})
emitter.off('pageRefresh')
emitter.on('pageRefresh', async () => {
favoriteResources.length = 0
handleSearch()
})
emitter.off('topBarVisibleChange')
emitter.on('topBarVisibleChange', (val) => {
shouldMoveCtrlBarUp.value = false
@@ -60,11 +48,29 @@ onMounted(async () => {
})
onUnmounted(() => {
emitter.off('reachBottom')
emitter.off('pageRefresh')
emitter.off('topBarVisibleChange')
})
function initPageAction() {
handleReachBottom.value = async () => {
if (isLoading.value)
return
if (noMoreContent.value)
return
await getFavoriteResources(selectedCategory.value!.id, ++currentPageNum.value, keyword.value)
}
handlePageRefresh.value = () => {
if (isLoading.value)
return
favoriteResources.length = 0
currentPageNum.value = 1
noMoreContent.value = false
handleSearch()
}
}
async function getFavoriteCategories() {
await browser.runtime
.sendMessage({
@@ -128,14 +134,17 @@ async function getFavoriteResources(
async function changeCategory(categoryItem: FavoriteCategory) {
currentPageNum.value = 1
selectedCategory.value = categoryItem
noMoreContent.value = false
favoriteResources.length = 0
getFavoriteResources(categoryItem.id, 1)
}
function handleSearch() {
currentPageNum.value = 1
favoriteResources.length = 0
noMoreContent.value = false
getFavoriteResources(selectedCategory.value!.id, currentPageNum.value, keyword.value)
}
@@ -225,6 +234,9 @@ function handleUnfavorite(favoriteResource: FavoriteResource) {
</TransitionGroup>
</div>
<!-- no more content -->
<Empty v-if="noMoreContent" class="py-4" :description="$t('common.no_more_content')" />
<!-- loading -->
<Transition name="fade">
<Loading

View File

@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'
import { getCSRF, openLinkToNewTab, removeHttpFromUrl } from '~/utils/main'
import { calcCurrentTime } from '~/utils/dataFormatter'
import emitter from '~/utils/mitt'
import { Business } from '~/models/video/history'
import type { List as HistoryItem, HistoryResult } from '~/models/video/history'
import type { List as HistorySearchItem, HistorySearchResult } from '~/models/video/historySearch'
@@ -12,61 +11,44 @@ import type { List as HistorySearchItem, HistorySearchResult } from '~/models/vi
const { t } = useI18n()
const isLoading = ref<boolean>()
const noMoreContent = ref<boolean>()
const noMoreContent = ref<boolean>(false)
const historyList = reactive<Array<HistoryItem>>([])
const currentPageNum = ref<number>(1)
const keyword = ref<string>()
const historyStatus = ref<boolean>()
const { handlePageRefresh, handleReachBottom } = useBewlyApp()
const HistoryBusiness = computed(() => {
return Business
})
watch(
() => keyword.value,
(newValue, oldValue) => {
if (newValue === oldValue)
return
emitter.on('reachBottom', () => {
if (isLoading.value)
return
if (!noMoreContent.value) {
if (keyword.value)
searchHistoryList()
else getHistoryList()
}
})
},
)
onMounted(() => {
getHistoryList()
getHistoryPauseStatus()
emitter.off('reachBottom')
emitter.on('reachBottom', () => {
initPageAction()
})
function initPageAction() {
handleReachBottom.value = () => {
if (isLoading.value)
return
if (noMoreContent.value)
return
if (!noMoreContent.value) {
if (keyword.value)
searchHistoryList()
else getHistoryList()
}
})
if (keyword.value)
searchHistoryList()
else
getHistoryList()
}
emitter.off('pageRefresh')
emitter.on('pageRefresh', async () => {
handlePageRefresh.value = () => {
historyList.length = 0
currentPageNum.value = 1
noMoreContent.value = false
getHistoryList()
})
})
onUnmounted(() => {
emitter.off('reachBottom')
emitter.off('pageRefresh')
})
}
}
/**
* Get history list
@@ -128,6 +110,7 @@ function searchHistoryList() {
function handleSearch() {
historyList.length = 0
currentPageNum.value = 1
noMoreContent.value = false
if (keyword.value)
searchHistoryList()
else getHistoryList()
@@ -151,7 +134,7 @@ function deleteHistoryItem(index: number, historyItem: HistoryItem) {
* @param item history item
* @return {string} url
*/
function getHistoryUrl(item: HistoryItem) {
function getHistoryUrl(item: HistoryItem): string {
// anime
if (item.history.business === 'pgc') {
return removeHttpFromUrl(item.uri)
@@ -220,7 +203,6 @@ function clearAllHistory() {
}
function handleClearAllWatchHistory() {
// eslint-disable-next-line no-alert
const result = confirm(
t('history.clear_all_watch_history_confirm'),
)
@@ -229,7 +211,6 @@ function handleClearAllWatchHistory() {
}
function handlePauseWatchHistory() {
// eslint-disable-next-line no-alert
const result = confirm(
t('history.pause_watch_history_confirm'),
)
@@ -238,7 +219,6 @@ function handlePauseWatchHistory() {
}
function handleTurnOnWatchHistory() {
// eslint-disable-next-line no-alert
const result = confirm(
t('history.turn_on_watch_history_confirm'),
)
@@ -258,7 +238,7 @@ function jumpToLoginPage() {
{{ $t('history.title') }}
</h3>
<!-- historyList -->
<transition-group name="list">
<TransitionGroup name="list">
<a
v-for="(historyItem, index) in historyList"
:key="historyItem.kid"
@@ -479,7 +459,11 @@ function jumpToLoginPage() {
</div>
</section>
</a>
</transition-group>
</TransitionGroup>
<!-- no more content -->
<Empty v-if="noMoreContent" class="py-4" :description="$t('common.no_more_content')" />
<!-- loading -->
<Transition name="fade">
<loading

View File

@@ -1,10 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import ForYou from './components/ForYou.vue'
import Following from './components/Following.vue'
import Trending from './components/Trending.vue'
import Ranking from './components/Ranking.vue'
import SubscribedSeries from './components/SubscribedSeries.vue'
import type { HomeTab } from './types'
import { HomeSubPage } from './types'
import emitter from '~/utils/mitt'
@@ -12,13 +7,19 @@ import { settings } from '~/logic'
const { t } = useI18n()
const handleBackToTop = inject('handleBackToTop') as (targetScrollTop: number) => void
const { handleBackToTop } = useBewlyApp()
const recommendContentKey = ref<string>(`recommendContent${Number(new Date())}`)
const activatedPage = ref<HomeSubPage>(HomeSubPage.ForYou)
const pages = { ForYou, Following, SubscribedSeries, Trending, Ranking }
const pages = {
[HomeSubPage.ForYou]: defineAsyncComponent(() => import('./components/ForYou.vue')),
[HomeSubPage.Following]: defineAsyncComponent(() => import('./components/Following.vue')),
[HomeSubPage.SubscribedSeries]: defineAsyncComponent(() => import('./components/SubscribedSeries.vue')),
[HomeSubPage.Trending]: defineAsyncComponent(() => import('./components/Trending.vue')),
[HomeSubPage.Ranking]: defineAsyncComponent(() => import('./components/Ranking.vue')),
}
const showSearchPageMode = ref<boolean>(false)
const shouldMoveTabsUp = ref<boolean>(false)
const tabContentLoading = ref<boolean>(false)
const tabs = computed((): HomeTab[] => {
return [
@@ -51,10 +52,6 @@ watch(() => activatedPage.value, () => {
onMounted(() => {
showSearchPageMode.value = true
emitter.off('pageRefresh')
emitter.on('pageRefresh', async () => {
recommendContentKey.value = `recommendContent${Number(new Date())}`
})
emitter.off('topBarVisibleChange')
emitter.on('topBarVisibleChange', (val) => {
shouldMoveTabsUp.value = false
@@ -73,9 +70,18 @@ onMounted(() => {
})
onUnmounted(() => {
emitter.off('pageRefresh')
emitter.off('topBarVisibleChange')
})
function handleChangeTab(tab: HomeTab) {
// When the content of a tab is loading, prevent switching to another tab.
// Since `initPageAction()` within the tab replaces the `handleReachBottom` and `handlePageRefresh` functions.
// Therefore, this will lead to a failure in refreshing the data of the current tab
// because `handlePageRefresh` and `handleReachBottom` has been replaced
// now they are set to refresh the data of the tab you switched to
if (!tabContentLoading.value)
activatedPage.value = tab.value
}
</script>
<template>
@@ -150,7 +156,8 @@ onUnmounted(() => {
px-4 lh-35px bg="$bew-elevated-1 hover:$bew-elevated-1-hover" backdrop-glass rounded="$bew-radius"
cursor-pointer shadow="$bew-shadow-1" box-border border="1 $bew-border-color" duration-300
:class="{ 'tab-activated': activatedPage === tab.value }"
@click="activatedPage = tab.value"
:style="{ opacity: activatedPage !== tab.value && tabContentLoading ? 0.4 : 1 }"
@click="handleChangeTab(tab)"
>
<span class="text-center">{{ tab.label }}</span>
</li>
@@ -158,7 +165,13 @@ onUnmounted(() => {
</header>
<Transition name="page-fade">
<Component :is="pages[activatedPage]" :key="recommendContentKey" />
<KeepAlive include="ForYou">
<Component
:is="pages[activatedPage]" :key="activatedPage"
@before-loading="tabContentLoading = true"
@after-loading="tabContentLoading = false"
/>
</KeepAlive>
</Transition>
<!-- <RecommendContent :key="recommendContentKey" /> -->
</main>

View File

@@ -1,7 +1,11 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { DataItem as MomentItem, MomentResult } from '~/models/moment/moment'
import emitter from '~/utils/mitt'
const emit = defineEmits<{
(e: 'beforeLoading'): void
(e: 'afterLoading'): void
}>()
const videoList = reactive<MomentItem[]>([])
const isLoading = ref<boolean>(false)
@@ -10,28 +14,52 @@ const containerRef = ref<HTMLElement>() as Ref<HTMLElement>
const offset = ref<string>('')
const updateBaseline = ref<string>('')
const noMoreContent = ref<boolean>(false)
const { handleReachBottom, handlePageRefresh } = useBewlyApp()
onMounted(async () => {
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
emitter.off('reachBottom')
emitter.on('reachBottom', async () => {
if (!isLoading.value) {
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
})
initPageAction()
})
onUnmounted(() => {
emitter.off('reachBottom')
onActivated(() => {
initPageAction()
})
function initPageAction() {
handleReachBottom.value = async () => {
if (isLoading.value)
return
if (noMoreContent.value)
return
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
handlePageRefresh.value = async () => {
if (isLoading.value)
return
offset.value = ''
updateBaseline.value = ''
videoList.length = 0
noMoreContent.value = false
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
}
async function getFollowedUsersVideos() {
if (noMoreContent.value)
return
if (offset.value === '0') {
noMoreContent.value = true
return
}
emit('beforeLoading')
isLoading.value = true
try {
const response: MomentResult = await browser.runtime.sendMessage({
@@ -72,6 +100,7 @@ async function getFollowedUsersVideos() {
}
finally {
isLoading.value = false
emit('afterLoading')
}
}
@@ -116,6 +145,9 @@ function jumpToLoginPage() {
</template>
</div>
<!-- no more content -->
<Empty v-if="noMoreContent" class="pb-4" :description="$t('common.no_more_content')" />
<Transition name="fade">
<Loading v-if="isLoading" />
</Transition>

View File

@@ -2,11 +2,15 @@
import type { Ref } from 'vue'
import type { AppForYouResult, Item as AppVideoItem } from '~/models/video/appForYou'
import type { Item as VideoItem, forYouResult } from '~/models/video/forYou'
import emitter from '~/utils/mitt'
import { accessKey, settings } from '~/logic'
import { LanguageType } from '~/enums/appEnums'
import { TVAppKey } from '~/utils/authProvider'
const emit = defineEmits<{
(e: 'beforeLoading'): void
(e: 'afterLoading'): void
}>()
const videoList = reactive<VideoItem[]>([])
const appVideoList = reactive<AppVideoItem[]>([])
const isLoading = ref<boolean>(true)
@@ -14,6 +18,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 } = useBewlyApp()
watch(() => settings.value.recommendationMode, (newValue) => {
videoList.length = 0
@@ -41,28 +46,48 @@ onMounted(async () => {
}
}, 200)
emitter.off('reachBottom')
emitter.on('reachBottom', async () => {
if (!isLoading.value) {
if (settings.value.recommendationMode === 'web') {
getRecommendVideos()
}
else {
for (let i = 0; i < 3; i++)
await getAppRecommendVideos()
}
}
})
initPageAction()
})
onUnmounted(() => {
emitter.off('reachBottom')
onActivated(() => {
initPageAction()
})
function initPageAction() {
handleReachBottom.value = async () => {
if (isLoading.value)
return
if (noMoreContent.value)
return
if (settings.value.recommendationMode === 'web') {
getRecommendVideos()
}
else {
for (let i = 0; i < 3; i++)
await getAppRecommendVideos()
}
}
handlePageRefresh.value = async () => {
if (isLoading.value)
return
videoList.length = 0
appVideoList.length = 0
noMoreContent.value = false
if (settings.value.recommendationMode === 'web') {
await getRecommendVideos()
}
else {
for (let i = 0; i < 3; i++)
await getAppRecommendVideos()
}
}
}
async function getRecommendVideos() {
if (noMoreContent.value)
return
emit('beforeLoading')
isLoading.value = true
try {
const response: forYouResult = await browser.runtime.sendMessage({
@@ -97,10 +122,12 @@ async function getRecommendVideos() {
}
finally {
isLoading.value = false
emit('afterLoading')
}
}
async function getAppRecommendVideos() {
emit('beforeLoading')
isLoading.value = true
try {
const response: AppForYouResult = await browser.runtime.sendMessage({
@@ -136,6 +163,7 @@ async function getAppRecommendVideos() {
}
finally {
isLoading.value = false
emit('afterLoading')
}
}
@@ -202,6 +230,9 @@ function jumpToLoginPage() {
</template>
</div>
<!-- no more content -->
<Empty v-if="noMoreContent" class="pb-4" :description="$t('common.no_more_content')" />
<Transition name="fade">
<Loading v-if="isLoading" />
</Transition>

View File

@@ -6,9 +6,13 @@ import type { List as RankingPgcItem, RankingPgcResult } from '~/models/video/ra
import { settings } from '~/logic'
import emitter from '~/utils/mitt'
const { t } = useI18n()
const emit = defineEmits<{
(e: 'beforeLoading'): void
(e: 'afterLoading'): void
}>()
const handleBackToTop = inject('handleBackToTop') as (targetScrollTop: number) => void
const { t } = useI18n()
const { handleBackToTop, handlePageRefresh } = useBewlyApp()
const rankingTypes = computed((): RankingType[] => {
return [
@@ -72,14 +76,30 @@ onMounted(() => {
})
getRankingVideos()
initPageAction()
})
onActivated(() => {
initPageAction()
})
function initPageAction() {
handlePageRefresh.value = async () => {
videoList.length = 0
PgcList.length = 0
if (isLoading.value)
return
getRankingVideos()
}
}
onBeforeUnmount(() => {
emitter.off('topBarVisibleChange')
})
function getRankingVideos() {
videoList.length = 0
emit('beforeLoading')
isLoading.value = true
browser.runtime.sendMessage({
contentScriptQuery: 'getRankingVideos',
@@ -90,7 +110,10 @@ function getRankingVideos() {
const { list } = response.data
Object.assign(videoList, list)
}
}).finally(() => isLoading.value = false)
}).finally(() => {
isLoading.value = false
emit('afterLoading')
})
}
function getRankingPgc() {

View File

@@ -1,7 +1,11 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { DataItem as MomentItem, MomentResult } from '~/models/moment/moment'
import emitter from '~/utils/mitt'
const emit = defineEmits<{
(e: 'beforeLoading'): void
(e: 'afterLoading'): void
}>()
const momentList = reactive<MomentItem[]>([])
const isLoading = ref<boolean>(false)
@@ -10,28 +14,57 @@ const containerRef = ref<HTMLElement>() as Ref<HTMLElement>
const offset = ref<string>('')
const updateBaseline = ref<string>('')
const noMoreContent = ref<boolean>(false)
const noMoreContentWarning = ref<boolean>(false)
const { handleReachBottom, handlePageRefresh } = useBewlyApp()
function initPageAction() {
handleReachBottom.value = async () => {
if (isLoading.value)
return
if (noMoreContent.value) {
noMoreContentWarning.value = true
return
}
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
handlePageRefresh.value = async () => {
if (isLoading.value)
return
offset.value = ''
updateBaseline.value = ''
momentList.length = 0
noMoreContent.value = false
noMoreContentWarning.value = false
if (isLoading.value)
return
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
}
onMounted(async () => {
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
emitter.off('reachBottom')
emitter.on('reachBottom', async () => {
if (!isLoading.value) {
for (let i = 0; i < 3; i++)
await getFollowedUsersVideos()
}
})
initPageAction()
})
onUnmounted(() => {
emitter.off('reachBottom')
onActivated(() => {
initPageAction()
})
async function getFollowedUsersVideos() {
if (noMoreContent.value)
return
if (offset.value === '0') {
noMoreContent.value = true
return
}
emit('beforeLoading')
isLoading.value = true
try {
const response: MomentResult = await browser.runtime.sendMessage({
@@ -72,6 +105,7 @@ async function getFollowedUsersVideos() {
}
finally {
isLoading.value = false
emit('afterLoading')
}
}
@@ -115,6 +149,9 @@ function jumpToLoginPage() {
</template>
</div>
<!-- no more content -->
<Empty v-if="noMoreContentWarning" class="pb-4" :description="$t('common.no_more_content')" />
<Transition name="fade">
<Loading v-if="isLoading" />
</Transition>

View File

@@ -1,28 +1,43 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TrendingResult, List as VideoItem } from '~/models/video/trending'
import emitter from '~/utils/mitt'
const emit = defineEmits<{
(e: 'beforeLoading'): void
(e: 'afterLoading'): void
}>()
const videoList = reactive<VideoItem[]>([])
const isLoading = ref<boolean>(false)
const containerRef = ref<HTMLElement>() as Ref<HTMLElement>
const pn = ref<number>(1)
const { handleReachBottom, handlePageRefresh } = useBewlyApp()
onMounted(async () => {
await getTrendingVideos()
emitter.off('reachBottom')
emitter.on('reachBottom', async () => {
initPageAction()
})
onActivated(() => {
initPageAction()
})
function initPageAction() {
handleReachBottom.value = async () => {
if (!isLoading.value)
await getTrendingVideos()
})
})
}
onUnmounted(() => {
emitter.off('reachBottom')
})
handlePageRefresh.value = async () => {
videoList.length = 0
pn.value = 1
await getTrendingVideos()
}
}
async function getTrendingVideos() {
emit('beforeLoading')
isLoading.value = true
try {
const response: TrendingResult = await browser.runtime.sendMessage({
@@ -50,6 +65,7 @@ async function getTrendingVideos() {
}
finally {
isLoading.value = false
emit('afterLoading')
}
}
</script>

View File

@@ -3,7 +3,6 @@ import { useDateFormat } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import { getCSRF, openLinkToNewTab, removeHttpFromUrl } from '~/utils/main'
import { calcCurrentTime } from '~/utils/dataFormatter'
import emitter from '~/utils/mitt'
import type { List as VideoItem, WatchLaterResult } from '~/models/video/watchLater'
const { t } = useI18n()
@@ -11,20 +10,22 @@ const { t } = useI18n()
const isLoading = ref<boolean>()
const noMoreContent = ref<boolean>()
const watchLaterList = reactive<VideoItem[]>([])
const { handlePageRefresh } = useBewlyApp()
onMounted(() => {
getAllWatchLaterList()
initPageAction()
})
function initPageAction() {
handlePageRefresh.value = () => {
if (isLoading.value)
return
emitter.off('pageRefresh')
emitter.on('pageRefresh', async () => {
watchLaterList.length = 0
getAllWatchLaterList()
})
})
onUnmounted(() => {
emitter.off('pageRefresh')
})
}
}
/**
* Get watch later list
@@ -58,7 +59,6 @@ function deleteWatchLaterItem(index: number, aid: number) {
}
function handleClearAllWatchLater() {
// eslint-disable-next-line no-alert
const result = confirm(
t('watch_later.clear_all_confirm'),
)
@@ -77,7 +77,6 @@ function handleClearAllWatchLater() {
}
function handleRemoveWatchedVideos() {
// eslint-disable-next-line no-alert
const result = confirm(
t('watch_later.remove_watched_videos_confirm'),
)

View File

@@ -4,7 +4,6 @@ import Toast, { POSITION } from 'vue-toastification'
import { createPinia } from 'pinia'
import { i18n } from '~/utils/i18n'
import 'vue-toastification/dist/index.css'
import 'overlayscrollbars/overlayscrollbars.css'
const pinia = createPinia()
@@ -29,6 +28,4 @@ export async function setupApp(app: App) {
position: POSITION.TOP_CENTER,
})
app.use(pinia)
const { OverlayScrollbarsComponent } = await import('overlayscrollbars-vue')
app.component('OverlayScrollbarsComponent', OverlayScrollbarsComponent)
}

View File

@@ -1,11 +1,4 @@
/**
* 感謝這份專案給出的獲取accessKey的方法
* https://github.com/indefined/UserScripts/blob/42e20281d2e4d7bce16b5c8033b67ccb6ad312e9/bilibiliHome/bilibiliHome.user.js#L1149
* TODO: 解耦避免直接操作DOM
*/
/* eslint-disable no-throw-literal */
import browser from 'webextension-polyfill'
// import browser from 'webextension-polyfill'
import { appSign } from './appSign'
import { accessKey } from '~/logic/storage'
@@ -14,63 +7,66 @@ export function revokeAccessKey() {
}
/**
* @deprecated
* @param t
* @param element
* 感謝這份專案給出的獲取accessKey的方法
* https://github.com/indefined/UserScripts/blob/42e20281d2e4d7bce16b5c8033b67ccb6ad312e9/bilibiliHome/bilibiliHome.user.js#L1149
*/
export function grantAccessKey(t: any, element: HTMLButtonElement): void {
const originalInnerHTML = element.innerHTML
element.innerHTML = `
<span class="animate-pulse">Loading...</span>
`
element.style.pointerEvents = 'none'
element.disabled = true
// /**
// * @deprecated
// * @param t
// * @param element
// */
// export function grantAccessKey(t: any, element: HTMLButtonElement): void {
// const originalInnerHTML = element.innerHTML
// element.innerHTML = `
// <span class="animate-pulse">Loading...</span>
// `
// element.style.pointerEvents = 'none'
// element.disabled = true
const tip = t('auth.err_tip')
fetch(
'https://passport.bilibili.com/login/app/third?appkey=5fd5a7d8bfd9b0e6'
+ '&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a',
{
method: 'GET',
credentials: 'include',
},
)
.then(res => res.json())
.then((data) => {
if (data.code || !data.data)
throw { tip, msg: data.msg || data.message || data.code, data }
else if (!data.data.has_login)
throw { tip, msg: t('auth.plz_login_first'), data }
else if (!data.data.confirm_uri)
throw { tip, msg: t('auth.receive_verified_url_err'), data }
else return data.data.confirm_uri
})
.then(
url =>
browser.runtime
.sendMessage({
contentScriptQuery: 'getAccessKey',
confirmUri: url,
})
.then((res: { accessKey: string }) => {
accessKey.value = res.accessKey
return Promise.resolve()
})
.catch((err: any) => {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({ tip, msg: t('auth.failed_to_get_accesskey'), data: err })
}),
)
.catch((error) => {
element.innerHTML = originalInnerHTML
element.style.pointerEvents = 'auto'
element.disabled = false
// const tip = t('auth.err_tip')
// fetch(
// 'https://passport.bilibili.com/login/app/third?appkey=5fd5a7d8bfd9b0e6'
// + '&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a',
// {
// method: 'GET',
// credentials: 'include',
// },
// )
// .then(res => res.json())
// .then((data) => {
// if (data.code || !data.data)
// throw { tip, msg: data.msg || data.message || data.code, data }
// else if (!data.data.has_login)
// throw { tip, msg: t('auth.plz_login_first'), data }
// else if (!data.data.confirm_uri)
// throw { tip, msg: t('auth.receive_verified_url_err'), data }
// else return data.data.confirm_uri
// })
// .then(
// url =>
// browser.runtime
// .sendMessage({
// contentScriptQuery: 'getAccessKey',
// confirmUri: url,
// })
// .then((res: { accessKey: string }) => {
// accessKey.value = res.accessKey
// return Promise.resolve()
// })
// .catch((err: any) => {
// // eslint-disable-next-line prefer-promise-reject-errors
// return Promise.reject({ tip, msg: t('auth.failed_to_get_accesskey'), data: err })
// }),
// )
// .catch((error) => {
// element.innerHTML = originalInnerHTML
// element.style.pointerEvents = 'auto'
// element.disabled = false
// eslint-disable-next-line no-alert
alert(`${error.tip}: ${error.msg}`)
console.error(`${error.msg}: `, error.data)
})
}
// alert(`${error.tip}: ${error.msg}`)
// console.error(`${error.msg}: `, error.data)
// })
// }
// https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/APPKey.html#appkey
export const TVAppKey = {

View File

@@ -100,11 +100,11 @@ export function injectCSS(css: string): HTMLStyleElement {
/**
* delay
* @param time delay time
* @param ms milliseconds delay time
*/
export function delay(time: number) {
export function delay(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, time)
setTimeout(resolve, ms)
})
}

View File

@@ -33,11 +33,13 @@ export const sharedConfig: UserConfig = {
],
},
],
dirs: [r('src/composables')],
dts: r('src/auto-imports.d.ts'),
}),
// https://github.com/antfu/unplugin-vue-components
Components({
extensions: ['vue', 'ts'],
dirs: [r('src/components')],
// generate `components.d.ts` for ts support with Volar
dts: r('src/components.d.ts'),