From 11927baf041279f9d42b09fe7d10c91fa098b5a2 Mon Sep 17 00:00:00 2001 From: lenafx Date: Sat, 10 Jan 2026 21:15:22 +0100 Subject: [PATCH] better schedule page --- desktop/src/scripts/schedule/schedule.js | 708 ++++++++++++++--------- desktop/views/css/schedule/schedule.css | 667 +++++++++++---------- desktop/views/schedule.html | 114 ++-- docker/src/scripts/schedule/schedule.js | 708 ++++++++++++++--------- docker/views/css/schedule/schedule.css | 697 +++++++++++----------- docker/views/schedule.html | 112 ++-- 6 files changed, 1691 insertions(+), 1315 deletions(-) diff --git a/desktop/src/scripts/schedule/schedule.js b/desktop/src/scripts/schedule/schedule.js index 3db7d8e..686cc7b 100644 --- a/desktop/src/scripts/schedule/schedule.js +++ b/desktop/src/scripts/schedule/schedule.js @@ -1,76 +1,373 @@ const ANILIST_API = 'https://graphql.anilist.co'; -const CACHE_NAME = 'waifuboard-schedule-v1'; -const CACHE_DURATION = 5 * 60 * 1000; +const CACHE_NAME = 'waifuboard-schedule-v5'; + +const CACHE_DURATION = 6 * 60 * 60 * 1000; const state = { currentDate: new Date(), viewType: 'MONTH', mode: 'SUB', + filter: 'ALL', loading: false, abortController: null, - refreshInterval: null + userListIds: new Set(), + scheduleData: [], + selectedWeekDayIndex: 0 }; -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { + await fetchUserList(); renderHeader(); fetchSchedule(); - state.refreshInterval = setInterval(() => { - console.log("Auto-refreshing schedule..."); - fetchSchedule(true); - }, CACHE_DURATION); + if (state.userListIds.size > 0) { + const filterGroup = document.getElementById('filter-group'); + if (filterGroup) filterGroup.style.display = 'flex'; + } }); -async function getCache(key) { +async function fetchUserList() { + const token = localStorage.getItem('token'); + if (!token) return; + try { - const cache = await caches.open(CACHE_NAME); + const res = await fetch('http://localhost:54322/api/list', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); - const response = await cache.match(`/schedule-cache/${key}`); + if (res.ok) { + const json = await res.json(); + if (json.results && Array.isArray(json.results)) { + state.userListIds.clear(); + json.results.forEach(item => { - if (!response) return null; - - const cached = await response.json(); - const age = Date.now() - cached.timestamp; - - if (age < CACHE_DURATION) { - console.log(`[Cache Hit] Loaded ${key} (Age: ${Math.round(age / 1000)}s)`); - return cached.data; + if (item.source === 'anilist') { + state.userListIds.add(item.entry_id); + } + }); + console.log(`[UserList] Loaded ${state.userListIds.size} entries.`); + } } - - console.log(`[Cache Stale] ${key} expired.`); - - cache.delete(`/schedule-cache/${key}`); - return null; } catch (e) { - console.error("Cache read failed", e); - return null; + console.warn("[UserList] Could not fetch user list:", e); } } -async function setCache(key, data) { - try { - const cache = await caches.open(CACHE_NAME); - const payload = JSON.stringify({ - timestamp: Date.now(), - data: data - }); +async function fetchSchedule(forceRefresh = false) { + const key = getCacheKey(); - const response = new Response(payload, { - headers: { 'Content-Type': 'application/json' } - }); - await cache.put(`/schedule-cache/${key}`, response); - } catch (e) { - console.warn("Cache write failed", e); + if (!forceRefresh) { + const cachedData = await getCache(key); + if (cachedData) { + console.log(`[Schedule] Using cached data for key: ${key}`); + state.scheduleData = cachedData; + renderContent(); + updateAmbient(cachedData); + return; + + } } -} -function getCacheKey() { + if (state.abortController) state.abortController.abort(); + state.abortController = new AbortController(); + const signal = state.abortController.signal; + + setLoading(true); + + let startObj, endObj; + if (state.viewType === 'MONTH') { - return `M_${state.currentDate.getFullYear()}_${state.currentDate.getMonth()}`; + const year = state.currentDate.getFullYear(); + const month = state.currentDate.getMonth(); + startObj = new Date(year, month, 1, 0, 0, 0, 0); + endObj = new Date(year, month + 1, 0, 23, 59, 59, 999); } else { const start = getWeekStart(state.currentDate); - return `W_${start.toISOString().split('T')[0]}`; + + startObj = new Date(start); + + endObj = new Date(startObj); + endObj.setDate(endObj.getDate() + 7); + endObj.setHours(23, 59, 59, 999); } + + const startTs = Math.floor(startObj.getTime() / 1000); + const endTs = Math.floor(endObj.getTime() / 1000); + + const query = ` + query ($start: Int, $end: Int, $page: Int) { + Page(page: $page, perPage: 50) { + pageInfo { hasNextPage } + airingSchedules(airingAt_greater: $start, airingAt_lesser: $end, sort: TIME) { + airingAt + episode + media { + id + title { userPreferred english } + coverImage { large extraLarge } + bannerImage + isAdult + countryOfOrigin + format + duration + popularity + } + } + } + } + `; + + let allData = []; + let page = 1; + let hasNext = true; + + try { + while (hasNext && page <= 10) { + if (signal.aborted) throw new DOMException("Aborted", "AbortError"); + + const res = await fetch(ANILIST_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ query, variables: { start: startTs, end: endTs, page } }), + signal: signal + }); + + if (res.status === 429) { + await delay(2000); + continue; + } + + const json = await res.json(); + if (json.errors) throw new Error("API Error"); + + const data = json.data.Page; + allData = [...allData, ...data.airingSchedules]; + hasNext = data.pageInfo.hasNextPage; + page++; + await delay(200); + } + + if (!signal.aborted) { + state.scheduleData = allData; + await setCache(key, allData); + + renderContent(); + updateAmbient(allData); + } + + } catch (e) { + if (e.name !== 'AbortError') console.error("Fetch failed:", e); + } finally { + if (!signal.aborted) { + setLoading(false); + state.abortController = null; + } + } +} + +function renderContent() { + + let items = state.scheduleData.filter(i => + !i.media.isAdult && + i.media.countryOfOrigin === 'JP' + + ); + + if (state.mode === 'DUB') { + items = items.filter(i => i.media.popularity > 20000); + } + + if (state.filter === 'MY_LIST') { + items = items.filter(i => state.userListIds.has(i.media.id)); + } + + const container = document.getElementById('schedule-content'); + if (!container) return; + + container.innerHTML = ''; + + if (state.viewType === 'MONTH') { + renderMonthView(container, items); + } else { + renderWeekView(container, items); + } +} + +function renderMonthView(container, items) { + const grid = document.createElement('div'); + grid.className = 'calendar-grid'; + + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + days.forEach(d => { + const h = document.createElement('div'); + h.className = 'weekday-header'; + h.textContent = d; + grid.appendChild(h); + }); + + const year = state.currentDate.getFullYear(); + const month = state.currentDate.getMonth(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + let firstDayIndex = new Date(year, month, 1).getDay() - 1; + if (firstDayIndex === -1) firstDayIndex = 6; + + for (let i = 0; i < firstDayIndex; i++) { + const empty = document.createElement('div'); + empty.className = 'day-cell empty'; + grid.appendChild(empty); + } + + for (let day = 1; day <= daysInMonth; day++) { + const cell = document.createElement('div'); + cell.className = 'day-cell'; + + const currentCellDate = new Date(year, month, day); + const now = new Date(); + + if (isSameDay(currentCellDate, now)) { + cell.classList.add('today'); + } + + cell.innerHTML = `${day}`; + + const dayEvents = items.filter(i => { + const releaseDate = new Date(i.airingAt * 1000); + return isSameDay(currentCellDate, releaseDate); + }); + + dayEvents.sort((a, b) => b.media.popularity - a.media.popularity); + + dayEvents.forEach(evt => { + const title = evt.media.title.english || evt.media.title.userPreferred; + const ep = evt.episode; + const time = new Date(evt.airingAt * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + const isMine = state.userListIds.has(evt.media.id); + + const el = document.createElement('a'); + el.className = `anime-item-month ${isMine ? 'is-mine' : ''}`; + el.href = `/anime/${evt.media.id}`; + el.innerHTML = ` + ${time} + + ${title} EP ${ep} + + `; + + cell.appendChild(el); + }); + + grid.appendChild(cell); + } + + container.appendChild(grid); +} + +function renderWeekView(container, items) { + const wrapper = document.createElement('div'); + wrapper.className = 'week-container'; + + const nav = document.createElement('div'); + nav.className = 'week-nav'; + + const startOfWeek = getWeekStart(state.currentDate); + const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + for (let i = 0; i < 7; i++) { + const d = new Date(startOfWeek); + d.setDate(startOfWeek.getDate() + i); + + const btn = document.createElement('button'); + btn.className = `day-btn ${i === state.selectedWeekDayIndex ? 'active' : ''}`; + + btn.onclick = () => { + state.selectedWeekDayIndex = i; + renderWeekCards(d, items); + + document.querySelectorAll('.day-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + }; + + btn.innerHTML = ` + ${dayNames[i]} + ${d.getDate()} + `; + nav.appendChild(btn); + } + wrapper.appendChild(nav); + + const grid = document.createElement('div'); + grid.className = 'week-grid'; + grid.id = 'weekGrid'; + wrapper.appendChild(grid); + + container.appendChild(wrapper); + + const selectedDate = new Date(startOfWeek); + selectedDate.setDate(selectedDate.getDate() + state.selectedWeekDayIndex); + renderWeekCards(selectedDate, items); +} + +function renderWeekCards(targetDate, allItems) { + const grid = document.getElementById('weekGrid'); + if (!grid) return; + grid.innerHTML = ''; + + const dayItems = allItems.filter(i => { + const releaseDate = new Date(i.airingAt * 1000); + return isSameDay(targetDate, releaseDate); + }); + + dayItems.sort((a, b) => b.media.popularity - a.media.popularity); + + if (dayItems.length === 0) { + grid.innerHTML = ` +
+

No releases found for this day

+
`; + return; + } + + dayItems.forEach(evt => { + const m = evt.media; + const title = m.title.english || m.title.userPreferred; + const time = new Date(evt.airingAt * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + + const card = document.createElement('div'); + card.className = `card`; + card.onclick = () => window.location.href = `/anime/${m.id}`; + + card.innerHTML = ` +
+ ${title} +
EP ${evt.episode} • ${time}
+
+
+

${title}

+

${m.format || 'TV'}

+
+ `; + grid.appendChild(card); + }); +} + +function isSameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +} + +function getWeekStart(d) { + const date = new Date(d); + date.setHours(0, 0, 0, 0); + + const day = date.getDay(); + const diff = date.getDate() - day + (day === 0 ? -6 : 1); + date.setDate(diff); + return date; } function navigate(delta) { @@ -93,10 +390,19 @@ function setViewType(type) { document.getElementById('btnViewMonth').classList.toggle('active', type === 'MONTH'); document.getElementById('btnViewWeek').classList.toggle('active', type === 'WEEK'); - if (state.abortController) state.abortController.abort(); + state.selectedWeekDayIndex = new Date().getDay() - 1; + if (state.selectedWeekDayIndex === -1) state.selectedWeekDayIndex = 6; renderHeader(); - fetchSchedule(); + + if (state.scheduleData.length) { + renderContent(); + + } else { + fetchSchedule(); + + } + } function setMode(mode) { @@ -104,8 +410,18 @@ function setMode(mode) { state.mode = mode; document.getElementById('btnSub').classList.toggle('active', mode === 'SUB'); document.getElementById('btnDub').classList.toggle('active', mode === 'DUB'); + renderContent(); - fetchSchedule(); +} + +function setFilter(filterType) { + if (state.filter === filterType) return; + state.filter = filterType; + + document.getElementById('btnAll').classList.toggle('active', filterType === 'ALL'); + document.getElementById('btnMyList').classList.toggle('active', filterType === 'MY_LIST'); + + renderContent(); } function renderHeader() { @@ -119,242 +435,86 @@ function renderHeader() { const startStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const endStr = end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - title = `Week of ${startStr} - ${endStr}`; + title = `${startStr} - ${endStr}`; } - document.getElementById('monthTitle').textContent = title; -} - -function getWeekStart(date) { - const d = new Date(date); - const day = d.getDay(); - const diff = d.getDate() - day + (day === 0 ? -6 : 1); - return new Date(d.setDate(diff)); -} - -async function fetchSchedule(forceRefresh = false) { - const key = getCacheKey(); - - if (!forceRefresh) { - const cachedData = await getCache(key); - if (cachedData) { - renderGrid(cachedData); - updateAmbient(cachedData); - return; - } - } - - if (state.abortController) state.abortController.abort(); - state.abortController = new AbortController(); - const signal = state.abortController.signal; - - if (!forceRefresh) setLoading(true); - - let startTs, endTs; - if (state.viewType === 'MONTH') { - const year = state.currentDate.getFullYear(); - const month = state.currentDate.getMonth(); - startTs = Math.floor(new Date(year, month, 1).getTime() / 1000); - endTs = Math.floor(new Date(year, month + 1, 0, 23, 59, 59).getTime() / 1000); - } else { - const start = getWeekStart(state.currentDate); - start.setHours(0, 0, 0, 0); - const end = new Date(start); - end.setDate(end.getDate() + 7); - startTs = Math.floor(start.getTime() / 1000); - endTs = Math.floor(end.getTime() / 1000); - } - - const query = ` - query ($start: Int, $end: Int, $page: Int) { - Page(page: $page, perPage: 50) { - pageInfo { hasNextPage } - airingSchedules(airingAt_greater: $start, airingAt_lesser: $end, sort: TIME) { - airingAt - episode - media { - id - title { userPreferred english } - coverImage { large } - bannerImage - isAdult - countryOfOrigin - popularity - } - } - } - } - `; - - let allData = []; - let page = 1; - let hasNext = true; - let retries = 0; - - try { - while (hasNext && page <= 6) { - if (signal.aborted) throw new DOMException("Aborted", "AbortError"); - - try { - const res = await fetch(ANILIST_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, - body: JSON.stringify({ - query, - variables: { start: startTs, end: endTs, page } - }), - signal: signal - }); - - if (res.status === 429) { - if (retries > 2) throw new Error("Rate Limited"); - console.warn("429 Hit. Waiting..."); - await delay(4000); - retries++; - continue; - } - - const json = await res.json(); - if (json.errors) throw new Error("API Error"); - - const data = json.data.Page; - allData = [...allData, ...data.airingSchedules]; - hasNext = data.pageInfo.hasNextPage; - page++; - - await delay(600); - - } catch (e) { - if (e.name === 'AbortError') throw e; - console.error(e); - break; - } - } - - if (!signal.aborted) { - - await setCache(key, allData); - renderGrid(allData); - updateAmbient(allData); - } - - } catch (e) { - if (e.name !== 'AbortError') console.error("Fetch failed:", e); - } finally { - if (!signal.aborted) { - setLoading(false); - state.abortController = null; - } - } -} - -function renderGrid(data) { - const grid = document.getElementById('daysGrid'); - grid.innerHTML = ''; - - let items = data.filter(i => !i.media.isAdult && i.media.countryOfOrigin === 'JP'); - if (state.mode === 'DUB') { - items = items.filter(i => i.media.popularity > 20000); - } - - if (state.viewType === 'MONTH') { - const year = state.currentDate.getFullYear(); - const month = state.currentDate.getMonth(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - let firstDayIndex = new Date(year, month, 1).getDay() - 1; - if (firstDayIndex === -1) firstDayIndex = 6; - - for (let i = 0; i < firstDayIndex; i++) { - const empty = document.createElement('div'); - empty.className = 'day-cell empty'; - grid.appendChild(empty); - } - - for (let day = 1; day <= daysInMonth; day++) { - const dateObj = new Date(year, month, day); - renderDayCell(dateObj, items, grid); - } - } else { - const start = getWeekStart(state.currentDate); - for (let i = 0; i < 7; i++) { - const dateObj = new Date(start); - dateObj.setDate(start.getDate() + i); - renderDayCell(dateObj, items, grid); - } - } -} - -function renderDayCell(dateObj, items, grid) { - const cell = document.createElement('div'); - cell.className = 'day-cell'; - - if (state.viewType === 'WEEK') cell.style.minHeight = '300px'; - - const day = dateObj.getDate(); - const month = dateObj.getMonth(); - const year = dateObj.getFullYear(); - - const now = new Date(); - if (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) { - cell.classList.add('today'); - } - - const dayEvents = items.filter(i => { - const eventDate = new Date(i.airingAt * 1000); - return eventDate.getDate() === day && eventDate.getMonth() === month && eventDate.getFullYear() === year; - }); - - dayEvents.sort((a, b) => b.media.popularity - a.media.popularity); - - if (dayEvents.length > 0) { - const top = dayEvents[0].media; - const bg = document.createElement('div'); - bg.className = 'cell-backdrop'; - bg.style.backgroundImage = `url('${top.coverImage.large}')`; - cell.appendChild(bg); - } - - const header = document.createElement('div'); - header.className = 'day-header'; - header.innerHTML = ` - ${day} - Today - `; - cell.appendChild(header); - - const list = document.createElement('div'); - list.className = 'events-list'; - - dayEvents.forEach(evt => { - const title = evt.media.title.english || evt.media.title.userPreferred; - const link = `/anime/${evt.media.id}`; - - const chip = document.createElement('a'); - chip.className = 'anime-chip'; - chip.href = link; - chip.innerHTML = ` - ${title} - Ep ${evt.episode} - `; - list.appendChild(chip); - }); - - cell.appendChild(list); - grid.appendChild(cell); + const titleEl = document.getElementById('monthTitle'); + if (titleEl) titleEl.textContent = title; } function setLoading(bool) { state.loading = bool; const loader = document.getElementById('loader'); - if (bool) loader.classList.add('active'); - else loader.classList.remove('active'); + if (loader) { + if (bool) loader.classList.add('active'); + else loader.classList.remove('active'); + } +} + +function updateAmbient(data) { + if (!data || !data.length) return; + + const top = data.reduce((prev, curr) => (prev.media.popularity > curr.media.popularity) ? prev : curr, data[0]); + + const img = top.media.bannerImage || top.media.coverImage.extraLarge; + const bgEl = document.getElementById('ambientBg'); + if (bgEl && img) { + bgEl.style.backgroundImage = `url('${img}')`; + } } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } -function updateAmbient(data) { - if (!data || !data.length) return; - const top = data.reduce((prev, curr) => (prev.media.popularity > curr.media.popularity) ? prev : curr); - const img = top.media.bannerImage || top.media.coverImage.large; - if (img) document.getElementById('ambientBg').style.backgroundImage = `url('${img}')`; -} \ No newline at end of file +async function getCache(key) { + try { + const cache = await caches.open(CACHE_NAME); + const response = await cache.match(`/schedule-cache/${key}`); + if (!response) return null; + + const cached = await response.json(); + const age = Date.now() - cached.timestamp; + + if (age < CACHE_DURATION) { + console.log(`[Cache Hit] ${key}`); + return cached.data; + } + cache.delete(`/schedule-cache/${key}`); + return null; + } catch (e) { + return null; + } +} + +function getCacheKey() { + + let d; + + if (state.viewType === 'MONTH') { + d = new Date(state.currentDate.getFullYear(), state.currentDate.getMonth(), 1); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + return `M_${year}_${month}`; + } else { + d = getWeekStart(state.currentDate); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `W_${year}_${month}_${day}`; + } +} + +async function setCache(key, data) { + try { + const cache = await caches.open(CACHE_NAME); + const payload = JSON.stringify({ timestamp: Date.now(), data: data }); + const response = new Response(payload, { headers: { 'Content-Type': 'application/json' } }); + await cache.put(`/schedule-cache/${key}`, response); + } catch (e) { + console.warn("Cache write failed", e); + } +} + +window.navigate = navigate; +window.setViewType = setViewType; +window.setMode = setMode; +window.setFilter = setFilter; \ No newline at end of file diff --git a/desktop/views/css/schedule/schedule.css b/desktop/views/css/schedule/schedule.css index 23b010f..2d28fdd 100644 --- a/desktop/views/css/schedule/schedule.css +++ b/desktop/views/css/schedule/schedule.css @@ -1,363 +1,440 @@ :root { - --bg-glass: rgba(18, 18, 21, 0.8); - --bg-cell: #0c0c0e; - --color-primary-glow: rgba(139, 92, 246, 0.3); + --header-height: 140px; } body { - margin: 0; - background-color: var(--color-bg-base); - color: var(--color-text-primary); - overflow: hidden; - height: 100vh; - display: flex; - flex-direction: column; -} - -html.electron body { - padding-top: 0; + background-color: #050505; + overflow-x: hidden; } .ambient-bg { - position: absolute; - inset: 0; - z-index: -1; - background-size: cover; - background-position: center; - opacity: 0.06; - filter: blur(120px) saturate(1.2); - transition: background-image 1s ease-in-out; - pointer-events: none; -} - -.calendar-wrapper { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - padding: 3rem; - max-width: 1920px; + position: fixed; + top: 0; + left: 0; width: 100%; - margin: 0 auto; + height: 100%; + background-size: cover; + background-position: center top; + opacity: 0.15; + filter: blur(80px) saturate(1.5); + z-index: -2; + transition: background-image 1s ease; } -.calendar-controls { - padding: 1.5rem 0; +.bg-overlay { + position: fixed; + inset: 0; + background: radial-gradient(circle at top, transparent 0%, #050505 80%); + z-index: -1; +} + +.schedule-container { + padding: calc(var(--nav-height) + 2rem) 3rem 2rem 3rem; + max-width: 1800px; + margin: 0 auto; + min-height: 100vh; +} + +.schedule-header { display: flex; justify-content: space-between; - align-items: center; - flex-shrink: 0; + align-items: flex-end; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(255,255,255,0.08); + flex-wrap: wrap; + gap: 1.5rem; } -.month-selector { +.page-title { + font-size: 2.5rem; + font-weight: 900; + margin: 0 0 0.5rem 0; + letter-spacing: -1px; +} + +.header-left { + display: flex; + flex-direction: column; +} + +.month-navigator { + display: flex; + align-items: center; + gap: 1rem; +} + +.current-date-label { + font-size: 1.2rem; + font-weight: 600; + color: var(--color-primary); + min-width: 180px; + text-align: center; +} + +.nav-btn { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + font-size: 1.2rem; + line-height: 0; +} + +.nav-btn:hover { + background: var(--color-primary); + border-color: var(--color-primary); +} + +.header-controls { display: flex; align-items: center; gap: 1.5rem; } -.month-title { - font-size: 2.2rem; - font-weight: 800; - letter-spacing: -0.03em; - background: linear-gradient(to right, #fff, #a1a1aa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - min-width: 350px; +.divider-vertical { + width: 1px; + height: 30px; + background: rgba(255,255,255,0.1); } -.icon-btn { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - width: 44px; - height: 44px; - border-radius: 12px; - color: white; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: 0.2s; -} - -.icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); - transform: translateY(-2px); -} - -.controls-right { - display: flex; - gap: 1rem; -} - -.view-toggles { - display: flex; - background: #0f0f12; +.toggle-group { + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.1); padding: 4px; border-radius: 99px; - border: 1px solid var(--border-subtle); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); + display: flex; + gap: 4px; } -.toggle-item { - padding: 10px 24px; - border-radius: 99px; - border: none; +.toggle-btn { background: transparent; + border: none; color: var(--color-text-secondary); + padding: 6px 16px; + border-radius: 99px; font-weight: 600; font-size: 0.9rem; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.2s; } -.toggle-item.active { - background: var(--color-primary); - color: white; - box-shadow: 0 2px 10px var(--color-primary-glow); -} - -.calendar-board { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); +.toggle-btn:hover { color: white; } +.toggle-btn.active { background: var(--color-bg-elevated); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); + color: white; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); } -.weekdays-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - border-bottom: 1px solid var(--border-subtle); - background: rgba(255, 255, 255, 0.02); - flex-shrink: 0; -} - -.weekday-header { - padding: 16px; - text-align: center; - text-transform: uppercase; - font-size: 0.75rem; - font-weight: 800; - color: var(--color-text-secondary); - letter-spacing: 0.1em; - border-right: 1px solid var(--border-subtle); -} - -.weekday-header:last-child { - border-right: none; -} - -.days-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - width: 100%; - overflow-y: auto; - flex: 1; - - grid-auto-rows: minmax(180px, 1fr); - background: var(--color-bg-base); -} - -.day-cell { - position: relative; - background: var(--bg-cell); - border-right: 1px solid var(--border-subtle); - border-bottom: 1px solid var(--border-subtle); +.view-switcher { display: flex; - flex-direction: column; - padding: 12px; - transition: background 0.2s; - overflow: hidden; + gap: 0.5rem; } -.day-cell:nth-child(7n) { - border-right: none; -} - -.day-cell.empty { - background: rgba(0, 0, 0, 0.2); - pointer-events: none; -} - -.day-cell:hover { - background: #16161a; -} - -.day-cell.today { - background: rgba(139, 92, 246, 0.03); - box-shadow: inset 0 0 0 1px var(--color-primary); -} - -.day-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - z-index: 2; - pointer-events: none; -} - -.day-number { - font-size: 1.1rem; - font-weight: 700; +.view-btn { + background: transparent; + border: 1px solid rgba(255,255,255,0.1); color: var(--color-text-secondary); - width: 32px; - height: 32px; + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; display: flex; align-items: center; justify-content: center; - border-radius: 50%; + transition: all 0.2s; } -.day-cell.today .day-number { +.view-btn:hover { border-color: white; color: white; } +.view-btn.active { background: var(--color-primary); + border-color: var(--color-primary); color: white; - box-shadow: 0 0 15px var(--color-primary-glow); } -.today-label { - font-size: 0.65rem; - font-weight: 800; - color: var(--color-primary); - letter-spacing: 0.05em; - text-transform: uppercase; - display: none; -} - -.day-cell.today .today-label { - display: block; -} - -.events-list { - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; - overflow-y: auto; - z-index: 2; - padding-right: 4px; -} - -.events-list::-webkit-scrollbar { - width: 4px; -} - -.events-list::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; -} - -.anime-chip { - display: flex; - align-items: center; - justify-content: space-between; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); - padding: 8px 10px; - border-radius: 8px; - font-size: 0.8rem; - color: #d4d4d8; - text-decoration: none; - transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); - cursor: pointer; - position: relative; +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 1px; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.05); + border-radius: 12px; overflow: hidden; } -.anime-chip::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--color-primary); - opacity: 0; - transition: opacity 0.2s; -} - -.anime-chip:hover { - background: rgba(255, 255, 255, 0.1); - color: white; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - padding-left: 14px; -} - -.anime-chip:hover::before { - opacity: 1; -} - -.chip-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 500; - margin-right: 8px; -} - -.chip-ep { - font-size: 0.7rem; +.weekday-header { + background: var(--color-bg-card); + padding: 1rem; + text-align: center; font-weight: 700; color: var(--color-text-secondary); - background: rgba(0, 0, 0, 0.4); - padding: 2px 6px; - border-radius: 4px; - white-space: nowrap; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 1px; } -.cell-backdrop { - position: absolute; - inset: 0; - background-size: cover; - background-position: center; - opacity: 0; - transition: opacity 0.4s ease; - filter: grayscale(100%) brightness(0.25); - z-index: 1; - pointer-events: none; +.day-cell { + background: var(--color-bg-base); + min-height: 160px; + max-height: 160px; + padding: 0.8rem; + position: relative; + display: flex; + flex-direction: column; + gap: 0.5rem; + + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; } -.day-cell:hover .cell-backdrop { - opacity: 1; -} +.day-cell::-webkit-scrollbar { display: none; } +.day-cell.empty { background: rgba(0,0,0,0.2); } +.day-cell.today { background: rgba(139, 92, 246, 0.05); box-shadow: inset 0 0 0 1px var(--color-primary); } -.loader { - position: fixed; - bottom: 30px; - right: 30px; - background: #18181b; - border: 1px solid var(--border-subtle); - padding: 12px 24px; - border-radius: 99px; +.day-number { + font-weight: 700; + font-size: 1rem; + color: var(--color-text-secondary); + margin-bottom: 4px; + display: block; +} +.today .day-number { color: var(--color-primary); } + +.anime-item-month { display: flex; align-items: center; - gap: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - transform: translateY(100px); - transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 1000; + gap: 8px; + padding: 6px; + background: rgba(255,255,255,0.03); + border-radius: 6px; + text-decoration: none; + transition: 0.2s; + border-left: 2px solid transparent; } -.loader.active { - transform: translateY(0); +.anime-item-month:hover { + background: rgba(255,255,255,0.08); + transform: translateX(2px); +} +.anime-item-month.is-mine { + border-left-color: var(--color-success); + background: rgba(34, 197, 94, 0.05); } -.spinner { - width: 18px; - height: 18px; - border: 2px solid rgba(255, 255, 255, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 0.8s infinite linear; +.item-time { font-size: 0.75rem; color: var(--color-text-muted); font-family: monospace; } +.item-title { font-size: 0.8rem; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } + +.week-container { + display: flex; + flex-direction: column; + gap: 2rem; } -@keyframes spin { - to { - transform: rotate(360deg); +.week-nav { + display: flex; + gap: 1rem; + overflow-x: auto; + padding-bottom: 1rem; + scrollbar-width: none; + mask-image: linear-gradient(to right, black 90%, transparent 100%); +} +.week-nav::-webkit-scrollbar { display: none; } + +.day-btn { + flex: 1; + min-width: 120px; + + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--color-text-secondary); + + border-radius: 12px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); + text-align: center; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.day-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: white; + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.2); +} + +.day-btn.active { + background: var(--color-primary); + border-color: var(--color-primary); + color: white; + box-shadow: 0 8px 20px var(--color-primary-glow); +} + +.day-btn span.name { + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.8; +} + +.day-btn span.date { + font-size: 1.8rem; + font-weight: 800; + line-height: 1; +} + +.week-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1.5rem; + animation: fadeInUp 0.4s ease; +} + +.card-ep-badge { + position: absolute; + top: 8px; + right: 8px; + background: rgba(0,0,0,0.8); + color: var(--color-primary); + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 800; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.card.mine .card-img-wrap { + box-shadow: 0 0 0 2px var(--color-success); +} +.card.mine::after { + content: "IN LIST"; + position: absolute; + top: 8px; left: 8px; + background: var(--color-success); + color: black; + font-size: 0.65rem; + font-weight: 900; + padding: 2px 6px; + border-radius: 4px; +} + +.loader-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(5px); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; +} +.loader-overlay.active { opacity: 1; pointer-events: auto; } + +@media (max-width: 1024px) { + .schedule-container { + padding: 5rem 1.5rem 2rem 1.5rem; } } + +@media (max-width: 768px) { + + .schedule-header { + flex-direction: column; + align-items: stretch; + gap: 1.5rem; + } + + .header-left { + align-items: center; + width: 100%; + } + + .header-controls { + flex-wrap: wrap; + justify-content: center; + width: 100%; + gap: 1rem; + } + + .page-title { + font-size: 2rem; + text-align: center; + } + + .calendar-grid { + display: flex; + flex-direction: column; + gap: 1rem; + background: transparent; + border: none; + } + + .weekday-header, .day-cell.empty { display: none; } + + .day-cell { + min-height: auto; + max-height: none; + overflow: visible; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 12px; + background: var(--color-bg-elevated); + } + + .week-nav { + margin: 0 -1.5rem; + padding: 0 1.5rem 1rem 1.5rem; + } + + .day-btn { + min-width: 90px; + padding: 0.8rem; + } + .day-btn span.name { font-size: 0.75rem; } + .day-btn span.date { font-size: 1.5rem; } + + .week-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .week-grid .card { + min-width: 0 !important; + width: 100% !important; + flex: none !important; + } + + .card-content h3 { font-size: 0.8rem; } + .card-ep-badge { font-size: 0.65rem; padding: 2px 4px; } +} + +@media (max-width: 380px) { + .header-controls { + gap: 0.5rem; + } + .toggle-btn { + padding: 6px 10px; + font-size: 0.8rem; + } + .nav-btn { + width: 28px; + height: 28px; + } + .current-date-label { + font-size: 1rem; + min-width: 140px; + } +} \ No newline at end of file diff --git a/desktop/views/schedule.html b/desktop/views/schedule.html index 5c03faa..f6c601a 100644 --- a/desktop/views/schedule.html +++ b/desktop/views/schedule.html @@ -4,85 +4,83 @@ WaifuBoard - Schedule - - - + + - + -
+
+
-
-
-
- -
Loading...
- -
+
-
- -
- - -
- -
- - -
+
+
+

Release Schedule

+
+ + Loading... +
-
-
-
Mon
-
Tue
-
Wed
-
Thu
-
Fri
-
Sat
-
Sun
+
+ -
+
+ +
+ + +
+ +
+ +
-
+
-
-
- Syncing Schedule... -
+
+
- - - Click To Download - -
+
+
+
- - - - - - + + + + + + + + \ No newline at end of file diff --git a/docker/src/scripts/schedule/schedule.js b/docker/src/scripts/schedule/schedule.js index 3db7d8e..686cc7b 100644 --- a/docker/src/scripts/schedule/schedule.js +++ b/docker/src/scripts/schedule/schedule.js @@ -1,76 +1,373 @@ const ANILIST_API = 'https://graphql.anilist.co'; -const CACHE_NAME = 'waifuboard-schedule-v1'; -const CACHE_DURATION = 5 * 60 * 1000; +const CACHE_NAME = 'waifuboard-schedule-v5'; + +const CACHE_DURATION = 6 * 60 * 60 * 1000; const state = { currentDate: new Date(), viewType: 'MONTH', mode: 'SUB', + filter: 'ALL', loading: false, abortController: null, - refreshInterval: null + userListIds: new Set(), + scheduleData: [], + selectedWeekDayIndex: 0 }; -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { + await fetchUserList(); renderHeader(); fetchSchedule(); - state.refreshInterval = setInterval(() => { - console.log("Auto-refreshing schedule..."); - fetchSchedule(true); - }, CACHE_DURATION); + if (state.userListIds.size > 0) { + const filterGroup = document.getElementById('filter-group'); + if (filterGroup) filterGroup.style.display = 'flex'; + } }); -async function getCache(key) { +async function fetchUserList() { + const token = localStorage.getItem('token'); + if (!token) return; + try { - const cache = await caches.open(CACHE_NAME); + const res = await fetch('http://localhost:54322/api/list', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); - const response = await cache.match(`/schedule-cache/${key}`); + if (res.ok) { + const json = await res.json(); + if (json.results && Array.isArray(json.results)) { + state.userListIds.clear(); + json.results.forEach(item => { - if (!response) return null; - - const cached = await response.json(); - const age = Date.now() - cached.timestamp; - - if (age < CACHE_DURATION) { - console.log(`[Cache Hit] Loaded ${key} (Age: ${Math.round(age / 1000)}s)`); - return cached.data; + if (item.source === 'anilist') { + state.userListIds.add(item.entry_id); + } + }); + console.log(`[UserList] Loaded ${state.userListIds.size} entries.`); + } } - - console.log(`[Cache Stale] ${key} expired.`); - - cache.delete(`/schedule-cache/${key}`); - return null; } catch (e) { - console.error("Cache read failed", e); - return null; + console.warn("[UserList] Could not fetch user list:", e); } } -async function setCache(key, data) { - try { - const cache = await caches.open(CACHE_NAME); - const payload = JSON.stringify({ - timestamp: Date.now(), - data: data - }); +async function fetchSchedule(forceRefresh = false) { + const key = getCacheKey(); - const response = new Response(payload, { - headers: { 'Content-Type': 'application/json' } - }); - await cache.put(`/schedule-cache/${key}`, response); - } catch (e) { - console.warn("Cache write failed", e); + if (!forceRefresh) { + const cachedData = await getCache(key); + if (cachedData) { + console.log(`[Schedule] Using cached data for key: ${key}`); + state.scheduleData = cachedData; + renderContent(); + updateAmbient(cachedData); + return; + + } } -} -function getCacheKey() { + if (state.abortController) state.abortController.abort(); + state.abortController = new AbortController(); + const signal = state.abortController.signal; + + setLoading(true); + + let startObj, endObj; + if (state.viewType === 'MONTH') { - return `M_${state.currentDate.getFullYear()}_${state.currentDate.getMonth()}`; + const year = state.currentDate.getFullYear(); + const month = state.currentDate.getMonth(); + startObj = new Date(year, month, 1, 0, 0, 0, 0); + endObj = new Date(year, month + 1, 0, 23, 59, 59, 999); } else { const start = getWeekStart(state.currentDate); - return `W_${start.toISOString().split('T')[0]}`; + + startObj = new Date(start); + + endObj = new Date(startObj); + endObj.setDate(endObj.getDate() + 7); + endObj.setHours(23, 59, 59, 999); } + + const startTs = Math.floor(startObj.getTime() / 1000); + const endTs = Math.floor(endObj.getTime() / 1000); + + const query = ` + query ($start: Int, $end: Int, $page: Int) { + Page(page: $page, perPage: 50) { + pageInfo { hasNextPage } + airingSchedules(airingAt_greater: $start, airingAt_lesser: $end, sort: TIME) { + airingAt + episode + media { + id + title { userPreferred english } + coverImage { large extraLarge } + bannerImage + isAdult + countryOfOrigin + format + duration + popularity + } + } + } + } + `; + + let allData = []; + let page = 1; + let hasNext = true; + + try { + while (hasNext && page <= 10) { + if (signal.aborted) throw new DOMException("Aborted", "AbortError"); + + const res = await fetch(ANILIST_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ query, variables: { start: startTs, end: endTs, page } }), + signal: signal + }); + + if (res.status === 429) { + await delay(2000); + continue; + } + + const json = await res.json(); + if (json.errors) throw new Error("API Error"); + + const data = json.data.Page; + allData = [...allData, ...data.airingSchedules]; + hasNext = data.pageInfo.hasNextPage; + page++; + await delay(200); + } + + if (!signal.aborted) { + state.scheduleData = allData; + await setCache(key, allData); + + renderContent(); + updateAmbient(allData); + } + + } catch (e) { + if (e.name !== 'AbortError') console.error("Fetch failed:", e); + } finally { + if (!signal.aborted) { + setLoading(false); + state.abortController = null; + } + } +} + +function renderContent() { + + let items = state.scheduleData.filter(i => + !i.media.isAdult && + i.media.countryOfOrigin === 'JP' + + ); + + if (state.mode === 'DUB') { + items = items.filter(i => i.media.popularity > 20000); + } + + if (state.filter === 'MY_LIST') { + items = items.filter(i => state.userListIds.has(i.media.id)); + } + + const container = document.getElementById('schedule-content'); + if (!container) return; + + container.innerHTML = ''; + + if (state.viewType === 'MONTH') { + renderMonthView(container, items); + } else { + renderWeekView(container, items); + } +} + +function renderMonthView(container, items) { + const grid = document.createElement('div'); + grid.className = 'calendar-grid'; + + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + days.forEach(d => { + const h = document.createElement('div'); + h.className = 'weekday-header'; + h.textContent = d; + grid.appendChild(h); + }); + + const year = state.currentDate.getFullYear(); + const month = state.currentDate.getMonth(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + let firstDayIndex = new Date(year, month, 1).getDay() - 1; + if (firstDayIndex === -1) firstDayIndex = 6; + + for (let i = 0; i < firstDayIndex; i++) { + const empty = document.createElement('div'); + empty.className = 'day-cell empty'; + grid.appendChild(empty); + } + + for (let day = 1; day <= daysInMonth; day++) { + const cell = document.createElement('div'); + cell.className = 'day-cell'; + + const currentCellDate = new Date(year, month, day); + const now = new Date(); + + if (isSameDay(currentCellDate, now)) { + cell.classList.add('today'); + } + + cell.innerHTML = `${day}`; + + const dayEvents = items.filter(i => { + const releaseDate = new Date(i.airingAt * 1000); + return isSameDay(currentCellDate, releaseDate); + }); + + dayEvents.sort((a, b) => b.media.popularity - a.media.popularity); + + dayEvents.forEach(evt => { + const title = evt.media.title.english || evt.media.title.userPreferred; + const ep = evt.episode; + const time = new Date(evt.airingAt * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + const isMine = state.userListIds.has(evt.media.id); + + const el = document.createElement('a'); + el.className = `anime-item-month ${isMine ? 'is-mine' : ''}`; + el.href = `/anime/${evt.media.id}`; + el.innerHTML = ` + ${time} + + ${title} EP ${ep} + + `; + + cell.appendChild(el); + }); + + grid.appendChild(cell); + } + + container.appendChild(grid); +} + +function renderWeekView(container, items) { + const wrapper = document.createElement('div'); + wrapper.className = 'week-container'; + + const nav = document.createElement('div'); + nav.className = 'week-nav'; + + const startOfWeek = getWeekStart(state.currentDate); + const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + for (let i = 0; i < 7; i++) { + const d = new Date(startOfWeek); + d.setDate(startOfWeek.getDate() + i); + + const btn = document.createElement('button'); + btn.className = `day-btn ${i === state.selectedWeekDayIndex ? 'active' : ''}`; + + btn.onclick = () => { + state.selectedWeekDayIndex = i; + renderWeekCards(d, items); + + document.querySelectorAll('.day-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + }; + + btn.innerHTML = ` + ${dayNames[i]} + ${d.getDate()} + `; + nav.appendChild(btn); + } + wrapper.appendChild(nav); + + const grid = document.createElement('div'); + grid.className = 'week-grid'; + grid.id = 'weekGrid'; + wrapper.appendChild(grid); + + container.appendChild(wrapper); + + const selectedDate = new Date(startOfWeek); + selectedDate.setDate(selectedDate.getDate() + state.selectedWeekDayIndex); + renderWeekCards(selectedDate, items); +} + +function renderWeekCards(targetDate, allItems) { + const grid = document.getElementById('weekGrid'); + if (!grid) return; + grid.innerHTML = ''; + + const dayItems = allItems.filter(i => { + const releaseDate = new Date(i.airingAt * 1000); + return isSameDay(targetDate, releaseDate); + }); + + dayItems.sort((a, b) => b.media.popularity - a.media.popularity); + + if (dayItems.length === 0) { + grid.innerHTML = ` +
+

No releases found for this day

+
`; + return; + } + + dayItems.forEach(evt => { + const m = evt.media; + const title = m.title.english || m.title.userPreferred; + const time = new Date(evt.airingAt * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + + const card = document.createElement('div'); + card.className = `card`; + card.onclick = () => window.location.href = `/anime/${m.id}`; + + card.innerHTML = ` +
+ ${title} +
EP ${evt.episode} • ${time}
+
+
+

${title}

+

${m.format || 'TV'}

+
+ `; + grid.appendChild(card); + }); +} + +function isSameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +} + +function getWeekStart(d) { + const date = new Date(d); + date.setHours(0, 0, 0, 0); + + const day = date.getDay(); + const diff = date.getDate() - day + (day === 0 ? -6 : 1); + date.setDate(diff); + return date; } function navigate(delta) { @@ -93,10 +390,19 @@ function setViewType(type) { document.getElementById('btnViewMonth').classList.toggle('active', type === 'MONTH'); document.getElementById('btnViewWeek').classList.toggle('active', type === 'WEEK'); - if (state.abortController) state.abortController.abort(); + state.selectedWeekDayIndex = new Date().getDay() - 1; + if (state.selectedWeekDayIndex === -1) state.selectedWeekDayIndex = 6; renderHeader(); - fetchSchedule(); + + if (state.scheduleData.length) { + renderContent(); + + } else { + fetchSchedule(); + + } + } function setMode(mode) { @@ -104,8 +410,18 @@ function setMode(mode) { state.mode = mode; document.getElementById('btnSub').classList.toggle('active', mode === 'SUB'); document.getElementById('btnDub').classList.toggle('active', mode === 'DUB'); + renderContent(); - fetchSchedule(); +} + +function setFilter(filterType) { + if (state.filter === filterType) return; + state.filter = filterType; + + document.getElementById('btnAll').classList.toggle('active', filterType === 'ALL'); + document.getElementById('btnMyList').classList.toggle('active', filterType === 'MY_LIST'); + + renderContent(); } function renderHeader() { @@ -119,242 +435,86 @@ function renderHeader() { const startStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const endStr = end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - title = `Week of ${startStr} - ${endStr}`; + title = `${startStr} - ${endStr}`; } - document.getElementById('monthTitle').textContent = title; -} - -function getWeekStart(date) { - const d = new Date(date); - const day = d.getDay(); - const diff = d.getDate() - day + (day === 0 ? -6 : 1); - return new Date(d.setDate(diff)); -} - -async function fetchSchedule(forceRefresh = false) { - const key = getCacheKey(); - - if (!forceRefresh) { - const cachedData = await getCache(key); - if (cachedData) { - renderGrid(cachedData); - updateAmbient(cachedData); - return; - } - } - - if (state.abortController) state.abortController.abort(); - state.abortController = new AbortController(); - const signal = state.abortController.signal; - - if (!forceRefresh) setLoading(true); - - let startTs, endTs; - if (state.viewType === 'MONTH') { - const year = state.currentDate.getFullYear(); - const month = state.currentDate.getMonth(); - startTs = Math.floor(new Date(year, month, 1).getTime() / 1000); - endTs = Math.floor(new Date(year, month + 1, 0, 23, 59, 59).getTime() / 1000); - } else { - const start = getWeekStart(state.currentDate); - start.setHours(0, 0, 0, 0); - const end = new Date(start); - end.setDate(end.getDate() + 7); - startTs = Math.floor(start.getTime() / 1000); - endTs = Math.floor(end.getTime() / 1000); - } - - const query = ` - query ($start: Int, $end: Int, $page: Int) { - Page(page: $page, perPage: 50) { - pageInfo { hasNextPage } - airingSchedules(airingAt_greater: $start, airingAt_lesser: $end, sort: TIME) { - airingAt - episode - media { - id - title { userPreferred english } - coverImage { large } - bannerImage - isAdult - countryOfOrigin - popularity - } - } - } - } - `; - - let allData = []; - let page = 1; - let hasNext = true; - let retries = 0; - - try { - while (hasNext && page <= 6) { - if (signal.aborted) throw new DOMException("Aborted", "AbortError"); - - try { - const res = await fetch(ANILIST_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, - body: JSON.stringify({ - query, - variables: { start: startTs, end: endTs, page } - }), - signal: signal - }); - - if (res.status === 429) { - if (retries > 2) throw new Error("Rate Limited"); - console.warn("429 Hit. Waiting..."); - await delay(4000); - retries++; - continue; - } - - const json = await res.json(); - if (json.errors) throw new Error("API Error"); - - const data = json.data.Page; - allData = [...allData, ...data.airingSchedules]; - hasNext = data.pageInfo.hasNextPage; - page++; - - await delay(600); - - } catch (e) { - if (e.name === 'AbortError') throw e; - console.error(e); - break; - } - } - - if (!signal.aborted) { - - await setCache(key, allData); - renderGrid(allData); - updateAmbient(allData); - } - - } catch (e) { - if (e.name !== 'AbortError') console.error("Fetch failed:", e); - } finally { - if (!signal.aborted) { - setLoading(false); - state.abortController = null; - } - } -} - -function renderGrid(data) { - const grid = document.getElementById('daysGrid'); - grid.innerHTML = ''; - - let items = data.filter(i => !i.media.isAdult && i.media.countryOfOrigin === 'JP'); - if (state.mode === 'DUB') { - items = items.filter(i => i.media.popularity > 20000); - } - - if (state.viewType === 'MONTH') { - const year = state.currentDate.getFullYear(); - const month = state.currentDate.getMonth(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - let firstDayIndex = new Date(year, month, 1).getDay() - 1; - if (firstDayIndex === -1) firstDayIndex = 6; - - for (let i = 0; i < firstDayIndex; i++) { - const empty = document.createElement('div'); - empty.className = 'day-cell empty'; - grid.appendChild(empty); - } - - for (let day = 1; day <= daysInMonth; day++) { - const dateObj = new Date(year, month, day); - renderDayCell(dateObj, items, grid); - } - } else { - const start = getWeekStart(state.currentDate); - for (let i = 0; i < 7; i++) { - const dateObj = new Date(start); - dateObj.setDate(start.getDate() + i); - renderDayCell(dateObj, items, grid); - } - } -} - -function renderDayCell(dateObj, items, grid) { - const cell = document.createElement('div'); - cell.className = 'day-cell'; - - if (state.viewType === 'WEEK') cell.style.minHeight = '300px'; - - const day = dateObj.getDate(); - const month = dateObj.getMonth(); - const year = dateObj.getFullYear(); - - const now = new Date(); - if (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) { - cell.classList.add('today'); - } - - const dayEvents = items.filter(i => { - const eventDate = new Date(i.airingAt * 1000); - return eventDate.getDate() === day && eventDate.getMonth() === month && eventDate.getFullYear() === year; - }); - - dayEvents.sort((a, b) => b.media.popularity - a.media.popularity); - - if (dayEvents.length > 0) { - const top = dayEvents[0].media; - const bg = document.createElement('div'); - bg.className = 'cell-backdrop'; - bg.style.backgroundImage = `url('${top.coverImage.large}')`; - cell.appendChild(bg); - } - - const header = document.createElement('div'); - header.className = 'day-header'; - header.innerHTML = ` - ${day} - Today - `; - cell.appendChild(header); - - const list = document.createElement('div'); - list.className = 'events-list'; - - dayEvents.forEach(evt => { - const title = evt.media.title.english || evt.media.title.userPreferred; - const link = `/anime/${evt.media.id}`; - - const chip = document.createElement('a'); - chip.className = 'anime-chip'; - chip.href = link; - chip.innerHTML = ` - ${title} - Ep ${evt.episode} - `; - list.appendChild(chip); - }); - - cell.appendChild(list); - grid.appendChild(cell); + const titleEl = document.getElementById('monthTitle'); + if (titleEl) titleEl.textContent = title; } function setLoading(bool) { state.loading = bool; const loader = document.getElementById('loader'); - if (bool) loader.classList.add('active'); - else loader.classList.remove('active'); + if (loader) { + if (bool) loader.classList.add('active'); + else loader.classList.remove('active'); + } +} + +function updateAmbient(data) { + if (!data || !data.length) return; + + const top = data.reduce((prev, curr) => (prev.media.popularity > curr.media.popularity) ? prev : curr, data[0]); + + const img = top.media.bannerImage || top.media.coverImage.extraLarge; + const bgEl = document.getElementById('ambientBg'); + if (bgEl && img) { + bgEl.style.backgroundImage = `url('${img}')`; + } } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } -function updateAmbient(data) { - if (!data || !data.length) return; - const top = data.reduce((prev, curr) => (prev.media.popularity > curr.media.popularity) ? prev : curr); - const img = top.media.bannerImage || top.media.coverImage.large; - if (img) document.getElementById('ambientBg').style.backgroundImage = `url('${img}')`; -} \ No newline at end of file +async function getCache(key) { + try { + const cache = await caches.open(CACHE_NAME); + const response = await cache.match(`/schedule-cache/${key}`); + if (!response) return null; + + const cached = await response.json(); + const age = Date.now() - cached.timestamp; + + if (age < CACHE_DURATION) { + console.log(`[Cache Hit] ${key}`); + return cached.data; + } + cache.delete(`/schedule-cache/${key}`); + return null; + } catch (e) { + return null; + } +} + +function getCacheKey() { + + let d; + + if (state.viewType === 'MONTH') { + d = new Date(state.currentDate.getFullYear(), state.currentDate.getMonth(), 1); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + return `M_${year}_${month}`; + } else { + d = getWeekStart(state.currentDate); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `W_${year}_${month}_${day}`; + } +} + +async function setCache(key, data) { + try { + const cache = await caches.open(CACHE_NAME); + const payload = JSON.stringify({ timestamp: Date.now(), data: data }); + const response = new Response(payload, { headers: { 'Content-Type': 'application/json' } }); + await cache.put(`/schedule-cache/${key}`, response); + } catch (e) { + console.warn("Cache write failed", e); + } +} + +window.navigate = navigate; +window.setViewType = setViewType; +window.setMode = setMode; +window.setFilter = setFilter; \ No newline at end of file diff --git a/docker/views/css/schedule/schedule.css b/docker/views/css/schedule/schedule.css index 6eb455a..2d28fdd 100644 --- a/docker/views/css/schedule/schedule.css +++ b/docker/views/css/schedule/schedule.css @@ -1,457 +1,440 @@ :root { - --bg-glass: rgba(18, 18, 21, 0.8); - --bg-cell: #0c0c0e; - --color-primary-glow: rgba(139, 92, 246, 0.3); + --header-height: 140px; } body { - margin: 0; - background-color: var(--color-bg-base); - color: var(--color-text-primary); - overflow: hidden; - height: 100vh; - display: flex; - flex-direction: column; -} - -html.electron body { - padding-top: 0; + background-color: #050505; + overflow-x: hidden; } .ambient-bg { - position: absolute; - inset: 0; - z-index: -1; - background-size: cover; - background-position: center; - opacity: 0.06; - filter: blur(120px) saturate(1.2); - transition: background-image 1s ease-in-out; - pointer-events: none; -} - -.calendar-wrapper { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - padding: 3rem; - max-width: 1920px; + position: fixed; + top: 0; + left: 0; width: 100%; - margin: 0 auto; + height: 100%; + background-size: cover; + background-position: center top; + opacity: 0.15; + filter: blur(80px) saturate(1.5); + z-index: -2; + transition: background-image 1s ease; } -.calendar-controls { - padding: 1.5rem 0; +.bg-overlay { + position: fixed; + inset: 0; + background: radial-gradient(circle at top, transparent 0%, #050505 80%); + z-index: -1; +} + +.schedule-container { + padding: calc(var(--nav-height) + 2rem) 3rem 2rem 3rem; + max-width: 1800px; + margin: 0 auto; + min-height: 100vh; +} + +.schedule-header { display: flex; justify-content: space-between; - align-items: center; - flex-shrink: 0; + align-items: flex-end; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(255,255,255,0.08); + flex-wrap: wrap; + gap: 1.5rem; } -.month-selector { +.page-title { + font-size: 2.5rem; + font-weight: 900; + margin: 0 0 0.5rem 0; + letter-spacing: -1px; +} + +.header-left { + display: flex; + flex-direction: column; +} + +.month-navigator { + display: flex; + align-items: center; + gap: 1rem; +} + +.current-date-label { + font-size: 1.2rem; + font-weight: 600; + color: var(--color-primary); + min-width: 180px; + text-align: center; +} + +.nav-btn { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + font-size: 1.2rem; + line-height: 0; +} + +.nav-btn:hover { + background: var(--color-primary); + border-color: var(--color-primary); +} + +.header-controls { display: flex; align-items: center; gap: 1.5rem; } -.month-title { - font-size: 2.2rem; - font-weight: 800; - letter-spacing: -0.03em; - background: linear-gradient(to right, #fff, #a1a1aa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - min-width: 350px; +.divider-vertical { + width: 1px; + height: 30px; + background: rgba(255,255,255,0.1); } -.icon-btn { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - width: 44px; - height: 44px; - border-radius: 12px; - color: white; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: 0.2s; -} - -.icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); - transform: translateY(-2px); -} - -.controls-right { - display: flex; - gap: 1rem; -} - -.view-toggles { - display: flex; - background: #0f0f12; +.toggle-group { + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.1); padding: 4px; border-radius: 99px; - border: 1px solid var(--border-subtle); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); + display: flex; + gap: 4px; } -.toggle-item { - padding: 10px 24px; - border-radius: 99px; - border: none; +.toggle-btn { background: transparent; + border: none; color: var(--color-text-secondary); + padding: 6px 16px; + border-radius: 99px; font-weight: 600; font-size: 0.9rem; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.2s; } -.toggle-item.active { - background: var(--color-primary); - color: white; - box-shadow: 0 2px 10px var(--color-primary-glow); -} - -.calendar-board { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); +.toggle-btn:hover { color: white; } +.toggle-btn.active { background: var(--color-bg-elevated); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); + color: white; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); } -.weekdays-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - border-bottom: 1px solid var(--border-subtle); - background: rgba(255, 255, 255, 0.02); - flex-shrink: 0; -} - -.weekday-header { - padding: 16px; - text-align: center; - text-transform: uppercase; - font-size: 0.75rem; - font-weight: 800; - color: var(--color-text-secondary); - letter-spacing: 0.1em; - border-right: 1px solid var(--border-subtle); -} - -.weekday-header:last-child { - border-right: none; -} - -.days-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - width: 100%; - overflow-y: auto; - flex: 1; - - grid-auto-rows: minmax(180px, 1fr); - background: var(--color-bg-base); -} - -.day-cell { - position: relative; - background: var(--bg-cell); - border-right: 1px solid var(--border-subtle); - border-bottom: 1px solid var(--border-subtle); +.view-switcher { display: flex; - flex-direction: column; - padding: 12px; - transition: background 0.2s; - overflow: hidden; + gap: 0.5rem; } -.day-cell:nth-child(7n) { - border-right: none; -} - -.day-cell.empty { - background: rgba(0, 0, 0, 0.2); - pointer-events: none; -} - -.day-cell:hover { - background: #16161a; -} - -.day-cell.today { - background: rgba(139, 92, 246, 0.03); - box-shadow: inset 0 0 0 1px var(--color-primary); -} - -.day-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - z-index: 2; - pointer-events: none; -} - -.day-number { - font-size: 1.1rem; - font-weight: 700; +.view-btn { + background: transparent; + border: 1px solid rgba(255,255,255,0.1); color: var(--color-text-secondary); - width: 32px; - height: 32px; + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; display: flex; align-items: center; justify-content: center; - border-radius: 50%; + transition: all 0.2s; } -.day-cell.today .day-number { +.view-btn:hover { border-color: white; color: white; } +.view-btn.active { background: var(--color-primary); + border-color: var(--color-primary); color: white; - box-shadow: 0 0 15px var(--color-primary-glow); } -.today-label { - font-size: 0.65rem; - font-weight: 800; - color: var(--color-primary); - letter-spacing: 0.05em; - text-transform: uppercase; - display: none; -} - -.day-cell.today .today-label { - display: block; -} - -.events-list { - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; - overflow-y: auto; - z-index: 2; - padding-right: 4px; -} - -.events-list::-webkit-scrollbar { - width: 4px; -} - -.events-list::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; -} - -.anime-chip { - display: flex; - align-items: center; - justify-content: space-between; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); - padding: 8px 10px; - border-radius: 8px; - font-size: 0.8rem; - color: #d4d4d8; - text-decoration: none; - transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); - cursor: pointer; - position: relative; +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 1px; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.05); + border-radius: 12px; overflow: hidden; } -.anime-chip::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--color-primary); - opacity: 0; - transition: opacity 0.2s; -} - -.anime-chip:hover { - background: rgba(255, 255, 255, 0.1); - color: white; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - padding-left: 14px; -} - -.anime-chip:hover::before { - opacity: 1; -} - -.chip-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 500; - margin-right: 8px; -} - -.chip-ep { - font-size: 0.7rem; +.weekday-header { + background: var(--color-bg-card); + padding: 1rem; + text-align: center; font-weight: 700; color: var(--color-text-secondary); - background: rgba(0, 0, 0, 0.4); - padding: 2px 6px; - border-radius: 4px; - white-space: nowrap; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 1px; } -.cell-backdrop { - position: absolute; - inset: 0; - background-size: cover; - background-position: center; - opacity: 0; - transition: opacity 0.4s ease; - filter: grayscale(100%) brightness(0.25); - z-index: 1; - pointer-events: none; +.day-cell { + background: var(--color-bg-base); + min-height: 160px; + max-height: 160px; + padding: 0.8rem; + position: relative; + display: flex; + flex-direction: column; + gap: 0.5rem; + + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; } -.day-cell:hover .cell-backdrop { - opacity: 1; -} +.day-cell::-webkit-scrollbar { display: none; } +.day-cell.empty { background: rgba(0,0,0,0.2); } +.day-cell.today { background: rgba(139, 92, 246, 0.05); box-shadow: inset 0 0 0 1px var(--color-primary); } -.loader { - position: fixed; - bottom: 30px; - right: 30px; - background: #18181b; - border: 1px solid var(--border-subtle); - padding: 12px 24px; - border-radius: 99px; +.day-number { + font-weight: 700; + font-size: 1rem; + color: var(--color-text-secondary); + margin-bottom: 4px; + display: block; +} +.today .day-number { color: var(--color-primary); } + +.anime-item-month { display: flex; align-items: center; - gap: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - transform: translateY(100px); - transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 1000; + gap: 8px; + padding: 6px; + background: rgba(255,255,255,0.03); + border-radius: 6px; + text-decoration: none; + transition: 0.2s; + border-left: 2px solid transparent; } -.loader.active { - transform: translateY(0); +.anime-item-month:hover { + background: rgba(255,255,255,0.08); + transform: translateX(2px); +} +.anime-item-month.is-mine { + border-left-color: var(--color-success); + background: rgba(34, 197, 94, 0.05); } -.spinner { - width: 18px; - height: 18px; - border: 2px solid rgba(255, 255, 255, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 0.8s infinite linear; +.item-time { font-size: 0.75rem; color: var(--color-text-muted); font-family: monospace; } +.item-title { font-size: 0.8rem; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } + +.week-container { + display: flex; + flex-direction: column; + gap: 2rem; } -@keyframes spin { - to { - transform: rotate(360deg); +.week-nav { + display: flex; + gap: 1rem; + overflow-x: auto; + padding-bottom: 1rem; + scrollbar-width: none; + mask-image: linear-gradient(to right, black 90%, transparent 100%); +} +.week-nav::-webkit-scrollbar { display: none; } + +.day-btn { + flex: 1; + min-width: 120px; + + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--color-text-secondary); + + border-radius: 12px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); + text-align: center; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.day-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: white; + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.2); +} + +.day-btn.active { + background: var(--color-primary); + border-color: var(--color-primary); + color: white; + box-shadow: 0 8px 20px var(--color-primary-glow); +} + +.day-btn span.name { + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.8; +} + +.day-btn span.date { + font-size: 1.8rem; + font-weight: 800; + line-height: 1; +} + +.week-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1.5rem; + animation: fadeInUp 0.4s ease; +} + +.card-ep-badge { + position: absolute; + top: 8px; + right: 8px; + background: rgba(0,0,0,0.8); + color: var(--color-primary); + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 800; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.card.mine .card-img-wrap { + box-shadow: 0 0 0 2px var(--color-success); +} +.card.mine::after { + content: "IN LIST"; + position: absolute; + top: 8px; left: 8px; + background: var(--color-success); + color: black; + font-size: 0.65rem; + font-weight: 900; + padding: 2px 6px; + border-radius: 4px; +} + +.loader-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(5px); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; +} +.loader-overlay.active { opacity: 1; pointer-events: auto; } + +@media (max-width: 1024px) { + .schedule-container { + padding: 5rem 1.5rem 2rem 1.5rem; } } @media (max-width: 768px) { - body { - height: auto; - overflow-y: auto; - overflow-x: hidden; - } - .calendar-wrapper { - padding: 1rem; - height: auto; - overflow: visible; - display: block; - } - - .calendar-controls { + .schedule-header { flex-direction: column; - gap: 1rem; - padding-bottom: 1rem; + align-items: stretch; + gap: 1.5rem; } - .month-selector { + .header-left { + align-items: center; width: 100%; - justify-content: space-between; - gap: 0.5rem; } - .month-title { - font-size: 1.5rem; - min-width: auto; - text-align: center; - flex: 1; - } - - .controls-right { - width: 100%; + .header-controls { + flex-wrap: wrap; justify-content: center; + width: 100%; + gap: 1rem; } - .calendar-board { - border: none; - background: transparent; - box-shadow: none; - overflow: visible; + .page-title { + font-size: 2rem; + text-align: center; } - .weekdays-grid { - display: none; - } - - .days-grid { + .calendar-grid { display: flex; flex-direction: column; gap: 1rem; background: transparent; - height: auto; - overflow: visible; + border: none; } - .day-cell.empty { - display: none; - } + .weekday-header, .day-cell.empty { display: none; } .day-cell { - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - background: var(--color-bg-elevated); min-height: auto; - padding: 1rem; + max-height: none; + overflow: visible; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 12px; + background: var(--color-bg-elevated); } - .day-cell:nth-child(7n) { - border-right: 1px solid var(--border-subtle); + .week-nav { + margin: 0 -1.5rem; + padding: 0 1.5rem 1rem 1.5rem; } - .day-header { - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - padding-bottom: 0.5rem; - margin-bottom: 0.8rem; + .day-btn { + min-width: 90px; + padding: 0.8rem; + } + .day-btn span.name { font-size: 0.75rem; } + .day-btn span.date { font-size: 1.5rem; } + + .week-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; } - .day-number { - background: rgba(255, 255, 255, 0.05); + .week-grid .card { + min-width: 0 !important; + width: 100% !important; + flex: none !important; } - .anime-chip { - padding: 12px; - font-size: 0.95rem; - } - - .cell-backdrop { - display: none; - } + .card-content h3 { font-size: 0.8rem; } + .card-ep-badge { font-size: 0.65rem; padding: 2px 4px; } } + +@media (max-width: 380px) { + .header-controls { + gap: 0.5rem; + } + .toggle-btn { + padding: 6px 10px; + font-size: 0.8rem; + } + .nav-btn { + width: 28px; + height: 28px; + } + .current-date-label { + font-size: 1rem; + min-width: 140px; + } +} \ No newline at end of file diff --git a/docker/views/schedule.html b/docker/views/schedule.html index 3070e15..080c5c7 100644 --- a/docker/views/schedule.html +++ b/docker/views/schedule.html @@ -4,82 +4,80 @@ WaifuBoard - Schedule - - - + + - + -
+
+
-
-
-
- -
Loading...
- -
+
-
- -
- - -
- -
- - -
+
+
+

Release Schedule

+
+ + Loading... +
-
-
-
Mon
-
Tue
-
Wed
-
Thu
-
Fri
-
Sat
-
Sun
+
+ -
+
+ +
+ + +
+ +
+ +
-
+
-
-
- Syncing Schedule... -
+
+
- - - Click To Download - -
+
+
+
- - - - - + + + + + + + \ No newline at end of file