579 lines
21 KiB
Svelte
579 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { onMount, untrack } from "svelte";
|
|
import { page } from "$app/stores";
|
|
import { goto } from "$app/navigation";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import type { MediaItem, Library } from "$lib/api/types";
|
|
import { library, libraryItems, isLibraryLoading, currentLibrary, libraries } from "$lib/stores/library";
|
|
import { auth } from "$lib/stores/auth";
|
|
import { isServerReachable } from "$lib/stores/connectivity";
|
|
import { downloads } from "$lib/stores/downloads";
|
|
import LibraryGrid from "$lib/components/library/LibraryGrid.svelte";
|
|
import TrackList from "$lib/components/library/TrackList.svelte";
|
|
import AlbumDownloadButton from "$lib/components/library/AlbumDownloadButton.svelte";
|
|
import SeasonSection from "$lib/components/library/SeasonSection.svelte";
|
|
import EpisodeFocusView from "$lib/components/library/EpisodeFocusView.svelte";
|
|
import SeriesDownloadButton from "$lib/components/library/SeriesDownloadButton.svelte";
|
|
import VideoDownloadButton from "$lib/components/library/VideoDownloadButton.svelte";
|
|
import CastSection from "$lib/components/library/CastSection.svelte";
|
|
import PersonDetailView from "$lib/components/library/PersonDetailView.svelte";
|
|
import RelatedItemsSection from "$lib/components/library/RelatedItemsSection.svelte";
|
|
import ArtistDetailView from "$lib/components/library/ArtistDetailView.svelte";
|
|
import PlaylistDetailView from "$lib/components/library/PlaylistDetailView.svelte";
|
|
import CrewLinks from "$lib/components/library/CrewLinks.svelte";
|
|
import GenreTags from "$lib/components/library/GenreTags.svelte";
|
|
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
|
|
|
interface SeasonData {
|
|
season: MediaItem;
|
|
episodes: MediaItem[];
|
|
}
|
|
|
|
let item = $state<MediaItem | null>(null);
|
|
let loading = $state(true);
|
|
let error = $state<string | null>(null);
|
|
let seasonData = $state<SeasonData[]>([]);
|
|
let directFetchedEpisode = $state<MediaItem | null>(null);
|
|
|
|
// Track if we've done an initial load and previous server state
|
|
let hasLoadedOnce = false;
|
|
let previousServerReachable = false;
|
|
|
|
const itemId = $derived($page.params.id);
|
|
const focusedEpisodeId = $derived($page.url.searchParams.get("episode"));
|
|
|
|
onMount(async () => {
|
|
await loadItem();
|
|
hasLoadedOnce = true;
|
|
});
|
|
|
|
$effect(() => {
|
|
if (itemId) {
|
|
loadItem();
|
|
hasLoadedOnce = true;
|
|
}
|
|
});
|
|
|
|
// Reload when server becomes reachable (handles cache-first timing issue)
|
|
$effect(() => {
|
|
const serverReachable = $isServerReachable;
|
|
|
|
// If server just became reachable and we've already loaded, reload to get fresh data
|
|
if (serverReachable && !previousServerReachable && hasLoadedOnce && itemId) {
|
|
loadItem();
|
|
}
|
|
|
|
previousServerReachable = serverReachable;
|
|
});
|
|
|
|
async function loadItem() {
|
|
if (!itemId) return;
|
|
// Only show spinner when navigating to a different item
|
|
// untrack prevents $effect from tracking `item` as a dependency (avoids infinite loop)
|
|
const isNewItem = untrack(() => !item || item.id !== itemId);
|
|
if (isNewItem) {
|
|
loading = true;
|
|
error = null;
|
|
seasonData = [];
|
|
directFetchedEpisode = null;
|
|
}
|
|
|
|
try {
|
|
item = await library.loadItem(itemId);
|
|
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.type})`);
|
|
console.log(`[LibraryDetail] - Has people? ${item?.people ? `YES (${item.people.length})` : 'NO'}`);
|
|
if (item?.people) {
|
|
item.people.forEach((p, i) => {
|
|
console.log(`[LibraryDetail] [${i}] ${p.name} (${p.type})`);
|
|
});
|
|
}
|
|
|
|
// Set currentLibrary for music items if not already set
|
|
// This ensures navigation to music library pages works correctly
|
|
if ((item?.type === "MusicAlbum" || item?.type === "MusicArtist" || item?.type === "Audio") && !$currentLibrary) {
|
|
// Find the music library
|
|
if ($libraries.length === 0) {
|
|
await library.loadLibraries();
|
|
}
|
|
const musicLibrary = $libraries.find(lib => lib.collectionType === "music");
|
|
if (musicLibrary) {
|
|
library.setCurrentLibrary(musicLibrary);
|
|
console.log("[LibraryDetail] Set current library to music library for music item");
|
|
}
|
|
}
|
|
|
|
await library.loadItems(itemId, { limit: 100 });
|
|
|
|
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
|
// Some APIs/caches may not include people data on first load
|
|
if ((item?.type === "Movie" || item?.type === "Series" || item?.type === "Episode") && (!item.people || item.people.length === 0)) {
|
|
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.type}...`);
|
|
try {
|
|
const repo = auth.getRepository();
|
|
const fullItem = await repo.getItem(itemId);
|
|
console.log(`[LibraryDetail] - Reloaded has people? ${fullItem.people ? `YES (${fullItem.people.length})` : 'NO'}`);
|
|
if (fullItem.people && fullItem.people.length > 0) {
|
|
item = fullItem;
|
|
console.log(`[LibraryDetail] ✓ Updated item with ${fullItem.people.length} people`);
|
|
fullItem.people.forEach((p, i) => {
|
|
console.log(`[LibraryDetail] [${i}] ${p.name} (${p.type})`);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn(`Could not reload ${item?.type} with full cast data:`, e);
|
|
}
|
|
}
|
|
|
|
// For Series, load seasons and their episodes
|
|
if (item?.type === "Series") {
|
|
const seasons = $libraryItems.filter((i) => i.type === "Season");
|
|
const repo = auth.getRepository();
|
|
|
|
// Load episodes for each season in parallel
|
|
const seasonDataPromises = seasons.map(async (season) => {
|
|
const result = await repo.getItems(season.id, { limit: 100 });
|
|
const episodes = result.items
|
|
.filter((i) => i.type === "Episode")
|
|
.sort((a, b) => (a.indexNumber || 0) - (b.indexNumber || 0));
|
|
return { season, episodes };
|
|
});
|
|
|
|
seasonData = await Promise.all(seasonDataPromises);
|
|
// Sort seasons by index number
|
|
seasonData.sort((a, b) => (a.season.indexNumber || 0) - (b.season.indexNumber || 0));
|
|
|
|
// If we have a focused episode ID but couldn't find it in the seasons,
|
|
// fetch it directly (handles ID mismatch between APIs)
|
|
const episodeIdParam = $page.url.searchParams.get("episode");
|
|
if (episodeIdParam) {
|
|
const allEps = seasonData.flatMap((s) => s.episodes);
|
|
const foundInSeasons = allEps.some((e) => e.id === episodeIdParam);
|
|
if (!foundInSeasons) {
|
|
try {
|
|
directFetchedEpisode = await repo.getItem(episodeIdParam);
|
|
} catch {
|
|
console.warn("Could not fetch focused episode directly:", episodeIdParam);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : "Failed to load item";
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
// Images now handled by CachedImage component
|
|
|
|
function formatDuration(ticks?: number): string {
|
|
if (!ticks) return "";
|
|
const seconds = Math.floor(ticks / 10000000);
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
return `${minutes}m`;
|
|
}
|
|
|
|
function handleItemClick(clickedItem: MediaItem | Library) {
|
|
if (!("type" in clickedItem)) {
|
|
// Library item - navigate to library
|
|
goto(`/library/${clickedItem.id}`);
|
|
return;
|
|
}
|
|
switch (clickedItem.type) {
|
|
case "Series":
|
|
case "Season":
|
|
case "MusicAlbum":
|
|
case "MusicArtist":
|
|
case "Folder":
|
|
case "Playlist":
|
|
case "Channel":
|
|
case "ChannelFolderItem":
|
|
case "Episode":
|
|
case "Movie":
|
|
goto(`/library/${clickedItem.id}`);
|
|
break;
|
|
default:
|
|
goto(`/player/${clickedItem.id}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Removed custom handleTrackClick - let TrackList use its built-in playback logic
|
|
// This fixes Android playback issues where navigation-based approach was hanging
|
|
|
|
function handleEpisodeClick(episode: MediaItem) {
|
|
// Play the episode with the series queued for next episode
|
|
goto(`/player/${episode.id}`);
|
|
}
|
|
|
|
async function handlePlayAll() {
|
|
// For single items (Episode, Movie), play the item directly
|
|
if (item?.type === "Episode" || item?.type === "Movie") {
|
|
goto(`/player/${itemId}`);
|
|
} else if (item?.type === "MusicAlbum" && $libraryItems.length > 0) {
|
|
// For albums, use the backend command (backend fetches and queues all tracks)
|
|
try {
|
|
const repo = auth.getRepository();
|
|
const repositoryHandle = repo.getHandle();
|
|
const firstTrack = $libraryItems[0];
|
|
await invoke("player_play_album_track", {
|
|
repositoryHandle,
|
|
request: {
|
|
albumId: item.id,
|
|
albumName: item.name,
|
|
trackId: firstTrack.id,
|
|
shuffle: false,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to play album:", e);
|
|
alert(`Failed to play album: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
}
|
|
} else if ($libraryItems.length > 0) {
|
|
// For other collections, start playing first item
|
|
goto(`/player/${$libraryItems[0].id}?queue=parent:${itemId}`);
|
|
}
|
|
}
|
|
|
|
async function handleShufflePlay() {
|
|
if (item?.type === "MusicAlbum" && $libraryItems.length > 0) {
|
|
// For albums, use the backend command with shuffle
|
|
try {
|
|
const repo = auth.getRepository();
|
|
const repositoryHandle = repo.getHandle();
|
|
// Pick a random track to start with
|
|
const randomTrack = $libraryItems[Math.floor(Math.random() * $libraryItems.length)];
|
|
await invoke("player_play_album_track", {
|
|
repositoryHandle,
|
|
request: {
|
|
albumId: item.id,
|
|
albumName: item.name,
|
|
trackId: randomTrack.id,
|
|
shuffle: true,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to shuffle play album:", e);
|
|
alert(`Failed to shuffle play: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
}
|
|
} else if ($libraryItems.length > 0) {
|
|
const randomIndex = Math.floor(Math.random() * $libraryItems.length);
|
|
goto(`/player/${$libraryItems[randomIndex].id}?queue=parent:${itemId}&shuffle=true`);
|
|
}
|
|
}
|
|
|
|
// For episode focus view: get all episodes across all seasons
|
|
const allEpisodes = $derived(
|
|
seasonData.flatMap((s) => s.episodes)
|
|
);
|
|
|
|
// Find the focused episode (check allEpisodes first, then fall back to directly fetched)
|
|
const focusedEpisode = $derived(
|
|
focusedEpisodeId
|
|
? allEpisodes.find((e) => e.id === focusedEpisodeId) ?? directFetchedEpisode
|
|
: null
|
|
);
|
|
|
|
const isMusicItem = $derived(
|
|
item?.type === "Audio" || item?.type === "MusicAlbum" || item?.type === "MusicArtist" || item?.type === "Playlist"
|
|
);
|
|
|
|
function handleBackToSeries() {
|
|
// Navigate to series page without the episode param
|
|
goto(`/library/${itemId}`);
|
|
}
|
|
</script>
|
|
|
|
<div class="relative">
|
|
<!-- Backdrop -->
|
|
{#if item?.backdropImageTags?.[0]}
|
|
<div class="absolute inset-0 -z-10 h-96 overflow-hidden">
|
|
<CachedImage
|
|
itemId={item.id}
|
|
imageType="Backdrop"
|
|
tag={item.backdropImageTags[0]}
|
|
maxWidth={1920}
|
|
class="w-full h-full object-cover opacity-30"
|
|
/>
|
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-[var(--color-background)]"></div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if loading}
|
|
<div class="flex justify-center py-12">
|
|
<div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
|
|
</div>
|
|
{:else if error}
|
|
<div class="text-center py-12">
|
|
<p class="text-red-400">{error}</p>
|
|
<button
|
|
onclick={() => goto("/library")}
|
|
class="mt-4 text-[var(--color-jellyfin)] hover:underline"
|
|
>
|
|
Back to library
|
|
</button>
|
|
</div>
|
|
{:else if item}
|
|
<!-- Person Detail View - shown for Person items -->
|
|
{#if item.type === "Person"}
|
|
<PersonDetailView person={item} />
|
|
<!-- Episode Focus View - shown when navigating with ?episode param -->
|
|
{:else if item.type === "Series" && focusedEpisode}
|
|
<EpisodeFocusView
|
|
episode={focusedEpisode}
|
|
series={item}
|
|
{allEpisodes}
|
|
onBack={handleBackToSeries}
|
|
/>
|
|
{:else}
|
|
<div class="space-y-8">
|
|
<!-- Header with item info -->
|
|
<div class="flex gap-6 pt-4">
|
|
<!-- Poster -->
|
|
<div class="flex-shrink-0 w-48">
|
|
{#if item.primaryImageTag}
|
|
<CachedImage
|
|
itemId={item.id}
|
|
imageType="Primary"
|
|
tag={item.primaryImageTag}
|
|
maxWidth={400}
|
|
alt={item.name}
|
|
class="w-full {isMusicItem ? 'aspect-square' : ''} rounded-lg shadow-lg"
|
|
/>
|
|
{:else}
|
|
<div class="w-full {isMusicItem ? 'aspect-square' : 'aspect-[2/3]'} bg-[var(--color-surface)] rounded-lg flex items-center justify-center">
|
|
<svg class="w-16 h-16 text-gray-600" fill="currentColor" viewBox="0 0 24 24">
|
|
{#if isMusicItem}
|
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
|
{:else}
|
|
<path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/>
|
|
{/if}
|
|
</svg>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1 space-y-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white">{item.name}</h1>
|
|
{#if item.type === "Episode" && (item.parentIndexNumber || item.indexNumber)}
|
|
<p class="text-lg text-gray-400 mt-1">
|
|
{#if item.parentIndexNumber}Season {item.parentIndexNumber}{/if}
|
|
{#if item.parentIndexNumber && item.indexNumber}, {/if}
|
|
{#if item.indexNumber}Episode {item.indexNumber}{/if}
|
|
</p>
|
|
{:else if item.productionYear || item.artists?.length}
|
|
<p class="text-lg text-gray-400 mt-1">
|
|
{item.artists?.join(", ") || item.productionYear}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Metadata -->
|
|
<div class="flex items-center gap-4 text-sm text-gray-400">
|
|
{#if item.type}
|
|
<span class="px-2 py-1 bg-[var(--color-surface)] rounded">{item.type}</span>
|
|
{/if}
|
|
{#if item.runTimeTicks}
|
|
<span>{formatDuration(item.runTimeTicks)}</span>
|
|
{/if}
|
|
{#if item.communityRating}
|
|
<span class="flex items-center gap-1">
|
|
<svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
</svg>
|
|
{item.communityRating.toFixed(1)}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-3 flex-wrap">
|
|
<button
|
|
onclick={handlePlayAll}
|
|
class="px-6 py-2 bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin-dark)] rounded-lg font-medium flex items-center gap-2 transition-colors"
|
|
>
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z"/>
|
|
</svg>
|
|
Play
|
|
</button>
|
|
{#if item.type !== "Episode" && item.type !== "Movie"}
|
|
<button
|
|
onclick={handleShufflePlay}
|
|
class="px-6 py-2 bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)] rounded-lg font-medium flex items-center gap-2 transition-colors"
|
|
>
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/>
|
|
</svg>
|
|
Shuffle
|
|
</button>
|
|
{/if}
|
|
{#if item.type === "MusicAlbum"}
|
|
<AlbumDownloadButton
|
|
albumId={item.id}
|
|
albumName={item.name}
|
|
tracks={$libraryItems}
|
|
/>
|
|
{:else if item.type === "Series"}
|
|
<SeriesDownloadButton
|
|
seriesId={item.id}
|
|
seriesName={item.name}
|
|
episodeCount={allEpisodes.length || undefined}
|
|
/>
|
|
{:else if item.type === "Movie"}
|
|
<VideoDownloadButton
|
|
itemId={item.id}
|
|
itemName={item.name}
|
|
isMovie={true}
|
|
size="lg"
|
|
/>
|
|
{:else if item.type === "Episode"}
|
|
<VideoDownloadButton
|
|
itemId={item.id}
|
|
itemName={item.name}
|
|
isMovie={false}
|
|
size="lg"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Overview -->
|
|
{#if item.overview}
|
|
<p class="text-gray-300 leading-relaxed max-w-2xl">{item.overview}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Crew Links - for Movies and Series -->
|
|
{#if item.people && (item.type === "Movie" || item.type === "Series")}
|
|
<div class="space-y-2">
|
|
{#if item.people.some(p => p.type === "Director")}
|
|
<CrewLinks
|
|
people={item.people}
|
|
roleFilter={["Director"]}
|
|
label="Directed by"
|
|
maxShow={3}
|
|
/>
|
|
{/if}
|
|
|
|
{#if item.people.some(p => p.type === "Writer")}
|
|
<CrewLinks
|
|
people={item.people}
|
|
roleFilter={["Writer"]}
|
|
label="Written by"
|
|
maxShow={3}
|
|
/>
|
|
{/if}
|
|
|
|
{#if item.people.some(p => p.type === "Composer")}
|
|
<CrewLinks
|
|
people={item.people}
|
|
roleFilter={["Composer"]}
|
|
label="Music by"
|
|
maxShow={2}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Genre Tags -->
|
|
{#if item.genres?.length}
|
|
<div>
|
|
<GenreTags genres={item.genres} maxShow={6} />
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Cast Section - for Movies, Series, and Episodes -->
|
|
{#if (item.type === "Movie" || item.type === "Series" || item.type === "Episode") && item.people?.length}
|
|
<CastSection people={item.people} />
|
|
{/if}
|
|
|
|
<!-- Related Items Section - for Movies and Series -->
|
|
{#if (item.type === "Movie" || item.type === "Series") && (item.genres?.length || item.people?.length)}
|
|
<RelatedItemsSection
|
|
currentItemId={item.id}
|
|
itemType={item.type}
|
|
genres={item.genres}
|
|
people={item.people}
|
|
limit={12}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Content items -->
|
|
<div>
|
|
{#if item.type === "MusicAlbum"}
|
|
<!-- Tracks in list view -->
|
|
<div class="space-y-8">
|
|
<div class="space-y-4">
|
|
<h2 class="text-xl font-semibold text-white">Tracks</h2>
|
|
<TrackList
|
|
tracks={$libraryItems}
|
|
loading={$isLibraryLoading}
|
|
showArtist={false}
|
|
showAlbum={false}
|
|
showDownload={true}
|
|
context={{ type: "album", albumId: item.id, albumName: item.name }}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Related Albums -->
|
|
{#if item.genres?.length || item.artistItems?.length}
|
|
<RelatedItemsSection
|
|
currentItemId={item.id}
|
|
itemType="MusicAlbum"
|
|
genres={item.genres}
|
|
artistIds={item.artistItems?.map(a => a.id)}
|
|
limit={12}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{:else if item.type === "Series"}
|
|
<!-- Series: Seasons with episodes -->
|
|
<div class="space-y-8">
|
|
{#if $isLibraryLoading}
|
|
<div class="flex justify-center py-8">
|
|
<div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
|
|
</div>
|
|
{:else if seasonData.length === 0}
|
|
<div class="text-center py-12 text-gray-400">
|
|
<p>No seasons found</p>
|
|
</div>
|
|
{:else}
|
|
{#each seasonData as { season, episodes } (season.id)}
|
|
<SeasonSection
|
|
{season}
|
|
{episodes}
|
|
focusedEpisodeId={focusedEpisodeId ?? undefined}
|
|
onEpisodeClick={handleEpisodeClick}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
{:else if item.type === "MusicArtist"}
|
|
<!-- Enhanced artist detail view with discography -->
|
|
<ArtistDetailView artist={item} />
|
|
{:else if item.type === "Playlist"}
|
|
<!-- Playlist detail view with track management -->
|
|
<PlaylistDetailView playlist={item} />
|
|
{:else}
|
|
<!-- Other content in grid view -->
|
|
<LibraryGrid
|
|
title="Contents"
|
|
items={$libraryItems}
|
|
loading={$isLibraryLoading}
|
|
onItemClick={handleItemClick}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|