Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions assets/js/BackupHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface IBackupState {
export async function createBackupState(): Promise<IBackupState> {
const { userBooruList } = useBooruList()
const { tagCollections } = useTagCollections()
const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia } = useUserSettings()
const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia, preferClassicVideoPlayer } = useUserSettings()

// TODO: Only save data that is not defaulted

Expand All @@ -30,7 +30,8 @@ export async function createBackupState(): Promise<IBackupState> {
settings: {
postFullSizeImages: postFullSizeImages.value,
postsPerPage: postsPerPage.value,
autoplayAnimatedMedia: autoplayAnimatedMedia.value
autoplayAnimatedMedia: autoplayAnimatedMedia.value,
preferClassicVideoPlayer: preferClassicVideoPlayer.value
}
}

Expand All @@ -51,7 +52,7 @@ async function restoreV3Backup(backupState: IBackupState) {
}

if (backupState.settings) {
const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia } = useUserSettings()
const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia, preferClassicVideoPlayer } = useUserSettings()

if (backupState.settings.postFullSizeImages != null) {
postFullSizeImages.value = backupState.settings.postFullSizeImages
Expand All @@ -64,6 +65,10 @@ async function restoreV3Backup(backupState: IBackupState) {
if (backupState.settings.autoplayAnimatedMedia != null) {
autoplayAnimatedMedia.value = backupState.settings.autoplayAnimatedMedia
}

if (backupState.settings.preferClassicVideoPlayer != null) {
preferClassicVideoPlayer.value = backupState.settings.preferClassicVideoPlayer
}
}
}

Expand Down
44 changes: 43 additions & 1 deletion components/pages/posts/post/PostMedia.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

const requestUrl = useRequestURL()
const { isPremium } = useUserData()
const { autoplayAnimatedMedia } = useUserSettings()
const { autoplayAnimatedMedia, preferClassicVideoPlayer } = useUserSettings()
let { timesVideoHasRendered } = useEthics()
const { wasCurrentPageSSR } = useSSRDetection()

Expand Down Expand Up @@ -44,6 +44,7 @@
const isAnimatedMedia = computed(
() => props.mediaType === 'animated' || (props.mediaType === 'image' && props.mediaSrc.endsWith('.gif'))
)
const shouldUseClassicVideoPlayer = computed(() => isVideo.value && isPremium.value && preferClassicVideoPlayer.value)

const triedToLoadWithProxy = shallowRef(false)
const triedToLoadPosterWithProxy = shallowRef(false)
Expand All @@ -61,6 +62,10 @@

switch (true) {
case isVideo.value:
if (shouldUseClassicVideoPlayer.value) {
break
}

createVideoPlayer()
break

Expand All @@ -72,6 +77,14 @@
}
})

function getVideoElement() {
if (!mediaElement.value || !isVideo.value) {
return null
}

return mediaElement.value as HTMLVideoElement
}

