Compare commits

..
Author SHA1 Message Date
DorianandClaude Fable 5 3d986b81a0 fix(ui): login keeps the intro video until the first login; rotation after
The login page sometimes swapped to a random bg-intro-N still on load,
breaking the seamless handoff from the intro video. Now: until anyone
has logged in / set a password (neode_first_login_done, stamped by every
Login success path incl. TOTP and setup), /login keeps the VIDEO
background — one continuous shot from the splash. From the second login
onward the lock screen rotates through the static backgrounds as
designed, and if the static path is ever hit pre-first-login it pins to
bg-intro.jpg (the video's end frame) instead of rotating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:34:06 +01:00
ai 2efed23afd Merge pull request 'fix(cloud): music opens the bottom-bar player, never the lightbox' (#88) from audio-bottom-bar into main
Demo images / Build & push demo images (push) Successful in 2m36s
2026-07-15 10:24:19 +00:00
DorianandClaude Fable 5 df403c5a69 fix(cloud): music opens the bottom-bar player, never the lightbox
FileCard audio clicks now emit play (global GlobalAudioPlayer bar) —
wired through the FileGrid list view, the Cloud My Files list and own
search results. Peer Files already used the bottom bar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:23:57 +01:00
ai a34634e00f Merge pull request 'feat(demo): intro on every fresh visit + neutral desktop Cloud tabs' (#87) from demo-intro-every-visit into main
Demo images / Build & push demo images (push) Successful in 2m35s
2026-07-15 10:13:30 +00:00
5 changed files with 41 additions and 17 deletions
+5 -2
View File
@@ -103,6 +103,7 @@ const emit = defineEmits<{
delete: [path: string]
share: [path: string, name: string, isDir: boolean]
preview: [path: string]
play: [path: string, name: string]
}>()
const cloudStore = useCloudStore()
@@ -122,8 +123,10 @@ const downloadHref = computed(() => cloudStore.downloadUrl(props.item.path))
function handleClick() {
if (props.item.isDir) {
emit('navigate', props.item.path)
} else if (isImage.value || isVideo.value || isAudio.value) {
// MediaLightbox handles all three media kinds.
} else if (isAudio.value) {
// Music goes to the global bottom-bar player, not the lightbox.
emit('play', props.item.path, props.item.name)
} else if (isImage.value || isVideo.value) {
emit('preview', props.item.path)
} else {
// Non-media files open by downloading — a click should always act on the file.
@@ -53,6 +53,7 @@
:item="item"
@navigate="$emit('navigate', $event)"
@delete="$emit('delete', $event)"
@play="(path, name) => $emit('play', path, name)"
@share="(path, name, isDir) => $emit('share', path, name, isDir)"
@preview="$emit('preview', $event)"
/>
+10
View File
@@ -93,6 +93,7 @@
:item="r.item"
@delete="handleDelete"
@share="handleShare"
@play="handlePlay"
@preview="(p: string) => handlePreview(p, searchMineItems)"
/>
<button
@@ -141,6 +142,7 @@
:item="item"
@delete="handleDelete"
@share="handleShare"
@play="handlePlay"
@preview="(p: string) => handlePreview(p, filteredMyFiles)"
/>
</div>
@@ -359,6 +361,7 @@ import { useCloudStore } from '../stores/cloud'
import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-client'
import { rpcClient } from '@/api/rpc-client'
import { getFileCategory } from '../composables/useFileType'
import { useAudioPlayer } from '../composables/useAudioPlayer'
import FileCard from '../components/cloud/FileCard.vue'
import ShareModal from '../components/cloud/ShareModal.vue'
import MediaLightbox from '../components/cloud/MediaLightbox.vue'
@@ -366,6 +369,7 @@ import MediaLightbox from '../components/cloud/MediaLightbox.vue'
const router = useRouter()
const store = useAppStore()
const cloudStore = useCloudStore()
const audioPlayer = useAudioPlayer()
const sectionCounts = ref<Record<string, number>>({})
const countsLoading = ref(false)
@@ -565,6 +569,12 @@ function handleShare(path: string, name: string, isDir: boolean) {
shareTarget.value = { path, name, isDir }
}
/** Music opens in the global bottom-bar player (GlobalAudioPlayer in App.vue). */
async function handlePlay(path: string, name: string) {
const url = await cloudStore.streamUrl(path)
audioPlayer.play(url, name)
}
function handlePreview(path: string, context: FileBrowserItem[]) {
// MediaLightbox filters to media internally; index within that filtered list.
const mediaItems = context.filter(item => {
+7 -1
View File
@@ -426,6 +426,8 @@ async function handleSetup() {
playLoginSuccessWhoosh()
loginTransition.setJustCompletedOnboarding(true)
loginTransition.setJustLoggedIn(true)
// First password setup counts as the first login (video → static rotation).
try { localStorage.setItem('neode_first_login_done', '1') } catch { /* ignore */ }
await new Promise(r => setTimeout(r, 520))
await router.replace(loginRedirectTo.value).catch(() => {
window.location.href = loginRedirectTo.value
@@ -538,13 +540,17 @@ async function handleTotpVerify() {
/** A login right after the onboarding wizard (flag set by OnboardingDone)
* gets the FULL dashboard entrance — zoom + oomph — even though it's a
* regular password login (e.g. the demo). Subsequent logins stay
* deliberately low-key (justLoggedIn only). */
* deliberately low-key (justLoggedIn only).
* Also stamps neode_first_login_done: the login page keeps the intro VIDEO
* background until someone has logged in once (OnboardingWrapper reads it
* to pick video vs the rotating static backgrounds). */
function consumeOnboardingFinale() {
try {
if (sessionStorage.getItem('archy_onboarding_finale') === '1') {
sessionStorage.removeItem('archy_onboarding_finale')
loginTransition.setJustCompletedOnboarding(true)
}
localStorage.setItem('neode_first_login_done', '1')
} catch { /* ignore */ }
}
+18 -14
View File
@@ -86,22 +86,22 @@ const videoBackgroundRoutes = ['/onboarding/intro', '/login']
// Login uses video when coming from splash, or static + glitch when direct
const isLoginRoute = computed(() => route.path === '/login')
// True once onboarding is complete. Used to skip the intro video on
// the /login route so that returning (logged-out) users go straight
// to the screensaver-style static + glitch background instead of
// replaying the full intro every time.
const onboardingDone = computed(() => {
// Check if current route should use video background.
// The FIRST login (nobody has logged in / set a password yet) keeps the VIDEO
// running so the splash → login handoff is one continuous shot; only after a
// successful login does /login switch to the rotating static backgrounds.
// Login.vue sets neode_first_login_done on every successful login.
const hasLoggedInBefore = () => {
try {
return localStorage.getItem('neode_onboarding_complete') === '1'
return localStorage.getItem('neode_first_login_done') === '1'
} catch {
return false
}
})
// Check if current route should use video background
}
const useVideoBackground = computed(() => {
if (!videoBackgroundRoutes.includes(route.path)) return false
if (route.path === '/login' && onboardingDone.value) return false
if (route.path === '/login' && hasLoggedInBefore()) return false
return true
})
@@ -122,9 +122,12 @@ const routeBackgrounds: Record<string, string> = {
'/login': 'bg-intro.jpg' // Video loops from splash (same as intro)
}
// Rotate the login background so the lock screen doesn't look
// identical on every logout. Cycles through bg-intro-1..6 using a
// counter persisted to localStorage so subsequent visits advance.
// Until the user has logged in (or set their password) once, the login
// background stays PINNED to the same still the intro video ends on — the
// video → login handoff only reads as one continuous shot when both show the
// identical frame. From the second login onward the lock screen rotates
// through bg-intro-1..6 for variety (counter persisted in localStorage).
// Login.vue sets neode_first_login_done on every successful login.
const LOGIN_BACKGROUNDS = [
'bg-intro-1.webp',
'bg-intro-2.jpg',
@@ -135,13 +138,14 @@ const LOGIN_BACKGROUNDS = [
]
function pickNextLoginBackground(): string {
try {
if (localStorage.getItem('neode_first_login_done') !== '1') return 'bg-intro.jpg'
const raw = localStorage.getItem('neode_login_bg_idx')
const prev = raw !== null ? parseInt(raw, 10) : -1
const next = (Number.isFinite(prev) ? prev + 1 : 0) % LOGIN_BACKGROUNDS.length
localStorage.setItem('neode_login_bg_idx', String(next))
return LOGIN_BACKGROUNDS[next]!
} catch {
return LOGIN_BACKGROUNDS[Math.floor(Math.random() * LOGIN_BACKGROUNDS.length)]!
return 'bg-intro.jpg'
}
}
const loginBackground = ref(pickNextLoginBackground())