pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

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

Feat: Added whole season watched button

authored by

SimSalabimse and committed by
Pas
cbf1d678 65ea4c50

+181 -12
+57
src/components/overlays/details/ConfirmOverlay.tsx
··· 1 + import { Button } from "@/components/buttons/Button"; 2 + import { 3 + OverlayDisplay, 4 + OverlayPortal, 5 + } from "@/components/overlays/OverlayDisplay"; 6 + 7 + interface ConfirmOverlayProps { 8 + isOpen: boolean; 9 + message: string; 10 + onConfirm: (event: React.MouseEvent) => void; 11 + onCancel: () => void; 12 + confirmButtonTheme?: "white" | "purple" | "secondary" | "danger" | "glass"; 13 + cancelButtonTheme?: "white" | "purple" | "secondary" | "danger" | "glass"; 14 + backdropOpacity?: number; 15 + backdropColor?: string; 16 + } 17 + 18 + export function ConfirmOverlay({ 19 + isOpen, 20 + message, 21 + onConfirm, 22 + onCancel, 23 + confirmButtonTheme = "purple", 24 + cancelButtonTheme = "secondary", 25 + backdropOpacity = 0.5, 26 + backdropColor = "black", 27 + }: ConfirmOverlayProps) { 28 + return ( 29 + <OverlayPortal show={isOpen}> 30 + <div 31 + className={`fixed inset-0 bg-${backdropColor} bg-opacity-${backdropOpacity * 100} flex items-center justify-center z-50`} 32 + > 33 + <OverlayDisplay> 34 + <div className="bg-background-main text-white p-4 rounded-lg shadow-md flex flex-col items-center pointer-events-auto gap-3"> 35 + <p className="mb-4, text-center">{message}</p> 36 + <div className="flex space-x-2"> 37 + <Button 38 + theme={confirmButtonTheme} 39 + onClick={onConfirm} 40 + padding="px-3 py-1" 41 + > 42 + Confirm 43 + </Button> 44 + <Button 45 + theme={cancelButtonTheme} 46 + onClick={onCancel} 47 + padding="px-3 py-1" 48 + > 49 + Cancel 50 + </Button> 51 + </div> 52 + </div> 53 + </OverlayDisplay> 54 + </div> 55 + </OverlayPortal> 56 + ); 57 + }
+124 -12
src/components/overlays/details/EpisodeCarousel.tsx
··· 6 6 import { Button } from "@/components/buttons/Button"; 7 7 import { Dropdown } from "@/components/form/Dropdown"; 8 8 import { Icon, Icons } from "@/components/Icon"; 9 + import { ConfirmOverlay } from "@/components/overlays/details/ConfirmOverlay"; 9 10 import { hasAired } from "@/components/player/utils/aired"; 10 11 import { useProgressStore } from "@/stores/progress"; 11 12 ··· 25 26 const [showEpisodeMenu, setShowEpisodeMenu] = useState(false); 26 27 const [customSeason, setCustomSeason] = useState(""); 27 28 const [customEpisode, setCustomEpisode] = useState(""); 29 + const [SeasonWatched, setSeasonWatched] = useState(false); 30 + const [isConfirmOpen, setIsConfirmOpen] = useState(false); 28 31 const [expandedEpisodes, setExpandedEpisodes] = useState<{ 29 32 [key: number]: boolean; 30 33 }>({}); ··· 203 206 } 204 207 }; 205 208 209 + // Toggle whole season watch status 210 + const toggleSeasonWatchStatus = (event: React.MouseEvent) => { 211 + event.preventDefault(); 212 + event.stopPropagation(); 213 + 214 + setIsConfirmOpen(true); 215 + }; 216 + 217 + const handleCancel = () => { 218 + setIsConfirmOpen(false); 219 + }; 220 + 206 221 const currentSeasonEpisodes = episodes.filter( 207 222 (ep) => ep.season_number === selectedSeason, 208 223 ); 209 224 225 + const handleConfirm = (event: React.MouseEvent) => { 226 + try { 227 + const episodeWatchedStatus: boolean[] = []; 228 + currentSeasonEpisodes.forEach((episode: any) => { 229 + const episodeProgress = 230 + progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; 231 + const percentage = episodeProgress 232 + ? (episodeProgress.progress.watched / 233 + episodeProgress.progress.duration) * 234 + 100 235 + : 0; 236 + const isAired = hasAired(episode.air_date); 237 + const isWatched = percentage > 90; 238 + if (isAired && !isWatched) { 239 + episodeWatchedStatus.push(isWatched); 240 + } 241 + }); 242 + 243 + const hasUnwatched = episodeWatchedStatus.length >= 1; 244 + 245 + currentSeasonEpisodes.forEach((episode: any) => { 246 + const episodeProgress = 247 + progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; 248 + const percentage = episodeProgress 249 + ? (episodeProgress.progress.watched / 250 + episodeProgress.progress.duration) * 251 + 100 252 + : 0; 253 + const isAired = hasAired(episode.air_date); 254 + const isWatched = percentage > 90; 255 + if (hasUnwatched && isAired && !isWatched) { 256 + toggleWatchStatus(episode.id, event); // Mark unwatched as watched 257 + } else if (!hasUnwatched && isAired && isWatched) { 258 + toggleWatchStatus(episode.id, event); // Mark watched as unwatched 259 + } 260 + }); 261 + 262 + setIsConfirmOpen(false); 263 + } catch (error) { 264 + console.error("Error in handleConfirm:", error); 265 + setIsConfirmOpen(false); 266 + } 267 + }; 268 + 210 269 const toggleEpisodeExpansion = ( 211 270 episodeId: number, 212 271 event: React.MouseEvent, ··· 259 318 }; 260 319 }, [episodes, expandedEpisodes]); 261 320 321 + useEffect(() => { 322 + const episodeWatchedStatus: boolean[] = []; 323 + 324 + currentSeasonEpisodes.forEach((episode: any) => { 325 + const episodeProgress = 326 + progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; 327 + const percentage = episodeProgress 328 + ? (episodeProgress.progress.watched / 329 + episodeProgress.progress.duration) * 330 + 100 331 + : 0; 332 + const isAired = hasAired(episode.air_date); 333 + const isWatched = percentage > 90; 334 + 335 + if (isAired && !isWatched) { 336 + episodeWatchedStatus.push(isWatched); 337 + } 338 + }); 339 + 340 + let toggle: boolean; 341 + 342 + if (episodeWatchedStatus.length >= 1) { 343 + setSeasonWatched(true); // If no episodes are watched, we want to mark all as watched 344 + } else { 345 + setSeasonWatched(false); // if all episodes are watched, we want to mark all as unwatched 346 + } 347 + }, [currentSeasonEpisodes, episodes, mediaId, progress]); 348 + 262 349 return ( 263 350 <div className="mt-6 md:mt-0"> 264 351 {/* Season Selector */} ··· 323 410 )} 324 411 </div> 325 412 </div> 326 - <Dropdown 327 - options={seasons.map((season) => ({ 328 - id: season.season_number.toString(), 329 - name: `${t("details.season")} ${season.season_number}`, 330 - }))} 331 - selectedItem={{ 332 - id: selectedSeason.toString(), 333 - name: `${t("details.season")} ${selectedSeason}`, 334 - }} 335 - setSelectedItem={(item) => onSeasonChange(Number(item.id))} 336 - /> 413 + <div className="flex items-center justify-between gap-2"> 414 + {isConfirmOpen && ( 415 + <ConfirmOverlay 416 + isOpen={isConfirmOpen} 417 + message={ 418 + SeasonWatched 419 + ? "Are you sure you want to mark the season as watched?" 420 + : "Are you sure you want to mark the season as unwatched?" 421 + } 422 + onConfirm={handleConfirm} 423 + onCancel={handleCancel} 424 + /> 425 + )} 426 + <button 427 + type="button" 428 + onClick={(e) => toggleSeasonWatchStatus(e)} 429 + className="p-1.5 bg-black/50 rounded-full hover:bg-black/80 transition-colors" 430 + title={t("Mark season as watched")} 431 + > 432 + <Icon 433 + icon={SeasonWatched ? Icons.EYE : Icons.EYE_SLASH} 434 + className="h-5 w-5 text-white" 435 + /> 436 + </button> 437 + 438 + <Dropdown 439 + options={seasons.map((season) => ({ 440 + id: season.season_number.toString(), 441 + name: `${t("details.season")} ${season.season_number}`, 442 + }))} 443 + selectedItem={{ 444 + id: selectedSeason.toString(), 445 + name: `${t("details.season")} ${selectedSeason}`, 446 + }} 447 + setSelectedItem={(item) => onSeasonChange(Number(item.id))} 448 + /> 449 + </div> 337 450 </div> 338 451 339 452 {/* Episodes Carousel */} ··· 359 472 > 360 473 {/* Add padding before the first card */} 361 474 <div className="flex-shrink-0 w-4" /> 362 - 363 475 {currentSeasonEpisodes.map((episode) => { 364 476 const isActive = 365 477 showProgress?.episode?.id === episode.id.toString();