onBeforeUnmount(() => {
let finalMediaElement = mediaElement.value

Expand All @@ -96,6 +109,10 @@

//
else if (isVideo.value) {
if (shouldUseClassicVideoPlayer.value) {
return
}

destroyVideoPlayer()
}
Comment on lines +112 to 117
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard player teardown when classic/fluid mode changes reactively.

shouldUseClassicVideoPlayer is reactive, but Line 294 still assumes Fluid was initialized. If the component entered classic mode first, destroyVideoPlayer() throws (Player not found) on reload/unmount once the computed mode flips.

Proposed fix
 function destroyVideoPlayer() {
-  if (!videoPlayer) {
-    throw new Error('Player not found')
-  }
+  if (!videoPlayer) {
+    return
+  }

   videoPlayer.destroy()
+  videoPlayer = undefined
 }

 onBeforeUnmount(() => {
   let finalMediaElement = mediaElement.value
@@
   else if (isVideo.value) {
     if (shouldUseClassicVideoPlayer.value) {
       return
     }

-    destroyVideoPlayer()
+    if (videoPlayer) {
+      destroyVideoPlayer()
+    }
   }
 })

 function reloadVideoPlayer(shouldPlay: boolean = false) {
@@
   nextTick(() => {
-    destroyVideoPlayer()
+    if (videoPlayer) {
+      destroyVideoPlayer()
+    }

     nextTick(() => {
       createVideoPlayer()

Also applies to: 273-295

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/post/PostMedia.vue` around lines 112 - 117, The
teardown path calls destroyVideoPlayer() even when no Fluid player was
initialized because shouldUseClassicVideoPlayer is reactive; change the guard to
verify both the mode and the presence of an initialized player before
destroying: in the reactive/unmount teardown logic referencing
shouldUseClassicVideoPlayer and destroyVideoPlayer, first check the computed
shouldUseClassicVideoPlayer.value and also check the actual player
instance/state (e.g., a videoPlayer variable, isPlayerInitialized flag, or
similar) and only call destroyVideoPlayer() when that player exists; apply the
same guard update to the other teardown block mentioned around the 273-295
region.

})
Expand Down Expand Up @@ -253,6 +270,26 @@
}

function reloadVideoPlayer(shouldPlay: boolean = false) {
if (shouldUseClassicVideoPlayer.value) {
nextTick(() => {
const videoElement = getVideoElement()

if (!videoElement) {
return
}

videoElement.load()

if (!shouldPlay) {
return
}

void videoElement.play().catch(() => undefined)
})

return
}

nextTick(() => {
destroyVideoPlayer()

Expand Down Expand Up @@ -368,6 +405,11 @@
const entry = entries[0]

if (!entry.isIntersecting) {
if (shouldUseClassicVideoPlayer.value) {
getVideoElement()?.pause()
return
}

videoPlayer?.pause()
}
}
Expand Down
12 changes: 7 additions & 5 deletions components/pages/settings/SettingSwitch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</script>

<script setup>
const props = defineProps(['modelValue'])
const props = defineProps(['modelValue', 'disabled'])
const emit = defineEmits(['update:modelValue'])
</script>

Expand All @@ -17,28 +17,30 @@
<span class="flex grow flex-col">
<HeadlessSwitchLabel
as="span"
class="font-medium leading-8 text-base-content-highlight"
class="text-base-content-highlight leading-8 font-medium"
passive
>
<slot name="name" />
</HeadlessSwitchLabel>

<HeadlessSwitchDescription
as="span"
class="text-sm text-base-content"
class="text-base-content text-sm"
>
<slot name="description" />
</HeadlessSwitchDescription>
</span>

<HeadlessSwitch
:disabled="disabled"
:modelValue="modelValue"
class="focus-visible:focus-outline-util relative inline-flex h-6 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent ring-1 ring-base-0/20 transition-colors duration-200 ease-in-out ui-checked:bg-primary-700 ui-not-checked:bg-base-1000"
class="focus-visible:focus-outline-util ring-base-0/20 ui-checked:bg-primary-700 ui-not-checked:bg-base-1000 relative inline-flex h-6 w-12 shrink-0 rounded-full border-2 border-transparent ring-1 transition-colors duration-200 ease-in-out"
:class="disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
@update:modelValue="emit('update:modelValue', $event)"
>
<span
aria-hidden="true"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-base-content-highlight shadow-sm ring-0 transition duration-200 ease-in-out ui-checked:translate-x-6 ui-not-checked:translate-x-0"
class="bg-base-content-highlight ui-checked:translate-x-6 ui-not-checked:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full shadow-sm ring-0 transition duration-200 ease-in-out"
/>
</HeadlessSwitch>
</HeadlessSwitchGroup>
Expand Down
5 changes: 5 additions & 0 deletions composables/useUserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default function () {
let postFullSizeImages = ref<boolean>(false)
let postsPerPage = ref<number>(29)
let autoplayAnimatedMedia = ref<boolean>(false)
let preferClassicVideoPlayer = ref<boolean>(false)
let blockAiGeneratedImages = ref<boolean>(false)

if (import.meta.client) {
Expand All @@ -16,6 +17,9 @@ export default function () {
autoplayAnimatedMedia = useLocalStorage('settings-autoplayAnimatedMedia', false, {
writeDefaults: false
})
preferClassicVideoPlayer = useLocalStorage('settings-preferClassicVideoPlayer', false, {
writeDefaults: false
})
blockAiGeneratedImages = useLocalStorage('settings-blockAiGeneratedImages', false, {
writeDefaults: false
})
Expand All @@ -25,6 +29,7 @@ export default function () {
postFullSizeImages,
postsPerPage,
autoplayAnimatedMedia,
preferClassicVideoPlayer,
blockAiGeneratedImages
}
}
17 changes: 16 additions & 1 deletion pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

const appVersion = version

const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia, blockAiGeneratedImages } = useUserSettings()
const { postFullSizeImages, postsPerPage, autoplayAnimatedMedia, preferClassicVideoPlayer, blockAiGeneratedImages } =
useUserSettings()
const { isPremium } = useUserData()
const { selectedList, selectedBlockList, defaultBlockList, customBlockList, resetCustomBlockList } = useBlockLists()

Expand Down Expand Up @@ -142,6 +143,20 @@
</SettingSwitch>
</li>

<li>
<SettingSwitch
v-model="preferClassicVideoPlayer"
:disabled="!isPremium"
>
<template #name> Classic video controls </template>

<template #description>
Use your browser's native video player instead of the JS player. Premium only so video ad support stays
intact.
</template>
</SettingSwitch>
</li>

<!-- postFullSizeImages -->
<li>
<SettingSwitch v-model="postFullSizeImages">
Expand Down
1 change: 1 addition & 0 deletions test/pages/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ describe('/settings', async () => {
await page.waitForSelector('h1')

expect(await page.textContent('h1')).toBe('Settings')
expect(await page.textContent('body')).toContain('Classic video controls')
})
})