[READ-ONLY] Mirror of https://github.com/probablykasper/kadium. App for staying ontop of YouTube channels' uploads kadium.kasper.space
linux macos notifications tauri windows youtube
0

Configure Feed

Select the types of activity you want to include in your feed.

Make scrolling/selection more resilient

Kasper (Jan 10, 2024, 12:33 PM +0100) 79fc4878 462754f8

+70 -34
+2
CHANGELOG.md
··· 7 7 - Allow selecting bottom row diagonally with the down arrow key 8 8 - Fix channel tags sometimes not saving 9 9 - Add ability to edit tags by double-clicking 10 + - Keep scroll position when archiving and unarchiving 11 + - Keep video selected if the selected video stays in the same spot when videos get refreshed 10 12 11 13 ## 1.5.0 - 2023 Nov 19 12 14 - Add support for all channel URLs. URLs like `/lacunarecs`, `/@lacunarecs`, `/c/lacunarecs`, `/user/lacunarecs` and even `/lacunarecs/playlists` etc work now.
+68 -34
src/routes/+page.svelte
··· 11 11 let videos: Video[] = [] 12 12 let allLoaded = false 13 13 $: getVideos($viewOptions) 14 - let loading = false 14 + let loading = 0 15 15 async function getVideos(options: ViewOptions) { 16 - loading = true 16 + loading++ 17 + 18 + const selectedId = videos[selectedIndex]?.id 17 19 18 20 const newVideos = await commands.getVideos(options, null) 19 21 allLoaded = newVideos.length < $viewOptions.limit 20 22 videos = newVideos 21 - selectedIndex = 0 22 - selectionVisible = false 23 - 24 - loading = false 23 + // Remove selection if the video changes position 24 + if (!selectedId && selectedId !== videos[selectedIndex]?.id) { 25 + selectedIndex = 0 26 + selectionVisible = false 27 + } 25 28 26 29 await tick() 27 30 await autoloadHandler() 31 + loading-- 28 32 } 29 - async function softRefresh(options: ViewOptions) { 30 - loading = true 33 + async function softRefresh(startIndex: number) { 34 + loading++ 31 35 32 - const newVideos = await commands.getVideos(options, null) 33 - allLoaded = newVideos.length < $viewOptions.limit 34 - videos = newVideos 35 - 36 - loading = false 36 + if (startIndex === 0) { 37 + const newVideos = await commands.getVideos($viewOptions, null) 38 + allLoaded = newVideos.length < $viewOptions.limit 39 + videos = newVideos 40 + } else { 41 + const prevVideo = videos[startIndex - 1] 42 + const prevVideos = videos.slice(0, startIndex) 43 + const maxLength = videos.length 44 + const reloadedVideos = await commands.getVideos( 45 + { 46 + ...$viewOptions, 47 + limit: $viewOptions.limit * 2, 48 + }, 49 + { 50 + publishTimeMs: prevVideo.publishTimeMs, 51 + id: prevVideo.id, 52 + }, 53 + ) 54 + videos = prevVideos.concat(reloadedVideos) 55 + // Shorten length to a mulpitle of `limit` 56 + if (videos.length > $viewOptions.limit) { 57 + videos = videos.slice(0, maxLength - (maxLength % $viewOptions.limit)) 58 + } 59 + videos = videos.slice(0, maxLength) 60 + } 37 61 38 62 await tick() 39 63 await autoloadHandler() 64 + loading-- 40 65 } 41 66 async function getMoreVideos() { 42 - loading = true 67 + loading++ 43 68 44 69 const newVideos = await commands.getVideos($viewOptions, { 45 70 publishTimeMs: videos[videos.length - 1].publishTimeMs, ··· 48 73 allLoaded = newVideos.length < $viewOptions.limit 49 74 videos = videos.concat(newVideos) 50 75 51 - loading = false 52 76 await tick() 53 77 await autoloadHandler() 78 + loading-- 54 79 } 55 80 async function autoloadHandler() { 56 81 if (!allLoaded && isScrolledNearBottom() && !loading) { ··· 83 108 let ts = new Date(timestamp) 84 109 return ts.getDate() + ' ' + months[ts.getMonth()] + ' ' + ts.getFullYear() 85 110 } 86 - async function archive(id: string) { 87 - await commands.archive(id) 88 - await softRefresh($viewOptions) 111 + async function archive(i: number) { 112 + loading++ 113 + const video = videos[i] 114 + await commands.archive(video.id) 115 + await softRefresh(i) 89 116 selectedIndex = Math.min(selectedIndex, videos.length - 1) 117 + loading-- 90 118 } 91 - async function unarchive(id: string) { 92 - await commands.unarchive(id) 93 - await softRefresh($viewOptions) 119 + async function unarchive(i: number) { 120 + loading++ 121 + const video = videos[i] 122 + await commands.unarchive(video.id) 123 + await softRefresh(i) 94 124 selectedIndex = Math.min(selectedIndex, videos.length - 1) 95 - } 96 - async function archiveToggleClick(id: string, isArchived: boolean) { 97 - if (isArchived) unarchive(id) 98 - else archive(id) 125 + loading-- 99 126 } 100 127 101 128 let scrollDiv: HTMLElement | null = null ··· 224 251 } else if (payload === 'Open Selected Channel' && selectionVisible) { 225 252 openChannel(selectedIndex) 226 253 } else if (payload === 'Archive' && selectionVisible) { 227 - archive(videos[selectedIndex].id) 254 + archive(selectedIndex) 228 255 } else if (payload === 'Unarchive' && selectionVisible) { 229 - unarchive(videos[selectedIndex].id) 256 + unarchive(selectedIndex) 230 257 } 231 258 }) 232 259 onDestroy(async () => { ··· 262 289 263 290 <main on:scroll={autoloadHandler} bind:this={scrollDiv}> 264 291 <div class="grid" bind:this={grid}> 265 - {#each videos as video, i} 292 + {#each videos as video, i (video.id)} 266 293 <!-- svelte-ignore a11y-click-events-have-key-events --> 267 294 <!-- svelte-ignore a11y-no-noninteractive-element-interactions --> 268 295 <div ··· 289 316 <!-- svelte-ignore a11y-no-static-element-interactions --> 290 317 <div 291 318 class="archive" 292 - on:click={() => archiveToggleClick(video.id, video.archived)} 319 + on:click={() => { 320 + if (video.archived) unarchive(i) 321 + else archive(i) 322 + }} 293 323 on:dblclick|stopPropagation 294 324 title="Archive" 295 325 > ··· 337 367 main 338 368 overflow-y: auto 339 369 max-width: 100% 370 + padding-bottom: 30px 371 + scroll-padding-bottom: 30px 340 372 .drag-ghost 341 373 font-size: 14px 342 374 top: -1000px ··· 349 381 max-width: 300px 350 382 border-radius: 3px 351 383 .grid 352 - height: 100% 353 - max-height: 100% 354 - flex-grow: 0 355 384 box-sizing: border-box 356 385 display: grid 357 - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) 386 + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)) 358 387 grid-gap: 5px 388 + @media screen and (min-width: 1000px) 389 + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)) 390 + grid-gap: 10px 391 + @media screen and (min-width: 1500px) 392 + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)) 393 + grid-gap: 10px 359 394 padding: var(--page-padding) 360 - padding-top: 15px 361 395 @media screen and (max-width: 450px) 362 396 grid-template-columns: 1fr 363 397 .box