init
BIN
animex/.DS_Store
vendored
Normal file
BIN
animex/README.md
Normal file
BIN
animex/Resources/.DS_Store
vendored
Normal file
BIN
animex/Resources/Images/Launch.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
animex/Resources/Images/Launch_screen.png
Normal file
|
After Width: | Height: | Size: 569 KiB |
BIN
animex/Resources/Images/aesthetic.jpg
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
animex/Resources/Images/logo-196.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
animex/Resources/Images/logo-256.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
animex/Resources/Images/logo-512.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
3
animex/Resources/Svgs/Tabbar/Anime.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="33" height="29" viewBox="0 0 33 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1507 24.6181C13.4752 24.6181 15.737 26.2917 16.0776 27.0829C16.0912 27.1286 16.1185 27.159 16.1594 27.1894V3.74294C15.3146 2.93654 12.9983 1.73455 10.1643 1.73455C6.68992 1.73455 3.46078 3.71251 3.4744 4.79279V26.8547C3.4744 27.0981 3.62428 27.2655 3.84228 27.2655C4.04665 27.2655 4.19653 27.1894 4.25103 27.0829C4.59166 26.307 6.8398 24.6181 10.1507 24.6181ZM22.8493 24.6181C26.1738 24.6181 28.422 26.307 28.7626 27.0829C28.8035 27.1894 28.9533 27.2655 29.1577 27.2655C29.3757 27.2655 29.5256 27.0981 29.5256 26.8547V4.79279C29.5392 3.71251 26.3237 1.73455 22.8357 1.73455C20.0017 1.73455 17.699 2.93654 16.8406 3.74294V27.1894C16.8951 27.159 16.9088 27.1286 16.9224 27.0829C17.263 26.2917 19.5248 24.6181 22.8493 24.6181Z" fill="#5B574C" stroke="#B6521B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 874 B |
3
animex/Resources/Svgs/item-list-1.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="58" height="64" viewBox="0 0 58 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="58" height="64" rx="9" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 203 B |
3
animex/Resources/Svgs/item-list-2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="58" height="24" viewBox="0 0 58 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="58" height="24" rx="12" fill="#22221C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 204 B |
4
animex/Resources/Svgs/item-list-arrow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="23" height="32" viewBox="0 0 23 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_12_142_item1)"><path d="M4.43147 31.0806C4.56956 31.0806 4.62135 31.0016 4.72491 30.9029L18.7066 16.5629C18.862 16.3852 18.9828 16.2074 18.9828 15.9901C18.9828 15.7728 18.8792 15.6148 18.7239 15.4568L4.75944 1.11687C4.63861 1.01811 4.56956 0.919348 4.43147 0.919348C4.20707 0.919348 4.0172 1.13662 4.0172 1.41315C4.0172 1.53166 4.03446 1.66992 4.12076 1.74893L17.9471 16.0099L4.12076 30.2313C4.03446 30.3301 4.0172 30.4683 4.0172 30.5868C4.0172 30.8634 4.20707 31.0806 4.43147 31.0806Z" fill="#878472" stroke="#878472" stroke-width="3"/></g>
|
||||
<defs><clipPath id="clip0_12_142_item1"><rect width="23" height="32" fill="white"/></clipPath></defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 843 B |
BIN
animex/Resources/favicon.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
885
animex/Resources/manga.css
Normal file
@@ -0,0 +1,885 @@
|
||||
:root {
|
||||
/* Accent and Palette */
|
||||
--brand-accent: #ff9500;
|
||||
--background-primary: #0a0a0a;
|
||||
--background-secondary: #161616;
|
||||
--background-tertiary: #202020; /* Added for consistency in related cards */
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #999999;
|
||||
--text-muted: #666; /* Added for info messages */
|
||||
--border-color: #2a2a2a;
|
||||
|
||||
/* Effects */
|
||||
--brand-glow: rgba(255, 149, 0, 0.5);
|
||||
--shadow-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Animation and Sizing */
|
||||
--transition-duration: 0.3s;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#manga-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- HERO SECTION --- */
|
||||
#hero-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
min-height: 450px;
|
||||
padding: 0 5%;
|
||||
background-color: var(--background-tertiary);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
transition: background-image var(--transition-duration) ease-in-out;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background-primary) 20%,
|
||||
rgba(10, 10, 10, 0.7) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: var(--background-secondary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.vertical-title-jp {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 30px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
writing-mode: vertical-rl;
|
||||
font-size: clamp(2.5rem, 6vw, 4rem);
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
letter-spacing: 6px;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease 0.2s;
|
||||
}
|
||||
|
||||
.vertical-title-jp.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cover-art-container {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
height: 65%;
|
||||
max-height: 420px;
|
||||
aspect-ratio: 2 / 3;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.cover-art-container.loaded {
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
#manga-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 20px 40px -10px var(--shadow-color);
|
||||
}
|
||||
|
||||
/* --- CONTENT SHEET --- */
|
||||
#content-sheet {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
max-width: 1200px;
|
||||
min-height: 50vh;
|
||||
margin: -50px auto 0;
|
||||
padding: 2rem 5%;
|
||||
background: transparent;
|
||||
border-radius: 24px 24px 0 0;
|
||||
}
|
||||
|
||||
.manga-title-en {
|
||||
font-size: clamp(2.2rem, 6vw, 3rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.manga-title-jp {
|
||||
margin-bottom: 2rem;
|
||||
font-size: clamp(1.1rem, 3vw, 1.3rem);
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.genres-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.genre-tag {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: color var(--transition-duration) ease,
|
||||
border-color var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.genre-tag:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--brand-accent);
|
||||
}
|
||||
|
||||
.synopsis-container {
|
||||
margin: 0 0 3rem 0;
|
||||
}
|
||||
|
||||
.synopsis-container h3 {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#manga-synopsis {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* --- TABS SYSTEM --- */
|
||||
.tabs-section {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
position: relative;
|
||||
padding: 1rem 0.25rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.tab-btn::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: var(--brand-accent);
|
||||
transform: scaleX(0);
|
||||
transition: transform var(--transition-duration)
|
||||
cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
animation: fadeIn 0.4s ease;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- CHAPTER LIST --- */
|
||||
#chapter-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-secondary);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease-in-out, border-color 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
border-color: var(--brand-accent);
|
||||
box-shadow: 0 8px 25px -5px var(--shadow-color);
|
||||
}
|
||||
|
||||
.chapter-item.is-read .chapter-details .chapter-title {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chapter-item.is-read:hover .chapter-details .chapter-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chapter-thumbnail {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 180px;
|
||||
background-color: #333;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.chapter-thumbnail .chapter-number-display {
|
||||
z-index: 2;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.chapter-thumbnail::after {
|
||||
content: "\f04b";
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
z-index: 3;
|
||||
font-size: 32px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.chapter-item:hover .chapter-thumbnail::after {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.chapter-item .chapter-thumbnail::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(0, 0, 0, 0.3) 0%,
|
||||
rgba(0, 0, 0, 0.7) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.chapter-item:hover .chapter-thumbnail::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.read-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
width: 0;
|
||||
height: 5px;
|
||||
background-color: var(--brand-accent);
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.chapter-item.is-read .read-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
gap: 6px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chapter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.chapter-item:hover .action-btn {
|
||||
opacity: 1;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: var(--brand-accent);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
.add-to-list-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
/* --- READER MODAL --- */
|
||||
#reader-modal-container {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background-color: #121212;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
|
||||
}
|
||||
|
||||
#reader-modal-container.visible {
|
||||
display: block;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#reader-modal-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* --- UTILITIES & PLACEHOLDERS --- */
|
||||
.loader {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 2rem auto;
|
||||
border: 4px solid var(--border-color);
|
||||
border-top-color: var(--brand-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clear-cache-btn {
|
||||
float: right;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
border: 4px solid var(--brand-accent);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: inset 0 0 15px rgba(135, 135, 135, 0.1),
|
||||
0 0 18px 3px rgba(0, 0, 0, 0.3);
|
||||
color: white;
|
||||
font-family: Tahoma, sans-serif;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jump-read-btn {
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
border: 4px solid var(--brand-accent);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: inset 0 0 15px rgba(135, 135, 135, 0.1),
|
||||
0 0 18px 3px rgba(0, 0, 0, 0.3);
|
||||
color: white;
|
||||
font-family: Tahoma, sans-serif;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- RELATED & RECOMMENDATIONS --- */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.recommendation-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-secondary);
|
||||
box-shadow: 0 2px 8px var(--shadow-color);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
transition: transform var(--transition-duration) ease,
|
||||
box-shadow var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.recommendation-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px var(--shadow-color);
|
||||
}
|
||||
|
||||
.recommendation-card-img-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 3;
|
||||
background-color: var(--background-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recommendation-card-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.recommendation-card:hover .recommendation-card-img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.recommendation-card-title {
|
||||
flex-grow: 1;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.related-content-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-secondary);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
transition: background-color var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.related-card-img-container {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
aspect-ratio: 2 / 3;
|
||||
background-color: var(--background-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.related-card-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.related-card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Prevents flexbox overflow */
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.related-card-title {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.related-card-tag {
|
||||
align-self: flex-start;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--brand-accent);
|
||||
color: var(--background-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* --- MANGADEX & META STYLES --- */
|
||||
.mangadex-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: #333;
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.mangadex-button:hover {
|
||||
background-color: #444;
|
||||
border-color: var(--brand-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mangadex-button img {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.chapter-meta-mangadex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.meta-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
transition: background-color var(--transition-duration) ease,
|
||||
border-color var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.meta-tag .fas {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.chapter-item:hover .meta-tag {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--brand-glow);
|
||||
}
|
||||
|
||||
/* --- PAGINATION --- */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.pagination-container button {
|
||||
padding: 10px 18px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-duration) ease,
|
||||
border-color var(--transition-duration) ease,
|
||||
color var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.pagination-container button:hover:not(:disabled) {
|
||||
background-color: var(--brand-accent);
|
||||
border-color: var(--brand-accent);
|
||||
color: var(--background-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.pagination-container button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-container span {
|
||||
padding: 0 1rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --- RESPONSIVE ADJUSTMENTS --- */
|
||||
@media (max-width: 768px) {
|
||||
#hero-section {
|
||||
height: 50vh;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.vertical-title-jp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cover-art-container {
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
#content-sheet {
|
||||
margin-top: -60px;
|
||||
padding: 3rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.manga-title-en,
|
||||
.manga-title-jp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.genres-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
transform: translateY(-2px) scale(1.01);
|
||||
}
|
||||
|
||||
.chapter-thumbnail {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.chapter-details {
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.chapter-actions {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.action-btn.download-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.1rem;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
opacity: 1; /* Always visible on touch devices */
|
||||
}
|
||||
.action-btn.add-to-list-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.1rem;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
opacity: 1; /* Always visible on touch devices */
|
||||
}
|
||||
|
||||
.related-content-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chapter-meta-mangadex {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chapter-item .chapter-details {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
}
|
||||
.add-list-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-action-btn {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: color var(--transition-duration) ease
|
||||
}
|
||||
|
||||
.hero-action-btn:hover {
|
||||
color: var(--brand-accent);
|
||||
border-color: var(--brand-accent);
|
||||
user-select: none;
|
||||
}
|
||||
27
animex/Resources/manifest.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Animex",
|
||||
"short_name": "Animex",
|
||||
"description": "Animex - Anime and Manga Search and Player",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0b0b",
|
||||
"theme_color": "#FF9500",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/Resources/Images/logo-256.png",
|
||||
"sizes": "400x400",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/Resources/Images/logo-196.png",
|
||||
"sizes": "196x196",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/Resources/Images/logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
186
animex/Resources/old/Manga.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Animex</title>
|
||||
<link rel="icon" href="Resources/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="Resources/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app-wrapper">
|
||||
<!-- Header Image -->
|
||||
<img class="app-absolute header-main-image" style="left: -44px; top: -26px;" src="https://images.unsplash.com/photo-1579803815617-16568f90450e?q=80&w=1933&auto=format&fit=crop" alt="Header Background"/>
|
||||
|
||||
<!-- Featured Content -->
|
||||
<img id="featured-poster" class="app-absolute featured-poster-img" style="left: 36px; top: 59px;" src="https://placehold.co/126x193/99AAB5/FFFFFF?text=Featured&font=montserrat" alt="Featured Poster"/>
|
||||
<div id="featured-info" class="app-absolute flex-center-col featured-small-text" style="left: 176px; top: 82px;">TYPE | YEAR</div>
|
||||
<div id="featured-title" class="app-absolute featured-title-text" style="left: 176px; top: 105px; width: 184px; height: auto; max-height: 80px;">Featured Manga Title</div>
|
||||
<div class="app-absolute featured-button-background" style="left: 176px; top: 203px;"></div>
|
||||
<div class="app-absolute flex-center-col featured-small-text" style="left: 205px; top: 207px;">READ MORE</div>
|
||||
<div class="app-absolute featured-button-icon-svg-wrapper" style="left: 185px; top: 207px;">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.15329 12.2561C2.28704 12.2561 2.38735 12.2214 2.55453 12.1171L10.9203 6.98663C11.1543 6.85455 11.2948 6.72246 11.2948 6.5C11.2948 6.28449 11.1543 6.15241 10.9203 6.01337L2.55453 0.882887C2.38735 0.778609 2.28704 0.74385 2.15329 0.74385C1.8858 0.74385 1.70525 0.945454 1.70525 1.29305V11.707C1.70525 12.0545 1.8858 12.2561 2.15329 12.2561Z" fill="white" stroke="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Explore Tab Backing & Content -->
|
||||
<div class="explore-backing">
|
||||
<div class="explore-title">
|
||||
Explore Manga
|
||||
</div>
|
||||
<div id="scrollable-list-content">
|
||||
<!-- Manga items will be injected here by JavaScript -->
|
||||
<div class="loading-message">Loading manga...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Bar -->
|
||||
<div class="bottom-tabbar">
|
||||
<!-- MANGA Tab (Active) -->
|
||||
<div class="tab-item">
|
||||
<div class="app-absolute tab-icon-svg-wrapper" style="left: 24px; top: 12px;">
|
||||
<svg width="33" height="29" viewBox="0 0 33 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1507 24.6181C13.4752 24.6181 15.737 26.2917 16.0776 27.0829C16.0912 27.1286 16.1185 27.159 16.1594 27.1894V3.74294C15.3146 2.93654 12.9983 1.73455 10.1643 1.73455C6.68992 1.73455 3.46078 3.71251 3.4744 4.79279V26.8547C3.4744 27.0981 3.62428 27.2655 3.84228 27.2655C4.04665 27.2655 4.19653 27.1894 4.25103 27.0829C4.59166 26.307 6.8398 24.6181 10.1507 24.6181ZM22.8493 24.6181C26.1738 24.6181 28.422 26.307 28.7626 27.0829C28.8035 27.1894 28.9533 27.2655 29.1577 27.2655C29.3757 27.2655 29.5256 27.0981 29.5256 26.8547V4.79279C29.5392 3.71251 26.3237 1.73455 22.8357 1.73455C20.0017 1.73455 17.699 2.93654 16.8406 3.74294V27.1894C16.8951 27.159 16.9088 27.1286 16.9224 27.0829C17.263 26.2917 19.5248 24.6181 22.8493 24.6181Z" fill="#B6521B" stroke="#B6521B"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-absolute tab-label-text" style="left: 17px; color: #B6521B;">MANGA</div>
|
||||
</div>
|
||||
|
||||
<!-- ANIME Tab -->
|
||||
<div class="tab-item">
|
||||
<div class="app-absolute tab-icon-svg-wrapper" style="left: 104px; top: 12px;">
|
||||
<svg width="35" height="29" viewBox="0 0 35 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.73459 26.8169H30.2654C31.3442 26.8169 31.9435 26.1127 31.9435 24.8451V4.15492C31.9435 2.9014 31.3322 2.18309 30.2654 2.18309H4.73459C3.66781 2.18309 3.05651 2.9014 3.05651 4.15492V24.8451C3.05651 26.1127 3.65582 26.8169 4.73459 26.8169ZM4.73459 26.1268C3.97945 26.1268 3.64384 25.7324 3.64384 24.8451V4.15492C3.64384 3.28168 3.99144 2.87323 4.73459 2.87323H30.2654C31.0086 2.87323 31.3562 3.28168 31.3562 4.15492V24.8451C31.3562 25.7324 31.0325 26.1268 30.2654 26.1268H4.73459ZM4.95034 6.25351C4.95034 6.59154 5.19007 6.88732 5.53767 6.88732H6.77226C7.11986 6.88732 7.33562 6.59154 7.33562 6.25351V4.92957C7.33562 4.52112 7.11986 4.25351 6.77226 4.25351H5.53767C5.1661 4.25351 4.95034 4.52112 4.95034 4.92957V6.25351ZM27.6764 6.25351C27.6764 6.59154 27.8921 6.88732 28.2397 6.88732H29.4863C29.8219 6.88732 30.0497 6.59154 30.0497 6.25351V4.92957C30.0497 4.52112 29.8219 4.25351 29.4863 4.25351H28.2397C27.8681 4.25351 27.6764 4.52112 27.6764 4.92957V6.25351ZM8.72603 12.5352C8.72603 13 8.97774 13.2676 9.37329 13.2676H25.6267C26.0342 13.2676 26.274 13 26.274 12.5352V5.0845C26.274 4.60563 26.0342 4.33802 25.6267 4.33802H9.37329C8.97774 4.33802 8.72603 4.60563 8.72603 5.0845V12.5352ZM4.95034 10.7042C4.95034 11.0563 5.19007 11.3521 5.53767 11.3521H6.77226C7.11986 11.3521 7.33562 11.0563 7.33562 10.7042V9.38027C7.33562 8.97182 7.11986 8.7183 6.77226 8.7183H5.53767C5.1661 8.7183 4.95034 8.97182 4.95034 9.38027V10.7042ZM27.6764 10.7042C27.6764 11.0563 27.8921 11.3521 28.2397 11.3521H29.4863C29.8219 11.3521 30.0497 11.0563 30.0497 10.7042V9.38027C30.0497 8.97182 29.8219 8.7183 29.4863 8.7183H28.2397C27.8681 8.7183 27.6764 8.97182 27.6764 9.38027V10.7042ZM4.95034 15.169C4.95034 15.507 5.19007 15.7887 5.53767 15.7887H6.77226C7.11986 15.7887 7.33562 15.507 7.33562 15.169V13.8451C7.33562 13.4366 7.11986 13.169 6.77226 13.169H5.53767C5.1661 13.169 4.95034 13.4366 4.95034 13.8451V15.169ZM27.6764 15.169C27.6764 15.507 27.8921 15.7887 28.2397 15.7887H29.4863C29.8219 15.7887 30.0497 15.507 30.0497 15.169V13.8451C30.0497 13.4366 29.8219 13.169 29.4863 13.169H28.2397C27.8681 13.169 27.6764 13.4366 27.6764 13.8451V15.169ZM8.72603 23.8873C8.72603 24.3521 8.97774 24.6197 9.37329 24.6197H25.6267C26.0342 24.6197 26.274 24.3521 26.274 23.8873V16.4225C26.274 15.9577 26.0342 15.6901 25.6267 15.6901H9.37329C8.97774 15.6901 8.72603 15.9577 8.72603 16.4225V23.8873ZM4.95034 19.6197C4.95034 19.9577 5.19007 20.2394 5.53767 20.2394H6.77226C7.11986 20.2394 7.33562 19.9577 7.33562 19.6197V18.2958C7.33562 17.8873 7.11986 17.6197 6.77226 17.6197H5.53767C5.1661 17.6197 4.95034 17.8873 4.95034 18.2958V19.6197ZM27.6764 19.6197C27.6764 19.9577 27.8921 20.2394 28.2397 20.2394H29.4863C29.8219 20.2394 30.0497 19.9577 30.0497 19.6197V18.2958C30.0497 17.8873 29.8219 17.6197 29.4863 17.6197H28.2397C27.8681 17.6197 27.6764 17.8873 27.6764 18.2958V19.6197ZM4.95034 24.0563C4.95034 24.4225 5.19007 24.7042 5.53767 24.7042H6.77226C7.11986 24.7042 7.33562 24.4225 7.33562 24.0563V22.7324C7.33562 22.3521 7.11986 22.0845 6.77226 22.0845H5.53767C5.1661 22.0845 4.95034 22.3521 4.95034 22.7324V24.0563ZM27.6764 24.0563C27.6764 24.4225 27.8921 24.7042 28.2397 24.7042H29.4863C29.8219 24.7042 30.0497 24.4225 30.0497 24.0563V22.7324C30.0497 22.3521 29.8219 22.0845 29.4863 22.0845H28.2397C27.8681 22.0845 27.6764 22.3521 27.6764 22.7324V24.0563Z" fill="#5B574C" stroke="#5B574C"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-absolute tab-label-text" style="left: 101px; color: #5B574C;">ANIME</div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH Tab -->
|
||||
<div class="tab-item">
|
||||
<div class="app-absolute tab-icon-svg-wrapper" style="left: 183px; top: 10px;">
|
||||
<svg width="35" height="37" viewBox="0 0 35 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 33.2075C25.625 33.2075 32.2713 26.5933 32.2713 18.4924C32.2713 10.4067 25.625 3.79253 17.5 3.79253C9.375 3.79253 2.72866 10.4067 2.72866 18.4924C2.72866 26.5933 9.375 33.2075 17.5 33.2075ZM17.5 32.4641C9.77134 32.4641 3.47561 26.1988 3.47561 18.4924C3.47561 10.8011 9.77134 4.53587 17.5 4.53587C25.2439 4.53587 31.5244 10.8011 31.5244 18.4924C31.5244 26.1988 25.2439 32.4641 17.5 32.4641ZM15.8079 22.2243C17.1341 22.2243 18.3537 21.7692 19.314 20.9803L23.75 25.41C23.9024 25.5617 24.0091 25.6376 24.2073 25.6376C24.5122 25.6376 24.6951 25.4252 24.6951 25.1673C24.6951 24.9701 24.6189 24.879 24.4817 24.7425L20.0305 20.2977C20.8689 19.3268 21.3872 18.0525 21.3872 16.672C21.3872 13.6228 18.872 11.1045 15.8079 11.1045C12.7287 11.1045 10.2134 13.6228 10.2134 16.672C10.2134 19.7364 12.7287 22.2243 15.8079 22.2243ZM15.8079 21.4658C13.1555 21.4658 10.9756 19.3116 10.9756 16.672C10.9756 14.0324 13.1555 11.8631 15.8079 11.8631C18.4604 11.8631 20.625 14.0324 20.625 16.672C20.625 19.3116 18.4604 21.4658 15.8079 21.4658Z" fill="#5B574C" stroke="#5B574C"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-absolute tab-label-text" style="left: 175px; color: #5B574C;">SEARCH</div>
|
||||
</div>
|
||||
|
||||
<!-- LIBRARY Tab -->
|
||||
<div class="tab-item">
|
||||
<div class="app-absolute tab-icon-svg-wrapper" style="left: 269px; top: 8px;">
|
||||
<svg width="24" height="39" viewBox="0 0 24 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.82036 34.5045C4.10779 34.5045 4.34731 34.344 4.58683 34.052L11.2814 25.893C11.5928 25.5135 11.7844 25.4405 12 25.4405C12.2156 25.4405 12.4192 25.5135 12.7186 25.893L19.4012 34.0375C19.6647 34.3732 19.8922 34.5045 20.1796 34.5045C20.6467 34.5045 20.9461 34.0958 20.9461 33.4828V8.24666C20.9461 5.80916 19.8802 4.49554 17.8683 4.49554H6.13174C4.11976 4.49554 3.05389 5.80916 3.05389 8.24666V33.4828C3.05389 34.0958 3.3533 34.5045 3.82036 34.5045Z" fill="#5B574C" stroke="#5B574C"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-absolute tab-label-text" style="left: 255px; color: #5B574C;">LIBRARY</div>
|
||||
</div>
|
||||
|
||||
<!-- SETTINGS Tab -->
|
||||
<div class="tab-item">
|
||||
<div class="app-absolute tab-icon-svg-wrapper" style="left: 347.28px; top: 12px;">
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.1824 12.5815L25.4 11.9632C25.1786 11.2042 24.8758 10.4746 24.497 9.78403L26.705 6.63715C26.8835 6.38307 26.8528 6.03683 26.6333 5.81667L24.1443 3.32899C23.9222 3.10755 23.5728 3.07875 23.3181 3.26243L20.2186 5.49027C19.5216 5.10499 18.7856 4.79907 18.0221 4.57699L17.3622 0.809949C17.3085 0.504029 17.0429 0.280029 16.7318 0.280029H13.2118C12.8982 0.280029 12.6307 0.507229 12.5802 0.816989L11.9683 4.56355C11.2003 4.78435 10.463 5.08707 9.76864 5.46723L6.67744 3.25923C6.42208 3.07683 6.07456 3.10627 5.85248 3.32707L3.3648 5.81475C3.14528 6.03427 3.11456 6.37987 3.29312 6.63395L5.46848 9.74627C5.08128 10.4471 4.7728 11.1888 4.54816 11.9607L0.81632 12.5821C0.50784 12.6333 0.281281 12.9008 0.281281 13.2131V16.7331C0.281281 17.0435 0.504 17.3091 0.80928 17.3636L4.54112 18.0253C4.76448 18.7952 5.07296 19.537 5.46144 20.2397L3.2592 23.32C3.07744 23.5741 3.10624 23.9229 3.32704 24.145L5.81536 26.6352C6.03488 26.8547 6.38112 26.8855 6.6352 26.7069L9.752 24.5239C10.4515 24.9085 11.1907 25.2138 11.9568 25.4352L12.5814 29.1863C12.632 29.4941 12.8989 29.72 13.2118 29.72H16.7318C17.0422 29.72 17.3078 29.4973 17.3616 29.192L18.0304 25.4224C18.7978 25.1965 19.5331 24.8893 20.2256 24.504L23.3648 26.7063C23.6195 26.8861 23.9651 26.8547 24.1853 26.6352L26.6736 24.145C26.895 23.9229 26.9238 23.5728 26.7402 23.3181L24.5014 20.2096C24.881 19.5184 25.1824 18.7875 25.4019 18.0285L29.1894 17.3636C29.496 17.3098 29.7187 17.0435 29.7187 16.7331V13.2131C29.7194 12.8995 29.4922 12.632 29.1824 12.5815ZM15 19.48C12.5258 19.48 10.52 17.4743 10.52 15C10.52 12.5258 12.5258 10.52 15 10.52C17.4742 10.52 19.48 12.5258 19.48 15C19.48 17.4743 17.4742 19.48 15 19.48Z" fill="#5B574C"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-absolute tab-label-text" style="left: 331px; color: #5B574C;">SETTINGS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="desktop-message">
|
||||
Desktop app isn't available now. Please use a mobile device or resize your browser to a mobile width (e.g., less than 420px wide).
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const PROXY_URL = 'https://cors-anywhere.herokuapp.com/'; // Example public proxy
|
||||
const API_URL = PROXY_URL + 'https://api.consumet.org/meta/anilist/trending?type=MANGA&perPage=15'; // Example: Fetch 15 trending manga
|
||||
const scrollableListContent = document.getElementById('scrollable-list-content');
|
||||
|
||||
// Featured content elements
|
||||
const featuredPoster = document.getElementById('featured-poster');
|
||||
const featuredInfo = document.getElementById('featured-info');
|
||||
const featuredTitle = document.getElementById('featured-title');
|
||||
|
||||
async function fetchManga() {
|
||||
try {
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
displayManga(data.results);
|
||||
if (data.results && data.results.length > 0) {
|
||||
displayFeaturedManga(data.results[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
scrollableListContent.innerHTML = `<div class="error-message">Failed to load manga. ${error.message}</div>`;
|
||||
console.error('Error fetching manga:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayManga(mangaList) {
|
||||
scrollableListContent.innerHTML = ''; // Clear loading message or previous content
|
||||
|
||||
if (!mangaList || mangaList.length === 0) {
|
||||
scrollableListContent.innerHTML = '<div class="info-message">No manga found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
mangaList.forEach(manga => {
|
||||
const title = manga.title.english || manga.title.romaji || 'N/A';
|
||||
const coverImage = manga.image || 'https://placehold.co/58x64/333333/FFFFFF?text=N/A&font=montserrat';
|
||||
const rating = manga.rating ? (manga.rating / 10).toFixed(1) : 'N/A';
|
||||
const chapters = manga.totalChapters || manga.chapters || 'N/A';
|
||||
const status = manga.status || 'N/A';
|
||||
const year = manga.releaseDate || 'N/A';
|
||||
|
||||
const listItemWrapper = document.createElement('div');
|
||||
listItemWrapper.className = 'list-item-wrapper';
|
||||
|
||||
// Adjust inline styles as needed or move more to CSS if positions become complex
|
||||
listItemWrapper.innerHTML = `
|
||||
<div class="app-absolute li-thumb-container" style="top: 5px; left: 10px;">
|
||||
<img class="li-thumb-img" src="${coverImage}" alt="${title} cover" />
|
||||
</div>
|
||||
<div class="app-absolute flex-center-col li-main-title-text" style="left: 85px; top: 8px; width: calc(100% - 125px);">${title}</div>
|
||||
|
||||
<div class="app-absolute li-rating-badge-bg" style="left: 85px; top: 48px;"></div>
|
||||
<div class="app-absolute flex-center-col li-badge-text li-rating-text-spacing" style="left: 88px; top: 51px; width: 80px;">${rating}/10</div>
|
||||
|
||||
<div class="app-absolute li-chapters-badge-bg" style="left: 180px; top: 48px;"></div>
|
||||
<div class="app-absolute flex-center-col li-badge-text" style="left: 183px; top: 51px; width: auto; padding: 0 5px;">${chapters} Ch.</div>
|
||||
|
||||
<div class="app-absolute li-arrow-icon-wrapper" style="right: 15px; top: 24px;">
|
||||
<img src="Resources/Svgs/item-list-arrow.svg" alt="Arrow icon" width="23" height="32"/>
|
||||
</div>
|
||||
`;
|
||||
// Note: If 'Resources/Svgs/item-list-arrow.svg' is not available, replace or remove this arrow.
|
||||
// For simplicity, I'm keeping the arrow SVG as an img, assuming it's a UI element not a background.
|
||||
|
||||
scrollableListContent.appendChild(listItemWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
function displayFeaturedManga(manga) {
|
||||
if (!manga) return;
|
||||
|
||||
const title = manga.title.english || manga.title.romaji || 'N/A';
|
||||
const coverImage = manga.image || 'https://placehold.co/126x193/333333/FFFFFF?text=N/A&font=montserrat';
|
||||
const type = manga.type || 'MANGA';
|
||||
const year = manga.releaseDate || 'N/A';
|
||||
|
||||
featuredPoster.src = coverImage;
|
||||
featuredPoster.alt = `${title} Poster`;
|
||||
featuredInfo.textContent = `${type} | ${year}`;
|
||||
featuredTitle.textContent = title;
|
||||
}
|
||||
|
||||
fetchManga();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
270
animex/Resources/old/styles.css
Normal file
@@ -0,0 +1,270 @@
|
||||
/* body, #app-wrapper, #desktop-message, @media (max-width: 420px) rules remain the same as your previous version */
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #e0e0e0; /* A light grey for desktop background */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: Verdana, sans-serif;
|
||||
}
|
||||
|
||||
#app-wrapper {
|
||||
width: 402px; /* Design reference width */
|
||||
height: 874px; /* Design reference height */
|
||||
position: relative;
|
||||
background: white;
|
||||
overflow: hidden; /* Crucial to contain absolutely positioned children */
|
||||
display: none; /* Hidden by default, shown via media query for mobile */
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.2); /* Optional: for better visual separation if shown on desktop */
|
||||
}
|
||||
|
||||
/* --- Desktop Message --- */
|
||||
#desktop-message {
|
||||
display: flex; /* Shown by default on desktop/larger screens */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh; /* Can be min-height: 100vh as well */
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- Styles for elements INSIDE #app-wrapper --- */
|
||||
/* --- Explore Tab Backing --- */
|
||||
.explore-backing {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 299px;
|
||||
width: 100%;
|
||||
background: #36362F;
|
||||
border-top-left-radius: 25px;
|
||||
border-top-right-radius: 25px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- Explore Title --- */
|
||||
.explore-title {
|
||||
position: absolute;
|
||||
left: 19px;
|
||||
top: 15px;
|
||||
color: white;
|
||||
font-size: 24px; /* Slightly reduced for potentially longer text */
|
||||
font-family: Verdana;
|
||||
font-weight: 700;
|
||||
word-wrap: break-word;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* --- Scrollable List --- */
|
||||
#scrollable-list-content {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px 10px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.loading-message, .error-message, .info-message {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* --- List Items --- */
|
||||
.list-item-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px; /* Increased height slightly for better spacing */
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(255, 255, 255, 0.05); /* Subtle background for items */
|
||||
border-radius: 8px;
|
||||
}
|
||||
.list-item-wrapper:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* --- Helper Classes --- */
|
||||
.app-absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.flex-center-col {
|
||||
justify-content: center; /* Vertically center text in its given height */
|
||||
align-items: flex-start; /* Align text to the start horizontally */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* --- Image Adjustments --- */
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* --- Bottom Navigation Bar --- */
|
||||
.bottom-tabbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 98px;
|
||||
background: #22221C;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* --- Header and Featured Content Specific Styles --- */
|
||||
.header-main-image {
|
||||
width: 540px;
|
||||
height: 409px;
|
||||
box-shadow: 1.6px 1.6px 1.6px rgba(0,0,0,0.1);
|
||||
filter: blur(1.5px) brightness(0.7); /* Adjusted for better text visibility */
|
||||
}
|
||||
|
||||
.featured-poster-img {
|
||||
width: 126px;
|
||||
height: 193px;
|
||||
box-shadow: 6px 6px 15px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.featured-small-text {
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-family: Verdana;
|
||||
font-weight: 600;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.featured-title-text { /* New class for the featured title div */
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-family: Verdana;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3; /* Limit to 3 lines */
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.featured-button-background {
|
||||
width: 160px;
|
||||
height: 21px;
|
||||
background: rgba(54, 54, 47, 0.8); /* Darker, slightly transparent */
|
||||
border-radius: 81px;
|
||||
}
|
||||
.featured-button-icon-svg-wrapper {
|
||||
/* Positioned by inline styles */
|
||||
}
|
||||
|
||||
|
||||
/* List Item Specific Styles (Updated for CSS backgrounds) */
|
||||
.li-thumb-container {
|
||||
/* No explicit background needed if img fills it */
|
||||
width: 58px; /* Ensure container size matches image */
|
||||
height: 64px;
|
||||
/* left: 10px; top: 5px; (from JS) */
|
||||
}
|
||||
.li-thumb-img {
|
||||
width:58px;
|
||||
height:64px;
|
||||
border-radius:9px; /* Rounded corners for the manga cover */
|
||||
}
|
||||
.li-main-title-text {
|
||||
/* top: 8px; left: 85px; (from JS) */
|
||||
color: white;
|
||||
font-size: 16px; /* Adjusted for better fit */
|
||||
font-family: Verdana;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
max-height: 38px; /* Approx 2 lines */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* width: calc(100% - 125px); (from JS - to prevent overlap with arrow) */
|
||||
display: -webkit-box; /* For multi-line ellipsis */
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.li-rating-badge-bg, .li-chapters-badge-bg { /* Combined for similar styling */
|
||||
height: 22px; /* Adjusted height */
|
||||
background: #22221C; /* Dark background for badges */
|
||||
border-radius: 11px; /* Rounded corners */
|
||||
/* top: 48px; (from JS) */
|
||||
}
|
||||
.li-rating-badge-bg {
|
||||
width: 85px; /* Fixed width for rating */
|
||||
/* left: 85px; (from JS) */
|
||||
}
|
||||
.li-chapters-badge-bg {
|
||||
min-width: 55px; /* Min width, can grow */
|
||||
padding: 0 8px; /* Padding for dynamic width */
|
||||
/* left: 180px; (from JS) */
|
||||
}
|
||||
|
||||
.li-badge-text {
|
||||
/* height: 19px; (Not strictly needed with flex centering) */
|
||||
/* top: 51px; (from JS, vertically align with badge bg) */
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 11px; /* Adjusted for badge size */
|
||||
font-family: Verdana;
|
||||
font-weight: 700;
|
||||
line-height: 22px; /* Match badge height for vertical center */
|
||||
white-space: nowrap;
|
||||
/* width and left are set in JS or specific badge bg above */
|
||||
}
|
||||
.li-rating-text-spacing {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.li-arrow-icon-wrapper {
|
||||
/* top: 24px; right: 15px; (from JS) */
|
||||
}
|
||||
|
||||
/* Bottom Tab Bar Specific Styles */
|
||||
.tab-icon-svg-wrapper {
|
||||
/* Positioned via inline styles (top, left) */
|
||||
}
|
||||
.tab-label-text {
|
||||
top: 66px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: Verdana;
|
||||
font-weight: 700;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
/* --- Mobile View Overrides & Enhancements --- */
|
||||
@media (max-width: 420px) {
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#app-wrapper {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#desktop-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.explore-backing {
|
||||
bottom: 98px;
|
||||
}
|
||||
|
||||
#scrollable-list-content {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
540
animex/Resources/series.css
Normal file
@@ -0,0 +1,540 @@
|
||||
/* =========================================
|
||||
1. VARIABLES & RESET
|
||||
========================================= */
|
||||
:root {
|
||||
/* Brand & Palette */
|
||||
--brand-accent: #ff9500;
|
||||
--brand-accent-hover: #ffae40;
|
||||
--background-primary: #0a0a0a;
|
||||
--background-secondary: #161616;
|
||||
--background-tertiary: #202020;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #999999;
|
||||
--text-muted: #666666;
|
||||
|
||||
/* UI Elements */
|
||||
--border-color: #2a2a2a;
|
||||
--shadow-color: rgba(0, 0, 0, 0.6);
|
||||
--brand-glow: rgba(255, 149, 0, 0.5);
|
||||
|
||||
/* Dimensions & Animation */
|
||||
--transition-duration: 0.3s;
|
||||
--border-radius: 8px;
|
||||
--nav-height: 60px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
2. APP STRUCTURE & HERO SECTION
|
||||
========================================= */
|
||||
.app-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.series-hero-section {
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
min-height: 550px;
|
||||
background-size: cover;
|
||||
background-position: center 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 0 5% 4rem;
|
||||
transition: background-image var(--transition-duration) ease-in-out;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background-primary) 15%,
|
||||
rgba(10, 10, 10, 0.9) 50%,
|
||||
rgba(10, 10, 10, 0.4) 80%,
|
||||
rgba(0,0,0,0.3) 100%
|
||||
);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.series-hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3rem;
|
||||
animation: fadeInUp 0.6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
/* Desktop Poster in Hero */
|
||||
.hero-poster-container {
|
||||
display: none; /* Hidden on mobile */
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.hero-poster-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-text-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.series-title-text {
|
||||
font-size: clamp(2rem, 5vw, 3.8rem);
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
text-shadow: 2px 2px 15px rgba(0, 0, 0, 0.9);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Hero Sub-metadata (Rating, Studio) */
|
||||
.hero-sub-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
color: #ccc;
|
||||
font-weight: 500;
|
||||
}
|
||||
.hero-rating-badge {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
.hero-rating-badge i { color: var(--brand-accent); font-size: 0.8em; }
|
||||
|
||||
/* --- HERO ACTIONS --- */
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Primary Watch Button */
|
||||
.hero-watch-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background-color: var(--brand-accent);
|
||||
color: #000;
|
||||
padding: 12px 28px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 15px rgba(255, 149, 0, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.hero-watch-btn:hover {
|
||||
background-color: var(--brand-accent-hover);
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 6px 25px rgba(255, 149, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Secondary Action Buttons */
|
||||
.hero-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-primary);
|
||||
padding: 0 20px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(5px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-action-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 25px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.back-btn:hover { background-color: rgba(255,255,255,0.2); }
|
||||
|
||||
/* =========================================
|
||||
3. MAIN CONTENT
|
||||
========================================= */
|
||||
.main-content {
|
||||
position: relative;
|
||||
padding: 0 5% 4rem;
|
||||
background-color: var(--background-primary);
|
||||
z-index: 3;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.series-details-section {
|
||||
margin-bottom: 3rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.series-meta-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.series-meta-info span:not(:last-child)::after {
|
||||
content: "•";
|
||||
margin-left: 16px;
|
||||
color: var(--border-color);
|
||||
}
|
||||
#series-score { color: var(--brand-accent); font-weight: 700; }
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 3rem;
|
||||
}
|
||||
#series-synopsis {
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
color: #ccc;
|
||||
}
|
||||
#series-info-additional p {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
#series-info-additional strong { color: #fff; }
|
||||
|
||||
/* Genres/Studios */
|
||||
.series-genres span, .series-studios span {
|
||||
display: inline-block;
|
||||
margin: 5px 5px 0 0;
|
||||
padding: 4px 10px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
4. TABS & SECTIONS LAYOUT
|
||||
========================================= */
|
||||
.tabs-section { margin-top: 2rem; }
|
||||
|
||||
/* Desktop: Hide Tabs, Stack Panels */
|
||||
@media (min-width: 769px) {
|
||||
.tabs-container { display: none; }
|
||||
.tab-panel {
|
||||
display: block !important;
|
||||
margin-bottom: 4rem;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Section Headings for Desktop Stacking */
|
||||
.tab-panel::before {
|
||||
content: attr(data-label);
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
border-left: 4px solid var(--brand-accent);
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: Show Tabs */
|
||||
@media (max-width: 768px) {
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 0;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.tab-btn.active { color: var(--text-primary); }
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -0.5rem;
|
||||
left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: var(--brand-accent);
|
||||
}
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; animation: fadeIn 0.3s; }
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
5. EPISODES
|
||||
========================================= */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.season-selector {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background: var(--background-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
outline: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.episode-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
.episode-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
height: 90px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.episode-item:hover { background: var(--background-tertiary); }
|
||||
.episode-item.watched .episode-title { color: var(--text-muted); }
|
||||
.episode-item.watched { border-left: 3px solid var(--brand-accent); }
|
||||
|
||||
.episode-thumbnail {
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.episode-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.episode-info { flex: 1; min-width: 0; }
|
||||
.episode-title { font-weight: 600; font-size: 1rem; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.episode-title-romanji { font-size: 0.85rem; color: var(--text-secondary); }
|
||||
|
||||
.episode-action-icon {
|
||||
background: none; border: none; color: var(--text-secondary);
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
cursor: pointer; margin-left: 5px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.episode-action-icon:hover { background: rgba(255,255,255,0.1); color: var(--brand-accent); }
|
||||
|
||||
/* =========================================
|
||||
6. HORIZONTAL SCROLL LISTS (Chars, Rel, Rec)
|
||||
========================================= */
|
||||
.horizontal-scroll-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 15px;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
/* Character Card */
|
||||
.character-card {
|
||||
flex: 0 0 280px;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
scroll-snap-align: start;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.char-image-box, .va-image-box {
|
||||
width: 70px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.char-image-box img, .va-image-box img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
.char-info {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.char-name { font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||
.char-role { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.va-name { text-align: right; font-size: 0.8rem; color: var(--text-primary); }
|
||||
.va-lang { text-align: right; font-size: 0.7rem; color: var(--text-muted); }
|
||||
|
||||
/* Language Tabs for Characters */
|
||||
.char-lang-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.lang-tab {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lang-tab.active {
|
||||
background: var(--text-primary);
|
||||
color: #000;
|
||||
border-color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Related & Rec Cards (Horizontal Style) */
|
||||
.scroll-card {
|
||||
flex: 0 0 160px;
|
||||
scroll-snap-align: start;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.scroll-card-img {
|
||||
width: 100%;
|
||||
aspect-ratio: 2/3;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.scroll-card:hover .scroll-card-img { transform: scale(1.05); }
|
||||
.scroll-card-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.scroll-card-meta { font-size: 0.8rem; color: var(--text-secondary); }
|
||||
|
||||
/* =========================================
|
||||
7. RESPONSIVE MEDIA QUERIES
|
||||
========================================= */
|
||||
@media (min-width: 769px) {
|
||||
/* Show poster on Desktop */
|
||||
.hero-poster-container { display: block; }
|
||||
|
||||
/* Make relations/recs grid on desktop? Or keep scroll?
|
||||
Prompt said "Relations (scrollable horizontal list)" & "Recommendations (scrollable)" */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.series-hero-section { height: auto; min-height: auto; padding-top: 250px; background-position: center top; }
|
||||
.series-hero-content { display: block; }
|
||||
.series-title-text { font-size: 2rem; margin-top: 0; }
|
||||
.hero-actions { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.hero-watch-btn { grid-column: span 2; width: 100%; }
|
||||
.details-grid { grid-template-columns: 1fr; gap: 1rem; }
|
||||
}
|
||||
647
animex/Resources/styles.css
Normal file
@@ -0,0 +1,647 @@
|
||||
:root {
|
||||
--bg-dark: #1C1C1E;
|
||||
--bg-content-area: #1C1C1E;
|
||||
--bg-content-elements: #3A3A3C;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #EBEBF599;
|
||||
--text-tertiary: #8A8A8E;
|
||||
--accent-color-active: #FF9500;
|
||||
--border-color: #38383A;
|
||||
--button-bg: #4A4A4F;
|
||||
--button-secondary-bg: rgba(74, 74, 79, 0.7);
|
||||
|
||||
/* Desktop-specific variables */
|
||||
--sidebar-width: 280px;
|
||||
--content-max-width: 1400px;
|
||||
--desktop-padding: 40px;
|
||||
--card-hover-lift: translateY(-4px);
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* NEW: Consistent border radius variables */
|
||||
--radius-large: 16px;
|
||||
--radius-medium: 12px;
|
||||
--radius-small: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
DESKTOP STYLES (REWORKED & CONSOLIDATED)
|
||||
================================================================================
|
||||
*/
|
||||
@media (min-width: 768px) {
|
||||
/* === BASE LAYOUT === */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto; /* Scroll is on the main container now */
|
||||
background: var(--bg-content-area);
|
||||
}
|
||||
|
||||
/* Hide mobile bottom nav on desktop */
|
||||
.bottom-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* === DESKTOP SIDEBAR NAVIGATION === */
|
||||
.desktop-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, rgba(28, 28, 30, 0.95) 0%, rgba(28, 28, 30, 0.98) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--desktop-padding) 24px;
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.desktop-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(255, 149, 0, 0.3) 20%, rgba(255, 149, 0, 0.1) 50%, rgba(255, 149, 0, 0.3) 80%, transparent 100%);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.sidebar-brand .brand-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%);
|
||||
border-radius: var(--radius-small);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
.sidebar-brand .brand-name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #FFFFFF 0%, #E0E0E0 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--radius-medium);
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
transition: var(--transition-smooth);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-nav-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(255, 149, 0, 0.1) 0%, rgba(255, 149, 0, 0.05) 100%);
|
||||
opacity: 0;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.sidebar-nav-item:hover {
|
||||
color: var(--text-primary);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.sidebar-nav-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-nav-item.active {
|
||||
background: linear-gradient(135deg, rgba(255, 149, 0, 0.15) 0%, rgba(255, 149, 0, 0.08) 100%);
|
||||
color: var(--accent-color-active);
|
||||
border: 1px solid rgba(255, 149, 0, 0.2);
|
||||
}
|
||||
|
||||
.sidebar-nav-item.active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-nav-item .nav-icon, .sidebar-nav-item .nav-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sidebar-nav-item .nav-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* === HERO SECTION === */
|
||||
.hero-section {
|
||||
/* FIX: Use min-height to prevent content squishing on smaller viewports */
|
||||
min-height: 400px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding: var(--desktop-padding);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.1) 40%, rgba(0,0,0,0.7) 100%), linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.1) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: var(--content-max-width);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-bottom: 20px; /* Reduced padding slightly for balance */
|
||||
}
|
||||
|
||||
.hero-poster img {
|
||||
/* FIX: Increased size for more impact on desktop */
|
||||
width: 180px;
|
||||
height: 270px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-medium);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.hero-poster img:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.hero-details .title-logo {
|
||||
/* FIX: clamp() makes font size fluid and responsive */
|
||||
font-size: clamp(2.5rem, 4vw, 3.5rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: 16px;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hero-details .episode-info {
|
||||
font-size: 1rem;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-small);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
padding: 14px 28px;
|
||||
font-size: 1.1rem;
|
||||
border-radius: var(--radius-small);
|
||||
transition: var(--transition-smooth);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.continue-button:hover {
|
||||
background-color: rgba(255, 149, 0, 0.2);
|
||||
border-color: rgba(255, 149, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
/* === MAIN CONTENT BODY === */
|
||||
.main-content {
|
||||
/* This is the inner container that centers the content */
|
||||
padding: var(--desktop-padding);
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, #FFFFFF 0%, #E0E0E0 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* FIX: Reduced minmax() width to make the grid more responsive on smaller desktop screens */
|
||||
.item-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
background: rgba(58, 58, 60, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--radius-large);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-smooth);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 149, 0, 0.05) 0%, rgba(255, 149, 0, 0.02) 100%);
|
||||
opacity: 0;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: rgba(58, 58, 60, 0.6);
|
||||
transform: var(--card-hover-lift);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 0 0 1px rgba(255, 149, 0, 0.1);
|
||||
border-color: rgba(255, 149, 0, 0.2);
|
||||
}
|
||||
|
||||
.list-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.item-thumbnail img {
|
||||
/* FIX: Increased size and set aspect ratio for better visual weight */
|
||||
width: 90px;
|
||||
height: 135px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-medium);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: var(--transition-smooth);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-item:hover .item-thumbnail img {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
margin-top: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.meta-pill {
|
||||
background-color: rgba(58, 58, 60, 0.8);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.item-arrow .fa-chevron-right {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 1.1rem;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.list-item:hover .item-arrow .fa-chevron-right {
|
||||
color: var(--accent-color-active);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* === SEARCH BAR === */
|
||||
.search-bar-container {
|
||||
/* FIX: Center the search bar within the main content area */
|
||||
margin: 0 auto 32px auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(58, 58, 60, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius-large);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.search-bar-container:focus-within {
|
||||
border-color: rgba(255, 149, 0, 0.4);
|
||||
box-shadow: 0 0 0 4px rgba(255, 149, 0, 0.1);
|
||||
background: rgba(58, 58, 60, 0.6);
|
||||
}
|
||||
|
||||
.search-bar-container input[type="text"] {
|
||||
font-size: 1.1rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.search-bar-container .fa-search {
|
||||
font-size: 1.4rem;
|
||||
margin-left: 16px;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.search-bar-container:focus-within .fa-search {
|
||||
color: var(--accent-color-active);
|
||||
}
|
||||
|
||||
/* === SETTINGS PAGE === */
|
||||
.settings-group {
|
||||
margin-bottom: 40px;
|
||||
background: rgba(58, 58, 60, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--radius-large);
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.settings-user-profile {
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.settings-user-profile img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 2px solid rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
.settings-user-profile .username {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
padding: 20px 12px;
|
||||
font-size: 1.1rem;
|
||||
transition: var(--transition-smooth);
|
||||
border-radius: var(--radius-small);
|
||||
margin: 0 -12px;
|
||||
}
|
||||
|
||||
.settings-item:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(28px);
|
||||
}
|
||||
|
||||
/* === SCROLLBAR STYLING === */
|
||||
.desktop-main::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.desktop-main::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.desktop-main::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--bg-dark);
|
||||
}
|
||||
|
||||
.desktop-main::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
MOBILE STYLES (UNCHANGED)
|
||||
================================================================================
|
||||
*/
|
||||
@media (max-width: 767px) {
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.desktop-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Original mobile hero section */
|
||||
.hero-section {
|
||||
height: 300px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.7) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 15px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-poster img {
|
||||
width: 100px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.hero-details .title-logo {
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
background-color: var(--bg-content-area);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.6em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
background: rgba(58, 58, 60, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: rgba(58, 58, 60, 0.7);
|
||||
}
|
||||
|
||||
/* Mobile bottom navigation */
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background-color: var(--bg-dark);
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
flex-grow: 1;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-item .nav-icon {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--accent-color-active);
|
||||
}
|
||||
}
|
||||
111
animex/about.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>About The Developer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<style>
|
||||
:root {
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--accent-primary: #ff9500;
|
||||
--foreground-primary: #e6e6e6;
|
||||
--foreground-secondary: #a0a0a0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: linear-gradient(45deg, #101010, #1a1a1a);
|
||||
color: var(--foreground-primary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
max-width: 350px;
|
||||
width: 100%;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid var(--accent-primary);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 0 20px rgba(255, 149, 0, 0.4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--accent-primary);
|
||||
margin: 0 0 1.5rem 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--foreground-secondary);
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
color: var(--foreground-secondary);
|
||||
font-size: 1.5rem;
|
||||
transition: color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.social-links a:hover {
|
||||
color: var(--accent-primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="glass-card">
|
||||
<img src="https://images.unsplash.com/photo-1516298252535-cf2ac5147f9b?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Developer Avatar" class="avatar" />
|
||||
<h1>arkm</h1>
|
||||
<h2>Creator & Developer</h2>
|
||||
<p>
|
||||
Just a passionate developer building cool things.
|
||||
Thanks for using the app!
|
||||
</p>
|
||||
<div class="social-links">
|
||||
<a href="#" target="_blank" title="GitHub"><i class="fab fa-github"></i></a>
|
||||
<a href="#" target="_blank" title="Website"><i class="fas fa-globe"></i></a>
|
||||
<a href="#" target="_blank" title="Twitter / X"><i class="fab fa-twitter"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1390
animex/anime.html
Normal file
1
animex/config.ini
Normal file
@@ -0,0 +1 @@
|
||||
{"GOOGLE_API_KEY": "AIzaSyDGfMj_Hyaw2uJKKrz_nvsgtAMf3tHRS-I"}
|
||||
304
animex/content.json
Normal file
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"spotlight": [
|
||||
{
|
||||
"id": 52299,
|
||||
"name": "Solo Leveling",
|
||||
"image_tall": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=1680,height=2520/keyart/GDKHZEJ0K-backdrop_tall",
|
||||
"image": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=cover,format=auto,quality=85,width=3840/keyart/GDKHZEJ0K-backdrop_wide",
|
||||
"logo": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=600/keyart/GDKHZEJ0K-title_logo-en-us"
|
||||
},
|
||||
{
|
||||
"id": 57658,
|
||||
"name": "Jujutsu Kaisen: Culling Game Part 1",
|
||||
"image": "https://images4.alphacoders.com/138/1389939.jpg",
|
||||
"logo": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=600/keyart/GRDV0019R-title_logo-en-us"
|
||||
},
|
||||
{
|
||||
"id": 52991,
|
||||
"name": "Frieren: Beyond Journey's End",
|
||||
"image": "https://res.cloudinary.com/dsa5iolcx/image/upload/v1774835260/GG5H5XQX4-backdrop_wide.webp",
|
||||
"image_tall": "https://res.cloudinary.com/dsa5iolcx/image/upload/v1774835256/GG5H5XQX4-backdrop_tall.webp",
|
||||
"logo": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=600/keyart/GG5H5XQX4-title_logo-en-us"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "One Piece",
|
||||
"image": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=cover,format=auto,quality=85,width=1920/keyart/GRMG8ZQZR-backdrop_wide",
|
||||
"image_tall": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=1680,height=2520/keyart/GRMG8ZQZR-backdrop_tall",
|
||||
"logo": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=600/keyart/GRMG8ZQZR-title_logo-en-us"
|
||||
},
|
||||
{
|
||||
"id": 52588,
|
||||
"name": "Kaiju No. 8",
|
||||
"image": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=cover,format=auto,quality=85,width=3840/keyart/GG5H5XQ7D-backdrop_wide",
|
||||
"logo": "https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=600/keyart/GG5H5XQ7D-title_logo-en-us"
|
||||
}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"title": "Fresh Out of Japan",
|
||||
"items": [
|
||||
{
|
||||
"id": 57658,
|
||||
"name": "Jujutsu Kaisen Season 3: The Culling Game",
|
||||
"image": "https://myanimelist.net/images/anime/1659/154920l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 59978,
|
||||
"name": "Frieren: Beyond Journey's End Season 2",
|
||||
"image": "https://myanimelist.net/images/anime/1921/154528l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 51553,
|
||||
"name": "Witch Hat Atelier",
|
||||
"image": "https://myanimelist.net/images/anime/1726/155542l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 58012,
|
||||
"name": "[Oshi No Ko] Season 3",
|
||||
"image": "https://myanimelist.net/images/anime/1873/141269l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 61469,
|
||||
"name": "JoJo's Bizarre Adventure: Steel Ball Run",
|
||||
"image": "https://myanimelist.net/images/anime/1448/154111l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 55825,
|
||||
"name": "Hell's Paradise Season 2",
|
||||
"image": "https://myanimelist.net/images/anime/1772/154456l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 56023,
|
||||
"name": "Re:ZERO -Starting Life in Another World- Season 4",
|
||||
"image": "https://myanimelist.net/images/anime/1275/137252l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 57034,
|
||||
"name": "Daemons of the Shadow Realm",
|
||||
"image": "https://myanimelist.net/images/anime/1880/139404l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "One Piece: Elbaph Arc",
|
||||
"image": "https://myanimelist.net/images/anime/1244/138851l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 57592,
|
||||
"name": "Dr. STONE: SCIENCE FUTURE",
|
||||
"image": "https://myanimelist.net/images/anime/1403/146479l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Award Winners",
|
||||
"items": [
|
||||
{
|
||||
"id": 51009,
|
||||
"name": "Jujutsu Kaisen",
|
||||
"image": "https://myanimelist.net/images/anime/1792/138022l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 52034,
|
||||
"name": "[Oshi No Ko]",
|
||||
"image": "https://myanimelist.net/images/anime/1812/134736l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 49387,
|
||||
"name": "Vinland Saga Season 2",
|
||||
"image": "https://myanimelist.net/images/anime/1170/124312l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 50602,
|
||||
"name": "Spy x Family",
|
||||
"image": "https://myanimelist.net/images/anime/1111/127508l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 41467,
|
||||
"name": "Bleach: Thousand-Year Blood War",
|
||||
"image": "https://myanimelist.net/images/anime/1908/135431l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 44511,
|
||||
"name": "Chainsaw Man",
|
||||
"image": "https://myanimelist.net/images/anime/1806/126216l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 51019,
|
||||
"name": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"image": "https://myanimelist.net/images/anime/1765/135099l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 40834,
|
||||
"name": "Ranking of Kings",
|
||||
"image": "https://myanimelist.net/images/anime/1347/117616l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 52299,
|
||||
"name": "Solo Leveling",
|
||||
"image": "https://myanimelist.net/images/anime/1801/142390l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 52991,
|
||||
"name": "Frieren: Beyond Journey's End",
|
||||
"image": "https://myanimelist.net/images/anime/1015/138006l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Global Fan Favorites",
|
||||
"items": [
|
||||
{
|
||||
"id": 38000,
|
||||
"name": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"image": "https://myanimelist.net/images/anime/1286/99889l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 40748,
|
||||
"name": "Jujutsu Kaisen",
|
||||
"image": "https://myanimelist.net/images/anime/1171/109222l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 31964,
|
||||
"name": "My Hero Academia",
|
||||
"image": "https://myanimelist.net/images/anime/10/78745l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 16498,
|
||||
"name": "Attack on Titan",
|
||||
"image": "https://myanimelist.net/images/anime/10/47347l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "One Piece",
|
||||
"image": "https://myanimelist.net/images/anime/1244/138851l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 50265,
|
||||
"name": "Spy x Family",
|
||||
"image": "https://myanimelist.net/images/anime/1441/122795l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 44511,
|
||||
"name": "Chainsaw Man",
|
||||
"image": "https://myanimelist.net/images/anime/1806/126216l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 269,
|
||||
"name": "Bleach",
|
||||
"image": "https://myanimelist.net/images/anime/1541/147774l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 52299,
|
||||
"name": "Solo Leveling",
|
||||
"image": "https://myanimelist.net/images/anime/1801/142390l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "Naruto",
|
||||
"image": "https://myanimelist.net/images/anime/1141/142503l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Gateway to Anime",
|
||||
"items": [
|
||||
{
|
||||
"id": 31964,
|
||||
"name": "My Hero Academia",
|
||||
"image": "https://myanimelist.net/images/anime/10/78745l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1535,
|
||||
"name": "Death Note",
|
||||
"image": "https://myanimelist.net/images/anime/1079/138100l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 16498,
|
||||
"name": "Attack on Titan",
|
||||
"image": "https://myanimelist.net/images/anime/10/47347l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 38000,
|
||||
"name": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"image": "https://myanimelist.net/images/anime/1286/99889l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 32281,
|
||||
"name": "Your Name.",
|
||||
"image": "https://myanimelist.net/images/anime/5/87048l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 527,
|
||||
"name": "Pokémon",
|
||||
"image": "https://myanimelist.net/images/anime/1787/140239l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 813,
|
||||
"name": "Dragon Ball Z",
|
||||
"image": "https://myanimelist.net/images/anime/1277/142022l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "Naruto",
|
||||
"image": "https://myanimelist.net/images/anime/1141/142503l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Action-Packed",
|
||||
"items": [
|
||||
{
|
||||
"id": 40748,
|
||||
"name": "Jujutsu Kaisen",
|
||||
"image": "https://myanimelist.net/images/anime/1171/109222l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 38000,
|
||||
"name": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"image": "https://myanimelist.net/images/anime/1286/99889l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 31964,
|
||||
"name": "My Hero Academia",
|
||||
"image": "https://myanimelist.net/images/anime/10/78745l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 16498,
|
||||
"name": "Attack on Titan",
|
||||
"image": "https://myanimelist.net/images/anime/10/47347l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 44511,
|
||||
"name": "Chainsaw Man",
|
||||
"image": "https://myanimelist.net/images/anime/1806/126216l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 50265,
|
||||
"name": "Spy x Family",
|
||||
"image": "https://myanimelist.net/images/anime/1441/122795l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 459,
|
||||
"name": "One Piece: The Movie",
|
||||
"image": "https://myanimelist.net/images/anime/1770/97704l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 269,
|
||||
"name": "Bleach",
|
||||
"image": "https://myanimelist.net/images/anime/1541/147774l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 38691,
|
||||
"name": "Dr. Stone",
|
||||
"image": "https://myanimelist.net/images/anime/1613/102576l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 58567,
|
||||
"name": "Solo Leveling Season 2: Arise from the Shadow",
|
||||
"image": "https://myanimelist.net/images/anime/1448/147351l.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
201
animex/cover.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import re
|
||||
|
||||
def normalize_string(s):
|
||||
"""Removes spaces, punctuation, and lowercases the string for robust comparison."""
|
||||
if not s:
|
||||
return ""
|
||||
return re.sub(r'[^a-z0-9]', '', str(s).lower())
|
||||
|
||||
def is_name_match(json_name, api_data):
|
||||
"""Checks if the JSON name matches any of the anime's titles from MAL."""
|
||||
norm_json = normalize_string(json_name)
|
||||
|
||||
# Gather all possible titles from the API response
|
||||
titles =[]
|
||||
if api_data.get("title"): titles.append(api_data["title"])
|
||||
if api_data.get("title_english"): titles.append(api_data["title_english"])
|
||||
if api_data.get("title_japanese"): titles.append(api_data["title_japanese"])
|
||||
|
||||
# Jikan v4 also provides a 'titles' array
|
||||
for t_obj in api_data.get("titles",[]):
|
||||
if "title" in t_obj:
|
||||
titles.append(t_obj["title"])
|
||||
|
||||
for syn in api_data.get("title_synonyms",[]):
|
||||
titles.append(syn)
|
||||
|
||||
# Check for a match
|
||||
for t in titles:
|
||||
norm_t = normalize_string(t)
|
||||
# Using "in" allows us to match variations like "Spy x Family Part 2" with "Spy x Family Cour 2"
|
||||
# as long as the base string closely aligns, but it leans toward exact matches.
|
||||
if norm_json == norm_t or norm_json in norm_t or norm_t in norm_json:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_anime_by_id(mal_id, retries=5):
|
||||
"""Fetches anime details by MAL ID with retry and rate-limit handling."""
|
||||
url = f"https://api.jikan.moe/v4/anime/{mal_id}"
|
||||
attempt = 0
|
||||
while attempt < retries:
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return response.json().get("data")
|
||||
elif response.status_code == 404:
|
||||
return None # ID does not exist
|
||||
elif response.status_code == 429:
|
||||
print(" [!] Rate limited. Waiting 1 second...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
else:
|
||||
print(f" [!] HTTP Error {response.status_code}. Retrying...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
except requests.exceptions.RequestException:
|
||||
print(" [!] Network error. Retrying...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
return None
|
||||
|
||||
def search_anime(query, retries=5):
|
||||
"""Searches Jikan for the anime by name. Sorts by 'members' (popularity)."""
|
||||
url = "https://api.jikan.moe/v4/anime"
|
||||
params = {
|
||||
"q": query,
|
||||
"limit": 7
|
||||
}
|
||||
|
||||
attempt = 0
|
||||
while attempt < retries:
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return response.json().get("data",[])
|
||||
elif response.status_code == 429:
|
||||
print(" [!] Rate limited. Waiting 1 second...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
else:
|
||||
print(f" [!] HTTP Error {response.status_code}. Retrying...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
except requests.exceptions.RequestException:
|
||||
print(" [!] Network error. Retrying...")
|
||||
time.sleep(1)
|
||||
attempt += 1
|
||||
|
||||
return[]
|
||||
|
||||
def prompt_user_choice(anime_name, current_id, results):
|
||||
"""Displays the search results and asks the user to pick the correct one."""
|
||||
print(f"\n==================================================")
|
||||
print(f"🔍 Searching for manually: {anime_name} (Current ID: {current_id})")
|
||||
print(f"==================================================")
|
||||
|
||||
if not results:
|
||||
print(" [!] No results found on MyAnimeList.")
|
||||
return None
|
||||
|
||||
for i, res in enumerate(results):
|
||||
title = res.get("title_english") or res.get("title")
|
||||
media_type = res.get("type", "Unknown")
|
||||
year = res.get("year", "N/A")
|
||||
mal_id = res["mal_id"]
|
||||
|
||||
print(f" [{i + 1}] {title} ({media_type}, {year}) - ID: {mal_id}")
|
||||
|
||||
print(" [0] Skip / Keep current")
|
||||
print(" [9] Enter a custom MAL ID manually")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input("\nSelect the correct anime (0-5, or 9): ").strip()
|
||||
if choice == "":
|
||||
continue
|
||||
|
||||
choice = int(choice)
|
||||
|
||||
if choice == 0:
|
||||
print(" -> Skipping. Kept original data.")
|
||||
return None
|
||||
elif choice == 9:
|
||||
custom_id = int(input(" -> Enter custom MAL ID: ").strip())
|
||||
return get_anime_by_id(custom_id)
|
||||
elif 1 <= choice <= len(results):
|
||||
return results[choice - 1]
|
||||
else:
|
||||
print(" [!] Invalid choice. Please enter a number from the list.")
|
||||
except ValueError:
|
||||
print(" [!] Please enter a valid number.")
|
||||
|
||||
def main():
|
||||
file_path = "content.json"
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Could not find '{file_path}'.")
|
||||
return
|
||||
|
||||
for section in data.get("sections", []):
|
||||
for item in section.get("items",[]):
|
||||
name = item.get("name")
|
||||
current_id = item.get("id")
|
||||
current_image = item.get("image")
|
||||
|
||||
if not name or not current_id:
|
||||
continue
|
||||
|
||||
print(f"\nChecking: {name} (ID: {current_id})")
|
||||
|
||||
# 1. Fetch data for the current ID
|
||||
api_data = get_anime_by_id(current_id)
|
||||
time.sleep(0.4) # Respect Jikan's 3 requests/second rate limit
|
||||
|
||||
# 2. Check if ID matches Name
|
||||
if api_data and is_name_match(name, api_data):
|
||||
# Name matches! Now verify the image
|
||||
images = api_data.get("images", {}).get("jpg", {})
|
||||
best_image = images.get("large_image_url") or images.get("image_url")
|
||||
|
||||
if current_image == best_image:
|
||||
print(" ✅ ID and Image are both correct. Skipping.")
|
||||
else:
|
||||
print(" ⚠️ ID is correct, but Image is outdated. Updating image automatically...")
|
||||
if best_image:
|
||||
item["image"] = best_image
|
||||
print(f" ✅ Updated Image: {best_image}")
|
||||
else:
|
||||
# Name does not match or ID is invalid. Prompt user.
|
||||
print(" ❌ ID does NOT match the Name (or ID is invalid). Searching MAL...")
|
||||
|
||||
results = search_anime(name)
|
||||
time.sleep(0.4)
|
||||
|
||||
selected_anime = prompt_user_choice(name, current_id, results)
|
||||
|
||||
if selected_anime:
|
||||
new_id = selected_anime["mal_id"]
|
||||
images = selected_anime.get("images", {}).get("jpg", {})
|
||||
new_image_url = images.get("large_image_url") or images.get("image_url")
|
||||
|
||||
item["id"] = new_id
|
||||
if new_image_url:
|
||||
item["image"] = new_image_url
|
||||
|
||||
print(f" ✅ Updated '{name}' -> ID: {new_id} | Image: {new_image_url}")
|
||||
|
||||
# Save the updated JSON back to the file
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
json.dump(data, file, indent=4, ensure_ascii=False)
|
||||
|
||||
print("\n🎉 Done! All selected IDs and Images have been verified/updated and saved to 'content.json'.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
457
animex/down.html
Normal file
@@ -0,0 +1,457 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Download Anime Series</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #FF9500;
|
||||
--primary-dark: #E68600;
|
||||
--primary-light: #FFB84D;
|
||||
--bg-dark: #0F0F0F;
|
||||
--bg-secondary: #1A1A1A;
|
||||
--bg-tertiary: #252525;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #B3B3B3;
|
||||
--text-muted: #808080;
|
||||
--border-color: #333333;
|
||||
--border-light: #404040;
|
||||
--success-color: #10B981;
|
||||
--error-color: #FF9500;
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #0F0F0F 0%, #1A1A1A 100%);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 2.5rem;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
|
||||
}
|
||||
|
||||
h1 {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.series-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
background: var(--bg-secondary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.series-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.series-list::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.series-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.series-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.series-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.series-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
.series-item.selected {
|
||||
background: linear-gradient(135deg, rgba(255, 149, 0, 0.15), rgba(255, 149, 0, 0.08));
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* Custom Checkbox */
|
||||
.custom-checkbox {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"] {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-visual {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkbox-visual::after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 2px solid var(--text-primary);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
transform: rotate(-45deg) scale(0);
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"]:checked + .checkbox-visual {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.2);
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"]:checked + .checkbox-visual::after {
|
||||
transform: rotate(-45deg) scale(1);
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
|
||||
.custom-checkbox:hover .checkbox-visual {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.1);
|
||||
}
|
||||
|
||||
.series-item label {
|
||||
flex-grow: 1;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.series-item:hover label {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
padding: 1rem 2.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 8px 25px rgba(255, 149, 0, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 35px rgba(255, 149, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: linear-gradient(135deg, var(--text-muted), #666);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn:disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 4px solid var(--border-color);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
box-shadow: 0 0 20px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
color: var(--text-primary);
|
||||
margin-top: 24px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-color);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.series-list {
|
||||
max-height: 35vh;
|
||||
}
|
||||
|
||||
.series-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.875rem 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-overlay">
|
||||
<div class="loader"></div>
|
||||
<p id="loading-text">Generating your files...</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>Select Anime to Download</h1>
|
||||
<form id="downloadForm">
|
||||
<ul id="seriesList" class="series-list"></ul>
|
||||
<div class="button-container">
|
||||
<button type="submit" class="btn" id="generateBtn" disabled>Generate Zip</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const seriesList = document.getElementById('seriesList');
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
const loadingOverlay = document.getElementById('loading-overlay');
|
||||
const apiBaseUrl = ''; // API is at the same origin
|
||||
|
||||
// Get the token from the URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
seriesList.innerHTML = '<li class="series-item" style="justify-content: center;"><span class="error-message">Authentication token not found in URL.</span></li>';
|
||||
generateBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the token from the URL for the Bearer authentication header
|
||||
const response = await fetch(`${apiBaseUrl}/users/me`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user data. Your session may have expired.');
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
const history = userData.watch_history_detailed;
|
||||
const uniqueSeries = [...new Set(Object.values(history).map(show => show.title))].sort();
|
||||
|
||||
if (uniqueSeries.length === 0) {
|
||||
seriesList.innerHTML = '<li class="series-item" style="justify-content: center;"><span class="empty-state">No watched series found.</span></li>';
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueSeries.forEach(title => {
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = 'series-item';
|
||||
listItem.innerHTML = `
|
||||
<div class="custom-checkbox">
|
||||
<input type="checkbox" id="${title}" name="series" value="${title}">
|
||||
<div class="checkbox-visual"></div>
|
||||
</div>
|
||||
<label for="${title}">${title}</label>
|
||||
`;
|
||||
|
||||
// Add click handler for the entire item
|
||||
listItem.addEventListener('click', (e) => {
|
||||
if (e.target.tagName !== 'INPUT') {
|
||||
const checkbox = listItem.querySelector('input[type="checkbox"]');
|
||||
checkbox.checked = !checkbox.checked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
|
||||
// Add change handler for checkbox
|
||||
const checkbox = listItem.querySelector('input[type="checkbox"]');
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
listItem.classList.add('selected');
|
||||
} else {
|
||||
listItem.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
seriesList.appendChild(listItem);
|
||||
});
|
||||
|
||||
generateBtn.disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
seriesList.innerHTML = `<li class="series-item" style="justify-content: center;"><span class="error-message">${error.message}</span></li>`;
|
||||
}
|
||||
|
||||
document.getElementById('downloadForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
loadingOverlay.style.display = 'flex';
|
||||
|
||||
const selectedSeries = Array.from(document.querySelectorAll('input[name="series"]:checked')).map(cb => cb.value);
|
||||
|
||||
if (selectedSeries.length === 0) {
|
||||
window.parent.showToast('Please select at least one series.');
|
||||
loadingOverlay.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const queryString = new URLSearchParams({ series_titles: selectedSeries.join(',') }).toString();
|
||||
|
||||
// Construct the download URL, passing the token as a query parameter
|
||||
const downloadUrl = `${apiBaseUrl}/generate-zip?${queryString}&token=${token}`;
|
||||
|
||||
window.open(downloadUrl, '_blank');
|
||||
|
||||
setTimeout(() => {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
579
animex/get.py
Normal file
@@ -0,0 +1,579 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import collections
|
||||
import getpass
|
||||
import re
|
||||
|
||||
# --- Dependency Check and Setup ---
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
except ImportError:
|
||||
print("Error: The 'google-generativeai' library is not installed.")
|
||||
print("Please install it by running: pip install google-generativeai")
|
||||
exit()
|
||||
|
||||
# --- Configuration ---
|
||||
JIKAN_API_BASE_URL = "https://api.jikan.moe/v4"
|
||||
CONTENT_FILE = "content.json"
|
||||
CONFIG_FILE = "config.ini"
|
||||
|
||||
# Jikan rate limiting
|
||||
REQUEST_TIMESTAMPS = collections.deque()
|
||||
REQUEST_LIMIT = 3 # 3 requests per second
|
||||
TIME_WINDOW = 1 # 1 second
|
||||
|
||||
# --- API Key and Configuration Management ---
|
||||
def get_api_key():
|
||||
"""Gets the Google AI API key, prompting the user if not found."""
|
||||
# Check environment variable first
|
||||
api_key = os.getenv("GOOGLE_API_KEY")
|
||||
if api_key:
|
||||
print("Loaded Google API Key from environment variable.")
|
||||
input("Press Enter to continue...")
|
||||
return api_key
|
||||
|
||||
# Check config file next
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
config = json.load(f)
|
||||
api_key = config.get("GOOGLE_API_KEY")
|
||||
if api_key:
|
||||
print("Loaded Google API Key from config.ini.")
|
||||
input("Press Enter to continue...")
|
||||
return api_key
|
||||
|
||||
# If not found, prompt the user
|
||||
print("\n--- Google AI API Key Required ---")
|
||||
print("To use the AI generation feature, you need a Google AI API Key.")
|
||||
print("You can get a free key from Google AI Studio.")
|
||||
print("The key will be stored locally in 'config.ini' so you don't have to enter it again.")
|
||||
|
||||
api_key = getpass.getpass("Please enter your Google AI API Key: ")
|
||||
|
||||
# Save the key to config.ini for future use
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump({"GOOGLE_API_KEY": api_key}, f)
|
||||
|
||||
print("API Key saved to config.ini.")
|
||||
return api_key
|
||||
|
||||
|
||||
# --- Jikan API Interaction with Rate Limiting ---
|
||||
def jikan_api_request(endpoint, params=None):
|
||||
"""
|
||||
Makes a rate-limited request to the Jikan API.
|
||||
Waits if the request limit has been reached in the last second.
|
||||
"""
|
||||
global REQUEST_TIMESTAMPS
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
# Remove timestamps older than the time window
|
||||
while REQUEST_TIMESTAMPS and REQUEST_TIMESTAMPS[0] < now - TIME_WINDOW:
|
||||
REQUEST_TIMESTAMPS.popleft()
|
||||
|
||||
if len(REQUEST_TIMESTAMPS) < REQUEST_LIMIT:
|
||||
break
|
||||
|
||||
# Calculate sleep time to respect the rate limit
|
||||
sleep_time = (REQUEST_TIMESTAMPS[0] + TIME_WINDOW) - now + 0.05 # small buffer
|
||||
print(f"Jikan rate limit reached. Waiting for {sleep_time:.2f} seconds...")
|
||||
time.sleep(sleep_time)
|
||||
|
||||
try:
|
||||
REQUEST_TIMESTAMPS.append(time.time())
|
||||
print(f"Making Jikan request to: {JIKAN_API_BASE_URL}{endpoint}")
|
||||
response = requests.get(f"{JIKAN_API_BASE_URL}{endpoint}", params=params)
|
||||
response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"\n--- Jikan API Error --- \n{e}\n------------------")
|
||||
return None
|
||||
|
||||
# --- Helper Functions ---
|
||||
def clear_screen():
|
||||
"""Clears the console screen."""
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def get_choice(max_choice, allow_back=True):
|
||||
"""Gets and validates a user's integer choice."""
|
||||
while True:
|
||||
try:
|
||||
prompt = "> "
|
||||
choice = input(prompt)
|
||||
if allow_back and choice.lower() == 'b':
|
||||
return 'b'
|
||||
choice = int(choice)
|
||||
if 1 <= choice <= max_choice:
|
||||
return choice
|
||||
else:
|
||||
print(f"Invalid choice. Please enter a number between 1 and {max_choice}.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
def format_anime_data(anime_obj):
|
||||
"""Formats Jikan anime data into the structure needed for content.json."""
|
||||
return {
|
||||
"id": anime_obj['mal_id'],
|
||||
"name": anime_obj.get('title_english') or anime_obj.get('title'),
|
||||
"image": anime_obj['images']['jpg']['large_image_url']
|
||||
}
|
||||
|
||||
def search_and_select_anime():
|
||||
"""Prompts user to search for an anime, displays results, and returns the selected one."""
|
||||
query = input("Enter search term (or 'b' to go back): ")
|
||||
if query.lower() == 'b':
|
||||
return None
|
||||
|
||||
results = jikan_api_request("/anime", params={"q": query, "limit": 10})
|
||||
if not results or not results.get('data'):
|
||||
print("No results found.")
|
||||
input("Press Enter to continue...")
|
||||
return None
|
||||
|
||||
clear_screen()
|
||||
print(f"--- Search Results for '{query}' ---")
|
||||
for i, anime in enumerate(results['data'], 1):
|
||||
print(f"[{i}] {anime.get('title_english') or anime.get('title')} ({anime.get('type', 'N/A')}, {anime.get('year', 'N/A')})")
|
||||
|
||||
print("\n[b] Back to previous menu")
|
||||
|
||||
print("\nSelect an anime to add:")
|
||||
choice = get_choice(len(results['data']))
|
||||
if choice == 'b':
|
||||
return None
|
||||
|
||||
return results['data'][choice - 1]
|
||||
|
||||
# --- Management Logic ---
|
||||
def manage_spotlight(data):
|
||||
"""Handles logic for managing the spotlight section."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Manage Spotlight Section ---")
|
||||
if not data['spotlight']:
|
||||
print("Spotlight is currently empty.")
|
||||
else:
|
||||
for i, item in enumerate(data['spotlight'], 1):
|
||||
print(f"[{i}] {item['name']} (ID: {item['id']})")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Add an anime to Spotlight")
|
||||
print("[2] Remove an anime from Spotlight")
|
||||
print("[b] Back to Main Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
anime_obj = search_and_select_anime()
|
||||
if anime_obj:
|
||||
if any(item['id'] == anime_obj['mal_id'] for item in data['spotlight']):
|
||||
print(f"'{anime_obj['title']}' is already in the spotlight.")
|
||||
else:
|
||||
formatted = format_anime_data(anime_obj)
|
||||
data['spotlight'].append(formatted)
|
||||
print(f"Added '{formatted['name']}' to spotlight.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
elif choice == '2':
|
||||
if not data['spotlight']:
|
||||
print("Nothing to remove.")
|
||||
input("Press Enter to continue...")
|
||||
continue
|
||||
print("Enter the number of the anime to remove (or 'b' to cancel):")
|
||||
remove_choice = get_choice(len(data['spotlight']))
|
||||
if remove_choice != 'b':
|
||||
removed = data['spotlight'].pop(remove_choice - 1)
|
||||
print(f"Removed '{removed['name']}' from spotlight.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def manage_sections(data):
|
||||
"""Handles logic for managing horizontal sections."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Manage Horizontal Sections ---")
|
||||
if not data['sections']:
|
||||
print("No sections created yet.")
|
||||
else:
|
||||
for i, section in enumerate(data['sections'], 1):
|
||||
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Create a new section")
|
||||
print("[2] Edit an existing section")
|
||||
print("[3] Delete a section")
|
||||
print("[b] Back to Main Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
title = input("Enter title for new section: ")
|
||||
data['sections'].append({"title": title, "items": []})
|
||||
print(f"Section '{title}' created.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '2':
|
||||
if not data['sections']:
|
||||
print("No sections to edit.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
for i, section in enumerate(data['sections'], 1):
|
||||
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
|
||||
print("Select a section to edit:")
|
||||
edit_choice = get_choice(len(data['sections']))
|
||||
if edit_choice != 'b':
|
||||
edit_section_menu(data['sections'][edit_choice - 1])
|
||||
|
||||
elif choice == '3':
|
||||
if not data['sections']:
|
||||
print("No sections to delete.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
print("Select a section to delete:")
|
||||
delete_choice = get_choice(len(data['sections']))
|
||||
if delete_choice != 'b':
|
||||
removed = data['sections'].pop(delete_choice - 1)
|
||||
print(f"Deleted section '{removed['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def edit_section_menu(section):
|
||||
"""Menu for editing a specific section."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print(f"--- Editing Section: {section['title']} ---")
|
||||
if not section['items']:
|
||||
print("This section is empty.")
|
||||
else:
|
||||
for i, item in enumerate(section['items'], 1):
|
||||
print(f" [{i}] {item['name']} (ID: {item['id']})")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Add an anime to this section (Manual Search)")
|
||||
print("[2] Remove an anime from this section")
|
||||
print("[3] Auto-populate this section (from Jikan)")
|
||||
print("[4] Generate content with AI")
|
||||
print("[5] Rename this section")
|
||||
print("[b] Back to Sections Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
anime_obj = search_and_select_anime()
|
||||
if anime_obj:
|
||||
if any(item['id'] == anime_obj['mal_id'] for item in section['items']):
|
||||
print(f"'{anime_obj['title']}' is already in this section.")
|
||||
else:
|
||||
formatted = format_anime_data(anime_obj)
|
||||
section['items'].append(formatted)
|
||||
print(f"Added '{formatted['name']}' to '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '2':
|
||||
if not section['items']:
|
||||
print("Nothing to remove.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
print("Enter the number of the anime to remove:")
|
||||
remove_choice = get_choice(len(section['items']))
|
||||
if remove_choice != 'b':
|
||||
removed = section['items'].pop(remove_choice - 1)
|
||||
print(f"Removed '{removed['name']}' from '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '3':
|
||||
auto_populate_section(section)
|
||||
|
||||
elif choice == '4':
|
||||
generate_with_ai(section)
|
||||
|
||||
elif choice == '5':
|
||||
new_title = input(f"Enter new title for '{section['title']}': ")
|
||||
section['title'] = new_title
|
||||
print("Section renamed.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def auto_populate_section(section):
|
||||
"""Automatically populates a section from a Jikan endpoint."""
|
||||
clear_screen()
|
||||
print(f"--- Auto-Populate Section: {section['title']} ---")
|
||||
print("Select a category to populate from:")
|
||||
print("[1] Top Anime by Popularity")
|
||||
print("[2] Upcoming Season")
|
||||
print("[3] Top Airing Anime")
|
||||
print("[b] Cancel")
|
||||
|
||||
choice = get_choice(3)
|
||||
if choice == 'b':
|
||||
return
|
||||
|
||||
endpoint_map = {
|
||||
1: ("/top/anime", {"filter": "bypopularity", "limit": 15}),
|
||||
2: ("/seasons/upcoming", {"limit": 15}),
|
||||
3: ("/top/anime", {"filter": "airing", "limit": 15})
|
||||
}
|
||||
endpoint, params = endpoint_map[choice]
|
||||
|
||||
results = jikan_api_request(endpoint, params=params)
|
||||
if not results or not results.get('data'):
|
||||
print("Could not fetch data for this category.")
|
||||
input("Press Enter...")
|
||||
return
|
||||
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for anime_obj in results['data']:
|
||||
if not any(item['id'] == anime_obj['mal_id'] for item in section['items']):
|
||||
section['items'].append(format_anime_data(anime_obj))
|
||||
added_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
print(f"Added {added_count} new items and skipped {skipped_count} duplicates in '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
def generate_with_ai(section):
|
||||
"""Generates content for a section using Google's Generative AI."""
|
||||
clear_screen()
|
||||
print(f"--- AI Content Generation for: {section['title']} ---")
|
||||
|
||||
# 1. Get and configure API Key
|
||||
try:
|
||||
api_key = get_api_key()
|
||||
genai.configure(api_key=api_key)
|
||||
model = genai.GenerativeModel('gemini-1.5-flash')
|
||||
except Exception as e:
|
||||
print(f"An error occurred while configuring the AI model: {e}")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
# 2. Get user prompt
|
||||
print("Describe the kind of anime you want to find.")
|
||||
print("Examples: 'top 10 classic sci-fi anime', 'underrated shows with great world-building', 'anime for beginners'")
|
||||
user_prompt = input("\nEnter your prompt: ")
|
||||
if not user_prompt:
|
||||
return
|
||||
|
||||
# 3. Query the AI model with improved prompt
|
||||
print("\nAsking the AI for suggestions... this may take a moment.")
|
||||
full_prompt = f"""List exactly 10 anime that fit this description: '{user_prompt}'.
|
||||
|
||||
IMPORTANT: Follow this exact format for your response:
|
||||
- Return ONLY the anime titles
|
||||
- One title per line
|
||||
- No numbers, bullets, dashes, or prefixes
|
||||
- No descriptions or explanations
|
||||
- No extra text before or after the list
|
||||
- Use the most commonly known English or romanized title
|
||||
|
||||
Example format:
|
||||
Attack on Titan
|
||||
Death Note
|
||||
Spirited Away
|
||||
|
||||
Your response for '{user_prompt}':"""
|
||||
|
||||
try:
|
||||
response = model.generate_content(full_prompt)
|
||||
if not response.text:
|
||||
print("The AI returned an empty response. Please try again.")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
# Clean and parse the response more robustly
|
||||
ai_suggestions = []
|
||||
lines = response.text.strip().split('\n')
|
||||
|
||||
for line in lines:
|
||||
# Clean each line of common prefixes and formatting
|
||||
cleaned = line.strip()
|
||||
# Remove common prefixes like "1.", "- ", "• ", etc.
|
||||
cleaned = re.sub(r'^[\d\.\-\•\*\s]+', '', cleaned)
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
if cleaned and len(cleaned) > 1: # Ensure it's not just whitespace or single character
|
||||
ai_suggestions.append(cleaned)
|
||||
|
||||
# Limit to 10 suggestions max
|
||||
ai_suggestions = ai_suggestions[:10]
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while communicating with the AI: {e}")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
if not ai_suggestions:
|
||||
print("The AI didn't return any valid suggestions. Try a different prompt.")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
print(f"\nAI suggested {len(ai_suggestions)} anime titles.")
|
||||
|
||||
# 4. Process suggestions: Search Jikan and get user confirmation
|
||||
print("\n--- Confirm AI Suggestions ---")
|
||||
print("For each suggestion, I will find the closest match on MyAnimeList.")
|
||||
print("Please confirm if the match is correct.")
|
||||
|
||||
confirmed_anime = []
|
||||
for i, suggestion in enumerate(ai_suggestions, 1):
|
||||
print(f"\n[{i}/{len(ai_suggestions)}] Searching for: '{suggestion}'...")
|
||||
results = jikan_api_request("/anime", params={"q": suggestion, "limit": 3})
|
||||
|
||||
if not results or not results.get('data'):
|
||||
print(f"--> Could not find any match for '{suggestion}'.")
|
||||
continue
|
||||
|
||||
# Show top match but also alternatives if the first doesn't seem right
|
||||
match = results['data'][0]
|
||||
title = match.get('title_english') or match.get('title')
|
||||
|
||||
print(f"--> Best match: '{title}' ({match.get('type', 'N/A')}, {match.get('year', 'N/A')})")
|
||||
|
||||
# Show alternatives if available
|
||||
if len(results['data']) > 1:
|
||||
print(" Alternatives:")
|
||||
for j, alt in enumerate(results['data'][1:3], 2):
|
||||
alt_title = alt.get('title_english') or alt.get('title')
|
||||
print(f" [{j}] {alt_title} ({alt.get('type', 'N/A')}, {alt.get('year', 'N/A')})")
|
||||
|
||||
while True:
|
||||
if len(results['data']) > 1:
|
||||
choice = input(" Choose: [1] Use best match, [2-3] Use alternative, [s] Skip, [Enter] Use best match: ").strip().lower()
|
||||
else:
|
||||
choice = input(" [Enter] Add this anime, [s] Skip: ").strip().lower()
|
||||
|
||||
if choice == '' or choice == '1':
|
||||
selected_match = results['data'][0]
|
||||
break
|
||||
elif choice == 's':
|
||||
selected_match = None
|
||||
break
|
||||
elif choice in ['2', '3'] and len(results['data']) > int(choice) - 1:
|
||||
selected_match = results['data'][int(choice) - 1]
|
||||
break
|
||||
else:
|
||||
print(" Invalid choice. Please try again.")
|
||||
|
||||
if selected_match:
|
||||
formatted = format_anime_data(selected_match)
|
||||
# Avoid adding duplicates
|
||||
if any(a['id'] == formatted['id'] for a in confirmed_anime):
|
||||
print(f"--> Already added '{formatted['name']}'. Skipping.")
|
||||
else:
|
||||
confirmed_anime.append(formatted)
|
||||
print(f"--> Added '{formatted['name']}' to the list.")
|
||||
else:
|
||||
print(f"--> Skipped '{suggestion}'.")
|
||||
|
||||
# 5. Final review and add to section
|
||||
if not confirmed_anime:
|
||||
print("\nNo new anime were confirmed. Returning to menu.")
|
||||
input("Press Enter...")
|
||||
return
|
||||
|
||||
clear_screen()
|
||||
print("--- Final Review ---")
|
||||
print("The following new anime will be added to the section:")
|
||||
for item in confirmed_anime:
|
||||
print(f"- {item['name']}")
|
||||
|
||||
final_confirm = input("\nAdd these items to the section? [Y/n]: ").lower()
|
||||
if final_confirm == '' or final_confirm == 'y':
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for anime in confirmed_anime:
|
||||
if not any(item['id'] == anime['id'] for item in section['items']):
|
||||
section['items'].append(anime)
|
||||
added_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
print(f"\nSuccessfully added {added_count} new anime.")
|
||||
if skipped_count > 0:
|
||||
print(f"Skipped {skipped_count} anime that were already in the section.")
|
||||
else:
|
||||
print("Operation cancelled. No changes were made.")
|
||||
|
||||
input("Press Enter to continue...")
|
||||
|
||||
|
||||
|
||||
# --- Main Application ---
|
||||
def main():
|
||||
"""Main function to run the content manager."""
|
||||
data = None
|
||||
|
||||
# Check if a content file exists and prompt the user.
|
||||
if os.path.exists(CONTENT_FILE):
|
||||
clear_screen()
|
||||
print("--- Welcome Back ---")
|
||||
print(f"Found existing content file: '{CONTENT_FILE}'")
|
||||
print("\nWhat would you like to do?")
|
||||
print("[1] Load the existing content")
|
||||
print("[2] Start from scratch (Warning: saving will overwrite the old file)")
|
||||
|
||||
while data is None:
|
||||
choice = input("> ")
|
||||
if choice == '1':
|
||||
try:
|
||||
with open(CONTENT_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
# Ensure the basic structure exists, in case the file is malformed
|
||||
if 'spotlight' not in data: data['spotlight'] = []
|
||||
if 'sections' not in data: data['sections'] = []
|
||||
print("Content loaded successfully.")
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
print(f"Error: Could not read or parse '{CONTENT_FILE}'. Starting from scratch.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
elif choice == '2':
|
||||
print("Starting with a blank slate.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
else:
|
||||
print("Invalid choice. Please enter 1 or 2.")
|
||||
input("Press Enter to continue...")
|
||||
else:
|
||||
# If no content file exists, start from scratch automatically.
|
||||
print(f"No '{CONTENT_FILE}' found. Starting with a blank slate.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
input("Press Enter to continue...")
|
||||
|
||||
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Anime Content Manager ---")
|
||||
print(" (with AI-Powered Suggestions)")
|
||||
print("\nSelect an option:")
|
||||
print("[1] Manage Spotlight Section")
|
||||
print("[2] Manage Horizontal Sections")
|
||||
print("[3] Save and Exit")
|
||||
print("[4] Exit Without Saving")
|
||||
|
||||
choice = input("> ")
|
||||
|
||||
if choice == '1':
|
||||
manage_spotlight(data)
|
||||
elif choice == '2':
|
||||
manage_sections(data)
|
||||
elif choice == '3':
|
||||
with open(CONTENT_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
print(f"Content saved to {CONTENT_FILE}.")
|
||||
break
|
||||
elif choice == '4':
|
||||
print("Exiting without saving changes.")
|
||||
break
|
||||
else:
|
||||
print("Invalid option. Please try again.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2628
animex/index.html
Normal file
458
animex/intro.html
Normal file
@@ -0,0 +1,458 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Welcome to Animex</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
/* Maintained the original brand color as requested */
|
||||
--brand-primary: #FF9500;
|
||||
--brand-primary-dark: #E6850E;
|
||||
|
||||
/* New theme colors inspired by the image */
|
||||
--bg-main: #0F1014;
|
||||
--bg-overlay: rgba(15, 16, 20, 0.2);
|
||||
--bg-button-secondary: rgba(45, 48, 58, 0.7);
|
||||
--bg-button-secondary-hover: rgba(55, 58, 68, 0.9);
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #A0AEC0;
|
||||
--text-tertiary: #718096;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||
--shadow-brand: rgba(255, 149, 0, 0.3);
|
||||
|
||||
/* UI Styles */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--radius-pill: 50px;
|
||||
--radius-m: 16px;
|
||||
--transition-speed: 0.4s;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-main);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
/* Fullscreen background inspired by the image */
|
||||
.background-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
z-index: 0;
|
||||
filter: blur(7px);
|
||||
transform: scale(1.1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Dark overlay to make text readable */
|
||||
.background-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(15, 16, 20, 0.0) 0%, var(--bg-main) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Main container now organizes content and nav vertically */
|
||||
.onboarding-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
padding: 40px 24px calc(24px + env(safe-area-inset-bottom)) 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Removed the card style, content now lives on the main view */
|
||||
#onboarding-content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
transition: opacity var(--transition-speed) ease, transform var(--transition-speed) ease;
|
||||
padding: 0;
|
||||
margin-top: 5vh; /* Pushes content down slightly from the top */
|
||||
}
|
||||
|
||||
#onboarding-content.is-changing {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.content-icon {
|
||||
color: var(--brand-primary);
|
||||
margin-bottom: 24px;
|
||||
transition: opacity var(--transition-speed) ease, transform var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.content-icon svg, .content-icon i {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
filter: drop-shadow(0 8px 20px var(--shadow-brand));
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 2.5rem; /* Larger, bolder title */
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
/* Navigation footer at the bottom */
|
||||
#onboarding-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-inner {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--brand-primary);
|
||||
border-radius: 4px;
|
||||
transition: width var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-tertiary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Restyled buttons to match new theme */
|
||||
.btn {
|
||||
font-family: var(--font-family);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed) ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 54px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brand-primary);
|
||||
color: var(--bg-main);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px var(--shadow-brand);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 12px 32px var(--shadow-brand);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-button-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-button-secondary-hover);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-tertiary:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#back-btn, #next-btn, #finish-btn, #skip-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Specific styles for PWA/Learn More sections */
|
||||
.pwa-guide, .learn-more-btn {
|
||||
margin-top: 24px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 20px;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.pwa-guide li { list-style: none; display: flex; align-items: center; gap: 16px; margin-bottom: 16px; padding: 0; }
|
||||
.pwa-guide li:last-child { margin-bottom: 0; }
|
||||
.pwa-guide-icon { background: var(--brand-primary); color: var(--bg-main); border-radius: 12px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 18px; box-shadow: 0 4px 12px var(--shadow-brand); }
|
||||
.learn-more-btn { color: var(--brand-primary); }
|
||||
.learn-more-btn:hover { background: rgba(255, 149, 0, 0.2); border-color: rgba(255, 149, 0, 0.5); }
|
||||
|
||||
/* Modal styles remain similar but use new variables for consistency */
|
||||
#learn-more-modal { position: fixed; z-index: 9999; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||
.learn-modal-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(8px); z-index: 0; }
|
||||
.learn-modal-card { position: relative; background: #1a1b20; backdrop-filter: blur(20px); border: 1px solid var(--border-color); border-radius: var(--radius-m); box-shadow: 0 20px 60px var(--shadow-color); padding: 32px 24px 24px; max-width: 480px; width: 100%; z-index: 1; color: var(--text-primary); }
|
||||
.learn-modal-card h2 { font-size: 1.5rem; font-weight: 700; margin: 0 0 24px 0; color: var(--text-primary); }
|
||||
.learn-modal-close { position: absolute; top: 16px; right: 18px; background: rgba(255, 255, 255, 0.1); border: none; border-radius: 50%; width: 32px; height: 32px; font-size: 18px; color: var(--text-tertiary); cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; }
|
||||
.learn-modal-close:hover { background: rgba(255, 149, 0, 0.2); color: var(--brand-primary); }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.content-icon svg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Simplified background structure -->
|
||||
<div id="background-container" class="background-container"></div>
|
||||
<div class="background-overlay"></div>
|
||||
|
||||
<div class="onboarding-container">
|
||||
<!-- Content and navigation are now direct children of the main container -->
|
||||
<main id="onboarding-content" aria-live="polite">
|
||||
<!-- Content injected by JavaScript -->
|
||||
</main>
|
||||
|
||||
<footer id="onboarding-nav">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-inner" id="progress-bar-inner"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progress-text">1/5</div>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<button class="btn btn-secondary" id="back-btn">Back</button>
|
||||
<button class="btn btn-tertiary" id="skip-btn">Skip</button>
|
||||
<button class="btn btn-primary" id="next-btn">Next</button>
|
||||
<button class="btn btn-primary" id="finish-btn">Get Started</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
// The JS content remains identical in terms of data and logic
|
||||
const introPages = [
|
||||
{
|
||||
icon: `<svg width="100" height="100" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="25" y="21" width="99.3511" height="13.6477" fill="currentColor"/><rect x="25" y="43.8692" width="99.3511" height="13.6477" fill="currentColor"/><path d="M119.419 129.813H103.179L86.6541 30.5903H103.179L119.419 129.813Z" fill="currentColor"/><path d="M31.6938 129.813H47.9337L64.4585 30.5903H47.9337L31.6938 129.813Z" fill="currentColor"/><rect x="64.4585" y="107.998" width="20.7862" height="21.8153" fill="currentColor"/><ellipse cx="74.8516" cy="107.796" rx="10.3931" ry="11.9176" fill="currentColor"/></svg>`,
|
||||
title: 'Welcome to Animex',
|
||||
desc: 'Your premium platform for discovering, watching, and reading the best anime and manga.',
|
||||
backgroundUrl: 'Resources/Images/Launch_Screen.png'
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M64 64l0 288 512 0 0-288L64 64zM0 64C0 28.7 28.7 0 64 0L576 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 416c-35.3 0-64-28.7-64-64L0 64zM128 448l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-384 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>`,
|
||||
title: 'Endless Content',
|
||||
desc: 'Thousands of anime episodes and manga chapters at your fingertips.',
|
||||
backgroundUrl: 'Resources/Images/Launch_Screen.png'
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M272 416c17.7 0 32-14.3 32-32s-14.3-32-32-32l-112 0c-17.7 0-32-14.3-32-32l0-128 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 128c0 53 43 96 96 96l112 0zM304 96c-17.7 0-32 14.3-32 32s14.3 32 32 32l112 0c17.7 0 32 14.3 32 32l0 128-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2 9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-128c0-53-43-96-96-96L304 96z"/></svg>`,
|
||||
title: 'Seamless Experience',
|
||||
desc: 'Pick up where you left off across all your devices.',
|
||||
backgroundUrl: 'Resources/Images/Launch_Screen.png'
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128l-368 0zm79-167l80 80c9.4 9.4 24.6 9.4 33.9 0l80-80c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-39 39L344 184c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 134.1-39-39c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9z"/></svg>`,
|
||||
title: 'Offline Downloads',
|
||||
desc: 'Download your favorite episodes and chapters to enjoy them anywhere, anytime.',
|
||||
learnMore: true,
|
||||
backgroundUrl: 'Resources/Images/Launch_Screen.png'
|
||||
},
|
||||
{
|
||||
pwa: true,
|
||||
icon: `<i class="fa-solid fa-clapperboard"></i>`,
|
||||
title: 'Install the App',
|
||||
desc: 'Add Animex to your home screen for the best experience.',
|
||||
pwaGuide: `
|
||||
<ul class="pwa-guide">
|
||||
<li><span class="pwa-guide-icon"><i class="fas fa-share-alt"></i></span> <span>Tap the <strong>Share</strong> button</span></li>
|
||||
<li><span class="pwa-guide-icon"><i class="fas fa-plus-square"></i></span> <span>Select <strong>'Add to Home Screen'</strong></span></li>
|
||||
</ul>
|
||||
`,
|
||||
backgroundUrl: 'Resources/Images/Launch_Screen.png'
|
||||
}
|
||||
];
|
||||
|
||||
const contentEl = document.getElementById('onboarding-content');
|
||||
const navEl = document.getElementById('onboarding-nav');
|
||||
const progressBarInnerEl = document.getElementById('progress-bar-inner');
|
||||
const progressTextEl = document.getElementById('progress-text');
|
||||
const backBtn = document.getElementById('back-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
const finishBtn = document.getElementById('finish-btn');
|
||||
const skipBtn = document.getElementById('skip-btn');
|
||||
const bgContainer = document.getElementById('background-container');
|
||||
|
||||
let currentPage = 0;
|
||||
let availablePages = [];
|
||||
|
||||
function finishOnboarding() {
|
||||
setTimeout(() => window.parent.showToast('Welcome to Animex! 🎉'), 600);
|
||||
}
|
||||
|
||||
function updateNavigation() {
|
||||
const total = availablePages.length;
|
||||
const isFirstPage = currentPage === 0;
|
||||
const isLastPage = currentPage === total - 1;
|
||||
progressBarInnerEl.style.width = `${((currentPage + 1) / total) * 100}%`;
|
||||
progressTextEl.textContent = `${currentPage + 1}/${total}`;
|
||||
backBtn.style.display = isFirstPage ? 'none' : 'flex';
|
||||
skipBtn.style.display = isLastPage ? 'none' : 'flex';
|
||||
nextBtn.style.display = isLastPage ? 'none' : 'flex';
|
||||
finishBtn.style.display = isLastPage ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function updateContent() {
|
||||
const pageData = availablePages[currentPage];
|
||||
|
||||
contentEl.classList.add('is-changing');
|
||||
|
||||
// Use a placeholder background. In a real app, you might use one per slide.
|
||||
// I'm using the first slide's image for a consistent look.
|
||||
const bgUrl = availablePages[0].backgroundUrl;
|
||||
bgContainer.style.backgroundImage = `url("${bgUrl}")`;
|
||||
|
||||
setTimeout(() => {
|
||||
let iconHtml = `<div class="content-icon">${pageData.icon}</div>`;
|
||||
let contentHtml = `
|
||||
${iconHtml}
|
||||
<h1 class="content-title">${pageData.title}</h1>
|
||||
<p class="content-desc">${pageData.desc}</p>
|
||||
`;
|
||||
|
||||
if (pageData.pwaGuide) contentHtml += pageData.pwaGuide;
|
||||
if (pageData.learnMore) contentHtml += `<button class="btn learn-more-btn" id="learn-more-btn">Learn More</button>`;
|
||||
|
||||
contentEl.innerHTML = contentHtml;
|
||||
|
||||
requestAnimationFrame(() => contentEl.classList.remove('is-changing'));
|
||||
|
||||
if (pageData.learnMore) {
|
||||
document.getElementById('learn-more-btn').onclick = showLearnMoreModal;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function handleNavClick(e) {
|
||||
const action = e.target.id;
|
||||
switch (action) {
|
||||
case 'next-btn':
|
||||
if (currentPage < availablePages.length - 1) {
|
||||
currentPage++;
|
||||
updateContent();
|
||||
updateNavigation();
|
||||
}
|
||||
break;
|
||||
case 'back-btn':
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
updateContent();
|
||||
updateNavigation();
|
||||
}
|
||||
break;
|
||||
case 'finish-btn': finishOnboarding(); break;
|
||||
case 'skip-btn':
|
||||
currentPage = availablePages.length - 1;
|
||||
updateContent();
|
||||
updateNavigation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal function remains unchanged
|
||||
function showLearnMoreModal() {
|
||||
if (document.getElementById('learn-more-modal')) return;
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'learn-more-modal';
|
||||
modal.innerHTML = `<div class="learn-modal-backdrop"></div> <div class="learn-modal-card"> <button class="learn-modal-close" id="close-learn-modal" title="Close">×</button> <h2>Organize Your Downloads</h2> <div class="folder-tree" style="font-size: 1rem; color: var(--text-secondary); text-align: left; line-height: 1.8; background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: var(--radius-m); padding: 20px; margin-bottom: 20px;"> <div style="font-size: 1.1rem; color: var(--brand-primary); font-weight: 700;"><span style="margin-right: 8px; color: var(--brand-primary);"><i class='fas fa-folder'></i></span> <strong>Anime</strong> <span style="font-size: 0.9em; color: var(--text-tertiary); margin-left: 6px; font-weight: 400;">(Main Folder)</span></div><div style="margin-left: 24px; color: var(--text-secondary); font-weight: 600;"><span style="margin-right: 8px; color: var(--brand-primary);"><i class='fas fa-folder'></i></span> <strong>Attack on Titan</strong> <div style="margin-left: 32px; color: var(--text-tertiary); font-weight: 400; display: flex; align-items: center; gap: 6px;"><span style="color: var(--brand-primary); margin-right: 4px;"><i class='fas fa-play-circle'></i></span> Episode 1.mp4</div></div></div><div class="learn-modal-desc" style="padding-left: 0;"> <ul style="padding-left: 20px; margin: 0; color: var(--text-secondary); font-size: 1rem; line-height: 1.6;"> <li style="margin-bottom: 12px;"><strong>Step 1:</strong> Create a main folder on your device.</li><li style="margin-bottom: 12px;"><strong>Step 2:</strong> Add subfolders for each series.</li><li style="margin-bottom: 12px;"><strong>Step 3:</strong> Animex will automatically organize your library!</li></ul> </div></div>`;
|
||||
document.body.appendChild(modal);
|
||||
document.getElementById('close-learn-modal').onclick = () => modal.remove();
|
||||
modal.onclick = (e) => { if (e.target.classList.contains('learn-modal-backdrop')) modal.remove(); };
|
||||
}
|
||||
|
||||
function init() {
|
||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||
availablePages = isStandalone ? introPages.filter(p => !p.pwa) : [...introPages];
|
||||
updateContent();
|
||||
updateNavigation();
|
||||
navEl.addEventListener('click', handleNavClick);
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1515
animex/library.html
Normal file
395
animex/lists.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Manage Lists</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #18181b;
|
||||
--surface-color: #27272a;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--text-primary: #f4f4f5;
|
||||
--text-secondary: #a1a1aa;
|
||||
--accent-color: #ff9500;
|
||||
--accent-hover: #ffac33;
|
||||
--danger-color: #ef4444;
|
||||
--font-family: "Inter", sans-serif;
|
||||
--radius: 8px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.manager-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
.manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.manager-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.item-info {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.item-info strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.lists-body {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.list-item:hover {
|
||||
background-color: #3f3f46;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.list-item.in-list {
|
||||
border-color: var(--accent-color);
|
||||
background-color: rgba(255, 149, 0, 0.1);
|
||||
}
|
||||
.list-item.in-list .list-name::after {
|
||||
content: " (Saved)";
|
||||
color: var(--accent-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.list-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.delete-list-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.delete-list-btn:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
.manager-footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.new-list-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.new-list-input {
|
||||
flex-grow: 1;
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.new-list-input:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
.add-list-btn {
|
||||
background-color: var(--accent-color);
|
||||
border: none;
|
||||
color: #000;
|
||||
padding: 0 20px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.add-list-btn:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="manager-container">
|
||||
<header class="manager-header">
|
||||
<h1 class="manager-title">Add to List</h1>
|
||||
<button class="close-btn" id="close-btn" title="Close">×</button>
|
||||
</header>
|
||||
|
||||
<div class="item-info" id="item-info">
|
||||
Select a list to save the item to.
|
||||
</div>
|
||||
|
||||
<div class="lists-body" id="lists-container">
|
||||
<!-- List items will be generated here -->
|
||||
<div class="placeholder" id="lists-placeholder">
|
||||
Create your first list below.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="manager-footer">
|
||||
<form id="new-list-form" class="new-list-form">
|
||||
<input
|
||||
type="text"
|
||||
id="new-list-input"
|
||||
class="new-list-input"
|
||||
placeholder="Create a new list..."
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="add-list-btn">Create</button>
|
||||
</form>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const itemInfoEl = document.getElementById("item-info");
|
||||
const listsContainer = document.getElementById("lists-container");
|
||||
const listsPlaceholder = document.getElementById("lists-placeholder");
|
||||
const newListForm = document.getElementById("new-list-form");
|
||||
const newListInput = document.getElementById("new-list-input");
|
||||
const closeBtn = document.getElementById("close-btn");
|
||||
|
||||
let currentItem = null;
|
||||
let userLists = {};
|
||||
const LISTS_STORAGE_KEY = "animex_lists_v1";
|
||||
|
||||
// --- DATA & STORAGE ---
|
||||
function loadLists() {
|
||||
try {
|
||||
const storedLists = localStorage.getItem(LISTS_STORAGE_KEY);
|
||||
userLists = storedLists ? JSON.parse(storedLists) : {};
|
||||
} catch (e) {
|
||||
console.error("Failed to parse user lists from localStorage", e);
|
||||
userLists = {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveLists() {
|
||||
try {
|
||||
localStorage.setItem(LISTS_STORAGE_KEY, JSON.stringify(userLists));
|
||||
} catch (e) {
|
||||
console.error("Failed to save user lists to localStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI RENDERING ---
|
||||
function renderLists() {
|
||||
listsContainer.innerHTML = "";
|
||||
const listNames = Object.keys(userLists);
|
||||
|
||||
if (listNames.length === 0) {
|
||||
listsContainer.appendChild(listsPlaceholder);
|
||||
listsPlaceholder.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
listsPlaceholder.style.display = "none";
|
||||
|
||||
listNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((listName) => {
|
||||
const listItem = document.createElement("div");
|
||||
listItem.className = "list-item";
|
||||
listItem.dataset.listName = listName;
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "list-name";
|
||||
nameSpan.textContent = listName;
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.className = "delete-list-btn";
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
||||
deleteBtn.title = `Delete "${listName}" list`;
|
||||
|
||||
listItem.appendChild(nameSpan);
|
||||
listItem.appendChild(deleteBtn);
|
||||
listsContainer.appendChild(listItem);
|
||||
});
|
||||
|
||||
updateListSelection();
|
||||
}
|
||||
|
||||
function updateItemInfo() {
|
||||
if (!currentItem) {
|
||||
itemInfoEl.innerHTML = "Select a list to save the item to.";
|
||||
return;
|
||||
}
|
||||
const itemType =
|
||||
currentItem.type === "anime" ? "Episode(s)" : "Chapter(s)";
|
||||
const itemsDisplay =
|
||||
currentItem.items.length > 3
|
||||
? `${currentItem.items.slice(0, 3).join(", ")}...`
|
||||
: currentItem.items.join(", ");
|
||||
|
||||
itemInfoEl.innerHTML = `Saving <strong>${currentItem.title}</strong> - ${itemType}: <strong>${itemsDisplay}</strong>`;
|
||||
}
|
||||
|
||||
function updateListSelection() {
|
||||
if (!currentItem) return;
|
||||
|
||||
document.querySelectorAll(".list-item").forEach((el) => {
|
||||
const listName = el.dataset.listName;
|
||||
const list = userLists[listName];
|
||||
|
||||
// Check if the series is in the list
|
||||
const seriesEntry = list.find(
|
||||
(entry) => entry[0] === currentItem.id
|
||||
);
|
||||
if (seriesEntry) {
|
||||
el.classList.add("in-list");
|
||||
} else {
|
||||
el.classList.remove("in-list");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOGIC ---
|
||||
function createList(listName) {
|
||||
if (!listName || userLists.hasOwnProperty(listName)) {
|
||||
window.parent.showToast("List name already exists or is invalid.");
|
||||
return;
|
||||
}
|
||||
userLists[listName] = [];
|
||||
saveLists();
|
||||
renderLists();
|
||||
}
|
||||
|
||||
function deleteList(listName) {
|
||||
if (
|
||||
!userLists.hasOwnProperty(listName) ||
|
||||
!confirm(`Are you sure you want to delete the "${listName}" list?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
delete userLists[listName];
|
||||
saveLists();
|
||||
renderLists();
|
||||
}
|
||||
|
||||
function toggleItemInList(listName) {
|
||||
if (!currentItem) return;
|
||||
|
||||
const list = userLists[listName];
|
||||
let seriesEntry = list.find((entry) => entry[0] === currentItem.id);
|
||||
|
||||
if (seriesEntry) {
|
||||
// Series exists, so remove it entirely for simplicity
|
||||
userLists[listName] = list.filter(
|
||||
(entry) => entry[0] !== currentItem.id
|
||||
);
|
||||
} else {
|
||||
// Series doesn't exist, add it
|
||||
const newEntry = [currentItem.id, currentItem.items.join(",")];
|
||||
list.push(newEntry);
|
||||
}
|
||||
|
||||
saveLists();
|
||||
updateListSelection();
|
||||
}
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "manage-item") {
|
||||
currentItem = event.data.data;
|
||||
console.log("Received item to manage:", currentItem);
|
||||
updateItemInfo();
|
||||
updateListSelection();
|
||||
}
|
||||
});
|
||||
|
||||
closeBtn.addEventListener("click", () => {
|
||||
window.parent.postMessage("close-list-manager", "*");
|
||||
});
|
||||
|
||||
newListForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const listName = newListInput.value.trim();
|
||||
createList(listName);
|
||||
newListInput.value = "";
|
||||
});
|
||||
|
||||
listsContainer.addEventListener("click", (e) => {
|
||||
const listItem = e.target.closest(".list-item");
|
||||
if (!listItem) return;
|
||||
|
||||
const listName = listItem.dataset.listName;
|
||||
|
||||
if (e.target.closest(".delete-list-btn")) {
|
||||
deleteList(listName);
|
||||
} else {
|
||||
toggleItemInList(listName);
|
||||
}
|
||||
});
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
function init() {
|
||||
loadLists();
|
||||
renderLists();
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
456
animex/login.html
Normal file
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Select Profile - Animex</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="manifest" href="Resources/manifest.json/" />
|
||||
<meta name="theme-color" content="#0b0b0b" />
|
||||
|
||||
<link rel="icon" href="/Resources/Images/icon-196.png" type="image/png" />
|
||||
<!-- iOS support -->
|
||||
<link rel="apple-touch-icon" href="/Resources/Images/icon-196.png" />
|
||||
<style>
|
||||
:root {
|
||||
--background-primary: #121212;
|
||||
--foreground-primary: #e6e6e6;
|
||||
--foreground-secondary: #a0a0a0;
|
||||
--background-modal: #252525;
|
||||
--input-border-color: #3a3a3c;
|
||||
--accent-primary: #ff9500;
|
||||
--accent-primary-rgb: 255, 149, 0;
|
||||
--destructive: #ff453a;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 16px;
|
||||
}
|
||||
body {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--foreground-primary);
|
||||
font-family: "Inter", sans-serif;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.profile-container {
|
||||
text-align: center;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.profile-container h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.profile-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.profile-item {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
.profile-item:not(.add) img {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: var(--radius-md);
|
||||
object-fit: cover;
|
||||
margin-bottom: 1rem;
|
||||
border: 4px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.profile-item:not(.add):hover img {
|
||||
transform: scale(1.05);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
.profile-item .profile-name {
|
||||
font-weight: 500;
|
||||
font-size: 1.2rem;
|
||||
color: var(--foreground-secondary);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.profile-item:hover .profile-name {
|
||||
color: var(--foreground-primary);
|
||||
}
|
||||
.profile-item.add .add-icon {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3rem;
|
||||
color: var(--foreground-secondary);
|
||||
background-color: #2a2a2a;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.profile-item.add:hover .add-icon {
|
||||
background-color: var(--accent-primary);
|
||||
color: white;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.manage-icon {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 55px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.manage-mode .manage-icon {
|
||||
display: flex;
|
||||
}
|
||||
.manage-mode .profile-item:hover img {
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
.manage-button {
|
||||
background: none;
|
||||
border: 1px solid var(--foreground-secondary);
|
||||
color: var(--foreground-secondary);
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.manage-button:hover {
|
||||
border-color: var(--foreground-primary);
|
||||
color: var(--foreground-primary);
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: var(--background-modal);
|
||||
padding: 2rem;
|
||||
border-radius: var(--radius-md);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--foreground-primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.primary-button {
|
||||
flex-grow: 1;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background-color: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
.secondary-button {
|
||||
flex-grow: 1;
|
||||
background-color: #3a3a3c;
|
||||
}
|
||||
.delete-button {
|
||||
background-color: var(--destructive);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="profile-container" id="profileContainer">
|
||||
<h1>Who's Watching?</h1>
|
||||
<div class="profile-grid" id="profileGrid">
|
||||
<!-- Profiles will be dynamically inserted here -->
|
||||
</div>
|
||||
<button class="manage-button" id="manageProfilesBtn">
|
||||
Manage Profiles
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- MODAL for Profile Create/Edit -->
|
||||
<div id="profileModal" class="modal-backdrop">
|
||||
<div class="modal-content">
|
||||
<h2 id="profileModalTitle">Create Profile</h2>
|
||||
<form id="profileForm">
|
||||
<div class="form-group">
|
||||
<label for="profileNameInput">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="profileNameInput"
|
||||
required
|
||||
maxlength="20"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="deleteProfileBtn"
|
||||
class="primary-button delete-button"
|
||||
style="display: none"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="closeProfileModalBtn"
|
||||
class="primary-button secondary-button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" id="saveProfileBtn" class="primary-button">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const profileContainer = document.getElementById("profileContainer");
|
||||
const profileGrid = document.getElementById("profileGrid");
|
||||
const manageProfilesBtn = document.getElementById("manageProfilesBtn");
|
||||
const profileModal = document.getElementById("profileModal");
|
||||
const profileForm = document.getElementById("profileForm");
|
||||
const profileModalTitle = document.getElementById("profileModalTitle");
|
||||
const profileNameInput = document.getElementById("profileNameInput");
|
||||
const closeProfileModalBtn = document.getElementById(
|
||||
"closeProfileModalBtn"
|
||||
);
|
||||
const saveProfileBtn = document.getElementById("saveProfileBtn");
|
||||
const deleteProfileBtn = document.getElementById("deleteProfileBtn");
|
||||
|
||||
let profiles = [];
|
||||
let isManageMode = false;
|
||||
let profileToEdit = null;
|
||||
|
||||
async function initialize() {
|
||||
await fetchAndRenderProfiles();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
async function fetchAndRenderProfiles() {
|
||||
try {
|
||||
const response = await fetch("/profiles");
|
||||
if (!response.ok) throw new Error("Could not fetch profiles");
|
||||
profiles = await response.json();
|
||||
renderProfileGrid();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
profileGrid.innerHTML =
|
||||
"<p>Could not load profiles. Is the server running?</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function renderProfileGrid() {
|
||||
profileGrid.innerHTML = "";
|
||||
profiles.forEach((profile) => {
|
||||
const item = createProfileElement(profile);
|
||||
profileGrid.appendChild(item);
|
||||
});
|
||||
// Add the "Add Profile" button if not at max capacity
|
||||
if (profiles.length < 10) {
|
||||
const addProfileItem = createAddProfileElement();
|
||||
profileGrid.appendChild(addProfileItem);
|
||||
}
|
||||
}
|
||||
|
||||
function createProfileElement(profile) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "profile-item";
|
||||
item.innerHTML = `
|
||||
<img src="${profile.avatar_url}" alt="${profile.name}">
|
||||
<div class="manage-icon"><i class="fas fa-pencil-alt"></i></div>
|
||||
<div class="profile-name">${profile.name}</div>
|
||||
`;
|
||||
item.addEventListener("click", () => handleProfileClick(profile));
|
||||
return item;
|
||||
}
|
||||
|
||||
function createAddProfileElement() {
|
||||
const item = document.createElement("div");
|
||||
item.className = "profile-item add";
|
||||
item.innerHTML = `
|
||||
<div class="add-icon"><i class="fas fa-plus"></i></div>
|
||||
<div class="profile-name">Add Profile</div>
|
||||
`;
|
||||
item.addEventListener("click", () => {
|
||||
profileToEdit = null; // Ensure it's a create operation
|
||||
openProfileModal();
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
function handleProfileClick(profile) {
|
||||
if (isManageMode) {
|
||||
profileToEdit = profile;
|
||||
openProfileModal(profile);
|
||||
} else {
|
||||
loginWithProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
function loginWithProfile(profile) {
|
||||
localStorage.setItem("currentProfileId", profile.id);
|
||||
// Redirect to the main app page or settings page
|
||||
console.log(`Logged in as ${profile.name}`);
|
||||
window.location.href = "index.html";
|
||||
}
|
||||
|
||||
function toggleManageMode() {
|
||||
isManageMode = !isManageMode;
|
||||
profileContainer.classList.toggle("manage-mode", isManageMode);
|
||||
manageProfilesBtn.textContent = isManageMode
|
||||
? "Done"
|
||||
: "Manage Profiles";
|
||||
}
|
||||
|
||||
function openProfileModal(profile = null) {
|
||||
profileForm.reset();
|
||||
if (profile) {
|
||||
profileModalTitle.textContent = "Edit Profile";
|
||||
profileNameInput.value = profile.name;
|
||||
deleteProfileBtn.style.display = "block";
|
||||
} else {
|
||||
profileModalTitle.textContent = "Create Profile";
|
||||
deleteProfileBtn.style.display = "none";
|
||||
}
|
||||
profileModal.style.display = "flex";
|
||||
profileNameInput.focus();
|
||||
}
|
||||
|
||||
function closeProfileModal() {
|
||||
profileModal.style.display = "none";
|
||||
}
|
||||
|
||||
async function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
const name = profileNameInput.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
saveProfileBtn.disabled = true;
|
||||
|
||||
try {
|
||||
if (profileToEdit) {
|
||||
// Edit existing profile - change PATCH to PUT
|
||||
const response = await fetch(`/profiles/${profileToEdit.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: profileToEdit.id,
|
||||
name: name,
|
||||
avatar_url: profileToEdit.avatar_url,
|
||||
settings: profileToEdit.settings,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to update profile");
|
||||
} else {
|
||||
// Create new profile
|
||||
const response = await fetch("/profiles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to create profile");
|
||||
}
|
||||
closeProfileModal();
|
||||
await fetchAndRenderProfiles();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.parent.showToast("An error occurred. Please try again.");
|
||||
} finally {
|
||||
saveProfileBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProfile() {
|
||||
if (
|
||||
!profileToEdit ||
|
||||
!confirm(`Delete profile "${profileToEdit.name}"?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/profiles/${profileToEdit.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to delete profile");
|
||||
closeProfileModal();
|
||||
await fetchAndRenderProfiles();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.parent.showToast("Could not delete profile.");
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
manageProfilesBtn.addEventListener("click", toggleManageMode);
|
||||
profileForm.addEventListener("submit", handleFormSubmit);
|
||||
closeProfileModalBtn.addEventListener("click", closeProfileModal);
|
||||
deleteProfileBtn.addEventListener("click", handleDeleteProfile);
|
||||
}
|
||||
|
||||
initialize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
38
animex/make.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# 1. Find all files, excluding .git and __pycache__
|
||||
def get_all_files(base_dir):
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
# Exclude .git and __pycache__
|
||||
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__']]
|
||||
for file in files:
|
||||
abs_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(abs_path, base_dir)
|
||||
# Convert to web root-relative path
|
||||
web_path = '/' + rel_path.replace('\\', '/').replace(' ', '%20')
|
||||
file_list.append(web_path)
|
||||
return sorted(file_list)
|
||||
|
||||
# 2. Update PRECACHE_URLS in sw.js
|
||||
def update_precache_urls(sw_path, new_urls):
|
||||
with open(sw_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# Regex to find the PRECACHE_URLS array
|
||||
pattern = re.compile(r'(const PRECACHE_URLS = \[)(.*?)(\];)', re.DOTALL)
|
||||
# Format new URLs
|
||||
url_lines = [f" '{url}'," for url in new_urls]
|
||||
new_array = '\n' + '\n'.join(url_lines) + '\n'
|
||||
new_content = pattern.sub(r"\\1" + new_array + r"\\3", content)
|
||||
with open(sw_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sw_path = os.path.join(base_dir, 'sw.js')
|
||||
files = get_all_files(base_dir)
|
||||
# Optionally filter out sw.js itself if you don't want to cache it
|
||||
files = [f for f in files if not f.endswith('/sw.js')]
|
||||
update_precache_urls(sw_path, files)
|
||||
print(f'Updated PRECACHE_URLS in {sw_path} with {len(files)} files.')
|
||||
917
animex/manga-info.html
Normal file
@@ -0,0 +1,917 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>Manga Info</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
/* --- CSS VARIABLES & RESET --- */
|
||||
:root {
|
||||
--brand-accent: #ff9500; /* Orange from screenshot */
|
||||
--brand-hover: #e08400;
|
||||
--background-primary: #0a0a0a;
|
||||
--background-secondary: #161616;
|
||||
--background-tertiary: #202020;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1a1;
|
||||
--text-muted: #666;
|
||||
--border-color: #2a2a2a;
|
||||
--success-color: #28a745;
|
||||
--ongoing-color: #17a2b8;
|
||||
|
||||
--brand-glow: rgba(255, 149, 0, 0.3);
|
||||
--shadow-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
--transition-duration: 0.2s;
|
||||
--border-radius: 6px;
|
||||
--container-width: 1400px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: "Inter", sans-serif;
|
||||
line-height: 1.5;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- HERO SECTION (DESKTOP LAYOUT LIKE SCREENSHOT) --- */
|
||||
#hero-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 45vh; /* Tall hero */
|
||||
display: flex;
|
||||
align-items: flex-end; /* Align content to bottom */
|
||||
padding-bottom: 4rem;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Dark Gradient Overlay */
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(10, 10, 10, 0.9) 0%,
|
||||
rgba(10, 10, 10, 0.7) 40%,
|
||||
rgba(10, 10, 10, 0.4) 100%
|
||||
),
|
||||
linear-gradient(to top, #0a0a0a 10%, transparent 60%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 5%;
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* Poster Image */
|
||||
.poster-container {
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px; /* Slight lift */
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Info Area */
|
||||
.info-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.manga-title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-tag {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.meta-tag.accent {
|
||||
color: var(--brand-accent);
|
||||
border-color: var(--brand-accent);
|
||||
}
|
||||
|
||||
/* Action Buttons (The "Start EP 1" style) */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background-color: var(--brand-accent);
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: background-color var(--transition-duration);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--brand-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
padding: 14px 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-duration);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.synopsis-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
max-width: 800px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- MAIN CONTENT --- */
|
||||
#main-content {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 5%;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 1rem 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--brand-accent);
|
||||
}
|
||||
|
||||
/* --- CHAPTER LIST --- */
|
||||
.list-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
background: var(--background-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: var(--brand-accent);
|
||||
color: #000;
|
||||
border-color: var(--brand-accent);
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#chapter-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, border-color 0.1s;
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
transform: scale(1.005);
|
||||
border-color: var(--brand-accent);
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
/* Status Indicators in List */
|
||||
.chapter-status-indicator {
|
||||
width: 4px;
|
||||
height: 40px;
|
||||
background: #333;
|
||||
margin-right: 15px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chapter-item.status-read .chapter-status-indicator {
|
||||
background: var(--success-color);
|
||||
box-shadow: 0 0 5px var(--success-color);
|
||||
}
|
||||
|
||||
.chapter-item.status-ongoing .chapter-status-indicator {
|
||||
background: var(--ongoing-color);
|
||||
box-shadow: 0 0 5px var(--ongoing-color);
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chapter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
color: var(--brand-accent);
|
||||
}
|
||||
|
||||
.chapter-item.status-read .chapter-title {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- READER MODAL --- */
|
||||
#reader-modal-container {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#reader-modal-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* --- MOBILE RESPONSIVE --- */
|
||||
@media (max-width: 768px) {
|
||||
#hero-section {
|
||||
min-height: auto;
|
||||
display: block;
|
||||
padding-bottom: 2rem;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1.5rem;
|
||||
padding-top: 4rem; /* Spacer for top nav usually */
|
||||
}
|
||||
|
||||
.poster-container {
|
||||
width: 180px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manga-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.synopsis-text {
|
||||
-webkit-line-clamp: 3;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.chapter-status-indicator {
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Hero Section -->
|
||||
<header id="hero-section">
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content">
|
||||
<div class="poster-container">
|
||||
<img
|
||||
id="manga-poster"
|
||||
class="poster-image"
|
||||
src="https://placehold.co/400x600/202020/666?text=..."
|
||||
alt="Manga Cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<div class="meta-row" id="meta-tags">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<h1 id="manga-title" class="manga-title">Loading...</h1>
|
||||
<p id="manga-synopsis" class="synopsis-text">
|
||||
Loading synopsis...
|
||||
</p>
|
||||
<div class="action-row">
|
||||
<button id="continue-reading-btn" class="btn-primary">
|
||||
<i class="fa-solid fa-play"></i> Start Reading
|
||||
</button>
|
||||
<button id="add-list-btn" class="btn-secondary">
|
||||
<i class="fa-solid fa-plus"></i> Add to List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main-content">
|
||||
<div class="tabs-container">
|
||||
<button class="tab-btn active" data-tab="chapters">Chapters</button>
|
||||
<button class="tab-btn" data-tab="details" style="display:none">Details</button> <!-- Hidden for now -->
|
||||
</div>
|
||||
|
||||
<div id="tab-chapters" class="tab-panel">
|
||||
<div class="list-controls">
|
||||
<span style="color:var(--text-secondary); font-size: 0.9rem;" id="chapter-count-label"></span>
|
||||
<div id="pagination-top" class="pagination-controls"></div>
|
||||
</div>
|
||||
|
||||
<ul id="chapter-list">
|
||||
<!-- Chapters generated here -->
|
||||
</ul>
|
||||
|
||||
<div class="list-controls" style="margin-top: 1rem; justify-content: center;">
|
||||
<div id="pagination-bottom" class="pagination-controls"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="reader-modal-container"></div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// --- DOM Elements ---
|
||||
const els = {
|
||||
hero: document.getElementById("hero-section"),
|
||||
poster: document.getElementById("manga-poster"),
|
||||
title: document.getElementById("manga-title"),
|
||||
synopsis: document.getElementById("manga-synopsis"),
|
||||
metaTags: document.getElementById("meta-tags"),
|
||||
continueBtn: document.getElementById("continue-reading-btn"),
|
||||
addListBtn: document.getElementById("add-list-btn"),
|
||||
chapterList: document.getElementById("chapter-list"),
|
||||
paginationTop: document.getElementById("pagination-top"),
|
||||
paginationBottom: document.getElementById("pagination-bottom"),
|
||||
countLabel: document.getElementById("chapter-count-label"),
|
||||
modal: document.getElementById("reader-modal-container"),
|
||||
};
|
||||
|
||||
// --- State ---
|
||||
const getServerUrl = () => {
|
||||
const extensionServerIp = localStorage.getItem("extension_server_ip") || "localhost";
|
||||
const url = "http://" + extensionServerIp + ":7275";
|
||||
console.log("[manga-info] serverUrl=", url);
|
||||
return url;
|
||||
};
|
||||
const serverUrl = getServerUrl();
|
||||
let state = {
|
||||
mangaId: null,
|
||||
source: "jikan", // default
|
||||
title: "",
|
||||
chapters: [], // Full list of chapters
|
||||
currentPage: 1,
|
||||
itemsPerPage: 30,
|
||||
readingHistory: {},
|
||||
mangaDetails: null
|
||||
};
|
||||
|
||||
// --- Initialization ---
|
||||
function getUrlParams() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
source: params.get("source") || "jikan",
|
||||
id: params.get("id"),
|
||||
};
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const params = getUrlParams();
|
||||
if (!params.id) {
|
||||
els.title.textContent = "Error: No ID provided";
|
||||
return;
|
||||
}
|
||||
state.mangaId = params.id;
|
||||
state.source = params.source;
|
||||
|
||||
// Load History
|
||||
loadReadingHistory();
|
||||
|
||||
// Fetch Data
|
||||
try {
|
||||
if (state.source === "mangadex") {
|
||||
await loadMangaDexData();
|
||||
} else {
|
||||
await loadJikanData();
|
||||
}
|
||||
|
||||
// Render UI
|
||||
updateHeroSection();
|
||||
updateContinueButton();
|
||||
renderPagination();
|
||||
renderChapterList();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
els.title.textContent = "Error loading manga";
|
||||
els.synopsis.textContent = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Fetching ---
|
||||
|
||||
async function loadJikanData() {
|
||||
// 1. Details
|
||||
const detRes = await fetch(`https://api.jikan.moe/v4/manga/${state.mangaId}/full`);
|
||||
if(!detRes.ok) throw new Error("Jikan API Error");
|
||||
const data = (await detRes.json()).data;
|
||||
|
||||
state.mangaDetails = {
|
||||
title: data.title,
|
||||
image: data.images?.jpg?.large_image_url,
|
||||
banner: data.images?.jpg?.large_image_url, // Jikan doesn't provide banners usually, fallback
|
||||
synopsis: data.synopsis,
|
||||
status: data.status,
|
||||
year: data.published?.from ? new Date(data.published.from).getFullYear() : "N/A",
|
||||
authors: data.authors?.map(a => a.name).join(", "),
|
||||
genres: data.genres?.map(g => g.name).slice(0,3)
|
||||
};
|
||||
state.title = data.title;
|
||||
|
||||
// 2. Chapters (From local proxy/server)
|
||||
const chRes = await fetch(`${serverUrl}/chapters/${state.mangaId}`);
|
||||
const chData = await chRes.json();
|
||||
|
||||
// Normalize Jikan Chapters
|
||||
state.chapters = chData.chapters.map(ch => ({
|
||||
id: String(ch.chapter_number), // ID is the number for Jikan
|
||||
number: ch.chapter_number,
|
||||
title: ch.title || `Chapter ${ch.chapter_number}`,
|
||||
date: null
|
||||
})).reverse(); // Newest first
|
||||
}
|
||||
|
||||
async function loadMangaDexData() {
|
||||
// 1. Details
|
||||
const detRes = await fetch(`${serverUrl}/mangadex/manga/${state.mangaId}`);
|
||||
if(!detRes.ok) throw new Error("MangaDex API Error");
|
||||
const data = await detRes.json();
|
||||
|
||||
const attrs = data.attributes;
|
||||
const title = attrs.title.en || Object.values(attrs.title)[0];
|
||||
const banner = `url('${serverUrl}/manga/${state.mangaId}/banner')`; // Use backend proxy for banner
|
||||
|
||||
state.mangaDetails = {
|
||||
title: title,
|
||||
image: data.image_url || `https://placehold.co/400x600/141414/333?text=${title}`,
|
||||
banner: null, // Will be handled by CSS background logic
|
||||
synopsis: attrs.description.en || "No description.",
|
||||
status: attrs.status,
|
||||
year: attrs.year || "N/A",
|
||||
authors: "", // Simplified
|
||||
genres: attrs.tags.filter(t => t.group === 'genre').map(t => t.attributes.name.en).slice(0,3)
|
||||
};
|
||||
state.title = title;
|
||||
|
||||
// 2. Chapters (Fetch all IDs then paginate logic is simplified for this demo,
|
||||
// but in production for MD you usually fetch by page.
|
||||
// To satisfy "Client side pagination" request for "long lists", we fetch a large chunk or all.)
|
||||
|
||||
// Note: For a robust implementation with thousands of chapters, we should use server pagination.
|
||||
// For this UI demo, we will fetch the first 500 or so to populate the list.
|
||||
const chRes = await fetch(`${serverUrl}/mangadex/manga/${state.mangaId}/chapters?limit=500`);
|
||||
const chData = await chRes.json();
|
||||
|
||||
state.chapters = chData.chapters.map(ch => ({
|
||||
id: ch.id, // UUID
|
||||
number: ch.attributes.chapter,
|
||||
title: ch.attributes.title,
|
||||
date: ch.attributes.publishAt,
|
||||
group: ch.relationships.find(r => r.type === 'scanlation_group')?.attributes?.name
|
||||
}));
|
||||
}
|
||||
|
||||
// --- History Logic ---
|
||||
|
||||
function loadReadingHistory() {
|
||||
const historyRaw = localStorage.getItem("reading_history");
|
||||
if (!historyRaw) return;
|
||||
|
||||
try {
|
||||
const historyArr = JSON.parse(historyRaw);
|
||||
// Find entry for this manga and source
|
||||
const entry = historyArr.find(item =>
|
||||
String(item.mangaId) === String(state.mangaId) &&
|
||||
item.source === state.source
|
||||
);
|
||||
|
||||
if (entry && entry.chapters) {
|
||||
state.readingHistory = entry.chapters;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse reading history", e);
|
||||
}
|
||||
}
|
||||
|
||||
function getChapterStatus(chapterId) {
|
||||
const chHistory = state.readingHistory[chapterId];
|
||||
if (!chHistory) return "unread";
|
||||
if (chHistory.state === "finished") return "read";
|
||||
if (chHistory.state === "ongoing") return "ongoing";
|
||||
return "unread";
|
||||
}
|
||||
|
||||
function getLastReadChapter() {
|
||||
// Find chapter with highest timestamp
|
||||
let lastChId = null;
|
||||
let maxTime = 0;
|
||||
|
||||
for (const [id, data] of Object.entries(state.readingHistory)) {
|
||||
if (data.timestamp > maxTime) {
|
||||
maxTime = data.timestamp;
|
||||
lastChId = id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastChId) return null;
|
||||
|
||||
// Find the object in state.chapters
|
||||
return state.chapters.find(c => String(c.id) === String(lastChId));
|
||||
}
|
||||
|
||||
// --- Render Functions ---
|
||||
|
||||
function updateHeroSection() {
|
||||
const d = state.mangaDetails;
|
||||
document.title = `${d.title} - Manga`;
|
||||
|
||||
// Background
|
||||
if (state.source === "jikan") {
|
||||
// Jikan fallback background
|
||||
try {
|
||||
els.hero.style.backgroundImage = `url('${serverUrl}/manga/${state.mangaId}/banner')`;
|
||||
} catch (e) {
|
||||
els.hero.style.backgroundImage = `url('${d.image}')`;
|
||||
}
|
||||
} else {
|
||||
els.hero.style.backgroundImage = `url('${serverUrl}/manga/${state.mangaId}/banner')`;
|
||||
}
|
||||
|
||||
els.poster.src = d.image;
|
||||
els.title.textContent = d.title;
|
||||
els.synopsis.textContent = d.synopsis;
|
||||
|
||||
// Meta Tags
|
||||
els.metaTags.innerHTML = `
|
||||
<span class="meta-tag accent">${state.source.toUpperCase()}</span>
|
||||
<span class="meta-tag">${d.year}</span>
|
||||
<span class="meta-tag" style="text-transform:capitalize">${d.status}</span>
|
||||
${d.genres.map(g => `<span class="meta-tag">${g}</span>`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function updateContinueButton() {
|
||||
const lastRead = getLastReadChapter();
|
||||
|
||||
if (lastRead) {
|
||||
els.continueBtn.innerHTML = `<i class="fa-solid fa-play"></i> Continue Ch ${lastRead.number}`;
|
||||
els.continueBtn.onclick = () => openReader(lastRead.id);
|
||||
} else {
|
||||
// Find first chapter (last in array usually if sorted desc, but let's be safe)
|
||||
const firstCh = state.chapters[state.chapters.length - 1]; // Assuming desc order
|
||||
if (firstCh) {
|
||||
els.continueBtn.innerHTML = `<i class="fa-solid fa-book-open"></i> Start Reading`;
|
||||
els.continueBtn.onclick = () => openReader(firstCh.id);
|
||||
} else {
|
||||
els.continueBtn.innerHTML = "No Chapters";
|
||||
els.continueBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const totalPages = Math.ceil(state.chapters.length / state.itemsPerPage);
|
||||
if (totalPages <= 1) {
|
||||
els.paginationTop.style.display = 'none';
|
||||
els.paginationBottom.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const createButtons = (container) => {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Prev
|
||||
const prev = document.createElement('button');
|
||||
prev.className = 'page-btn';
|
||||
prev.innerHTML = '<i class="fas fa-chevron-left"></i>';
|
||||
prev.disabled = state.currentPage === 1;
|
||||
prev.onclick = () => { state.currentPage--; renderChapterList(); renderPagination(); };
|
||||
container.appendChild(prev);
|
||||
|
||||
// Simple page info (Desktop style often uses numbers, keeping it simple for logic)
|
||||
const info = document.createElement('span');
|
||||
info.textContent = `${state.currentPage} / ${totalPages}`;
|
||||
info.style.color = 'var(--text-secondary)';
|
||||
info.style.alignSelf = 'center';
|
||||
info.style.margin = '0 10px';
|
||||
container.appendChild(info);
|
||||
|
||||
// Next
|
||||
const next = document.createElement('button');
|
||||
next.className = 'page-btn';
|
||||
next.innerHTML = '<i class="fas fa-chevron-right"></i>';
|
||||
next.disabled = state.currentPage === totalPages;
|
||||
next.onclick = () => { state.currentPage++; renderChapterList(); renderPagination(); };
|
||||
container.appendChild(next);
|
||||
};
|
||||
|
||||
createButtons(els.paginationTop);
|
||||
createButtons(els.paginationBottom);
|
||||
els.paginationTop.style.display = 'flex';
|
||||
els.paginationBottom.style.display = 'flex';
|
||||
}
|
||||
|
||||
function renderChapterList() {
|
||||
els.chapterList.innerHTML = '';
|
||||
|
||||
const start = (state.currentPage - 1) * state.itemsPerPage;
|
||||
const end = start + state.itemsPerPage;
|
||||
const currentSlice = state.chapters.slice(start, end);
|
||||
|
||||
els.countLabel.textContent = `${state.chapters.length} Chapters`;
|
||||
|
||||
if (currentSlice.length === 0) {
|
||||
els.chapterList.innerHTML = '<li style="padding:20px; text-align:center; color:gray">No chapters found.</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
currentSlice.forEach(ch => {
|
||||
const status = getChapterStatus(ch.id);
|
||||
const li = document.createElement('li');
|
||||
li.className = `chapter-item status-${status}`;
|
||||
|
||||
let metaText = `Ch. ${ch.number}`;
|
||||
if (ch.date) {
|
||||
const d = new Date(ch.date).toLocaleDateString();
|
||||
metaText += ` • ${d}`;
|
||||
}
|
||||
if (ch.group) {
|
||||
metaText += ` • ${ch.group}`;
|
||||
}
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="chapter-status-indicator"></div>
|
||||
<div class="chapter-info">
|
||||
<span class="chapter-title">${ch.title || 'Chapter ' + ch.number}</span>
|
||||
<div class="chapter-meta">${metaText}</div>
|
||||
</div>
|
||||
<div class="chapter-actions">
|
||||
<button class="icon-btn add-btn" title="Add to Queue"><i class="fas fa-list-ul"></i></button>
|
||||
<button class="icon-btn download-btn" title="Download"><i class="fas fa-download"></i></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
li.onclick = (e) => {
|
||||
// Prevent clicking buttons from triggering read
|
||||
if (e.target.closest('.chapter-actions')) return;
|
||||
openReader(ch.id);
|
||||
};
|
||||
|
||||
// Add to List Action (using parent method from prompt context)
|
||||
li.querySelector('.add-btn').onclick = () => {
|
||||
if (window.parent && typeof window.parent.openListManager === "function") {
|
||||
window.parent.openListManager({
|
||||
type: "manga",
|
||||
id: state.mangaId,
|
||||
title: state.title,
|
||||
items: [String(ch.id)],
|
||||
source: state.source,
|
||||
});
|
||||
} else {
|
||||
alert("List Manager not found in parent window.");
|
||||
}
|
||||
};
|
||||
|
||||
// Download Action
|
||||
li.querySelector('.download-btn').onclick = () => {
|
||||
window.open(`/download-manga/site/${state.source}/${state.mangaId}/${ch.id}`, "_blank");
|
||||
};
|
||||
|
||||
els.chapterList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
|
||||
function openReader(chapterId) {
|
||||
// Update local storage history immediately for UI responsiveness (mocking)
|
||||
// In a real app, the reader usually updates the history.
|
||||
// Here we just open the iframe.
|
||||
const url = `/read/${state.source}/${state.mangaId}/${chapterId}`;
|
||||
|
||||
els.modal.innerHTML = `<iframe src="${url}" allowfullscreen></iframe>`;
|
||||
els.modal.classList.add("visible");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data === "close-reader-modal") {
|
||||
els.modal.classList.remove("visible");
|
||||
els.modal.innerHTML = "";
|
||||
document.body.style.overflow = "auto";
|
||||
|
||||
// Refresh history logic on close
|
||||
loadReadingHistory();
|
||||
updateContinueButton();
|
||||
renderChapterList(); // Re-render to update status indicators
|
||||
}
|
||||
});
|
||||
|
||||
// Add List Button (Series level)
|
||||
els.addListBtn.addEventListener("click", () => {
|
||||
if (window.parent && typeof window.parent.openListManager === "function") {
|
||||
// Add all loaded chapters
|
||||
const allIds = state.chapters.map(c => String(c.id));
|
||||
window.parent.openListManager({
|
||||
type: "manga",
|
||||
id: state.mangaId,
|
||||
title: state.title,
|
||||
items: allIds,
|
||||
source: state.source,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Run
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1238
animex/manga.html
Normal file
585
animex/manga.py
Normal file
@@ -0,0 +1,585 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import collections
|
||||
import getpass
|
||||
import re
|
||||
|
||||
# --- Dependency Check and Setup ---
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
except ImportError:
|
||||
print("Error: The 'google-generativeai' library is not installed.")
|
||||
print("Please install it by running: pip install google-generativeai")
|
||||
exit()
|
||||
|
||||
# --- Configuration ---
|
||||
JIKAN_API_BASE_URL = "https://api.jikan.moe/v4"
|
||||
CONTENT_FILE = "manga_content.json"
|
||||
CONFIG_FILE = "config.ini"
|
||||
|
||||
# Jikan rate limiting
|
||||
REQUEST_TIMESTAMPS = collections.deque()
|
||||
REQUEST_LIMIT = 3 # 3 requests per second
|
||||
TIME_WINDOW = 1 # 1 second
|
||||
|
||||
# --- API Key and Configuration Management ---
|
||||
def get_api_key():
|
||||
"""Gets the Google AI API key, prompting the user if not found."""
|
||||
# Check environment variable first
|
||||
api_key = os.getenv("GOOGLE_API_KEY")
|
||||
if api_key:
|
||||
print("Loaded Google API Key from environment variable.")
|
||||
input("Press Enter to continue...")
|
||||
return api_key
|
||||
|
||||
# Check config file next
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
config = json.load(f)
|
||||
api_key = config.get("GOOGLE_API_KEY")
|
||||
if api_key:
|
||||
print("Loaded Google API Key from config.ini.")
|
||||
input("Press Enter to continue...")
|
||||
return api_key
|
||||
|
||||
# If not found, prompt the user
|
||||
print("\n--- Google AI API Key Required ---")
|
||||
print("To use the AI generation feature, you need a Google AI API Key.")
|
||||
print("You can get a free key from Google AI Studio.")
|
||||
print("The key will be stored locally in 'config.ini' so you don't have to enter it again.")
|
||||
|
||||
api_key = getpass.getpass("Please enter your Google AI API Key: ")
|
||||
|
||||
# Save the key to config.ini for future use
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump({"GOOGLE_API_KEY": api_key}, f)
|
||||
|
||||
print("API Key saved to config.ini.")
|
||||
return api_key
|
||||
|
||||
|
||||
# --- Jikan API Interaction with Rate Limiting ---
|
||||
def jikan_api_request(endpoint, params=None):
|
||||
"""
|
||||
Makes a rate-limited request to the Jikan API.
|
||||
Waits if the request limit has been reached in the last second.
|
||||
"""
|
||||
global REQUEST_TIMESTAMPS
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
# Remove timestamps older than the time window
|
||||
while REQUEST_TIMESTAMPS and REQUEST_TIMESTAMPS[0] < now - TIME_WINDOW:
|
||||
REQUEST_TIMESTAMPS.popleft()
|
||||
|
||||
if len(REQUEST_TIMESTAMPS) < REQUEST_LIMIT:
|
||||
break
|
||||
|
||||
# Calculate sleep time to respect the rate limit
|
||||
sleep_time = (REQUEST_TIMESTAMPS[0] + TIME_WINDOW) - now + 0.05 # small buffer
|
||||
print(f"Jikan rate limit reached. Waiting for {sleep_time:.2f} seconds...")
|
||||
time.sleep(sleep_time)
|
||||
|
||||
try:
|
||||
REQUEST_TIMESTAMPS.append(time.time())
|
||||
print(f"Making Jikan request to: {JIKAN_API_BASE_URL}{endpoint}")
|
||||
response = requests.get(f"{JIKAN_API_BASE_URL}{endpoint}", params=params)
|
||||
response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"\n--- Jikan API Error --- \n{e}\n------------------")
|
||||
return None
|
||||
|
||||
# --- Helper Functions ---
|
||||
def clear_screen():
|
||||
"""Clears the console screen."""
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def get_choice(max_choice, allow_back=True):
|
||||
"""Gets and validates a user's integer choice."""
|
||||
while True:
|
||||
try:
|
||||
prompt = "> "
|
||||
choice = input(prompt)
|
||||
if allow_back and choice.lower() == 'b':
|
||||
return 'b'
|
||||
choice = int(choice)
|
||||
if 1 <= choice <= max_choice:
|
||||
return choice
|
||||
else:
|
||||
print(f"Invalid choice. Please enter a number between 1 and {max_choice}.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
def format_manga_data(manga_obj):
|
||||
"""Formats Jikan manga data into the structure needed for manga_content.json."""
|
||||
return {
|
||||
"id": manga_obj['mal_id'],
|
||||
"name": manga_obj.get('title_english') or manga_obj.get('title'),
|
||||
"image": manga_obj['images']['jpg']['large_image_url'],
|
||||
# Optional: Add description for the hero section if needed
|
||||
"description": manga_obj.get('synopsis', ''),
|
||||
"year": manga_obj.get('published', {}).get('prop', {}).get('from', {}).get('year')
|
||||
}
|
||||
|
||||
def search_and_select_manga():
|
||||
"""Prompts user to search for a manga, displays results, and returns the selected one."""
|
||||
query = input("Enter search term (or 'b' to go back): ")
|
||||
if query.lower() == 'b':
|
||||
return None
|
||||
|
||||
results = jikan_api_request("/manga", params={"q": query, "limit": 10})
|
||||
if not results or not results.get('data'):
|
||||
print("No results found.")
|
||||
input("Press Enter to continue...")
|
||||
return None
|
||||
|
||||
clear_screen()
|
||||
print(f"--- Search Results for '{query}' ---")
|
||||
for i, manga in enumerate(results['data'], 1):
|
||||
year = manga.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
|
||||
print(f"[{i}] {manga.get('title_english') or manga.get('title')} ({manga.get('type', 'N/A')}, {year})")
|
||||
|
||||
print("\n[b] Back to previous menu")
|
||||
|
||||
print("\nSelect a manga to add:")
|
||||
choice = get_choice(len(results['data']))
|
||||
if choice == 'b':
|
||||
return None
|
||||
|
||||
return results['data'][choice - 1]
|
||||
|
||||
# --- Management Logic ---
|
||||
def manage_spotlight(data):
|
||||
"""Handles logic for managing the spotlight section."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Manage Spotlight Section ---")
|
||||
if not data['spotlight']:
|
||||
print("Spotlight is currently empty.")
|
||||
else:
|
||||
for i, item in enumerate(data['spotlight'], 1):
|
||||
print(f"[{i}] {item['name']} (ID: {item['id']})")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Add a manga to Spotlight")
|
||||
print("[2] Remove a manga from Spotlight")
|
||||
print("[b] Back to Main Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
manga_obj = search_and_select_manga()
|
||||
if manga_obj:
|
||||
if any(item['id'] == manga_obj['mal_id'] for item in data['spotlight']):
|
||||
print(f"'{manga_obj['title']}' is already in the spotlight.")
|
||||
else:
|
||||
formatted = format_manga_data(manga_obj)
|
||||
data['spotlight'].append(formatted)
|
||||
print(f"Added '{formatted['name']}' to spotlight.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
elif choice == '2':
|
||||
if not data['spotlight']:
|
||||
print("Nothing to remove.")
|
||||
input("Press Enter to continue...")
|
||||
continue
|
||||
print("Enter the number of the manga to remove (or 'b' to cancel):")
|
||||
remove_choice = get_choice(len(data['spotlight']))
|
||||
if remove_choice != 'b':
|
||||
removed = data['spotlight'].pop(remove_choice - 1)
|
||||
print(f"Removed '{removed['name']}' from spotlight.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def manage_sections(data):
|
||||
"""Handles logic for managing horizontal sections."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Manage Horizontal Sections ---")
|
||||
if not data['sections']:
|
||||
print("No sections created yet.")
|
||||
else:
|
||||
for i, section in enumerate(data['sections'], 1):
|
||||
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Create a new section")
|
||||
print("[2] Edit an existing section")
|
||||
print("[3] Delete a section")
|
||||
print("[b] Back to Main Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
title = input("Enter title for new section: ")
|
||||
data['sections'].append({"title": title, "items": []})
|
||||
print(f"Section '{title}' created.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '2':
|
||||
if not data['sections']:
|
||||
print("No sections to edit.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
for i, section in enumerate(data['sections'], 1):
|
||||
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
|
||||
print("Select a section to edit:")
|
||||
edit_choice = get_choice(len(data['sections']))
|
||||
if edit_choice != 'b':
|
||||
edit_section_menu(data['sections'][edit_choice - 1])
|
||||
|
||||
elif choice == '3':
|
||||
if not data['sections']:
|
||||
print("No sections to delete.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
print("Select a section to delete:")
|
||||
delete_choice = get_choice(len(data['sections']))
|
||||
if delete_choice != 'b':
|
||||
removed = data['sections'].pop(delete_choice - 1)
|
||||
print(f"Deleted section '{removed['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def edit_section_menu(section):
|
||||
"""Menu for editing a specific section."""
|
||||
while True:
|
||||
clear_screen()
|
||||
print(f"--- Editing Section: {section['title']} ---")
|
||||
if not section['items']:
|
||||
print("This section is empty.")
|
||||
else:
|
||||
for i, item in enumerate(section['items'], 1):
|
||||
print(f" [{i}] {item['name']} (ID: {item['id']})")
|
||||
|
||||
print("\nOptions:")
|
||||
print("[1] Add a manga to this section (Manual Search)")
|
||||
print("[2] Remove a manga from this section")
|
||||
print("[3] Auto-populate this section (from Jikan)")
|
||||
print("[4] Generate content with AI")
|
||||
print("[5] Rename this section")
|
||||
print("[b] Back to Sections Menu")
|
||||
|
||||
choice = input("> ").lower()
|
||||
|
||||
if choice == '1':
|
||||
manga_obj = search_and_select_manga()
|
||||
if manga_obj:
|
||||
if any(item['id'] == manga_obj['mal_id'] for item in section['items']):
|
||||
print(f"'{manga_obj['title']}' is already in this section.")
|
||||
else:
|
||||
formatted = format_manga_data(manga_obj)
|
||||
section['items'].append(formatted)
|
||||
print(f"Added '{formatted['name']}' to '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '2':
|
||||
if not section['items']:
|
||||
print("Nothing to remove.")
|
||||
input("Press Enter...")
|
||||
continue
|
||||
print("Enter the number of the manga to remove:")
|
||||
remove_choice = get_choice(len(section['items']))
|
||||
if remove_choice != 'b':
|
||||
removed = section['items'].pop(remove_choice - 1)
|
||||
print(f"Removed '{removed['name']}' from '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == '3':
|
||||
auto_populate_section(section)
|
||||
|
||||
elif choice == '4':
|
||||
generate_with_ai(section)
|
||||
|
||||
elif choice == '5':
|
||||
new_title = input(f"Enter new title for '{section['title']}': ")
|
||||
section['title'] = new_title
|
||||
print("Section renamed.")
|
||||
input("Press Enter...")
|
||||
|
||||
elif choice == 'b':
|
||||
return
|
||||
|
||||
def auto_populate_section(section):
|
||||
"""Automatically populates a section from a Jikan endpoint."""
|
||||
clear_screen()
|
||||
print(f"--- Auto-Populate Section: {section['title']} ---")
|
||||
print("Select a category to populate from:")
|
||||
print("[1] Top Manga by Popularity")
|
||||
print("[2] Top Manhwa")
|
||||
print("[3] Top Publishing Manga")
|
||||
print("[b] Cancel")
|
||||
|
||||
choice = get_choice(3)
|
||||
if choice == 'b':
|
||||
return
|
||||
|
||||
endpoint_map = {
|
||||
1: ("/top/manga", {"filter": "bypopularity", "limit": 15}),
|
||||
2: ("/manga", {"type": "manhwa", "order_by": "popularity", "limit": 15}),
|
||||
3: ("/top/manga", {"filter": "publishing", "limit": 15})
|
||||
}
|
||||
endpoint, params = endpoint_map[choice]
|
||||
|
||||
results = jikan_api_request(endpoint, params=params)
|
||||
if not results or not results.get('data'):
|
||||
print("Could not fetch data for this category.")
|
||||
input("Press Enter...")
|
||||
return
|
||||
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for manga_obj in results['data']:
|
||||
if not any(item['id'] == manga_obj['mal_id'] for item in section['items']):
|
||||
section['items'].append(format_manga_data(manga_obj))
|
||||
added_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
print(f"Added {added_count} new items and skipped {skipped_count} duplicates in '{section['title']}'.")
|
||||
input("Press Enter...")
|
||||
|
||||
def generate_with_ai(section):
|
||||
"""Generates content for a section using Google's Generative AI."""
|
||||
clear_screen()
|
||||
print(f"--- AI Content Generation for: {section['title']} ---")
|
||||
|
||||
# 1. Get and configure API Key
|
||||
try:
|
||||
api_key = get_api_key()
|
||||
genai.configure(api_key=api_key)
|
||||
model = genai.GenerativeModel('gemini-1.5-flash')
|
||||
except Exception as e:
|
||||
print(f"An error occurred while configuring the AI model: {e}")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
# 2. Get user prompt
|
||||
print("Describe the kind of manga/manhwa you want to find.")
|
||||
print("Examples: 'top 10 isekai manga', 'psychological thrillers similar to Monster', 'wholesome slice of life'")
|
||||
user_prompt = input("\nEnter your prompt: ")
|
||||
if not user_prompt:
|
||||
return
|
||||
|
||||
# 3. Query the AI model with improved prompt
|
||||
print("\nAsking the AI for suggestions... this may take a moment.")
|
||||
full_prompt = f"""List exactly 10 manga that fit this description: '{user_prompt}'.
|
||||
|
||||
IMPORTANT: Follow this exact format for your response:
|
||||
- Return ONLY the manga titles
|
||||
- One title per line
|
||||
- No numbers, bullets, dashes, or prefixes
|
||||
- No descriptions or explanations
|
||||
- No extra text before or after the list
|
||||
- Use the most commonly known English or romanized title
|
||||
|
||||
Example format:
|
||||
Berserk
|
||||
Vagabond
|
||||
One Piece
|
||||
|
||||
Your response for '{user_prompt}':"""
|
||||
|
||||
try:
|
||||
response = model.generate_content(full_prompt)
|
||||
if not response.text:
|
||||
print("The AI returned an empty response. Please try again.")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
# Clean and parse the response more robustly
|
||||
ai_suggestions = []
|
||||
lines = response.text.strip().split('\n')
|
||||
|
||||
for line in lines:
|
||||
# Clean each line of common prefixes and formatting
|
||||
cleaned = line.strip()
|
||||
# Remove common prefixes like "1.", "- ", "• ", etc.
|
||||
cleaned = re.sub(r'^[\d\.\-\•\*\s]+', '', cleaned)
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
if cleaned and len(cleaned) > 1: # Ensure it's not just whitespace or single character
|
||||
ai_suggestions.append(cleaned)
|
||||
|
||||
# Limit to 10 suggestions max
|
||||
ai_suggestions = ai_suggestions[:10]
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while communicating with the AI: {e}")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
if not ai_suggestions:
|
||||
print("The AI didn't return any valid suggestions. Try a different prompt.")
|
||||
input("Press Enter to return.")
|
||||
return
|
||||
|
||||
print(f"\nAI suggested {len(ai_suggestions)} manga titles.")
|
||||
|
||||
# 4. Process suggestions: Search Jikan and get user confirmation
|
||||
print("\n--- Confirm AI Suggestions ---")
|
||||
print("For each suggestion, I will find the closest match on MyAnimeList.")
|
||||
print("Please confirm if the match is correct.")
|
||||
|
||||
confirmed_manga = []
|
||||
for i, suggestion in enumerate(ai_suggestions, 1):
|
||||
print(f"\n[{i}/{len(ai_suggestions)}] Searching for: '{suggestion}'...")
|
||||
results = jikan_api_request("/manga", params={"q": suggestion, "limit": 3})
|
||||
|
||||
if not results or not results.get('data'):
|
||||
print(f"--> Could not find any match for '{suggestion}'.")
|
||||
continue
|
||||
|
||||
# Show top match but also alternatives if the first doesn't seem right
|
||||
match = results['data'][0]
|
||||
title = match.get('title_english') or match.get('title')
|
||||
year = match.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
|
||||
|
||||
print(f"--> Best match: '{title}' ({match.get('type', 'N/A')}, {year})")
|
||||
|
||||
# Show alternatives if available
|
||||
if len(results['data']) > 1:
|
||||
print(" Alternatives:")
|
||||
for j, alt in enumerate(results['data'][1:3], 2):
|
||||
alt_title = alt.get('title_english') or alt.get('title')
|
||||
alt_year = alt.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
|
||||
print(f" [{j}] {alt_title} ({alt.get('type', 'N/A')}, {alt_year})")
|
||||
|
||||
while True:
|
||||
if len(results['data']) > 1:
|
||||
choice = input(" Choose: [1] Use best match, [2-3] Use alternative, [s] Skip, [Enter] Use best match: ").strip().lower()
|
||||
else:
|
||||
choice = input(" [Enter] Add this manga, [s] Skip: ").strip().lower()
|
||||
|
||||
if choice == '' or choice == '1':
|
||||
selected_match = results['data'][0]
|
||||
break
|
||||
elif choice == 's':
|
||||
selected_match = None
|
||||
break
|
||||
elif choice in ['2', '3'] and len(results['data']) > int(choice) - 1:
|
||||
selected_match = results['data'][int(choice) - 1]
|
||||
break
|
||||
else:
|
||||
print(" Invalid choice. Please try again.")
|
||||
|
||||
if selected_match:
|
||||
formatted = format_manga_data(selected_match)
|
||||
# Avoid adding duplicates
|
||||
if any(a['id'] == formatted['id'] for a in confirmed_manga):
|
||||
print(f"--> Already added '{formatted['name']}'. Skipping.")
|
||||
else:
|
||||
confirmed_manga.append(formatted)
|
||||
print(f"--> Added '{formatted['name']}' to the list.")
|
||||
else:
|
||||
print(f"--> Skipped '{suggestion}'.")
|
||||
|
||||
# 5. Final review and add to section
|
||||
if not confirmed_manga:
|
||||
print("\nNo new manga were confirmed. Returning to menu.")
|
||||
input("Press Enter...")
|
||||
return
|
||||
|
||||
clear_screen()
|
||||
print("--- Final Review ---")
|
||||
print("The following new manga will be added to the section:")
|
||||
for item in confirmed_manga:
|
||||
print(f"- {item['name']}")
|
||||
|
||||
final_confirm = input("\nAdd these items to the section? [Y/n]: ").lower()
|
||||
if final_confirm == '' or final_confirm == 'y':
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for manga in confirmed_manga:
|
||||
if not any(item['id'] == manga['id'] for item in section['items']):
|
||||
section['items'].append(manga)
|
||||
added_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
print(f"\nSuccessfully added {added_count} new manga.")
|
||||
if skipped_count > 0:
|
||||
print(f"Skipped {skipped_count} manga that were already in the section.")
|
||||
else:
|
||||
print("Operation cancelled. No changes were made.")
|
||||
|
||||
input("Press Enter to continue...")
|
||||
|
||||
|
||||
|
||||
# --- Main Application ---
|
||||
def main():
|
||||
"""Main function to run the content manager."""
|
||||
data = None
|
||||
|
||||
# Check if a content file exists and prompt the user.
|
||||
if os.path.exists(CONTENT_FILE):
|
||||
clear_screen()
|
||||
print("--- Welcome Back ---")
|
||||
print(f"Found existing content file: '{CONTENT_FILE}'")
|
||||
print("\nWhat would you like to do?")
|
||||
print("[1] Load the existing content")
|
||||
print("[2] Start from scratch (Warning: saving will overwrite the old file)")
|
||||
|
||||
while data is None:
|
||||
choice = input("> ")
|
||||
if choice == '1':
|
||||
try:
|
||||
with open(CONTENT_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
# Ensure the basic structure exists, in case the file is malformed
|
||||
if 'spotlight' not in data: data['spotlight'] = []
|
||||
if 'sections' not in data: data['sections'] = []
|
||||
print("Content loaded successfully.")
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
print(f"Error: Could not read or parse '{CONTENT_FILE}'. Starting from scratch.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
elif choice == '2':
|
||||
print("Starting with a blank slate.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
else:
|
||||
print("Invalid choice. Please enter 1 or 2.")
|
||||
input("Press Enter to continue...")
|
||||
else:
|
||||
# If no content file exists, start from scratch automatically.
|
||||
print(f"No '{CONTENT_FILE}' found. Starting with a blank slate.")
|
||||
data = {"spotlight": [], "sections": []}
|
||||
input("Press Enter to continue...")
|
||||
|
||||
|
||||
while True:
|
||||
clear_screen()
|
||||
print("--- Manga Content Manager ---")
|
||||
print(" (with AI-Powered Suggestions)")
|
||||
print("\nSelect an option:")
|
||||
print("[1] Manage Spotlight Section")
|
||||
print("[2] Manage Horizontal Sections")
|
||||
print("[3] Save and Exit")
|
||||
print("[4] Exit Without Saving")
|
||||
|
||||
choice = input("> ")
|
||||
|
||||
if choice == '1':
|
||||
manage_spotlight(data)
|
||||
elif choice == '2':
|
||||
manage_sections(data)
|
||||
elif choice == '3':
|
||||
with open(CONTENT_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
print(f"Content saved to {CONTENT_FILE}.")
|
||||
break
|
||||
elif choice == '4':
|
||||
print("Exiting without saving changes.")
|
||||
break
|
||||
else:
|
||||
print("Invalid option. Please try again.")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
520
animex/manga_content.json
Normal file
@@ -0,0 +1,520 @@
|
||||
{
|
||||
"spotlight": [
|
||||
{
|
||||
"id": 113138,
|
||||
"name": "Jujutsu Kaisen",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/210341l.jpg",
|
||||
"description": "Hidden in plain sight, an age-old conflict rages on. Supernatural monsters known as Curses terrorize humanity from the shadows, and powerful humans known as Jujutsu sorcerers use mystical arts to exterminate them. When high school student Yuuji Itadori finds a dried-up finger of the legendary Curse Sukuna Ryoumen, he suddenly finds himself joining this bloody conflict.\n\nAttacked by a Curse attracted to the finger's power, Yuuji makes a reckless decision to protect himself, gaining the power to combat Curses in the process but also unwittingly unleashing the malicious Sukuna into the world once more. Though Yuuji can control and confine Sukuna to his own body, the Jujutsu world classifies Yuuji as a dangerous, high-level Curse who must be exterminated.\n\nDetained and sentenced to death, Yuuji meets Satoru Gojou\u2014a teacher at Jujutsu High School\u2014who explains that despite his imminent execution, there is an alternative for him. Being a rare vessel to Sukuna, if Yuuji were to die, then Sukuna would perish too. Therefore, if Yuuji were to consume the many other remnants of Sukuna, then Yuuji's subsequent execution would truly eradicate the malicious demon. Taking up this chance to make the world safer and live his life for a little longer, Yuuji enrolls in Tokyo Prefectural Jujutsu High School, jumping headfirst into a harsh and unforgiving battlefield.\n\n[Written by MAL Rewrite]",
|
||||
"year": 2018,
|
||||
"jp_name": "呪術廻戦"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Berserk",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/157897l.jpg",
|
||||
"description": "Guts, a former mercenary now known as the Black Swordsman, is out for revenge. After a tumultuous childhood, he finally finds someone he respects and believes he can trust, only to have everything fall apart when this person takes away everything important to Guts for the purpose of fulfilling his own desires. Now marked for death, Guts becomes condemned to a fate in which he is relentlessly pursued by demonic beings.\n\nSetting out on a dreadful quest riddled with misfortune, Guts, armed with a massive sword and monstrous strength, will let nothing stop him, not even death itself, until he is finally able to take the head of the one who stripped him\u2014and his loved one\u2014of their humanity.\n\n[Written by MAL Rewrite]\n\nIncluded one-shot:\nVolume 14: Berserk: The Prototype",
|
||||
"year": 1989,
|
||||
"jp_name": "ベルセルク"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "One Piece",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/253146l.jpg",
|
||||
"description": "Gol D. Roger, a man referred to as the King of the Pirates, is set to be executed by the World Government. But just before his demise, he confirms the existence of a great treasure, One Piece, located somewhere within the vast ocean known as the Grand Line. Announcing that One Piece can be claimed by anyone worthy enough to reach it, the King of the Pirates is executed and the Great Age of Pirates begins.\n\nTwenty-two years later, a young man by the name of Monkey D. Luffy is ready to embark on his own adventure, searching for One Piece and striving to become the new King of the Pirates. Armed with just a straw hat, a small boat, and an elastic body, he sets out on a fantastic journey to gather his own crew and a worthy ship that will take them across the Grand Line to claim the greatest status on the high seas.\n\n[Written by MAL Rewrite]",
|
||||
"year": 1997,
|
||||
"jp_name": "ワンピース"
|
||||
},
|
||||
{
|
||||
"id": 96792,
|
||||
"name": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/179023l.jpg",
|
||||
"description": "Tanjirou Kamado lives with his impoverished family on a remote mountain. As the oldest sibling, he took upon the responsibility of ensuring his family's livelihood after the death of his father. On a cold winter day, he goes down to the local village in order to sell some charcoal. As dusk falls, he is forced to spend the night in the house of a curious man who cautions him of strange creatures that roam the night: malevolent demons who crave human flesh.\n\nWhen he finally makes his way home, Tanjirou's worst nightmare comes true. His entire family has been brutally slaughtered with the sole exception of his sister Nezuko, who has turned into a flesh-eating demon. Engulfed in hatred and despair, Tanjirou desperately tries to stop Nezuko from attacking other people, setting out on a journey to avenge his family and find a way to turn his beloved sister back into a human.\n\n[Written by MAL Rewrite]",
|
||||
"year": 2016,
|
||||
"jp_name": "鬼滅の刃"
|
||||
},
|
||||
{
|
||||
"id": 23390,
|
||||
"name": "Attack on Titan",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/37846l.jpg",
|
||||
"description": "Hundreds of years ago, horrifying creatures which resembled humans appeared. These mindless, towering giants, called Titans, proved to be an existential threat, as they preyed on whatever humans they could find in order to satisfy a seemingly unending appetite. Unable to effectively combat the Titans, mankind was forced to barricade themselves within large walls surrounding what may very well be humanity's last safe haven in the world.\n\nIn the present day, life within the walls has finally found peace, since the residents have not dealt with Titans for many years. Eren Yeager, Mikasa Ackerman, and Armin Arlert are three young children who dream of experiencing all that the world has to offer, having grown up hearing stories of the wonders beyond the walls. But when the state of tranquility is suddenly shattered by the attack of a massive 60-meter Titan, they quickly learn just how cruel the world can be. On that day, Eren makes a promise to himself that he will do whatever it takes to eradicate every single Titan off the face of the Earth, with the hope that one day, humanity will once again be able to live outside the walls without fear.\n\n[Written by MAL Rewrite]",
|
||||
"year": 2009,
|
||||
"jp_name": "進撃の巨人"
|
||||
},
|
||||
{
|
||||
"id": 116778,
|
||||
"name": "Chainsaw Man",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/216464l.jpg",
|
||||
"description": "Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his crushing debts. Using his pet devil Pochita as a weapon, he is ready to do anything for a bit of cash.\n\nUnfortunately, he has outlived his usefulness and is murdered by a devil in contract with the yakuza. However, in an unexpected turn of events, Pochita merges with Denji's dead body and grants him the powers of a chainsaw devil. Now able to transform parts of his body into chainsaws, a revived Denji uses his new abilities to quickly and brutally dispatch his enemies. Catching the eye of the official devil hunters who arrive at the scene, he is offered work at the Public Safety Bureau as one of them. Now with the means to face even the toughest of enemies, Denji will stop at nothing to achieve his simple teenage dreams.\n\n[Written by MAL Rewrite]",
|
||||
"year": 2018,
|
||||
"jp_name": "チェンソーマン"
|
||||
},
|
||||
{
|
||||
"id": 121496,
|
||||
"name": "Solo Leveling",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/222295l.jpg",
|
||||
"description": "Ten years ago, \"the Gate\" appeared and connected the real world with the realm of magic and monsters. To combat these vile beasts, ordinary people received superhuman powers and became known as \"Hunters.\" Twenty-year-old Sung Jin-Woo is one such Hunter, but he is known as the \"World's Weakest,\" owing to his pathetic power compared to even a measly E-Rank. Still, he hunts monsters tirelessly in low-rank Gates to pay for his mother's medical bills. \n\nHowever, this miserable lifestyle changes when Jin-Woo\u2014believing himself to be the only one left to die in a mission gone terribly wrong\u2014awakens in a hospital three days later to find a mysterious screen floating in front of him. This \"Quest Log\" demands that Jin-Woo completes an unrealistic and intense training program, or face an appropriate penalty. Initially reluctant to comply because of the quest's rigor, Jin-Woo soon finds that it may just transform him into one of the world's most fearsome Hunters. \n\n[Written by MAL Rewrite]",
|
||||
"year": 2018,
|
||||
"jp_name": "ソロレベル"
|
||||
},
|
||||
{
|
||||
"id": 642,
|
||||
"name": "Vinland Saga",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/188925l.jpg",
|
||||
"description": "Thorfinn, son of one of the Vikings' greatest warriors, is among the finest fighters in the merry band of mercenaries run by the cunning Askeladd, an impressive feat for a person his age. However, Thorfinn is not part of the group for the plunder it entails\u2014instead, for having caused his family great tragedy, the boy has vowed to kill Askeladd in a fair duel. Not yet skilled enough to defeat him, but unable to abandon his vengeance, Thorfinn spends his boyhood with the mercenary crew, honing his skills on the battlefield among the war-loving Danes, where killing is just another pleasure of life.\n\nOne day, when Askeladd receives word that Danish prince Canute has been taken hostage, he hatches an ambitious plot\u2014one that will decide the next King of England and drastically alter the lives of Thorfinn, Canute, and himself. Set in 11th-century Europe, Vinland Saga tells a bloody epic in an era where violence, madness, and injustice are inescapable, providing a paradise for the battle-crazed and utter hell for the rest who live in it.\n\n[Written by MAL Rewrite]\n\nIncluded one-shots:\nVolume 23: Sayounara ga Chikai node (For Our Farewell Is Near)\nVolume 25: \"Assassin's Creed Valhalla\" Collab Bangai-hen (Assassin's Creed x Vinland Saga)",
|
||||
"year": 2005,
|
||||
"jp_name": "ヴィンランド・サガ"
|
||||
}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"title": "Fan Favorites",
|
||||
"items": [
|
||||
{
|
||||
"id": 25,
|
||||
"name": "Fullmetal Alchemist",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/243675l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Monster",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/258224l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 656,
|
||||
"name": "Vagabond",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/259070l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 90125,
|
||||
"name": "Kaguya-sama: Love Is War",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/188896l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Death Note",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/258245l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 44489,
|
||||
"name": "Land of the Lustrous",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/115443l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "Hunter x Hunter",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/253119l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 657,
|
||||
"name": "Real",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/115969l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trending Now",
|
||||
"items": [
|
||||
{
|
||||
"id": 126146,
|
||||
"name": "[Oshi no Ko]",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/233991l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 119161,
|
||||
"name": "Spy x Family",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/219741l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 113138,
|
||||
"name": "Jujutsu Kaisen",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/210341l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 118855,
|
||||
"name": "That Summer",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/228279l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 133800,
|
||||
"name": "Kedamono-tachi no Jikan: Kyouizon Shoukougun",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/249520l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 114755,
|
||||
"name": "Change the World: Bloodthirsty Killer from Today",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/219770l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 127907,
|
||||
"name": "Kaiju No. 8",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/252929l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 129868,
|
||||
"name": "Tsuyuno-chan Likes to Show Off",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/234412l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Action-Packed Adventures",
|
||||
"items": [
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Naruto",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/249658l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Bleach",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/180031l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "Dragon Ball",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/267793l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 86337,
|
||||
"name": "Black Clover",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/166254l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 75989,
|
||||
"name": "My Hero Academia",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/209370l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 31499,
|
||||
"name": "Nisekoi: False Love",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/181212l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 100128,
|
||||
"name": "The Promised Neverland",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/186922l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Yokohama Kaidashi Kikou",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/171813l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Dark & Gritty",
|
||||
"items": [
|
||||
{
|
||||
"id": 642,
|
||||
"name": "Vinland Saga",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/188925l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 4632,
|
||||
"name": "Goodnight Punpun",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/266834l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 33327,
|
||||
"name": "Tokyo Ghoul",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/114037l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 100448,
|
||||
"name": "I sold my life for ten thousand yen per year.",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/5/260043l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 3006,
|
||||
"name": "JoJo's Bizarre Adventure Part 4: Diamond Is Unbreakable",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/269910l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 651,
|
||||
"name": "Blade of the Immortal",
|
||||
"image": "https://media.kitsu.app/manga/poster_images/1487/large.jpg"
|
||||
},
|
||||
{
|
||||
"id": 104314,
|
||||
"name": "Jagaaaaaan",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/273180l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1706,
|
||||
"name": "JoJo's Bizarre Adventure Part 7: Steel Ball Run",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/179882l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Otherworldly Fantasies",
|
||||
"items": [
|
||||
{
|
||||
"id": 70259,
|
||||
"name": "Mushoku Tensei: Jobless Reincarnation",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/181049l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 126287,
|
||||
"name": "Frieren: Beyond Journey's End",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/232121l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 89554,
|
||||
"name": "That Time I Got Reincarnated as a Slime",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/157796l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 110485,
|
||||
"name": "Record of Ragnarok",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/209957l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 85737,
|
||||
"name": "Sensei no Sumika",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/148845l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 118586,
|
||||
"name": "Sousei no Onmyouji: Tenen Jakko - Nishoku Kokkeiga",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/232292l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 98436,
|
||||
"name": "The Saga of Tanya the Evil",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/186468l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 44347,
|
||||
"name": "One-Punch Man",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/80661l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Heartwarming & Wholesome",
|
||||
"items": [
|
||||
{
|
||||
"id": 104,
|
||||
"name": "Yotsuba&!",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/5/259524l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 13245,
|
||||
"name": "Chihayafuru",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/245072l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 42451,
|
||||
"name": "Horimiya",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/245008l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 35507,
|
||||
"name": "Arcana Famiglia: Amore Mangiare Cantare!",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/81392l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 85802,
|
||||
"name": "The boy who cried wolf tells a lie today also",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/148976l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 99007,
|
||||
"name": "Komi Can't Communicate",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/188962l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 117060,
|
||||
"name": "My Dress-Up Darling",
|
||||
"image": "https://media.kitsu.app/manga/poster_images/41006/large.jpg"
|
||||
},
|
||||
{
|
||||
"id": 96573,
|
||||
"name": "Something About Us",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/189603l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Love is in the Air",
|
||||
"items": [
|
||||
{
|
||||
"id": 103851,
|
||||
"name": "The Quintessential Quintuplets",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/201572l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 116539,
|
||||
"name": "Destroy All Humans. They Can't Be Regenerated.",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/220187l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 14455,
|
||||
"name": "Little Butterfly: Gush Limited",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/20679l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 128594,
|
||||
"name": "One Piece: Ace's Story",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/266074l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 109229,
|
||||
"name": "Yakuza Fianc\u00e9: Raise wa Tanin ga Ii",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/203176l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 56805,
|
||||
"name": "A Silent Voice",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/120529l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 70851,
|
||||
"name": "Futanari Musume ni Okasarechau!",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/121643l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "One Piece",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/253146l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Manhwa Masterpieces",
|
||||
"items": [
|
||||
{
|
||||
"id": 121496,
|
||||
"name": "Solo Leveling",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/222295l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 122663,
|
||||
"name": "Tower of God",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/223694l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 8586,
|
||||
"name": "The Breaker",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/270151l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 111213,
|
||||
"name": "Bastard",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/205549l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 125036,
|
||||
"name": "The Horizon",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/229331l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 121948,
|
||||
"name": "Can I Have You All to Myself?",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/235219l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 110938,
|
||||
"name": "Hoshi no Koe: The Voices of a Distant Star",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/240830l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 116966,
|
||||
"name": "Hidoku Shinaide dj - Akakute Oishii",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/305350l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mind Games & Mysteries",
|
||||
"items": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "20th Century Boys",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/5/260006l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 7458,
|
||||
"name": "Death Note Another Note: The Los Angeles BB Murder Cases",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/253578l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 10010,
|
||||
"name": "Beelzebub",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/188942l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 108639,
|
||||
"name": "Tsuiraku JK to Haijin Kyoushi",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/301288l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 393,
|
||||
"name": "The Kindaichi Case Files",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/84823l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 139158,
|
||||
"name": "Tomodachi Game",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/261962l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 13601,
|
||||
"name": "Be Free!",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/4/76293l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 3573,
|
||||
"name": "Tobaku Hakairoku Kaiji",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/302202l.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "School Life & Sports",
|
||||
"items": [
|
||||
{
|
||||
"id": 35243,
|
||||
"name": "Haikyu!!",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/258225l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "Ouran High School Host Club",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/267782l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 107931,
|
||||
"name": "Blue Period",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/204827l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 150863,
|
||||
"name": "Windbreak Samurai",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/296855l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 418,
|
||||
"name": "Mushishi",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/159514l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1061,
|
||||
"name": "Case Closed",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/1/97267l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 11211,
|
||||
"name": "Tenshi no Tame no Shohousen.",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/3/275687l.jpg"
|
||||
},
|
||||
{
|
||||
"id": 78523,
|
||||
"name": "ReLIFE",
|
||||
"image": "https://cdn.myanimelist.net/images/manga/2/171573l.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
240
animex/offline.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Offline - Media App</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="manifest" href="Resources/manifest.json/">
|
||||
<meta name="theme-color" content="#0b0b0b" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/all.css"
|
||||
>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/sharp-solid.css"
|
||||
>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/sharp-regular.css"
|
||||
>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/sharp-light.css"
|
||||
>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/duotone.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://site-assets.fontawesome.com/releases/v6.7.2/css/brands.css"
|
||||
/>
|
||||
|
||||
<link rel="icon" href="/Resources/Images/icon-196.png" type="image/png">
|
||||
<!-- iOS support -->
|
||||
<link rel="apple-touch-icon" href="/Resources/Images/icon-196.png">
|
||||
<style>
|
||||
/* --- REDESIGNED UI WITH GLASSMORPHISM --- */
|
||||
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--glass-bg: rgba(35, 35, 40, 0.6);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--blur-amount: 12px;
|
||||
--border-radius-lg: 18px;
|
||||
--border-radius-md: 12px;
|
||||
--border-radius-sm: 8px;
|
||||
|
||||
--text-primary: #F5F5F7;
|
||||
--text-secondary: #A1A1A6;
|
||||
--accent-color: #FF9500;
|
||||
--accent-color-hover: #ffae45;
|
||||
--action-color: #FF9500; /* Changed for Retry button */
|
||||
--action-color-hover: #ffae45;
|
||||
|
||||
--font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* --- Global & Base Styles --- */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-family);
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Decorative background blobs for depth */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -20vh;
|
||||
left: -20vw;
|
||||
width: 60vw;
|
||||
height: 60vw;
|
||||
background: radial-gradient(circle, rgba(255, 149, 0, 0.2), transparent 70%);
|
||||
filter: blur(100px);
|
||||
z-index: -1;
|
||||
animation: spin 30s linear infinite alternate;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
bottom: -30vh;
|
||||
right: -30vw;
|
||||
width: 80vw;
|
||||
height: 80vw;
|
||||
background: radial-gradient(circle, rgba(80, 20, 150, 0.15), transparent 70%);
|
||||
filter: blur(120px);
|
||||
z-index: -1;
|
||||
animation: spin 40s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg) scale(1); }
|
||||
to { transform: rotate(360deg) scale(1.1); }
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* --- Glass Pane Utility --- */
|
||||
.glass-pane {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--blur-amount));
|
||||
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* --- Offline Controls --- */
|
||||
.offline-controls {
|
||||
padding: 40px 20px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
color: var(--text-secondary);
|
||||
margin-top: -15px; /* Pull up closer to title */
|
||||
margin-bottom: 25px;
|
||||
max-width: 350px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
text-decoration: none; /* For <a> tag */
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out, transform 0.1s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.action-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.action-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#retryButton {
|
||||
background-color: var(--action-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
#retryButton:hover {
|
||||
background-color: var(--action-color-hover);
|
||||
}
|
||||
|
||||
#importButton {
|
||||
background-color: var(--accent-color);
|
||||
color: #000;
|
||||
}
|
||||
#importButton:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
/* --- Media Queries --- */
|
||||
@media (max-width: 600px) {
|
||||
.main-content { margin: 20px auto; padding: 0 15px; }
|
||||
.section-title { font-size: 1.5rem; }
|
||||
:root { --border-radius-lg: 16px; --border-radius-md: 10px; }
|
||||
.offline-controls { padding: 30px 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="main-content">
|
||||
<div class="offline-controls glass-pane">
|
||||
<i class="fas fa-wifi-slash offline-icon"></i>
|
||||
|
||||
<h2 class="section-title">No Internet Connection</h2>
|
||||
<p class="offline-message">
|
||||
You appear to be offline. You can retry the connection or access your offline library.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="retryButton" class="action-button" onclick="window.location.reload()">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
Retry
|
||||
</button>
|
||||
<a href="library.html" id="importButton" class="action-button">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
Go to Library
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- No script tag needed for this static page -->
|
||||
</body>
|
||||
</html>
|
||||
503
animex/pdf.html
Normal file
@@ -0,0 +1,503 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Animex PDF Reader</title>
|
||||
|
||||
<!-- PDF.js Library from CDN -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-bg: #121212;
|
||||
--secondary-bg: rgba(30, 30, 30, 0.9);
|
||||
--text-color: #e0e0e0;
|
||||
--accent-color: #FF9500;
|
||||
--border-color: #333333;
|
||||
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||
--icon-fill: #e0e0e0;
|
||||
--header-height: 55px;
|
||||
--footer-height: 60px;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0; width: 100%; height: 100%;
|
||||
background-color: var(--primary-bg); color: var(--text-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app-container { display: flex; flex-direction: column; height: 100vh; width: 100vw; position: relative; }
|
||||
|
||||
/* --- Welcome/Message Screen --- */
|
||||
#message-container {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
background-color: var(--primary-bg); z-index: 100;
|
||||
flex-direction: column; padding: 20px; box-sizing: border-box; text-align: center;
|
||||
}
|
||||
.loader {
|
||||
border: 5px solid var(--border-color); border-top: 5px solid var(--accent-color);
|
||||
border-radius: 50%; width: 50px; height: 50px;
|
||||
animation: spin 1s linear infinite; margin-bottom: 20px;
|
||||
}
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
#file-input-button {
|
||||
background-color: var(--accent-color); color: #121212; border: none; padding: 12px 24px;
|
||||
border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; display: none; margin-top: 15px;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
#file-input-button:hover { background-color: #ffae42; transform: scale(1.05); }
|
||||
#file-input { display: none; }
|
||||
|
||||
/* --- Viewer --- */
|
||||
#viewer-container {
|
||||
flex-grow: 1; position: relative; overflow: auto;
|
||||
display: flex; justify-content: center;
|
||||
padding-top: calc(var(--header-height) + 20px);
|
||||
padding-bottom: calc(var(--footer-height) + 20px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#pdf-container canvas {
|
||||
display: block; margin: 0 auto;
|
||||
max-width: 100%; height: auto;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
#pdf-container.paged-view canvas {
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 8px 25px var(--shadow-color);
|
||||
}
|
||||
#pdf-container.webtoon-view canvas {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
/* --- Controls --- */
|
||||
.controls-bar {
|
||||
position: fixed; left: 0; width: 100%;
|
||||
background-color: var(--secondary-bg);
|
||||
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 0 15px var(--shadow-color);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0 20px; box-sizing: border-box; z-index: 20;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
#header { top: 0; height: var(--header-height); border-bottom: 1px solid var(--border-color);}
|
||||
#footer { bottom: 0; height: var(--footer-height); border-top: 1px solid var(--border-color);}
|
||||
.controls-hidden #header { transform: translateY(-100%); }
|
||||
.controls-hidden #footer { transform: translateY(100%); }
|
||||
|
||||
.control-group { display: flex; align-items: center; gap: 8px; }
|
||||
.control-button {
|
||||
background: none; border: none; color: var(--icon-fill);
|
||||
cursor: pointer; padding: 10px; border-radius: 50%;
|
||||
width: 44px; height: 44px; display: flex; justify-content: center; align-items: center;
|
||||
font-size: 18px; transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
.control-button:hover:not(:disabled) { background-color: rgba(255, 255, 255, 0.1); }
|
||||
.control-button:disabled { color: #666; cursor: not-allowed; }
|
||||
.control-button.active { color: var(--accent-color); background-color: rgba(255, 149, 0, 0.15); }
|
||||
|
||||
#file-name {
|
||||
font-size: 16px; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; max-width: calc(100vw - 400px);
|
||||
}
|
||||
#series-title, #chapter-title {
|
||||
max-width: calc(100vw - 400px);
|
||||
}
|
||||
#page-info { font-size: 16px; min-width: 90px; text-align: center; cursor: pointer; padding: 8px 12px; border-radius: 20px; transition: background-color 0.2s ease; user-select: none; }
|
||||
#page-info:hover { background-color: rgba(255, 255, 255, 0.1); }
|
||||
#page-input { width: 50px; background-color: var(--primary-bg); color: var(--text-color); border: 1px solid var(--accent-color); text-align: center; font-size: 16px; border-radius: 4px; }
|
||||
.divider { width: 1px; height: 25px; background-color: var(--border-color); margin: 0 10px; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#series-title, #chapter-title { max-width: calc(100vw - 250px); }
|
||||
#file-name { display: none; }
|
||||
.divider:not(.mobile-visible) { display: none; }
|
||||
.control-group { gap: 5px; }
|
||||
.controls-bar { padding: 0 10px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container">
|
||||
<!-- Initial Message/Loader -->
|
||||
<div id="message-container">
|
||||
<div id="loader" class="loader"></div>
|
||||
<p id="message-text">Loading...</p>
|
||||
<input type="file" id="file-input" accept="application/pdf" />
|
||||
<button id="file-input-button">Choose a Local PDF</button>
|
||||
</div>
|
||||
|
||||
<!-- PDF Viewer Area -->
|
||||
<div id="viewer-container">
|
||||
<div id="pdf-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Top Controls -->
|
||||
<header id="header" class="controls-bar">
|
||||
<div class="control-group">
|
||||
<!-- <button id="back-button" class="control-button" title="Back" style="display: none;"><i class="fa-solid fa-arrow-left"></i></button> -->
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-start; margin-left: 10px;">
|
||||
<span id="series-title" style="font-size: 1em; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></span>
|
||||
<span id="chapter-title" style="font-size: 0.8em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.8;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="view-webtoon-btn" class="control-button" title="Webtoon View"><i class="fa-solid fa-arrows-up-down"></i></button>
|
||||
<button id="view-paged-btn" class="control-button" title="Paged View"><i class="fa-solid fa-file-lines"></i></button>
|
||||
<div class="divider"></div>
|
||||
<button id="rotate-button" class="control-button" title="Rotate Clockwise"><i class="fa-solid fa-rotate-right"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Bottom Controls -->
|
||||
<footer id="footer" class="controls-bar">
|
||||
<div class="control-group">
|
||||
<button id="zoom-out-button" class="control-button" title="Zoom Out"><i class="fa-solid fa-magnifying-glass-minus"></i></button>
|
||||
<button id="zoom-fit-width-button" class="control-button" title="Fit to Width"><i class="fa-solid fa-arrows-left-right-to-line"></i></button>
|
||||
<button id="zoom-fit-page-button" class="control-button" title="Fit to Page"><i class="fa-solid fa-up-right-and-down-left-from-center"></i></button>
|
||||
<button id="zoom-in-button" class="control-button" title="Zoom In"><i class="fa-solid fa-magnifying-glass-plus"></i></button>
|
||||
</div>
|
||||
<div id="page-controls" class="control-group">
|
||||
<span id="page-info">0 / 0</span>
|
||||
</div>
|
||||
<div id="progress-bar-container" style="display: none; width: 200px; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden;">
|
||||
<div id="progress-bar" style="width: 0%; height: 100%; background-color: var(--accent-color);"></div>
|
||||
</div>
|
||||
<div class="control-group" style="min-width: 176px; justify-content: flex-end;">
|
||||
<button id="next-chapter-button" class="control-button" title="Next Chapter" style="display: none;"><i class="fa-solid fa-step-forward"></i></button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js`;
|
||||
|
||||
// State variables
|
||||
let pdfDoc = null, currentPageNum = 1, totalPages = 0;
|
||||
let currentScale = 'auto', currentRotation = 0, currentMode = 'paged';
|
||||
let isRendering = false, pdfUrl = null, autoHideTimeout = null;
|
||||
let touchStartX = 0, touchMoveX = 0;
|
||||
|
||||
// DOM elements
|
||||
const appContainer = document.getElementById('app-container');
|
||||
const viewerContainer = document.getElementById('viewer-container');
|
||||
const pdfContainer = document.getElementById('pdf-container');
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
const messageContainer = document.getElementById('message-container');
|
||||
const loader = document.getElementById('loader');
|
||||
const messageText = document.getElementById('message-text');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const fileInputButton = document.getElementById('file-input-button');
|
||||
const seriesTitleEl = document.getElementById('series-title');
|
||||
const chapterTitleEl = document.getElementById('chapter-title');
|
||||
|
||||
// --- PDF Loading and Rendering ---
|
||||
async function loadAndRenderPdf(source, seriesTitle, chapterTitle) {
|
||||
try {
|
||||
showLoadingState(`Loading PDF...`);
|
||||
pdfUrl = source;
|
||||
const loadingTask = pdfjsLib.getDocument(source);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
totalPages = pdfDoc.numPages;
|
||||
currentPageNum = 1;
|
||||
currentRotation = 0;
|
||||
|
||||
seriesTitleEl.textContent = seriesTitle || 'Document';
|
||||
chapterTitleEl.textContent = chapterTitle || '';
|
||||
document.title = seriesTitle ? `${seriesTitle} - ${chapterTitle} - Reader` : 'PDF Reader';
|
||||
|
||||
messageContainer.style.display = 'none';
|
||||
appContainer.classList.remove('controls-hidden');
|
||||
|
||||
await detectAndSetLayout(); // Smart layout detection
|
||||
setupEventListeners();
|
||||
showControls();
|
||||
} catch (error) {
|
||||
console.error("Error loading PDF:", error);
|
||||
showErrorState(`Error: Could not load PDF. Check file/URL.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Smart feature from Animex
|
||||
async function detectAndSetLayout() {
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const isWebtoon = viewport.height > viewport.width * 2;
|
||||
setViewMode(isWebtoon ? 'webtoon' : 'paged', true); // Set mode without re-rendering yet
|
||||
await updateView(); // Now render with the correct mode
|
||||
}
|
||||
|
||||
async function renderPage(num) {
|
||||
if (isRendering) return;
|
||||
isRendering = true;
|
||||
|
||||
try {
|
||||
const page = await pdfDoc.getPage(num);
|
||||
const scale = await calculateScale();
|
||||
const viewport = page.getViewport({ scale, rotation: currentRotation });
|
||||
|
||||
const canvasId = `page-${num}`;
|
||||
let canvas = document.getElementById(canvasId);
|
||||
if (!canvas) { return; }
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
||||
canvas.dataset.rendered = "true";
|
||||
} finally {
|
||||
isRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateView() {
|
||||
if (!pdfDoc) return;
|
||||
|
||||
pdfContainer.innerHTML = '';
|
||||
viewerContainer.scrollTop = 0;
|
||||
|
||||
if (currentMode === 'paged') {
|
||||
viewerContainer.style.overflow = 'hidden';
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = `page-${currentPageNum}`;
|
||||
pdfContainer.appendChild(canvas);
|
||||
await renderPage(currentPageNum);
|
||||
} else { // webtoon mode
|
||||
viewerContainer.style.overflow = 'auto';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = `page-${i}`;
|
||||
pdfContainer.appendChild(canvas);
|
||||
}
|
||||
lazyLoadVisiblePages();
|
||||
}
|
||||
updateControls();
|
||||
}
|
||||
|
||||
async function calculateScale() {
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1.0, rotation: currentRotation });
|
||||
const containerWidth = viewerContainer.clientWidth - 30;
|
||||
const containerHeight = viewerContainer.clientHeight - 30;
|
||||
|
||||
if (currentScale === 'auto') {
|
||||
return Math.min(containerWidth / viewport.width, containerHeight / viewport.height);
|
||||
} else if (currentScale === 'width') {
|
||||
return containerWidth / viewport.width;
|
||||
}
|
||||
return currentScale; // It's a number
|
||||
}
|
||||
|
||||
// --- UI and Controls ---
|
||||
function updateControls() {
|
||||
pageInfo.textContent = `PG ${currentPageNum} / ${totalPages}`;
|
||||
|
||||
document.getElementById('view-paged-btn').classList.toggle('active', currentMode === 'paged');
|
||||
document.getElementById('view-webtoon-btn').classList.toggle('active', currentMode === 'webtoon');
|
||||
|
||||
['zoom-fit-page-button', 'zoom-fit-width-button'].forEach(id => document.getElementById(id).classList.remove('active'));
|
||||
if(currentScale === 'auto') document.getElementById('zoom-fit-page-button').classList.add('active');
|
||||
if(currentScale === 'width') document.getElementById('zoom-fit-width-button').classList.add('active');
|
||||
}
|
||||
|
||||
function showLoadingState(message) {
|
||||
messageContainer.style.display = 'flex';
|
||||
loader.style.display = 'block';
|
||||
fileInputButton.style.display = 'none';
|
||||
messageText.textContent = message;
|
||||
}
|
||||
|
||||
function showErrorState(message) {
|
||||
loader.style.display = 'none';
|
||||
fileInputButton.style.display = 'block';
|
||||
messageText.innerHTML = message;
|
||||
}
|
||||
|
||||
function showControls() {
|
||||
clearTimeout(autoHideTimeout);
|
||||
appContainer.classList.remove('controls-hidden');
|
||||
autoHideTimeout = setTimeout(() => {
|
||||
if(!document.querySelector('#page-input')) appContainer.classList.add('controls-hidden');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// --- Event Handlers ---
|
||||
function goToPrevPage() { if (currentPageNum > 1) { currentPageNum--; updateView(); } }
|
||||
function goToNextPage() { if (currentPageNum < totalPages) { currentPageNum++; updateView(); } }
|
||||
|
||||
function goToPage(num) {
|
||||
const pageNumber = parseInt(num);
|
||||
if (pageNumber > 0 && pageNumber <= totalPages) {
|
||||
currentPageNum = pageNumber;
|
||||
if (currentMode === 'paged') {
|
||||
updateView();
|
||||
} else {
|
||||
document.getElementById(`page-${currentPageNum}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setZoom(type) {
|
||||
if (type === 'in') currentScale = (typeof currentScale !== 'number' ? 1.0 : currentScale) + 0.2;
|
||||
else if (type === 'out') currentScale = Math.max(0.2, (typeof currentScale !== 'number' ? 1.0 : currentScale) - 0.2);
|
||||
else currentScale = type;
|
||||
updateView();
|
||||
}
|
||||
|
||||
function rotate() { currentRotation = (currentRotation + 90) % 360; updateView(); }
|
||||
|
||||
function setViewMode(mode, isInitial = false) {
|
||||
if (currentMode === mode && !isInitial) return;
|
||||
currentMode = mode;
|
||||
pdfContainer.className = `${mode}-view`;
|
||||
|
||||
const pageControls = document.getElementById('page-controls');
|
||||
const progressBar = document.getElementById('progress-bar-container');
|
||||
|
||||
if (mode === 'webtoon') {
|
||||
pageControls.style.display = 'none';
|
||||
progressBar.style.display = 'block';
|
||||
} else {
|
||||
pageControls.style.display = 'flex';
|
||||
progressBar.style.display = 'none';
|
||||
}
|
||||
|
||||
if (!isInitial) updateView(); // Only re-render if it's a manual switch
|
||||
}
|
||||
|
||||
// --- Lazy Loading for Webtoon Mode ---
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const canvas = entry.target;
|
||||
const pageNum = parseInt(canvas.id.split('-')[1]);
|
||||
|
||||
if (entry.intersectionRatio > 0.5) {
|
||||
if (currentPageNum !== pageNum) {
|
||||
currentPageNum = pageNum;
|
||||
updateControls();
|
||||
}
|
||||
}
|
||||
if (canvas.dataset.rendered !== "true") renderPage(pageNum);
|
||||
}
|
||||
});
|
||||
}, { root: viewerContainer, threshold: [0.1, 0.5, 0.9] });
|
||||
|
||||
function lazyLoadVisiblePages() {
|
||||
observer.disconnect();
|
||||
pdfContainer.querySelectorAll('canvas').forEach(canvas => observer.observe(canvas));
|
||||
}
|
||||
|
||||
// --- Setup and Initialization ---
|
||||
function setupEventListeners() {
|
||||
if (setupEventListeners.bound) return;
|
||||
setupEventListeners.bound = true;
|
||||
|
||||
// Header/Footer Controls
|
||||
document.getElementById('view-webtoon-btn').addEventListener('click', () => setViewMode('webtoon'));
|
||||
document.getElementById('view-paged-btn').addEventListener('click', () => setViewMode('paged'));
|
||||
document.getElementById('rotate-button').addEventListener('click', rotate);
|
||||
document.getElementById('zoom-in-button').addEventListener('click', () => setZoom('in'));
|
||||
document.getElementById('zoom-out-button').addEventListener('click', () => setZoom('out'));
|
||||
document.getElementById('zoom-fit-page-button').addEventListener('click', () => setZoom('auto'));
|
||||
document.getElementById('zoom-fit-width-button').addEventListener('click', () => setZoom('width'));
|
||||
|
||||
pageInfo.addEventListener('click', () => {
|
||||
pageInfo.innerHTML = `<input id="page-input" type="number" min="1" max="${totalPages}" value="${currentPageNum}" />`;
|
||||
const pageInput = document.getElementById('page-input');
|
||||
pageInput.focus(); pageInput.select();
|
||||
const revert = () => { if (document.getElementById('page-input')) updateControls(); };
|
||||
pageInput.addEventListener('blur', revert);
|
||||
pageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { goToPage(pageInput.value); pageInput.blur(); }
|
||||
if (e.key === 'Escape') pageInput.blur();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
if (e.key === 'ArrowLeft') goToPrevPage();
|
||||
else if (e.key === 'ArrowRight') goToNextPage();
|
||||
});
|
||||
|
||||
['mousemove', 'mousedown', 'touchstart'].forEach(evt => window.addEventListener(evt, showControls));
|
||||
|
||||
viewerContainer.addEventListener('touchstart', (e) => { if(currentMode === 'paged') { touchStartX = e.changedTouches[0].screenX; touchMoveX = touchStartX; } }, { passive: true });
|
||||
viewerContainer.addEventListener('touchmove', (e) => { if(currentMode === 'paged') { touchMoveX = e.changedTouches[0].screenX; } }, { passive: true });
|
||||
viewerContainer.addEventListener('touchend', () => { if(currentMode === 'paged') { if (touchStartX - touchMoveX > 50) goToNextPage(); else if (touchMoveX - touchStartX > 50) goToPrevPage(); } });
|
||||
|
||||
viewerContainer.addEventListener('scroll', () => {
|
||||
if (currentMode === 'webtoon') {
|
||||
const { scrollTop, scrollHeight, clientHeight } = viewerContainer;
|
||||
const scrollPercent = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
||||
document.getElementById('progress-bar').style.width = `${scrollPercent}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fileUrlParam = params.get('file');
|
||||
const seriesTitleParam = params.get('seriesTitle');
|
||||
const chapterTitleParam = params.get('chapterTitle');
|
||||
const isEmbedded = params.get('embedded');
|
||||
const seriesIdParam = params.get('seriesId');
|
||||
const typeParam = params.get('type');
|
||||
const nextItemNumParam = params.get('nextItemNum');
|
||||
|
||||
if (seriesIdParam && typeParam && nextItemNumParam) {
|
||||
const nextChapterButton = document.getElementById('next-chapter-button');
|
||||
nextChapterButton.style.display = 'flex';
|
||||
nextChapterButton.addEventListener('click', () => {
|
||||
window.parent.postMessage({
|
||||
type: 'pdf-reader-next',
|
||||
seriesId: seriesIdParam,
|
||||
type: typeParam,
|
||||
itemNum: nextItemNumParam
|
||||
}, '*');
|
||||
});
|
||||
}
|
||||
|
||||
if (isEmbedded) {
|
||||
const backButton = document.getElementById('back-button');
|
||||
backButton.style.display = 'flex';
|
||||
backButton.addEventListener('click', () => {
|
||||
window.parent.postMessage({ type: 'pdf-reader-back' }, '*');
|
||||
});
|
||||
|
||||
// Listen for the PDF data from the parent
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'load-pdf') {
|
||||
const { fileData, seriesTitle, chapterTitle } = event.data;
|
||||
// Create a URL that is valid in *this* document's context
|
||||
const localPdfUrl = URL.createObjectURL(fileData);
|
||||
loadAndRenderPdf(localPdfUrl, seriesTitle, chapterTitle);
|
||||
}
|
||||
});
|
||||
|
||||
// Tell the parent that this iframe is ready to receive the file
|
||||
window.parent.postMessage({ type: 'pdf-reader-ready' }, '*');
|
||||
|
||||
} else if (fileUrlParam) {
|
||||
loadAndRenderPdf(fileUrlParam, decodeURIComponent(seriesTitleParam || ''), decodeURIComponent(chapterTitleParam || ''));
|
||||
} else {
|
||||
showErrorState(`Provide a PDF via the <code>?file=...</code> URL parameter, or choose one locally.`);
|
||||
}
|
||||
|
||||
fileInputButton.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file?.type === 'application/pdf') {
|
||||
loadAndRenderPdf(URL.createObjectURL(file), file.name, '');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
225
animex/portal.html
Normal file
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hub</title>
|
||||
|
||||
<!-- Google Fonts: Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--accent-color: #ff9500;
|
||||
--accent-glow: rgba(255, 149, 0, 0.5);
|
||||
--bg-dark: #0a0a0a;
|
||||
--bg-light: #141414;
|
||||
--text-main: #ffffff;
|
||||
--text-muted: #888888;
|
||||
--card-bg: rgba(30, 30, 30, 0.6);
|
||||
--card-border: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-dark);
|
||||
/* Subtle radial gradient for depth */
|
||||
background-image: radial-gradient(circle at center, var(--bg-light) 0%, var(--bg-dark) 100%);
|
||||
color: var(--text-main);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Prevents scrollbars during hover animations */
|
||||
}
|
||||
|
||||
/* Background decorative glow behind everything */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: var(--accent-color);
|
||||
opacity: 0.03;
|
||||
filter: blur(100px);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header {
|
||||
z-index: 10;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
animation: fadeInDown 0.8s ease-out;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 4em;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
letter-spacing: -2px;
|
||||
line-height: 1;
|
||||
transition: transform 0.3s ease;
|
||||
text-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.logo span {
|
||||
color: var(--accent-color);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin-top: 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
font-weight: 400;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hub-container {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 20px;
|
||||
z-index: 10;
|
||||
animation: fadeInUp 0.8s ease-out 0.2s backwards;
|
||||
}
|
||||
|
||||
.hub-card {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
/* Glassmorphism settings */
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 24px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--text-main);
|
||||
position: relative;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.hub-icon {
|
||||
font-size: 42px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-main);
|
||||
transition: transform 0.4s ease, color 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hub-text {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 2;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover Effects */
|
||||
.hub-card:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: rgba(255, 149, 0, 0.3);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.4),
|
||||
0 0 30px var(--accent-glow); /* Outer glow */
|
||||
}
|
||||
|
||||
.hub-card:hover .hub-icon {
|
||||
color: var(--accent-color);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hub-card:hover .hub-text {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Subtle inner highlight on hover */
|
||||
.hub-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top, rgba(255,255,255,0.1), transparent 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.hub-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInDown {
|
||||
from { opacity: 0; transform: translateY(-20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.hub-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.hub-card {
|
||||
width: 100%;
|
||||
min-width: 140px;
|
||||
height: 140px;
|
||||
flex-direction: column; /* Horizontal layout on mobile looks better */
|
||||
gap: 20px;
|
||||
}
|
||||
.hub-icon {
|
||||
margin-bottom: 0;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo" onclick="window.location.href='/'">
|
||||
Sinimex<span>.</span>
|
||||
</div>
|
||||
<div class="tagline">Media Hub</div>
|
||||
</header>
|
||||
|
||||
<div class="hub-container">
|
||||
<!-- Feed Card -->
|
||||
<a href="https://feeder-main.vercel.app/feed" class="hub-card">
|
||||
<i class="fa-solid fa-circle-play hub-icon"></i>
|
||||
<div class="hub-text">Feed</div>
|
||||
</a>
|
||||
|
||||
<!-- Read Card -->
|
||||
<a href="/ext/nsfw/hentai" class="hub-card">
|
||||
<i class="fa-solid fa-book-open hub-icon"></i>
|
||||
<div class="hub-text">Read</div>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
486
animex/reader.html
Normal file
@@ -0,0 +1,486 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Animex - Modern Manga Reader</title>
|
||||
|
||||
<!-- Google Fonts & Font Awesome -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #121212;
|
||||
--bg-panel: #1E1E1E;
|
||||
--text-light: #EAEAEA;
|
||||
--text-muted: #8A8A8A;
|
||||
--accent: #FF9500;
|
||||
--border-color: #333333;
|
||||
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||
--border-radius: 12px;
|
||||
--transition-speed: 0.2s;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(circle at top left, #2a2a2d, var(--bg-dark));
|
||||
color: var(--text-light);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* --- Main Layout --- */
|
||||
#app-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#left-panel {
|
||||
flex: 0 0 25%;
|
||||
max-width: 380px;
|
||||
min-width: 280px;
|
||||
background-color: var(--bg-panel);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 5px 0px 25px var(--shadow-color);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* --- Left Panel: Library --- */
|
||||
#library-header {
|
||||
padding: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#library-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(45deg, var(--accent), #ffbf66);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
#import-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: calc(100% - 48px);
|
||||
margin: 0 24px 24px 24px;
|
||||
padding: 14px;
|
||||
background-color: var(--accent);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed) ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
#import-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 149, 0, 0.3);
|
||||
background-color: #ffae42;
|
||||
}
|
||||
|
||||
#file-list-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#file-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#file-list li {
|
||||
padding: 16px 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-speed) ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-left: 4px solid transparent;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
#file-list li:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
#file-list li.active {
|
||||
background-color: rgba(255, 149, 0, 0.1);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
/* --- Right Panel: Viewer --- */
|
||||
#viewer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 30px;
|
||||
background-color: rgba(30, 30, 30, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#document-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#viewer-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background-color: rgba(255,255,255,0.08);
|
||||
border: none;
|
||||
color: var(--text-light);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed) ease;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.control-btn:hover {
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.control-btn.active {
|
||||
background-color: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#zoom-controls { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
#zoom-level {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
border-radius: 20px;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#pdf-viewer-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
#pdf-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
#pdf-viewer.paged-view {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#pdf-viewer.paged-view .pdf-page {
|
||||
box-shadow: 0 15px 35px var(--shadow-color);
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#placeholder .fa-solid {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* --- Custom Scrollbar --- */
|
||||
::-webkit-scrollbar { width: 12px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-panel); }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 10px;
|
||||
border: 3px solid var(--bg-panel);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover { background-color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app-container">
|
||||
<!-- LEFT PANEL -->
|
||||
<aside id="left-panel">
|
||||
<header id="library-header">
|
||||
<h1>Animex</h1>
|
||||
</header>
|
||||
<button id="import-btn">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
<span>Import Folder</span>
|
||||
</button>
|
||||
<div id="file-list-container">
|
||||
<ul id="file-list">
|
||||
<!-- Files will be dynamically added here -->
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- RIGHT PANEL -->
|
||||
<main id="right-panel">
|
||||
<header id="viewer-header">
|
||||
<h2 id="document-title">No file selected</h2>
|
||||
<div id="viewer-controls">
|
||||
<button id="view-webtoon-btn" class="control-btn" title="Webtoon View (Continuous Scroll)">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</button>
|
||||
<button id="view-paged-btn" class="control-btn" title="Paged View">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
</button>
|
||||
<div id="zoom-controls">
|
||||
<button id="zoom-out-btn" class="control-btn" title="Zoom Out"><i class="fa-solid fa-magnifying-glass-minus"></i></button>
|
||||
<span id="zoom-level">100%</span>
|
||||
<button id="zoom-in-btn" class="control-btn" title="Zoom In"><i class="fa-solid fa-magnifying-glass-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="pdf-viewer-container">
|
||||
<div id="placeholder">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<h2>Select a folder to start reading</h2>
|
||||
<p>Your files are processed locally and never uploaded.</p>
|
||||
</div>
|
||||
<div id="pdf-viewer">
|
||||
<!-- PDF pages (canvases) will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- PDF.js Library -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.min.js"></script>
|
||||
<script>
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js`;
|
||||
|
||||
const importBtn = document.getElementById('import-btn');
|
||||
const fileList = document.getElementById('file-list');
|
||||
const documentTitle = document.getElementById('document-title');
|
||||
const pdfViewer = document.getElementById('pdf-viewer');
|
||||
const placeholder = document.getElementById('placeholder');
|
||||
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
|
||||
const webtoonViewBtn = document.getElementById('view-webtoon-btn');
|
||||
const pagedViewBtn = document.getElementById('view-paged-btn');
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
const zoomLevelDisplay = document.getElementById('zoom-level');
|
||||
|
||||
let fileHandles = [];
|
||||
let currentPdf = null;
|
||||
let currentFileHandle = null;
|
||||
let currentPageNum = 1;
|
||||
let zoomLevel = 1.0;
|
||||
let viewMode = 'webtoon';
|
||||
|
||||
importBtn.addEventListener('click', handleImportFolder);
|
||||
|
||||
fileList.addEventListener('click', (e) => {
|
||||
const li = e.target.closest('li');
|
||||
if (li) {
|
||||
const index = parseInt(li.dataset.index, 10);
|
||||
const handle = fileHandles[index];
|
||||
if (handle && handle !== currentFileHandle) {
|
||||
loadPdf(handle, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webtoonViewBtn.addEventListener('click', () => setViewMode('webtoon'));
|
||||
pagedViewBtn.addEventListener('click', () => setViewMode('paged'));
|
||||
zoomInBtn.addEventListener('click', () => changeZoom(0.1));
|
||||
zoomOutBtn.addEventListener('click', () => changeZoom(-0.1));
|
||||
window.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
async function handleImportFolder() {
|
||||
try {
|
||||
const dirHandle = await window.showDirectoryPicker();
|
||||
fileHandles = [];
|
||||
for await (const entry of dirHandle.values()) {
|
||||
if (entry.kind === 'file' && entry.name.toLowerCase().endsWith('.pdf')) {
|
||||
fileHandles.push(entry);
|
||||
}
|
||||
}
|
||||
fileHandles.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
||||
populateFileList();
|
||||
if (fileHandles.length > 0) {
|
||||
loadPdf(fileHandles[0], 0);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') {
|
||||
console.error("Error importing folder:", err);
|
||||
alert('Could not read directory. Please check permissions.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateFileList() {
|
||||
fileList.innerHTML = '';
|
||||
if (fileHandles.length === 0) {
|
||||
fileList.innerHTML = '<li>No PDF files found.</li>';
|
||||
return;
|
||||
}
|
||||
fileHandles.forEach((handle, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = handle.name.replace(/\.pdf$/i, '');
|
||||
li.dataset.index = index;
|
||||
fileList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPdf(fileHandle, index) {
|
||||
currentFileHandle = fileHandle;
|
||||
placeholder.style.display = 'none';
|
||||
pdfViewer.innerHTML = `<h2 style="color: var(--text-muted)">Loading ${fileHandle.name}...</h2>`;
|
||||
pdfViewerContainer.scrollTop = 0;
|
||||
|
||||
document.querySelectorAll('#file-list li').forEach(li => li.classList.remove('active'));
|
||||
document.querySelector(`#file-list li[data-index="${index}"]`).classList.add('active');
|
||||
documentTitle.textContent = fileHandle.name.replace(/\.pdf$/i, '');
|
||||
|
||||
try {
|
||||
const file = await fileHandle.getFile();
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = async (e) => {
|
||||
const typedarray = new Uint8Array(e.target.result);
|
||||
const pdf = await pdfjsLib.getDocument(typedarray).promise;
|
||||
currentPdf = pdf;
|
||||
currentPageNum = 1;
|
||||
await detectAndSetLayout();
|
||||
await renderPdf();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
} catch (err) {
|
||||
console.error("Error loading PDF:", err);
|
||||
pdfViewer.innerHTML = '<h2>Error loading file.</h2>';
|
||||
}
|
||||
}
|
||||
|
||||
async function detectAndSetLayout() {
|
||||
if (!currentPdf) return;
|
||||
const page = await currentPdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
setViewMode(viewport.height > viewport.width * 2 ? 'webtoon' : 'paged');
|
||||
}
|
||||
|
||||
async function renderPdf() {
|
||||
if (!currentPdf) return;
|
||||
pdfViewer.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= currentPdf.numPages; i++) {
|
||||
const page = await currentPdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: zoomLevel });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'pdf-page';
|
||||
canvas.id = `page-${i}`;
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
pdfViewer.appendChild(canvas);
|
||||
|
||||
page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
viewMode = mode;
|
||||
pdfViewer.className = mode === 'webtoon' ? 'webtoon-view' : 'paged-view';
|
||||
webtoonViewBtn.classList.toggle('active', mode === 'webtoon');
|
||||
pagedViewBtn.classList.toggle('active', mode === 'paged');
|
||||
}
|
||||
|
||||
function changeZoom(delta) {
|
||||
zoomLevel = Math.max(0.2, Math.min(5, zoomLevel + delta));
|
||||
zoomLevelDisplay.textContent = `${Math.round(zoomLevel * 100)}%`;
|
||||
if (currentPdf) renderPdf();
|
||||
}
|
||||
|
||||
function handleKeyPress(e) {
|
||||
if (!currentPdf || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
const viewer = pdfViewerContainer;
|
||||
if (viewMode === 'paged') {
|
||||
const pageHeight = viewer.querySelector('.pdf-page')?.clientHeight + 40 || viewer.clientHeight;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
viewer.scrollBy({ top: pageHeight, behavior: 'smooth' });
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
viewer.scrollBy({ top: -pageHeight, behavior: 'smooth' });
|
||||
}
|
||||
} else {
|
||||
const scrollAmount = e.shiftKey ? viewer.clientHeight * 0.8 : 300;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
viewer.scrollBy({ top: scrollAmount, behavior: 'smooth' });
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
viewer.scrollBy({ top: -scrollAmount, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
setViewMode('webtoon');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
870
animex/search.html
Normal file
@@ -0,0 +1,870 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Search & Browse - Media App</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="Resources/styles.css">
|
||||
<style>
|
||||
/* =========================================
|
||||
1. VARIABLES & RESET (From Reference)
|
||||
========================================= */
|
||||
:root {
|
||||
/* Brand & Palette */
|
||||
--brand-accent: #ff9500;
|
||||
--brand-accent-hover: #ffae40;
|
||||
--background-primary: #0a0a0a;
|
||||
--background-secondary: #161616;
|
||||
--background-tertiary: #202020;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #999999;
|
||||
--text-muted: #666666;
|
||||
|
||||
/* UI Elements */
|
||||
--border-color: #2a2a2a;
|
||||
--shadow-color: rgba(0, 0, 0, 0.6);
|
||||
--brand-glow: rgba(255, 149, 0, 0.5);
|
||||
|
||||
/* Dimensions & Animation */
|
||||
--transition-duration: 0.3s;
|
||||
--border-radius: 8px;
|
||||
--nav-height: 60px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--background-secondary); }
|
||||
::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
|
||||
/* =========================================
|
||||
2. APP LAYOUT
|
||||
========================================= */
|
||||
.app-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px 5%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 30px 0 15px 0;
|
||||
padding-left: 5px;
|
||||
border-left: 4px solid var(--brand-accent);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
3. UI COMPONENTS
|
||||
========================================= */
|
||||
|
||||
/* Back Button */
|
||||
.back-button {
|
||||
padding: 8px 18px;
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.back-button:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
.back-button i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search-bar-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 10px auto 25px auto;
|
||||
padding: 5px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 15px var(--shadow-color);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.search-bar-container:focus-within {
|
||||
border-color: var(--brand-accent);
|
||||
box-shadow: 0 0 0 1px var(--brand-accent), 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
.search-bar-container input {
|
||||
flex-grow: 1;
|
||||
padding: 12px 10px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.05em;
|
||||
outline: none;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
.search-bar-container input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.search-bar-container .fa-search {
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1em;
|
||||
margin-right: 10px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.search-bar-container input:focus + .fa-search,
|
||||
.search-bar-container:focus-within .fa-search {
|
||||
color: var(--brand-accent);
|
||||
}
|
||||
.search-bar-container #clear-search-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
font-size: 1.1em;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.search-bar-container #clear-search-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Recent Search Items */
|
||||
.recent-search-item {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
margin: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.recent-search-item:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
.recent-search-item span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.recent-search-item .fa-times {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.recent-search-item:hover .fa-times {
|
||||
opacity: 1;
|
||||
color: var(--brand-accent);
|
||||
}
|
||||
.item-list#search-item-list.recent-searches-active {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* List Items (Cards) */
|
||||
.list-item.card-style {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item.card-style:hover {
|
||||
background-color: #1a1a1a;
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--brand-accent);
|
||||
box-shadow: 0 5px 15px -5px var(--shadow-color);
|
||||
}
|
||||
.list-item.card-style .item-thumbnail img {
|
||||
width: 60px;
|
||||
height: 85px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 15px;
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
.list-item.card-style .item-details {
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Prevents flex overflow */
|
||||
}
|
||||
.list-item.card-style .item-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.list-item.card-style .item-meta {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.list-item.card-style .meta-pill {
|
||||
background-color: rgba(255, 149, 0, 0.1);
|
||||
color: var(--brand-accent);
|
||||
border: 1px solid rgba(255, 149, 0, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
.list-item.card-style .item-description {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.list-item.card-style .item-arrow {
|
||||
margin-left: 15px;
|
||||
color: var(--border-color);
|
||||
font-size: 0.9em;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
}
|
||||
.list-item.card-style:hover .item-arrow {
|
||||
color: var(--brand-accent);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Search Options Tabs */
|
||||
.search-options-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 0 auto 20px auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.search-option-btn {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.search-option-btn:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.search-option-btn.active {
|
||||
background: rgba(255, 149, 0, 0.15);
|
||||
color: var(--brand-accent);
|
||||
border-color: rgba(255, 149, 0, 0.3);
|
||||
box-shadow: 0 0 15px rgba(255, 149, 0, 0.1);
|
||||
}
|
||||
.search-option-btn i {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* --- Source Toggle (for Manga) --- */
|
||||
.source-toggle-container {
|
||||
position: relative;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 25px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
border: 1px solid var(--border-color);
|
||||
max-width: 240px;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px auto;
|
||||
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
|
||||
}
|
||||
#source-toggle-indicator {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
height: calc(100% - 8px);
|
||||
background-color: var(--brand-accent);
|
||||
border-radius: 20px;
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 8px rgba(255, 149, 0, 0.4);
|
||||
}
|
||||
.source-toggle-btn {
|
||||
padding: 6px 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 700;
|
||||
background-color: transparent;
|
||||
color: var(--text-muted);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
z-index: 2;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.source-toggle-btn.active {
|
||||
color: #000; /* Contrast against orange pill */
|
||||
}
|
||||
/* Fix for when toggle isn't active/hovered */
|
||||
.source-toggle-btn:hover:not(.active) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Browse View --- */
|
||||
.horizontal-scroll-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 10px 0 25px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
gap: 15px;
|
||||
}
|
||||
.horizontal-scroll-container::-webkit-scrollbar { height: 6px; }
|
||||
.horizontal-scroll-container::-webkit-scrollbar-thumb { background-color: var(--border-color); }
|
||||
|
||||
.genre-chip {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-secondary);
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px; /* Matching border radius variable generally */
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.genre-chip:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--brand-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Popular Cards (Vertical) */
|
||||
.popular-item-card {
|
||||
flex: 0 0 150px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.popular-item-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px var(--shadow-color);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
.popular-item-card img {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
background-color: var(--background-tertiary);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.popular-item-card:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.popular-item-card .item-title {
|
||||
padding: 10px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--background-secondary);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#genre-results-list {
|
||||
padding-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
/* Override list items inside genre results to grid if preferred,
|
||||
but based on JS structure it appends .card-style divs.
|
||||
We'll keep them consistent with search results for now,
|
||||
but handle the container layout. */
|
||||
#genre-results-list .list-item.card-style {
|
||||
margin-bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Loading / Error States */
|
||||
p { color: var(--text-secondary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<main class="main-content">
|
||||
<!-- Universal Header: Search Bar and Tabs -->
|
||||
<div class="search-bar-container" id="search-bar-container">
|
||||
<input type="text" id="search-input" placeholder="Type to Search Anime...">
|
||||
<i class="fas fa-search" id="search-icon"></i>
|
||||
<button id="clear-search-btn" style="display:none;" aria-label="Clear search" tabindex="0"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="source-toggle-container" id="manga-source-toggle" style="display: none;">
|
||||
<span id="source-toggle-indicator"></span>
|
||||
<button class="source-toggle-btn active" data-source="jikan">Jikan</button>
|
||||
<button class="source-toggle-btn" data-source="mangadex">MangaDex</button>
|
||||
</div>
|
||||
|
||||
<ul class="search-options-list" id="search-options-list">
|
||||
<li><button class="search-option-btn active" data-mode="anime" type="button"><i class="fa-solid fa-tv"></i> Anime</button></li>
|
||||
<li><button class="search-option-btn" data-mode="manga" type="button"><i class="fa-solid fa-book"></i> Manga</button></li>
|
||||
<li><button class="search-option-btn" data-mode="browse" type="button"><i class="fa-solid fa-compass"></i> Browse</button></li>
|
||||
</ul>
|
||||
|
||||
<!-- Search Content Area -->
|
||||
<section id="search-interaction-area">
|
||||
<h2 class="section-title" id="results-title">Recents</h2>
|
||||
<div class="item-list" id="search-item-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Browse Content Area -->
|
||||
<section id="browse-interaction-area" style="display: none;">
|
||||
<!-- Genre results view -->
|
||||
<div id="genre-results-view" style="display: none;">
|
||||
<button id="back-to-genres-btn" class="back-button"><i class="fas fa-arrow-left"></i> Back to Genres</button>
|
||||
<h2 class="section-title" id="genre-results-title">Genre Results</h2>
|
||||
<ul class="search-options-list" id="genre-results-tabs">
|
||||
<li><button class="search-option-btn active" id="genre-anime-btn" type="button"><i class="fa-solid fa-tv"></i> Anime</button></li>
|
||||
<li><button class="search-option-btn" id="genre-manga-btn" type="button"><i class="fa-solid fa-book"></i> Manga</button></li>
|
||||
</ul>
|
||||
<div class="item-list" id="genre-results-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main browse view -->
|
||||
<div id="main-browse-view">
|
||||
<h2 class="section-title">Browse by Genre</h2>
|
||||
<div class="horizontal-scroll-container" id="genre-list-container"></div>
|
||||
<h2 class="section-title">Popular Anime</h2>
|
||||
<div class="horizontal-scroll-container" id="popular-anime-list"></div>
|
||||
<h2 class="section-title">Popular Manga</h2>
|
||||
<div class="horizontal-scroll-container" id="popular-manga-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- DOM Element Selectors ---
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchIcon = document.getElementById('search-icon');
|
||||
const searchItemList = document.getElementById('search-item-list');
|
||||
const resultsTitle = document.getElementById('results-title');
|
||||
const clearSearchBtn = document.getElementById('clear-search-btn');
|
||||
|
||||
// Main Sections & Tabs
|
||||
const searchInteractionArea = document.getElementById('search-interaction-area');
|
||||
const browseInteractionArea = document.getElementById('browse-interaction-area');
|
||||
const searchBarContainer = document.getElementById('search-bar-container');
|
||||
const mainTabs = document.querySelectorAll('#search-options-list .search-option-btn');
|
||||
const mangaSourceToggle = document.getElementById('manga-source-toggle');
|
||||
const sourceToggleIndicator = document.getElementById('source-toggle-indicator');
|
||||
|
||||
// Browse View Elements
|
||||
const mainBrowseView = document.getElementById('main-browse-view');
|
||||
const genreListContainer = document.getElementById('genre-list-container');
|
||||
const popularAnimeList = document.getElementById('popular-anime-list');
|
||||
const popularMangaList = document.getElementById('popular-manga-list');
|
||||
|
||||
// Genre Results View Elements
|
||||
const genreResultsView = document.getElementById('genre-results-view');
|
||||
const backToGenresBtn = document.getElementById('back-to-genres-btn');
|
||||
const genreResultsTitle = document.getElementById('genre-results-title');
|
||||
const genreResultsList = document.getElementById('genre-results-list');
|
||||
const genreAnimeBtn = document.getElementById('genre-anime-btn');
|
||||
const genreMangaBtn = document.getElementById('genre-manga-btn');
|
||||
|
||||
// --- State Variables ---
|
||||
let searchMode = 'anime'; // 'anime', 'manga'
|
||||
let mangaSource = 'jikan'; // 'jikan', 'mangadex'
|
||||
let currentView = 'search'; // 'search' or 'browse'
|
||||
let currentGenre = { id: null, name: null };
|
||||
let allSearchResults = [];
|
||||
let searchTimeout;
|
||||
const serverUrl = `http://${localStorage.getItem("extension_server_ip") || "localhost"}:7275`;
|
||||
|
||||
// --- View Management ---
|
||||
function updateToggleIndicator() {
|
||||
const activeButton = mangaSourceToggle.querySelector('.source-toggle-btn.active');
|
||||
if (activeButton) {
|
||||
sourceToggleIndicator.style.width = `${activeButton.offsetWidth}px`;
|
||||
sourceToggleIndicator.style.transform = `translateX(${activeButton.offsetLeft - 4}px)`; // Adjusted for padding
|
||||
}
|
||||
}
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
searchInteractionArea.style.display = 'none';
|
||||
browseInteractionArea.style.display = 'none';
|
||||
searchBarContainer.style.display = (view === 'browse') ? 'none' : 'flex';
|
||||
mangaSourceToggle.style.display = (view === 'search' && searchMode === 'manga') ? 'flex' : 'none';
|
||||
|
||||
if (view === 'search') {
|
||||
searchInteractionArea.style.display = 'block';
|
||||
if (!searchInput.value.trim()) displayRecentSearches();
|
||||
} else if (view === 'browse') {
|
||||
browseInteractionArea.style.display = 'block';
|
||||
if (genreListContainer.innerHTML.trim() === '') loadBrowseContent();
|
||||
}
|
||||
if (mangaSourceToggle.style.display === 'flex') {
|
||||
setTimeout(updateToggleIndicator, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Iframe Overlay Logic ---
|
||||
function openSeriesInfo(id, type) {
|
||||
let url;
|
||||
if (type === 'mangadex') {
|
||||
url = `manga-info.html?source=mangadex&id=${id}`;
|
||||
} else {
|
||||
url = type === 'manga' ? `manga-info.html?id=${id}` : `series-info.html?id=${id}`;
|
||||
}
|
||||
window.parent.openPopup(url);
|
||||
}
|
||||
|
||||
// --- API & Rendering Helper Functions ---
|
||||
const apiCall = async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
function createResultCard(item, type) {
|
||||
const listItem = document.createElement('div');
|
||||
listItem.className = 'list-item card-style';
|
||||
|
||||
let id, title, imageUrl, score, episodes, chapters, synopsis;
|
||||
|
||||
if (type === 'mangadex') {
|
||||
id = item.id;
|
||||
title = item.attributes.title.en || Object.values(item.attributes.title)[0];
|
||||
imageUrl = item.cover_url || '';
|
||||
score = null; // Not available in this search result
|
||||
episodes = null;
|
||||
chapters = null; // Not available
|
||||
synopsis = item.attributes.description.en || 'No description available.';
|
||||
listItem.addEventListener('click', () => openSeriesInfo(id, 'mangadex'));
|
||||
} else {
|
||||
id = item.mal_id;
|
||||
title = item.title_english || item.title;
|
||||
imageUrl = item.images?.jpg?.image_url || '';
|
||||
score = item.score;
|
||||
episodes = item.episodes;
|
||||
chapters = item.chapters;
|
||||
synopsis = item.synopsis || 'No description available.';
|
||||
listItem.addEventListener('click', () => openSeriesInfo(id, type));
|
||||
}
|
||||
|
||||
const thumbnail = `<div class="item-thumbnail"><img src="${imageUrl}" alt="${title}" loading="lazy"></div>`;
|
||||
const metaPills = [];
|
||||
if (score) metaPills.push(`<span class="meta-pill"><i class="fas fa-star" style="margin-right:3px; font-size:0.9em;"></i>${score}</span>`);
|
||||
if (type === 'anime' && episodes) metaPills.push(`<span class="meta-pill">${episodes} EP</span>`);
|
||||
if (type === 'manga' && chapters) metaPills.push(`<span class="meta-pill">${chapters} CH</span>`);
|
||||
const shortSynopsis = synopsis.length > 100 ? synopsis.substring(0, 100) + '...' : synopsis;
|
||||
|
||||
const details = `<div class="item-details">
|
||||
<h3 class="item-title">${title}</h3>
|
||||
<div class="item-meta">${metaPills.join('')}</div>
|
||||
<p class="item-description">${shortSynopsis}</p>
|
||||
</div>`;
|
||||
const arrow = `<div class="item-arrow"><i class="fas fa-chevron-right"></i></div>`;
|
||||
listItem.innerHTML = thumbnail + details + arrow;
|
||||
return listItem;
|
||||
}
|
||||
|
||||
// --- Search Logic ---
|
||||
const getRecentSearches = () => JSON.parse(localStorage.getItem(`${searchMode}RecentSearches`) || '[]');
|
||||
const addRecentSearch = (term) => {
|
||||
if (!term) return;
|
||||
let searches = getRecentSearches().filter(s => s.toLowerCase() !== term.toLowerCase());
|
||||
searches.unshift(term);
|
||||
localStorage.setItem(`${searchMode}RecentSearches`, JSON.stringify(searches.slice(0, 5)));
|
||||
};
|
||||
const removeRecentSearch = (term) => {
|
||||
let searches = getRecentSearches().filter(s => s.toLowerCase() !== term.toLowerCase());
|
||||
localStorage.setItem(`${searchMode}RecentSearches`, JSON.stringify(searches));
|
||||
displayRecentSearches();
|
||||
};
|
||||
function displayRecentSearches() {
|
||||
searchItemList.innerHTML = '';
|
||||
searchItemList.className = 'item-list recent-searches-active';
|
||||
const searches = getRecentSearches();
|
||||
resultsTitle.textContent = 'Recents';
|
||||
if (searches.length === 0) {
|
||||
searchItemList.innerHTML = '<p style="width: 100%; text-align: center; margin-top: 20px;">No recent searches.</p>';
|
||||
return;
|
||||
}
|
||||
searches.forEach(term => {
|
||||
const recentItem = document.createElement('div');
|
||||
recentItem.className = 'recent-search-item';
|
||||
recentItem.innerHTML = `<span>${term}</span><i class="fas fa-times"></i>`;
|
||||
recentItem.querySelector('span').addEventListener('click', () => {
|
||||
searchInput.value = term;
|
||||
performSearch(term);
|
||||
});
|
||||
recentItem.querySelector('i').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
removeRecentSearch(term);
|
||||
});
|
||||
searchItemList.appendChild(recentItem);
|
||||
});
|
||||
}
|
||||
async function performSearch(query) {
|
||||
if (!query) {
|
||||
displayRecentSearches();
|
||||
return;
|
||||
}
|
||||
resultsTitle.textContent = `Results for "${query}"`;
|
||||
searchItemList.innerHTML = '<p style="width: 100%; text-align: center; margin-top: 20px;">Loading...</p>';
|
||||
searchItemList.className = 'item-list';
|
||||
try {
|
||||
let url;
|
||||
let type;
|
||||
|
||||
if (searchMode === 'manga') {
|
||||
type = mangaSource; // 'jikan' or 'mangadex'
|
||||
if (mangaSource === 'mangadex') {
|
||||
url = `${serverUrl}/mangadex/search?q=${encodeURIComponent(query)}`;
|
||||
} else {
|
||||
url = `https://api.jikan.moe/v4/manga?q=${encodeURIComponent(query)}&limit=25`;
|
||||
}
|
||||
} else {
|
||||
type = 'anime';
|
||||
url = `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(query)}&limit=25`;
|
||||
}
|
||||
|
||||
const data = await apiCall(url);
|
||||
const results = type === 'mangadex' ? data.data : data.data;
|
||||
displaySearchResults(results, query, type === 'jikan' ? 'manga' : type);
|
||||
if (results?.length > 0) addRecentSearch(query);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch search results:", error);
|
||||
searchItemList.innerHTML = `<p style="width: 100%; text-align: center; color: var(--brand-accent); margin-top: 20px;">Error: ${error.message}.</p>`;
|
||||
}
|
||||
}
|
||||
function displaySearchResults(data, query, type) {
|
||||
allSearchResults = data || [];
|
||||
searchItemList.innerHTML = '';
|
||||
if (allSearchResults.length === 0) {
|
||||
searchItemList.innerHTML = `<p style="width: 100%; text-align: center; margin-top: 20px;">No results found for "${query}".</p>`;
|
||||
return;
|
||||
}
|
||||
allSearchResults.forEach(item => searchItemList.appendChild(createResultCard(item, type)));
|
||||
}
|
||||
|
||||
// --- Browse Logic ---
|
||||
function createPopularCard(item, type) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'popular-item-card';
|
||||
card.addEventListener('click', () => openSeriesInfo(item.mal_id, type));
|
||||
card.innerHTML = `
|
||||
<img src="${item.images?.jpg?.large_image_url || ''}" alt="${item.title}" loading="lazy">
|
||||
<div class="item-title">${item.title}</div>`;
|
||||
return card;
|
||||
}
|
||||
async function loadBrowseContent() {
|
||||
genreListContainer.innerHTML = '<p>Loading genres...</p>';
|
||||
popularAnimeList.innerHTML = '<p>Loading...</p>';
|
||||
popularMangaList.innerHTML = '<p>Loading...</p>';
|
||||
try {
|
||||
const [genresRes, popularAnimeRes, popularMangaRes] = await Promise.all([
|
||||
apiCall('https://api.jikan.moe/v4/genres/anime'),
|
||||
apiCall('https://api.jikan.moe/v4/top/anime?limit=15'),
|
||||
apiCall('https://api.jikan.moe/v4/top/manga?limit=15')
|
||||
]);
|
||||
genreListContainer.innerHTML = '';
|
||||
genresRes.data.sort((a, b) => a.name.localeCompare(b.name)).forEach(genre => {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = 'genre-chip';
|
||||
chip.textContent = genre.name;
|
||||
chip.addEventListener('click', () => showGenreResultsView(genre.mal_id, genre.name));
|
||||
genreListContainer.appendChild(chip);
|
||||
});
|
||||
popularAnimeList.innerHTML = '';
|
||||
popularAnimeRes.data.forEach(item => popularAnimeList.appendChild(createPopularCard(item, 'anime')));
|
||||
popularMangaList.innerHTML = '';
|
||||
popularMangaRes.data.forEach(item => popularMangaList.appendChild(createPopularCard(item, 'manga')));
|
||||
} catch (error) {
|
||||
console.error("Failed to load browse content:", error);
|
||||
genreListContainer.innerHTML = `<p style="color:var(--brand-accent);">Error loading genres.</p>`;
|
||||
popularAnimeList.innerHTML = `<p style="color:var(--brand-accent);">Error loading popular anime.</p>`;
|
||||
}
|
||||
}
|
||||
function showGenreResultsView(genreId, genreName) {
|
||||
currentGenre = { id: genreId, name: genreName };
|
||||
mainBrowseView.style.display = 'none';
|
||||
genreResultsView.style.display = 'block';
|
||||
genreResultsTitle.textContent = genreName;
|
||||
genreAnimeBtn.classList.add('active');
|
||||
genreMangaBtn.classList.remove('active');
|
||||
fetchAndDisplayGenreResults('anime');
|
||||
}
|
||||
function showMainBrowseView() {
|
||||
genreResultsView.style.display = 'none';
|
||||
mainBrowseView.style.display = 'block';
|
||||
}
|
||||
async function fetchAndDisplayGenreResults(type) {
|
||||
genreResultsList.innerHTML = '<p style="width: 100%; text-align: center;">Loading...</p>';
|
||||
try {
|
||||
const data = await apiCall(`https://api.jikan.moe/v4/${type}?genres=${currentGenre.id}&limit=24`);
|
||||
genreResultsList.innerHTML = '';
|
||||
if (!data.data || data.data.length === 0) {
|
||||
genreResultsList.innerHTML = `<p style="width: 100%; text-align: center;">No ${type} found.</p>`;
|
||||
return;
|
||||
}
|
||||
data.data.forEach(item => genreResultsList.appendChild(createResultCard(item, type)));
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch ${type}:`, error);
|
||||
genreResultsList.innerHTML = `<p style="width: 100%; text-align: center; color: var(--brand-accent);">Error.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Event Listeners ---
|
||||
document.getElementById('search-options-list').addEventListener('click', (e) => {
|
||||
const button = e.target.closest('.search-option-btn');
|
||||
if (!button) return;
|
||||
|
||||
const newMode = button.dataset.mode;
|
||||
|
||||
mainTabs.forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
if (newMode === 'browse') {
|
||||
switchView('browse');
|
||||
} else {
|
||||
searchMode = newMode;
|
||||
searchInput.placeholder = `Type to Search ${searchMode.charAt(0).toUpperCase() + searchMode.slice(1)}...`;
|
||||
switchView('search');
|
||||
if (searchInput.value.trim()) {
|
||||
performSearch(searchInput.value.trim());
|
||||
} else {
|
||||
displayRecentSearches();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mangaSourceToggle.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('.source-toggle-btn');
|
||||
if (button && button.dataset.source !== mangaSource) {
|
||||
mangaSource = button.dataset.source;
|
||||
mangaSourceToggle.querySelector('.active').classList.remove('active');
|
||||
button.classList.add('active');
|
||||
updateToggleIndicator();
|
||||
if (searchInput.value.trim()) {
|
||||
performSearch(searchInput.value.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => performSearch(searchInput.value.trim()), 500);
|
||||
clearSearchBtn.style.display = searchInput.value ? 'inline-flex' : 'none';
|
||||
});
|
||||
searchInput.addEventListener('keypress', e => e.key === 'Enter' && (clearTimeout(searchTimeout), performSearch(searchInput.value.trim())));
|
||||
searchIcon.addEventListener('click', () => (clearTimeout(searchTimeout), performSearch(searchInput.value.trim())));
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
clearSearchBtn.style.display = 'none';
|
||||
searchInput.focus();
|
||||
displayRecentSearches();
|
||||
});
|
||||
|
||||
backToGenresBtn.addEventListener('click', showMainBrowseView);
|
||||
|
||||
genreAnimeBtn.addEventListener('click', () => {
|
||||
genreAnimeBtn.classList.add('active');
|
||||
genreMangaBtn.classList.remove('active');
|
||||
fetchAndDisplayGenreResults('anime');
|
||||
});
|
||||
genreMangaBtn.addEventListener('click', () => {
|
||||
genreMangaBtn.classList.add('active');
|
||||
genreAnimeBtn.classList.remove('active');
|
||||
fetchAndDisplayGenreResults('manga');
|
||||
});
|
||||
|
||||
// --- Initial Load ---
|
||||
switchView('search');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1749
animex/series-info.html
Normal file
1410
animex/settings.html
Normal file
BIN
animex/src/.DS_Store
vendored
Normal file
503
animex/src/csPlayer.js
Normal file
@@ -0,0 +1,503 @@
|
||||
// csPlayer.js - Patched to hide Related Videos on Pause
|
||||
|
||||
function $(selector,parent){
|
||||
var x;
|
||||
try{
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if(elements.length == 1){x = elements[0]}
|
||||
else if(elements.length == 0){x = null}
|
||||
else{x = elements}
|
||||
}catch(error){
|
||||
x = error;
|
||||
}return x;
|
||||
}
|
||||
|
||||
var csPlayer ={
|
||||
csPlayers : {},
|
||||
preSetup: (videoTag,playerTagId,defaultId)=>{
|
||||
var theme =("theme" in csPlayer.csPlayers[videoTag]["params"]) ? csPlayer.csPlayers[videoTag]["params"]["theme"] : null;
|
||||
var themeClass = theme ? "theme-"+theme : "";
|
||||
return new Promise((resolve, reject) => {
|
||||
$("#"+videoTag).innerHTML =`
|
||||
<div class="csPlayer ${themeClass}">
|
||||
<div class="csPlayer-container">
|
||||
<span>
|
||||
<div></div>
|
||||
<i class="ti ti-player-play-filled csPlayer-loading"></i>
|
||||
<div></div>
|
||||
</span>
|
||||
<div id=${playerTagId}></div>
|
||||
</div>
|
||||
<div class="csPlayer-controls-box">
|
||||
<main>
|
||||
<i class="ti ti-rewind-backward-10"></i>
|
||||
<i class="ti csPlayer-play-pause-btn ti-player-play-filled"></i>
|
||||
<i class="ti ti-rewind-forward-10"></i>
|
||||
</main>
|
||||
<div class="csPlayer-controls">
|
||||
<p>00:00</p>
|
||||
<div><span></span>
|
||||
<input type="range" min="0" max="100" value="0" step="1"></div>
|
||||
<p>00:00</p>
|
||||
<i class="ti ti-settings settingsBtn"></i>
|
||||
<i class="ti ti-maximize fsBtn"></i>
|
||||
</div>
|
||||
<div class="csPlayer-settings-box">
|
||||
<p>Speed<b>1x</b><i class="ti ti-caret-right-filled"></i></p>
|
||||
<span>
|
||||
<label><input type="radio" name=${videoTag}1>0.75x</label>
|
||||
<label><input type="radio" name=${videoTag}1 checked>1x</label>
|
||||
<label><input type="radio" name=${videoTag}1>1.25x</label>
|
||||
<label><input type="radio" name=${videoTag}1>1.5x</label>
|
||||
<label><input type="radio" name=${videoTag}1>1.75x</label>
|
||||
<label><input type="radio" name=${videoTag}1>2x</label>
|
||||
</span>
|
||||
<p>Quality<b>auto</b><i class="ti ti-caret-right-filled"></i></p>
|
||||
<span>
|
||||
<label><input type="radio" name=${videoTag}2 checked>auto</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
pauseVideoWithPromise:(x)=>{
|
||||
return new Promise((resolve, reject) => {
|
||||
try{
|
||||
x.pauseVideo()
|
||||
resolve('Video paused');
|
||||
}catch(error){
|
||||
reject('Error pausing video: ' + error);
|
||||
}
|
||||
});
|
||||
},
|
||||
YtSetup:(videoTag,playerTagId,defaultId)=>{
|
||||
var parent = document.querySelector("#"+playerTagId).closest(".csPlayer");
|
||||
var controlsTimeout = null;
|
||||
return new Promise((resolve, reject) => {
|
||||
csPlayer.csPlayers[videoTag]["videoTag"] = new YT.Player(playerTagId,{
|
||||
videoId: csPlayer.csPlayers[videoTag]["params"]["defaultId"],
|
||||
playerVars:{
|
||||
controls: 0,
|
||||
mute: 1,
|
||||
autoplay: 1,
|
||||
disablekb: 1,
|
||||
color: "white",
|
||||
fs: 0,
|
||||
playsinline: 1,
|
||||
rel: 0,
|
||||
loop: 0,
|
||||
cc_load_policy: 3,
|
||||
showinfo: 0,
|
||||
iv_load_policy: 3,
|
||||
},
|
||||
events:{
|
||||
'onReady':()=>{
|
||||
if($("#"+videoTag) != null && videoTag){
|
||||
csPlayer.pauseVideoWithPromise(csPlayer.csPlayers[videoTag]["videoTag"]).then(()=>{
|
||||
parent.querySelector(".csPlayer-container iframe").addEventListener("load",()=>{
|
||||
parent.querySelector(".csPlayer-container span i").classList.remove("csPlayer-loading");
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].addEventListener('onStateChange', onPlayerStateChange);
|
||||
parent.querySelector(".csPlayer-controls-box main i:nth-of-type(1)").addEventListener("click", backward);
|
||||
parent.querySelector(".csPlayer-controls-box main i:nth-of-type(2)").addEventListener("click", togglePlayPause);
|
||||
parent.querySelector(".csPlayer-controls-box main i:nth-of-type(3)").addEventListener("click", forward);
|
||||
csPlayer.csPlayers[videoTag]["TextTimeInterval"] = setInterval(updateTextTime,1000);
|
||||
csPlayer.csPlayers[videoTag]["TimeSliderInterval"] = setInterval(updateTimeSlider,1000); parent.querySelector(".csPlayer-controls-box .csPlayer-controls input").addEventListener("input",updateSlider);
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls .fsBtn").addEventListener("click",toggleFullscreen);
|
||||
document.fullscreenEnabled ? parent.querySelector(".csPlayer-controls-box .csPlayer-controls .fsBtn").style.display ="block" : parent.querySelector(".csPlayer-controls-box .csPlayer-controls .fsBtn").style.display ="none";
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls .settingsBtn").addEventListener("click",toggleSettings);
|
||||
});
|
||||
});
|
||||
}},
|
||||
}
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
||||
function backward(){
|
||||
updateTextTime()
|
||||
updateTimeSlider()
|
||||
var currentTime = csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].seekTo(Math.max(0, currentTime - 10), true);
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
}
|
||||
|
||||
function forward(){
|
||||
updateTextTime()
|
||||
updateTimeSlider()
|
||||
var currentTime = csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].seekTo(currentTime + 10, true);
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
}
|
||||
|
||||
function togglePlayPause(){
|
||||
if(csPlayer.csPlayers[videoTag]["isPlaying"]){
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].pauseVideo();
|
||||
clearTimeout(controlsTimeout);
|
||||
}else{
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].playVideo();
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const h = Math.floor(seconds / 3600),
|
||||
m = Math.floor((seconds % 3600) / 60),
|
||||
s = Math.floor(seconds % 60);
|
||||
return h > 0 ? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function timeToSeconds(t){
|
||||
var p = t.split(":").map(Number);
|
||||
return p.length === 3 ? p[0] * 3600 + p[1] * 60 + p[2] : p[0] * 60 + p[1];
|
||||
}
|
||||
|
||||
function updateTextTime(){
|
||||
var currentTime = csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
var duration = csPlayer.csPlayers[videoTag]["videoTag"].getDuration();
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls p:nth-of-type(1)").innerHTML = formatTime(String(currentTime));
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls p:nth-of-type(2)").innerHTML = formatTime(String(duration));
|
||||
}
|
||||
|
||||
function updateTimeSlider(){
|
||||
var slider = parent.querySelector(".csPlayer-controls-box .csPlayer-controls div input");
|
||||
var currentTime = csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
var duration = csPlayer.csPlayers[videoTag]["videoTag"].getDuration();
|
||||
var progress = (currentTime/duration)*100;
|
||||
var loaded = (csPlayer.csPlayers[videoTag]["videoTag"].getVideoLoadedFraction())*100;
|
||||
slider.value = progress;
|
||||
slider.style.background =`linear-gradient(to right, var(--sliderSeekTrackColor) ${progress}%, transparent ${progress}%)`;
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls div span").style.width = loaded+"%";
|
||||
}
|
||||
|
||||
function updateSlider(){
|
||||
clearTimeout(controlsTimeout);
|
||||
var slider = parent.querySelector(".csPlayer-controls-box .csPlayer-controls div input");
|
||||
var duration = csPlayer.csPlayers[videoTag]["videoTag"].getDuration();
|
||||
var progress = slider.value;
|
||||
slider.style.background =`linear-gradient(to right, var(--sliderSeekTrackColor) ${progress}%, transparent ${progress}%)`;
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].seekTo((slider.value/100)*duration);
|
||||
slider.value = slider.value;
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
}
|
||||
|
||||
function toggleFullscreen(){
|
||||
const videoContainer = parent;
|
||||
if(!document.fullscreenElement && document.fullscreenEnabled){
|
||||
if(videoContainer.requestFullscreen){
|
||||
videoContainer.requestFullscreen();
|
||||
}else if(videoContainer.mozRequestFullScreen){
|
||||
videoContainer.mozRequestFullScreen();
|
||||
}else if(videoContainer.webkitRequestFullscreen){
|
||||
videoContainer.webkitRequestFullscreen();
|
||||
}else if(videoContainer.msRequestFullscreen){
|
||||
videoContainer.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
else if(document.fullscreenElement && document.fullscreenEnabled){
|
||||
if(document.exitFullscreen){
|
||||
document.exitFullscreen();
|
||||
}else if(document.mozCancelFullScreen){
|
||||
document.mozCancelFullScreen();
|
||||
}else if(document.webkitExitFullscreen){
|
||||
document.webkitExitFullscreen();
|
||||
}else if(document.msExitFullscreen){
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
}else{
|
||||
console.warn("Fullscreen api not supported in your browser.");
|
||||
}
|
||||
}
|
||||
|
||||
function resetSettings(){
|
||||
var settings = parent.querySelector(".csPlayer-controls-box .csPlayer-settings-box");
|
||||
settings.querySelectorAll("p").forEach(pin=>{
|
||||
pin.nextElementSibling.style.maxHeight ="0px";
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSettings(){
|
||||
const targetElement = parent.querySelector(".csPlayer-controls-box");
|
||||
var settings = parent.querySelector(".csPlayer-controls-box .csPlayer-settings-box");
|
||||
var qualities = csPlayer.csPlayers[videoTag]["videoTag"].getAvailableQualityLevels();
|
||||
for(x of qualities){
|
||||
if(!settings.querySelector("span:nth-of-type(2)").innerHTML.includes(x)){
|
||||
settings.querySelector("span:nth-of-type(2)").innerHTML +=`<label><input type="radio" name=${videoTag}2>${x}</label>`;
|
||||
}
|
||||
}
|
||||
const obsrvr = new MutationObserver((mutationsList)=>{
|
||||
mutationsList.forEach((mutation) => {
|
||||
if(mutation.attributeName === 'class'){
|
||||
if(!targetElement.className.includes("open")){
|
||||
settings.style.display ="none";
|
||||
resetSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
obsrvr.observe(targetElement,{
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
if(settings.style.display =="block"){
|
||||
settings.style.display ="none";
|
||||
}else{
|
||||
settings.style.display ="block";
|
||||
}
|
||||
settings.addEventListener("click",()=>{
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
});
|
||||
|
||||
settings.querySelectorAll("p").forEach(pin=>{
|
||||
pin.addEventListener("click",()=>{
|
||||
settings.querySelectorAll("p").forEach(Allpin=>{
|
||||
Allpin.nextElementSibling.style.maxHeight ="0px";
|
||||
});
|
||||
pin.nextElementSibling.style.maxHeight ="400px";
|
||||
});
|
||||
});
|
||||
|
||||
settings.querySelectorAll("span:nth-of-type(1) input").forEach(spdInput=>{
|
||||
spdInput.addEventListener("change",(e)=>{
|
||||
var value = e.target.parentElement.innerText.slice(0,-1);
|
||||
settings.querySelector("p:nth-of-type(1) b").innerText = value+"x";
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].setPlaybackRate(Number(value));
|
||||
});
|
||||
});
|
||||
|
||||
settings.querySelectorAll("span:nth-of-type(2) input").forEach(qualInput=>{
|
||||
qualInput.addEventListener("change",(e)=>{
|
||||
var value = e.target.parentElement.innerText;
|
||||
try{
|
||||
var currentTime = csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
settings.querySelector("p:nth-of-type(2) b").innerText = value;
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].setPlaybackQuality(value);
|
||||
}catch(error){
|
||||
throw new Error(error);
|
||||
settings.querySelector("p:nth-of-type(2) b").innerText = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// KEY CHANGE HERE: Hide Overlay logic
|
||||
// ==========================================
|
||||
function onPlayerStateChange(event){
|
||||
if(event.data == YT.PlayerState.PLAYING){
|
||||
csPlayer.csPlayers[videoTag]["isPlaying"] = true;
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="playing";
|
||||
parent.querySelector(".csPlayer-controls-box main .csPlayer-play-pause-btn").className ="ti csPlayer-play-pause-btn ti-player-pause-filled";
|
||||
|
||||
// Hide the cover overlay when playing
|
||||
parent.querySelector(".csPlayer-container span").style.display ="none";
|
||||
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].unMute();
|
||||
parent.querySelector(".csPlayer-container").style.pointerEvents ="none";
|
||||
parent.querySelector(".csPlayer-controls-box").style.display ="flex";
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
|
||||
parent.querySelector(".csPlayer-controls-box").onclick = function(e){
|
||||
if(!parent.querySelector(".csPlayer-controls-box main").contains(e.target) && !parent.querySelector(".csPlayer-controls-box .csPlayer-controls").contains(e.target) && !parent.querySelector(".csPlayer-controls-box .csPlayer-settings-box").contains(e.target)){
|
||||
if(parent.querySelector(".csPlayer-controls-box").classList.contains("csPlayer-controls-open")){
|
||||
parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");
|
||||
clearTimeout(controlsTimeout);
|
||||
}else{
|
||||
parent.querySelector(".csPlayer-controls-box").classList.add("csPlayer-controls-open");
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
parent.querySelector(".csPlayer-controls-box .csPlayer-controls").addEventListener("click", ()=>{
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(()=>{parent.querySelector(".csPlayer-controls-box").classList.remove("csPlayer-controls-open");},3000);
|
||||
});
|
||||
|
||||
} else if(event.data == YT.PlayerState.PAUSED){
|
||||
clearTimeout(controlsTimeout);
|
||||
csPlayer.csPlayers[videoTag]["isPlaying"] = false;
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="paused";
|
||||
parent.querySelector(".csPlayer-controls-box main .csPlayer-play-pause-btn").className ="ti csPlayer-play-pause-btn ti-player-play-filled";
|
||||
|
||||
// === NEW LINE: SHOW COVER ON PAUSE ===
|
||||
// This covers the YouTube "More Videos" grid with your thumbnail/color
|
||||
parent.querySelector(".csPlayer-container span").style.display = "flex";
|
||||
|
||||
if(!parent.querySelector(".csPlayer-controls-box").classList.contains("csPlayer-controls-open")){
|
||||
parent.querySelector(".csPlayer-controls-box").classList.add("csPlayer-controls-open");
|
||||
}
|
||||
} else if(event.data == YT.PlayerState.BUFFERING){
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="buffering";
|
||||
} else if(event.data == YT.PlayerState.CUED){
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="cued";
|
||||
} else if(event.data == YT.PlayerState.ENDED){
|
||||
if(csPlayer.csPlayers[videoTag]["params"]["loop"] == true || csPlayer.csPlayers[videoTag]["params"]["loop"] =="true"){
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].seekTo(0);
|
||||
}else{
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].seekTo(0);
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].pauseVideo();
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="ended";
|
||||
}
|
||||
}
|
||||
try{
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].unloadModule("captions");
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].unloadModule("cc");
|
||||
}catch(exception){}
|
||||
}
|
||||
},
|
||||
init:(videoTag,params)=>{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(videoTag && params && ("defaultId" in params)){
|
||||
if($("#"+videoTag)!=null){
|
||||
if(!(videoTag in csPlayer.csPlayers)){
|
||||
csPlayer.csPlayers[videoTag] = {}
|
||||
csPlayer.csPlayers[videoTag]["videoTag"] = videoTag;
|
||||
csPlayer.csPlayers[videoTag]["params"] = params;
|
||||
if("defaultId" in params){
|
||||
csPlayer.csPlayers[videoTag]["params"]["defaultId"] = params["defaultId"];
|
||||
}if("loop" in params){
|
||||
csPlayer.csPlayers[videoTag]["params"]["loop"] = params["loop"];
|
||||
}if("thumbnail" in params){
|
||||
csPlayer.csPlayers[videoTag]["params"]["thumbnail"] = params["thumbnail"];
|
||||
}if("theme" in params){
|
||||
csPlayer.csPlayers[videoTag]["params"]["theme"] = params["theme"];
|
||||
}
|
||||
csPlayer.csPlayers[videoTag]["isPlaying"] = false;
|
||||
csPlayer.csPlayers[videoTag]["playerState"] ="paused";
|
||||
csPlayer.csPlayers[videoTag]["initialized"] = false; csPlayer.preSetup(videoTag,playerTagId="csPlayer-"+videoTag,params["defaultId"]).then(()=>{
|
||||
var parent = document.querySelector("#"+playerTagId).closest(".csPlayer");
|
||||
if(("thumbnail" in csPlayer.csPlayers[videoTag]["params"])){
|
||||
if(csPlayer.csPlayers[videoTag]["params"]["thumbnail"] == true || csPlayer.csPlayers[videoTag]["params"]["thumbnail"] =="true"){
|
||||
parent.querySelector(".csPlayer-container span").style.backgroundImage =`url("https://img.youtube.com/vi/${csPlayer.csPlayers[videoTag]["params"]["defaultId"]}/maxresdefault.jpg")`;
|
||||
}else if(csPlayer.csPlayers[videoTag]["params"]["thumbnail"] == false || csPlayer.csPlayers[videoTag]["params"]["thumbnail"] =="false"){
|
||||
parent.querySelector(".csPlayer-container span").style.backgroundImage ="none";
|
||||
}else{
|
||||
parent.querySelector(".csPlayer-container span").style.backgroundImage =`url(${csPlayer.csPlayers[videoTag]["params"]["thumbnail"]})`;
|
||||
}} csPlayer.YtSetup(videoTag,playerTagId="csPlayer-"+videoTag,params["defaultId"]).then(()=>{
|
||||
csPlayer.csPlayers[videoTag]["initialized"] = true;
|
||||
console.log("Player",videoTag,"initialized.");
|
||||
});
|
||||
});
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" already exists.");
|
||||
}}else{
|
||||
throw new Error("No tag with id "+videoTag+" available in the document.");
|
||||
}}else{
|
||||
throw new Error("Init function must have two parameters and second parameter must have defaultId.");
|
||||
}
|
||||
resolve();});
|
||||
},
|
||||
|
||||
pause:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].pauseVideo();
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("pause function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
play:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
if(!csPlayer.csPlayers[videoTag]["videoTag"].isMuted()){
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].playVideo();
|
||||
}else{
|
||||
throw new Error("Before calling play function, the video must be played atleat once.");
|
||||
}}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("play function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
getDuration:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
return csPlayer.csPlayers[videoTag]["videoTag"].getDuration();
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("getDuration function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
getCurrentTime:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
return csPlayer.csPlayers[videoTag]["videoTag"].getCurrentTime();
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("getCurrentTime function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
getVideoTitle:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
return csPlayer.csPlayers[videoTag]["videoTag"].getVideoData().title;
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("getVideoTitle function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
getPlayerState:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
return csPlayer.csPlayers[videoTag]["playerState"];
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("getPlayerState function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
changeVideo:(videoTag,videoId)=>{
|
||||
if(videoTag && videoId){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
if(!csPlayer.csPlayers[videoTag]["videoTag"].isMuted()){
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].loadVideoById(videoId,0);
|
||||
}else{
|
||||
throw new Error("Before calling the changeVideo function, the previous video must be played in the player.");
|
||||
}
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("changeVideo function must have two parameters, first parameter as player Id and second as the new YouTube video ID.")
|
||||
}
|
||||
},
|
||||
destroy:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers) && csPlayer.csPlayers[videoTag]["initialized"] == true){
|
||||
if("TimeSliderInterval" in csPlayer.csPlayers[videoTag]){
|
||||
clearInterval(csPlayer.csPlayers[videoTag]["TimeSliderInterval"]);
|
||||
}
|
||||
if("TextTimeInterval" in csPlayer.csPlayers[videoTag]){
|
||||
clearInterval(csPlayer.csPlayers[videoTag]["TextTimeInterval"]);
|
||||
}
|
||||
csPlayer.csPlayers[videoTag]["videoTag"].destroy();
|
||||
delete csPlayer.csPlayers[videoTag];
|
||||
$("#"+videoTag+" .csPlayer").remove();
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" is not initialized yet.")
|
||||
}}else{
|
||||
throw new Error("changeVideo function must have two parameters, first parameter as player Id and second as the new YouTube video ID.")
|
||||
}
|
||||
},
|
||||
initialized:(videoTag)=>{
|
||||
if(videoTag){
|
||||
if((videoTag in csPlayer.csPlayers)){
|
||||
return csPlayer.csPlayers[videoTag]["initialized"];
|
||||
}else{
|
||||
throw new Error("Player "+videoTag+" doesn't exist. ")
|
||||
}}else{
|
||||
throw new Error("pause function must have player id as a parameter.")
|
||||
}
|
||||
},
|
||||
}
|
||||
434
animex/src/csplayer.css
Normal file
@@ -0,0 +1,434 @@
|
||||
@import url("./icons/tabler-icons.min.css");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
/* outline: 1px solid greenyellow !important;*/
|
||||
}
|
||||
|
||||
.csPlayer {
|
||||
--playerBg: #000;
|
||||
--playerColor: #e1e1e1;
|
||||
--playerBR: 5px;
|
||||
--startLoaderColor: #e1e1e1;
|
||||
--startBtnSize: 65px;
|
||||
--startBtnBg: var(--playerColor);
|
||||
--startBtnIconColor: var(--playerBg);
|
||||
--playPauseIconColor: var(--playerColor);
|
||||
--forwardIconColor: var(--playerColor);
|
||||
--backwardIconColor: var(--playerColor);
|
||||
--sliderBg: #383838;
|
||||
--sliderThumbSize: 15px;
|
||||
--sliderThumbColor: var(--playerColor);
|
||||
--sliderSeekTrackColor: var(--playerColor);
|
||||
--sliderLoadedTrackColor: #878787;
|
||||
--currentTimeTextColor: var(--playerColor);
|
||||
--durationTextColor: var(--playerColor);
|
||||
--settingsBtnColor: var(--playerColor);
|
||||
--fullscreenBtnColor: var(--playerColor);
|
||||
--settingsBg: #181818;
|
||||
--settingsTextColor: var(--playerColor);
|
||||
--settingsInputIconBg: #4b4b4b;
|
||||
--settingsInputIconColor: var(--playerColor);
|
||||
background: var(--playerBg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
margin: 0 auto;
|
||||
user-select: none !important;
|
||||
position: relative;
|
||||
border-radius: var(--playerBR);
|
||||
min-width: 270px !important;
|
||||
}
|
||||
.theme-default {
|
||||
--playerBg: #000;
|
||||
--playerColor: #e1e1e1;
|
||||
--playerBR: 5px;
|
||||
--startLoaderColor: #e1e1e1;
|
||||
--startBtnSize: 65px;
|
||||
--startBtnBg: var(--playerColor);
|
||||
--startBtnIconColor: var(--playerBg);
|
||||
--playPauseIconColor: var(--playerColor);
|
||||
--forwardIconColor: var(--playerColor);
|
||||
--backwardIconColor: var(--playerColor);
|
||||
--sliderBg: #383838;
|
||||
--sliderThumbSize: 15px;
|
||||
--sliderThumbColor: var(--playerColor);
|
||||
--sliderSeekTrackColor: var(--playerColor);
|
||||
--sliderLoadedTrackColor: #878787;
|
||||
--currentTimeTextColor: var(--playerColor);
|
||||
--durationTextColor: var(--playerColor);
|
||||
--settingsBtnColor: var(--playerColor);
|
||||
--fullscreenBtnColor: var(--playerColor);
|
||||
--settingsBg: #181818;
|
||||
--settingsTextColor: var(--playerColor);
|
||||
--settingsInputIconBg: #4b4b4b;
|
||||
--settingsInputIconColor: var(--playerColor);
|
||||
}
|
||||
.theme-youtube {
|
||||
--playerBg: #000;
|
||||
--playerColor: #e1e1e1;
|
||||
--playerBR: 5px;
|
||||
--startLoaderColor: var(--playerColor);
|
||||
--startBtnSize: 65px;
|
||||
--startBtnBg: #fe0001;
|
||||
--startBtnIconColor: var(--playerColor);
|
||||
--playPauseIconColor: var(--playerColor);
|
||||
--forwardIconColor: var(--playerColor);
|
||||
--backwardIconColor: var(--playerColor);
|
||||
--sliderBg: #383838;
|
||||
--sliderThumbSize: 15px;
|
||||
--sliderThumbColor: #fe0001;
|
||||
--sliderSeekTrackColor: #fe0001;
|
||||
--sliderLoadedTrackColor: #787672;
|
||||
--currentTimeTextColor: var(--playerColor);
|
||||
--durationTextColor: var(--playerColor);
|
||||
--settingsBtnColor: var(--playerColor);
|
||||
--fullscreenBtnColor: var(--playerColor);
|
||||
--settingsBg: #212121;
|
||||
--settingsTextColor: var(--playerColor);
|
||||
--settingsInputIconBg: #4b4b4b;
|
||||
--settingsInputIconColor: var(--playerColor);
|
||||
}
|
||||
.theme-plyr {
|
||||
--playerBg: #000000;
|
||||
--playerColor: #626a76;
|
||||
--playerBR: 10px;
|
||||
--startLoaderColor: #ffffff;
|
||||
--startBtnSize: 60px;
|
||||
--startBtnBg: #01b2ff;
|
||||
--startBtnIconColor: #ffffff;
|
||||
--playPauseIconColor: #ffffff;
|
||||
--forwardIconColor: #ffffff;
|
||||
--backwardIconColor: #ffffff;
|
||||
--sliderBg: rgba(255, 255, 255, 0.1);
|
||||
--sliderThumbSize: 15px;
|
||||
--sliderThumbColor: #01b2ff;
|
||||
--sliderSeekTrackColor: #01b2ff;
|
||||
--sliderLoadedTrackColor: #787672;
|
||||
--currentTimeTextColor: #ffffff;
|
||||
--durationTextColor: #ffffff;
|
||||
--settingsBtnColor: #ffffff;
|
||||
--fullscreenBtnColor: #ffffff;
|
||||
--settingsBg: rgba(255, 255, 255, 0.95);
|
||||
--settingsTextColor: #3d4049;
|
||||
--settingsInputIconBg: #d9dadc;
|
||||
--settingsInputIconColor: #01b2ff;
|
||||
}
|
||||
.csPlayer .csPlayer-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--playerBR);
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
.csPlayer .csPlayer-container iframe {
|
||||
width: 2400%;
|
||||
height: 100%;
|
||||
margin-left: -1149.95%;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: absolute;
|
||||
}
|
||||
.csPlayer .csPlayer-container span {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--playerBg);
|
||||
background-image: none;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.csPlayer .csPlayer-container span i {
|
||||
display: block;
|
||||
width: var(--startBtnSize);
|
||||
height: var(--startBtnSize);
|
||||
color: var(--startBtnIconColor);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100%;
|
||||
background: var(--startBtnBg);
|
||||
font-size: calc(var(--startBtnSize) / 2.5);
|
||||
}
|
||||
.csPlayer-loading {
|
||||
color: transparent !important;
|
||||
background: transparent !important;
|
||||
border: 5px solid var(--startLoaderColor) !important;
|
||||
border-bottom-color: transparent !important;
|
||||
animation: csPlayerSpin 1s linear infinite;
|
||||
}
|
||||
@keyframes csPlayerSpin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.csPlayer .csPlayer-container span div {
|
||||
width: 100%;
|
||||
height: calc(50% - (var(--startBtnSize) / 2));
|
||||
background: transparent !important;
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.csPlayer .csPlayer-container span div:nth-of-type(1) {
|
||||
top: 0;
|
||||
}
|
||||
.csPlayer .csPlayer-container span div:nth-of-type(2) {
|
||||
bottom: 0;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
background: transparent;
|
||||
border-radius: var(--playerBR);
|
||||
display: none;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.csPlayer-controls-open {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
.csPlayer-controls-open main {
|
||||
transform: translate(-50%, -50%) scale(1) !important;
|
||||
}
|
||||
.csPlayer-controls-open .csPlayer-controls {
|
||||
transform: scale(1) !important;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform 0s ease-in-out;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main i {
|
||||
font-size: 2rem;
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main i:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main i.ti-rewind-backward-10 {
|
||||
color: var(--backwardIconColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main i.ti-rewind-forward-10 {
|
||||
color: var(--forwardIconColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box main i.csPlayer-play-pause-btn {
|
||||
color: var(--playPauseIconColor);
|
||||
padding: 10px;
|
||||
border: 2px solid var(--playPauseIconColor);
|
||||
border-radius: 100%;
|
||||
margin: 0 3.5rem;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 8px 10px;
|
||||
transform: scale(0);
|
||||
transition: transform 0s ease-in-out;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls p {
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 0 4px;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls p:nth-of-type(1) {
|
||||
color: var(--currentTimeTextColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls p:nth-of-type(2) {
|
||||
color: var(--durationTextColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls i {
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 0 6px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls i:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls i.settingsBtn {
|
||||
color: var(--settingsBtnColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls i.fsBtn {
|
||||
color: var(--fullscreenBtnColor);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: calc(var(--sliderThumbSize) / 3);
|
||||
margin: 0 5px;
|
||||
position: relative;
|
||||
background: var(--sliderBg);
|
||||
border-radius: calc(var(--sliderThumbSize) / 3);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls div span {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: var(--sliderLoadedTrackColor);
|
||||
opacity: 0.5;
|
||||
border-radius: calc(var(--sliderThumbSize) / 3);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls div input {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
appearance: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--sliderSeekTrackColor) 0%,
|
||||
transparent 0%
|
||||
);
|
||||
border-radius: calc(var(--sliderThumbSize) / 3);
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-controls div input:focus {
|
||||
outline: none;
|
||||
}
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-controls
|
||||
div
|
||||
input::-webkit-slider-runnable-track {
|
||||
background: transparent;
|
||||
height: calc(var(--sliderThumbSize) / 3);
|
||||
}
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-controls
|
||||
div
|
||||
input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-top: calc(0px - (var(--sliderThumbSize) / 3));
|
||||
background-color: var(--sliderThumbColor);
|
||||
height: calc(var(--sliderThumbSize));
|
||||
width: calc(var(--sliderThumbSize));
|
||||
border-radius: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
/* Firefox */
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-controls
|
||||
div
|
||||
input::-moz-range-track {
|
||||
background: transparent;
|
||||
height: calc(var(--sliderThumbSize) / 3);
|
||||
}
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-controls
|
||||
div
|
||||
input::-moz-range-thumb {
|
||||
margin-top: calc(0px - (var(--sliderThumbSize) / 3));
|
||||
background-color: var(--sliderThumbColor);
|
||||
height: calc(var(--sliderThumbSize));
|
||||
width: calc(var(--sliderThumbSize));
|
||||
border-radius: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box {
|
||||
width: 150px;
|
||||
max-width: 200px;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 50px;
|
||||
padding: 4px 10px;
|
||||
background: var(--settingsBg);
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box p {
|
||||
width: 100%;
|
||||
color: var(--settingsTextColor);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box p b {
|
||||
font-weight: normal;
|
||||
margin: 0 0 0 auto;
|
||||
padding-left: 15px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box p i {
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box span {
|
||||
display: block;
|
||||
max-height: 0px;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.csPlayer .csPlayer-controls-box .csPlayer-settings-box span label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
font-size: 13px;
|
||||
color: var(--settingsTextColor);
|
||||
margin: 6px 0;
|
||||
opacity: 0.8;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-settings-box
|
||||
span
|
||||
label
|
||||
input[type="radio"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--settingsInputIconBg);
|
||||
margin: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 100%;
|
||||
margin-right: 6px;
|
||||
opacity: 1;
|
||||
}
|
||||
.csPlayer
|
||||
.csPlayer-controls-box
|
||||
.csPlayer-settings-box
|
||||
span
|
||||
label
|
||||
input[type="radio"]:checked {
|
||||
border: 5px solid var(--settingsInputIconColor);
|
||||
}
|
||||
4
animex/src/icons/tabler-icons.min.css
vendored
Normal file
BIN
animex/src/icons/tabler-icons.ttf
Normal file
BIN
animex/src/icons/tabler-icons.woff
Normal file
BIN
animex/src/icons/tabler-icons.woff2
Normal file
8
animex/src/mangadex.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<iframe
|
||||
src="https://mangadex.org/"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
title="MangaDex">
|
||||
</iframe>
|
||||
235
animex/sw.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/* sw.js
|
||||
PWA offline service worker
|
||||
- precaches core app shell + important pages
|
||||
- runtime caches images/videos/pdf with limits
|
||||
- navigation fallback to /offline.html
|
||||
- supports skipWaiting and downloadOffline messages
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = '2025-11-20-8'; // bump when you change assets
|
||||
const PRECACHE = `precache-${CACHE_VERSION}`;
|
||||
const RUNTIME = `runtime-${CACHE_VERSION}`;
|
||||
|
||||
// Offline fallback page
|
||||
const OFFLINE_URL = '/offline.html';
|
||||
const PRECACHE_URLS = [
|
||||
'/', // index
|
||||
'/index.html',
|
||||
'/Launch.html',
|
||||
'/about.html',
|
||||
'/in.html',
|
||||
'/intro.html',
|
||||
'/login.html',
|
||||
'/manga.html',
|
||||
'/library.html',
|
||||
'/offline.html',
|
||||
'/portal.html',
|
||||
'/reader.html',
|
||||
'/video_player.html',
|
||||
'/pdf.html',
|
||||
'/view.html',
|
||||
'/search.html',
|
||||
'/lists.html',
|
||||
'/settings.html',
|
||||
'/Resources/manifest.json',
|
||||
'/Resources/styles.css',
|
||||
'/Resources/manga.css',
|
||||
'/Resources/series.css',
|
||||
'/Resources/favicon.png',
|
||||
'/Resources/Images/Launch.png',
|
||||
'/Resources/Images/Launch_screen.png',
|
||||
'/Resources/Images/logo-256.png',
|
||||
'/Resources/Images/logo-512.png',
|
||||
'/sw.js'
|
||||
];
|
||||
|
||||
// Runtime cache limits
|
||||
const MAX_IMAGE_ITEMS = 80;
|
||||
const MAX_VIDEO_ITEMS = 15;
|
||||
const MAX_PDF_ITEMS = 20;
|
||||
|
||||
/* Utility: trim cache to max items (LRU-ish by deleting oldest) */
|
||||
async function trimCache(cacheName, maxItems) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
if (keys.length <= maxItems) return;
|
||||
const removeCount = keys.length - maxItems;
|
||||
for (let i = 0; i < removeCount; i++) {
|
||||
await cache.delete(keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Install: precache app shell */
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting(); // activate worker immediately (be careful with breaking changes)
|
||||
event.waitUntil(
|
||||
caches.open(PRECACHE)
|
||||
.then(cache => cache.addAll(PRECACHE_URLS))
|
||||
.catch(err => {
|
||||
console.error('Precache failed:', err);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* Activate: clean old caches */
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil((async () => {
|
||||
const names = await caches.keys();
|
||||
await Promise.all(
|
||||
names.filter(name => name !== PRECACHE && name !== RUNTIME)
|
||||
.map(name => caches.delete(name))
|
||||
);
|
||||
// Immediately take control of the pages
|
||||
await self.clients.claim();
|
||||
})());
|
||||
});
|
||||
|
||||
/* Fetch handler */
|
||||
self.addEventListener('fetch', event => {
|
||||
const req = event.request;
|
||||
const url = new URL(req.url);
|
||||
|
||||
// Only handle same-origin requests (adjust if assets are on a CDN)
|
||||
const sameOrigin = url.origin === self.location.origin;
|
||||
|
||||
// 1) Navigation requests -> network-first, fallback to cache -> offline page
|
||||
if (req.mode === 'navigate') {
|
||||
event.respondWith(networkFirstFallbackToCache(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) API / JSON/XHR requests -> network-first (don't cache large dynamic responses)
|
||||
if (sameOrigin && url.pathname.startsWith('/api')) {
|
||||
event.respondWith(networkFirst(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Images -> cache-first with size limit
|
||||
if (req.destination === 'image' || /\.(?:png|jpg|jpeg|gif|webp|svg)$/i.test(url.pathname)) {
|
||||
event.respondWith(cacheFirstWithRuntime(req, 'images-cache', MAX_IMAGE_ITEMS));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Video files -> cache-first but avoid precaching; runtime cache with small limit
|
||||
if (/\.(?:mp4|webm|m4v|mov)$/i.test(url.pathname)) {
|
||||
event.respondWith(cacheFirstWithRuntime(req, 'videos-cache', MAX_VIDEO_ITEMS));
|
||||
return;
|
||||
}
|
||||
|
||||
// 5) PDFs and other documents -> cache-first with limit
|
||||
if (/\.(?:pdf|epub|mobi)$/i.test(url.pathname) || req.destination === 'document') {
|
||||
event.respondWith(cacheFirstWithRuntime(req, 'docs-cache', MAX_PDF_ITEMS));
|
||||
return;
|
||||
}
|
||||
|
||||
// 6) CSS/JS/font -> stale-while-revalidate strategy
|
||||
if (/\.(?:js|css|woff2?|ttf|otf)$/i.test(url.pathname) || req.destination === 'script' || req.destination === 'style' || req.destination === 'font') {
|
||||
event.respondWith(staleWhileRevalidate(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: try cache first then network
|
||||
event.respondWith(
|
||||
caches.match(req).then(cached => cached || fetch(req).catch(() => {
|
||||
// if fetch failed and request is a navigation or HTML, show offline page
|
||||
if (req.headers.get('accept') && req.headers.get('accept').includes('text/html')) {
|
||||
return caches.match(OFFLINE_URL);
|
||||
}
|
||||
return new Response(null, { status: 503, statusText: 'Service Unavailable' });
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
/* Strategies */
|
||||
|
||||
async function networkFirstFallbackToCache(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
// Put navigation responses in runtime cache for offline use
|
||||
const cache = await caches.open(RUNTIME);
|
||||
cache.put(request, response.clone()).catch(() => {});
|
||||
return response;
|
||||
} catch (err) {
|
||||
// network failed -> try cache
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
// finally fallback to offline page
|
||||
return caches.match(OFFLINE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
// Update runtime cache
|
||||
const cache = await caches.open(RUNTIME);
|
||||
cache.put(request, response.clone()).catch(() => {});
|
||||
return response;
|
||||
} catch (err) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheFirstWithRuntime(request, cacheName, maxItems = 50) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
// Only cache successful responses (200)
|
||||
if (response && response.status === 200) {
|
||||
cache.put(request, response.clone()).catch(() => {});
|
||||
// trim if necessary
|
||||
trimCache(cacheName, maxItems).catch(() => {});
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
// fallback to precache or offline
|
||||
const fallback = await caches.match(request);
|
||||
if (fallback) return fallback;
|
||||
if (request.headers.get('accept') && request.headers.get('accept').includes('text/html')) {
|
||||
return caches.match(OFFLINE_URL);
|
||||
}
|
||||
return new Response(null, { status: 503, statusText: 'Service Unavailable' });
|
||||
}
|
||||
}
|
||||
|
||||
async function staleWhileRevalidate(request) {
|
||||
const cache = await caches.open(RUNTIME);
|
||||
const cached = await cache.match(request);
|
||||
const networkFetch = fetch(request).then(response => {
|
||||
if (response && response.status === 200) {
|
||||
cache.put(request, response.clone()).catch(() => {});
|
||||
}
|
||||
return response;
|
||||
}).catch(() => null);
|
||||
return cached || networkFetch;
|
||||
}
|
||||
|
||||
/* Listen for messages from the page to trigger SW actions (skipWaiting, downloadOffline) */
|
||||
self.addEventListener('message', event => {
|
||||
const data = event.data;
|
||||
if (!data) return;
|
||||
|
||||
if (data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (data.type === 'DOWNLOAD_OFFLINE') {
|
||||
// Cache urls that are missing from PRECACHE
|
||||
downloadOffline();
|
||||
}
|
||||
});
|
||||
|
||||
/* Pre-cache any resources that aren't yet cached */
|
||||
async function downloadOffline() {
|
||||
const cache = await caches.open(PRECACHE);
|
||||
const cachedRequests = await cache.keys();
|
||||
const cachedUrls = cachedRequests.map(r => new URL(r.url).pathname);
|
||||
const toCache = PRECACHE_URLS.filter(url => !cachedUrls.includes(url));
|
||||
return cache.addAll(toCache);
|
||||
}
|
||||
2192
animex/video_player.html
Normal file
611
animex/view.html
Normal file
@@ -0,0 +1,611 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Episode Player</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"/>
|
||||
<style>
|
||||
:root {
|
||||
--accent-color: #FF9500;
|
||||
--background-color: #0A0A0A;
|
||||
--surface-color: #141414;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #A0A0A0;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--indicator-left: 4px;
|
||||
--indicator-width: 0px;
|
||||
}
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--background-color);
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--text-primary);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- Loading Screen --- */
|
||||
#loading-container {
|
||||
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
||||
z-index: 200; background-color: #000; background-size: cover;
|
||||
background-position: center; display: flex; align-items: flex-end;
|
||||
justify-content: center; transition: opacity 0.3s ease;
|
||||
}
|
||||
#loading-container::before {
|
||||
content: ''; position: absolute; top: 0; left: 0; width: 100%;
|
||||
height: 100%; background: linear-gradient(to top, rgba(0,0,0,1) 20%, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0) 100%);
|
||||
}
|
||||
#loading-info {
|
||||
position: relative; text-align: left; padding: 1.5rem 1.5rem 3rem;
|
||||
max-width: 90%; max-height: 50%; overflow-y: auto; box-sizing: border-box;
|
||||
}
|
||||
#loading-info h2 { margin: 0 0 0.5rem 0; font-size: 1.5rem; }
|
||||
#loading-info p { margin: 0; font-size: 0.9rem; color: var(--text-secondary); line-height: 1.5; }
|
||||
.loader-spinner {
|
||||
border: 4px solid rgba(255, 255, 255, 0.2); border-radius: 50%;
|
||||
border-top: 4px solid var(--accent-color); width: 40px; height: 40px;
|
||||
animation: spin 1s linear infinite; position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@keyframes spin { 100% { transform: translate(-50%, -50%) rotate(360deg); } }
|
||||
|
||||
/* --- Player & Content --- */
|
||||
#player-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #000;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
#player-container iframe {
|
||||
width: 100%; height: 100%; border: none; background: #111;
|
||||
}
|
||||
#error-message {
|
||||
color: #ff6b6b; text-align: center; padding: 2rem 1rem; font-size: 1.1rem;
|
||||
display: flex; align-items: center; justify-content: center; height: 100%;
|
||||
}
|
||||
|
||||
#content-container {
|
||||
flex-grow: 1;
|
||||
padding: 1rem 1.2rem;
|
||||
background: var(--background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* --- Header Info --- */
|
||||
#header-info {
|
||||
text-align: left;
|
||||
}
|
||||
#series-title { font-size: 1.3rem; font-weight: 700; line-height: 1.3; }
|
||||
#meta-line { font-size: 0.9rem; color: var(--text-secondary); margin-top: 0.25rem; }
|
||||
|
||||
/* --- Controls --- */
|
||||
#controls-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
.control-btn {
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
height: 44px; /* Match height of other controls */
|
||||
width: 44px;
|
||||
}
|
||||
.control-btn:hover {
|
||||
background: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
#source-controls {
|
||||
display: flex;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 12px;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* SUB/DUB Toggle */
|
||||
.subdub-toggle {
|
||||
position: relative; display: flex;
|
||||
list-style: none; margin: 0; padding: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.subdub-toggle::before {
|
||||
content: ''; position: absolute; top: 0; left: var(--indicator-left);
|
||||
width: var(--indicator-width); height: 100%;
|
||||
background: var(--accent-color);
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
.subdub-btn {
|
||||
position: relative; /* Ensure z-index stacking context */
|
||||
background: none; border: none; color: var(--text-secondary);
|
||||
font-size: 0.9em; font-weight: 600;
|
||||
padding: 8px 16px; /* Add horizontal padding for spacing */
|
||||
border-radius: 8px; cursor: pointer;
|
||||
outline: none; z-index: 2; transition: color 0.3s ease;
|
||||
flex-grow: 1; text-align: center;
|
||||
}
|
||||
.subdub-btn.active { color: var(--text-primary); }
|
||||
|
||||
/* Source Selector */
|
||||
.source-selector-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
#source-changer-select {
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 28px 8px 12px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
text-align-last: center; /* For Firefox */
|
||||
}
|
||||
.source-selector-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid var(--text-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
#source-changer-select:focus { outline: none; }
|
||||
|
||||
/* Next Episode Button */
|
||||
#next-episode-btn {
|
||||
display: block; background: var(--accent-color); color: #fff;
|
||||
font-weight: 600; font-size: 0.95em; border: none; border-radius: 12px;
|
||||
padding: 12px 20px; cursor: pointer; transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#next-episode-btn:hover { background: #e6850a; }
|
||||
#series-over-msg {
|
||||
text-align: center; font-weight: 600; font-size: 0.95em;
|
||||
color: var(--accent-color); opacity: 0.8; padding: 12px 0;
|
||||
}
|
||||
|
||||
/* Synopsis */
|
||||
#synopsis-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#synopsis-container h4 {
|
||||
margin: 0 0 0.5rem 0; font-weight: 600; opacity: 0.9;
|
||||
}
|
||||
#synopsis-container p {
|
||||
font-size: 0.9em; line-height: 1.6; color: var(--text-secondary); margin: 0;
|
||||
}
|
||||
|
||||
/* --- Landscape Mode --- */
|
||||
@media (orientation: landscape) and (max-height: 600px) {
|
||||
body { flex-direction: row; }
|
||||
#player-container {
|
||||
width: 100vw; height: 100vh;
|
||||
position: fixed; top: 0; left: 0; z-index: 100;
|
||||
}
|
||||
#content-container {
|
||||
display: none; /* Hide by default in landscape */
|
||||
}
|
||||
/* Add specific landscape controls if needed, or rely on player's native controls */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="loading-container">
|
||||
<div class="loader-spinner"></div>
|
||||
<div id="loading-info">
|
||||
<h2 id="loading-title">—</h2>
|
||||
<p id="loading-synopsis">—</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="player-container"></div>
|
||||
|
||||
<div id="content-container">
|
||||
<div id="header-info">
|
||||
<div id="series-title">—</div>
|
||||
<div id="meta-line">
|
||||
<span>Type: <span id="series-type">—</span></span> |
|
||||
<span>Year: <span id="series-year">—</span></span> |
|
||||
<span>Episode: <span id="episode-number">—</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls-main">
|
||||
<div id="source-controls">
|
||||
<ul class="subdub-toggle" id="subdub-toggle">
|
||||
<li><button class="subdub-btn active" id="sub-option-btn" type="button">SUB</button></li>
|
||||
<li><button class="subdub-btn" id="dub-option-btn" type="button">DUB</button></li>
|
||||
</ul>
|
||||
<div class="source-selector-wrapper">
|
||||
<select id="source-changer-select" name="source-changer"></select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="reload-iframe-btn" class="control-btn" title="Reload Player">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<div id="next-episode-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="synopsis-container">
|
||||
<h4>Synopsis</h4>
|
||||
<p id="synopsis-text">—</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const jikanId = params.get('id');
|
||||
const episode = params.get('ep');
|
||||
|
||||
// DOM Elements
|
||||
const playerContainer = document.getElementById('player-container');
|
||||
const loadingContainer = document.getElementById('loading-container');
|
||||
const subOptionBtn = document.getElementById('sub-option-btn');
|
||||
const dubOptionBtn = document.getElementById('dub-option-btn');
|
||||
const sourceChangerSelect = document.getElementById('source-changer-select');
|
||||
const nextEpisodeContainer = document.getElementById('next-episode-container');
|
||||
const synopsisText = document.getElementById('synopsis-text');
|
||||
|
||||
// App State
|
||||
const serverIp = localStorage.getItem('extension_server_ip');
|
||||
const profileId = localStorage.getItem("currentProfileId");
|
||||
let currentProfile = null;
|
||||
let userPref = localStorage.getItem('animeDubPref') || 'sub';
|
||||
let currentModule = 'default';
|
||||
let fetchedAnimeData = null; // Declare here to make it accessible
|
||||
|
||||
// --- Main Initialization ---
|
||||
try {
|
||||
// Initial Checks
|
||||
if (!jikanId || !episode) {
|
||||
throw new Error('Missing "id" or "ep" parameter.');
|
||||
}
|
||||
if (!serverIp) {
|
||||
throw new Error('Extension server IP not set. Please configure it.');
|
||||
}
|
||||
|
||||
loadingContainer.style.display = 'flex';
|
||||
|
||||
await fetchProfile();
|
||||
fetchedAnimeData = await fetchAnimeData(); // Assign to the new variable
|
||||
updateUI(fetchedAnimeData);
|
||||
await logWatchHistory(jikanId, episode, fetchedAnimeData.title);
|
||||
await populateSourceChanger();
|
||||
|
||||
if (userPref === 'dub') {
|
||||
dubOptionBtn.classList.add('active');
|
||||
subOptionBtn.classList.remove('active');
|
||||
} else {
|
||||
subOptionBtn.classList.add('active');
|
||||
dubOptionBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
await loadIframe(userPref === 'dub', currentModule, fetchedAnimeData); // Pass fetchedAnimeData
|
||||
setupEventListeners();
|
||||
setTimeout(moveActiveIndicator, 150);
|
||||
|
||||
} catch (error) {
|
||||
console.error("A critical error occurred during initialization:", error);
|
||||
loadingContainer.style.display = 'none';
|
||||
playerContainer.innerHTML = `<div id="error-message">Failed to load player: ${error.message}</div>`;
|
||||
}
|
||||
|
||||
|
||||
// --- Functions ---
|
||||
|
||||
async function fetchProfile() {
|
||||
if (!profileId) return;
|
||||
try {
|
||||
const response = await fetch(`/profiles/${profileId}`);
|
||||
if (response.ok) currentProfile = await response.json();
|
||||
else console.warn("Could not load active profile.");
|
||||
} catch (e) {
|
||||
console.error("Error fetching profile:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function logWatchHistory(malId, episodeNumber, seriesTitle) {
|
||||
if (!currentProfile || !seriesTitle || seriesTitle === 'Unknown') return;
|
||||
const query = new URLSearchParams({
|
||||
mal_id: malId,
|
||||
episode_number: episodeNumber,
|
||||
series_title: seriesTitle,
|
||||
season_number: 1,
|
||||
});
|
||||
try {
|
||||
await fetch(`/profiles/${currentProfile.id}/watch-history?${query.toString()}`, { method: 'POST' });
|
||||
console.log(`Logged watch history for Ep. ${episodeNumber}.`);
|
||||
} catch (error) {
|
||||
console.error("Failed to log watch history:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function moveActiveIndicator() {
|
||||
const activeButton = document.querySelector('.subdub-btn.active');
|
||||
if (activeButton) {
|
||||
const indicator = document.querySelector('.subdub-toggle');
|
||||
const listRect = indicator.getBoundingClientRect();
|
||||
const buttonRect = activeButton.getBoundingClientRect();
|
||||
const leftPosition = buttonRect.left - listRect.left;
|
||||
indicator.style.setProperty('--indicator-left', `${leftPosition}px`);
|
||||
indicator.style.setProperty('--indicator-width', `${activeButton.offsetWidth}px`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAnimeData() {
|
||||
let title = 'Unknown', type = 'N/A', year = 'N/A', totalEpisodes = null, synopsis = 'No synopsis available.';
|
||||
let episodeSynopsis = 'No episode synopsis available.', episodeThumbnail = null;
|
||||
|
||||
try {
|
||||
const [jikanInfoResp, jikanEpisodesResp, kitsuMapResp] = await Promise.all([
|
||||
fetch(`https://api.jikan.moe/v4/anime/${jikanId}`),
|
||||
fetch(`https://api.jikan.moe/v4/anime/${jikanId}/episodes`),
|
||||
fetch(`http://${serverIp}:7275/map/mal/${jikanId}`)
|
||||
]);
|
||||
|
||||
if (jikanInfoResp.ok) {
|
||||
const data = (await jikanInfoResp.json()).data || {};
|
||||
title = data.title_english || data.title || 'Unknown';
|
||||
type = data.type || 'N/A';
|
||||
year = data.year || data.aired?.prop?.from?.year || 'N/A';
|
||||
}
|
||||
|
||||
if (jikanEpisodesResp.ok) {
|
||||
const data = await jikanEpisodesResp.json();
|
||||
totalEpisodes = data.pagination?.last_visible_page > 1 ? null : data.data?.length;
|
||||
}
|
||||
|
||||
if (kitsuMapResp.ok) {
|
||||
const mapData = await kitsuMapResp.json();
|
||||
const kitsuId = mapData.kitsu_id;
|
||||
const [kitsuAnimeResp, kitsuEpisodeResp] = await Promise.all([
|
||||
fetch(`https://kitsu.io/api/edge/anime/${kitsuId}`),
|
||||
fetch(`https://kitsu.io/api/edge/anime/${kitsuId}/episodes?filter[number]=${episode}`)
|
||||
]);
|
||||
if (kitsuAnimeResp.ok) {
|
||||
const kitsuData = await kitsuAnimeResp.json();
|
||||
if (kitsuData && kitsuData.data && kitsuData.data.attributes) {
|
||||
const attributes = kitsuData.data.attributes;
|
||||
synopsis = attributes.synopsis || synopsis;
|
||||
if (attributes.posterImage?.original) {
|
||||
loadingContainer.style.backgroundImage = `url(${attributes.posterImage.original})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kitsuEpisodeResp.ok) {
|
||||
const episodeData = await kitsuEpisodeResp.json();
|
||||
if (episodeData.data?.length > 0 && episodeData.data[0].attributes) {
|
||||
const episodeAttributes = episodeData.data[0].attributes;
|
||||
episodeSynopsis = episodeAttributes.synopsis || episodeAttributes.description || episodeSynopsis;
|
||||
episodeThumbnail = episodeAttributes.thumbnail?.original;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch some anime data:', err);
|
||||
// Return default data, but the main try/catch will handle the UI
|
||||
}
|
||||
|
||||
return { title, type, year, totalEpisodes, synopsis, episodeSynopsis, episodeThumbnail };
|
||||
}
|
||||
|
||||
async function populateSourceChanger() {
|
||||
try {
|
||||
const response = await fetch(`http://${serverIp}:7275/modules/streaming`);
|
||||
if (!response.ok) throw new Error('Failed to fetch streaming modules');
|
||||
const modules = await response.json();
|
||||
|
||||
sourceChangerSelect.innerHTML = '';
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.textContent = 'Default Source';
|
||||
defaultOption.value = 'default';
|
||||
sourceChangerSelect.appendChild(defaultOption);
|
||||
|
||||
if (modules.length > 0) {
|
||||
modules.forEach(module => {
|
||||
const option = document.createElement('option');
|
||||
option.value = module.id;
|
||||
option.textContent = module.name;
|
||||
sourceChangerSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not populate source changer:', error);
|
||||
const errorOption = document.createElement('option');
|
||||
errorOption.textContent = 'Error loading';
|
||||
errorOption.value = 'default';
|
||||
errorOption.disabled = true;
|
||||
sourceChangerSelect.innerHTML = '';
|
||||
sourceChangerSelect.appendChild(errorOption);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(data) {
|
||||
const safeData = data || {};
|
||||
document.getElementById('series-title').textContent = safeData.title || '—';
|
||||
document.getElementById('series-type').textContent = safeData.type || 'N/A';
|
||||
document.getElementById('series-year').textContent = safeData.year || 'N/A';
|
||||
document.getElementById('episode-number').textContent = episode || 'N/A';
|
||||
synopsisText.textContent = safeData.episodeSynopsis || 'No synopsis available.';
|
||||
document.getElementById('loading-title').textContent = safeData.title || 'Loading...';
|
||||
document.getElementById('loading-synopsis').textContent = safeData.episodeSynopsis || '';
|
||||
|
||||
if (safeData.episodeThumbnail) {
|
||||
loadingContainer.style.backgroundImage = `url(${safeData.episodeThumbnail})`;
|
||||
}
|
||||
|
||||
const currentEpNum = parseInt(episode, 10);
|
||||
nextEpisodeContainer.innerHTML = ''; // Clear previous button/message
|
||||
if (safeData.totalEpisodes && currentEpNum < safeData.totalEpisodes) {
|
||||
const nextEpBtn = document.createElement('button');
|
||||
nextEpBtn.id = 'next-episode-btn';
|
||||
nextEpBtn.textContent = `Next →`;
|
||||
nextEpBtn.addEventListener('click', () => {
|
||||
params.set('ep', currentEpNum + 1);
|
||||
window.location.search = params.toString();
|
||||
});
|
||||
nextEpisodeContainer.appendChild(nextEpBtn);
|
||||
} else if (safeData.totalEpisodes && currentEpNum >= safeData.totalEpisodes) {
|
||||
const seriesOverMsg = document.createElement('div');
|
||||
seriesOverMsg.id = 'series-over-msg';
|
||||
seriesOverMsg.textContent = 'Series Complete';
|
||||
nextEpisodeContainer.appendChild(seriesOverMsg);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIframe(dub, preferModule = 'default', animeData) {
|
||||
playerContainer.innerHTML = '';
|
||||
// Don't show loading screen again if it's already visible
|
||||
if (loadingContainer.style.display !== 'flex') {
|
||||
loadingContainer.style.display = 'flex';
|
||||
}
|
||||
|
||||
let iframeSrcURL = `http://${serverIp}:7275/iframe-src?mal_id=${jikanId}&episode=${episode}&dub=${dub}`;
|
||||
if (preferModule && preferModule !== 'default') {
|
||||
iframeSrcURL += `&prefer_module=${encodeURIComponent(preferModule)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(iframeSrcURL);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404 && preferModule !== 'default') {
|
||||
console.warn(`Module '${preferModule}' failed with 404. Retrying with 'default' module.`);
|
||||
// Clear previous error message if any
|
||||
playerContainer.innerHTML = '';
|
||||
// Retry with default module
|
||||
await loadIframe(dub, 'default', animeData);
|
||||
return; // Stop execution here as the retry is handled
|
||||
} else {
|
||||
throw new Error(`Proxy error ${res.status}`);
|
||||
}
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!json.src) throw new Error('No embed source found from any module');
|
||||
|
||||
// Append to json.src
|
||||
const playerUrl = new URL(json.src, window.location.origin);
|
||||
playerUrl.searchParams.set('series_title', animeData.title);
|
||||
playerUrl.searchParams.set('episode_num', episode); // Ensure episode is always passed explicitly
|
||||
if (animeData.episodeThumbnail) {
|
||||
playerUrl.searchParams.set('thumbnail_url', animeData.episodeThumbnail);
|
||||
}
|
||||
json.src = playerUrl.toString();
|
||||
|
||||
if (json.source_module) {
|
||||
sourceChangerSelect.value = json.source_module;
|
||||
currentModule = json.source_module;
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-fullscreen');
|
||||
iframe.setAttribute('src', json.src);
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
iframe.setAttribute('scrolling', 'no');
|
||||
playerContainer.appendChild(iframe);
|
||||
|
||||
// Hide the loading screen as soon as the iframe is added to the DOM.
|
||||
// The iframe itself will show its own loading progress.
|
||||
loadingContainer.style.display = 'none';
|
||||
} catch (err) {
|
||||
console.error('Error loading player:', err);
|
||||
loadingContainer.style.display = 'none';
|
||||
playerContainer.innerHTML = `<div id="error-message">Could not load player: ${err.message}. Please try another source or type.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
const reloadIframeBtn = document.getElementById('reload-iframe-btn');
|
||||
|
||||
subOptionBtn.addEventListener('click', async () => {
|
||||
if (userPref !== 'sub') {
|
||||
userPref = 'sub';
|
||||
localStorage.setItem('animeDubPref', 'sub');
|
||||
subOptionBtn.classList.add('active');
|
||||
dubOptionBtn.classList.remove('active');
|
||||
moveActiveIndicator();
|
||||
// FIX: Pass fetchedAnimeData as the 3rd argument
|
||||
await loadIframe(false, currentModule, fetchedAnimeData);
|
||||
}
|
||||
});
|
||||
|
||||
dubOptionBtn.addEventListener('click', async () => {
|
||||
if (userPref !== 'dub') {
|
||||
userPref = 'dub';
|
||||
localStorage.setItem('animeDubPref', 'dub');
|
||||
dubOptionBtn.classList.add('active');
|
||||
subOptionBtn.classList.remove('active');
|
||||
moveActiveIndicator();
|
||||
// FIX: Pass fetchedAnimeData as the 3rd argument
|
||||
await loadIframe(true, currentModule, fetchedAnimeData);
|
||||
}
|
||||
});
|
||||
|
||||
sourceChangerSelect.addEventListener('change', async () => {
|
||||
const selectedModule = sourceChangerSelect.value;
|
||||
if (selectedModule !== currentModule) {
|
||||
currentModule = selectedModule;
|
||||
// FIX: Pass fetchedAnimeData as the 3rd argument
|
||||
await loadIframe(userPref === 'dub', currentModule, fetchedAnimeData);
|
||||
}
|
||||
});
|
||||
|
||||
reloadIframeBtn.addEventListener('click', () => {
|
||||
const iframe = playerContainer.querySelector('iframe');
|
||||
if (iframe) {
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => setTimeout(moveActiveIndicator, 100));
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (!event.data || event.data.type !== 'animex-next-episode') return;
|
||||
const currentEp = parseInt(episode, 10);
|
||||
if (isNaN(currentEp)) return;
|
||||
params.set('ep', currentEp + 1);
|
||||
window.location.search = params.toString();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||