anilist integrated to my list
This commit is contained in:
@@ -19,7 +19,7 @@ const galleryRoutes = require('./dist/api/gallery/gallery.routes');
|
|||||||
const rpcRoutes = require('./dist/api/rpc/rpc.routes');
|
const rpcRoutes = require('./dist/api/rpc/rpc.routes');
|
||||||
const userRoutes = require('./dist/api/user/user.routes');
|
const userRoutes = require('./dist/api/user/user.routes');
|
||||||
const listRoutes = require('./dist/api/list/list.routes');
|
const listRoutes = require('./dist/api/list/list.routes');
|
||||||
const anilistRoute = require('./dist/api/anilist');
|
const anilistRoute = require('./dist/api/anilist/anilist');
|
||||||
|
|
||||||
|
|
||||||
fastify.addHook("preHandler", async (request) => {
|
fastify.addHook("preHandler", async (request) => {
|
||||||
|
|||||||
214
src/api/anilist/anilist.service.ts
Normal file
214
src/api/anilist/anilist.service.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { queryOne } from '../../shared/database';
|
||||||
|
|
||||||
|
const USER_DB = 'userdata';
|
||||||
|
|
||||||
|
export async function getUserAniList(appUserId: number) {
|
||||||
|
const sql = `
|
||||||
|
SELECT access_token, anilist_user_id
|
||||||
|
FROM UserIntegration
|
||||||
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
|
`;
|
||||||
|
|
||||||
|
const integration = await queryOne(sql, [appUserId], USER_DB) as any;
|
||||||
|
if (!integration) return [];
|
||||||
|
|
||||||
|
const { access_token, anilist_user_id } = integration;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
query ($userId: Int) {
|
||||||
|
anime: MediaListCollection(userId: $userId, type: ANIME) {
|
||||||
|
lists {
|
||||||
|
entries {
|
||||||
|
mediaId
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manga: MediaListCollection(userId: $userId, type: MANGA) {
|
||||||
|
lists {
|
||||||
|
entries {
|
||||||
|
mediaId
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const res = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: { userId: anilist_user_id }
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
const normalize = (lists: any[], type: 'ANIME' | 'MANGA') => {
|
||||||
|
const result: any[] = [];
|
||||||
|
|
||||||
|
for (const list of lists || []) {
|
||||||
|
for (const entry of list.entries || []) {
|
||||||
|
result.push({
|
||||||
|
user_id: appUserId,
|
||||||
|
entry_id: entry.mediaId,
|
||||||
|
source: 'anilist',
|
||||||
|
entry_type: type,
|
||||||
|
status: entry.status,
|
||||||
|
progress: entry.progress || 0,
|
||||||
|
score: entry.score || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...normalize(json?.data?.anime?.lists, 'ANIME'),
|
||||||
|
...normalize(json?.data?.manga?.lists, 'MANGA')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAniListEntry(token: string, params: {
|
||||||
|
mediaId: number | string;
|
||||||
|
status?: string | null;
|
||||||
|
progress?: number | null;
|
||||||
|
score?: number | null;
|
||||||
|
}) {
|
||||||
|
const mutation = `
|
||||||
|
mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
|
||||||
|
SaveMediaListEntry (
|
||||||
|
mediaId: $mediaId,
|
||||||
|
status: $status,
|
||||||
|
progress: $progress,
|
||||||
|
score: $score
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const variables: any = {
|
||||||
|
mediaId: Number(params.mediaId),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.status != null) variables.status = params.status;
|
||||||
|
if (params.progress != null) variables.progress = params.progress;
|
||||||
|
if (params.score != null) variables.score = params.score;
|
||||||
|
|
||||||
|
const res = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: mutation, variables }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || json?.errors?.length) {
|
||||||
|
throw new Error("AniList update failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.data?.SaveMediaListEntry || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAniListEntry(token: string, mediaId: number) {
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
query ($mediaId: Int) {
|
||||||
|
MediaList(mediaId: $mediaId) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const qRes = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query, variables: { mediaId } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const qJson = await qRes.json();
|
||||||
|
const listEntryId = qJson?.data?.MediaList?.id;
|
||||||
|
|
||||||
|
if (!listEntryId) {
|
||||||
|
throw new Error("Entry not found or unauthorized to delete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutation = `
|
||||||
|
mutation ($id: Int) {
|
||||||
|
DeleteMediaListEntry(id: $id) {
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mRes = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: mutation,
|
||||||
|
variables: { id: listEntryId }
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mJson = await mRes.json();
|
||||||
|
|
||||||
|
if (mJson?.errors?.length) {
|
||||||
|
throw new Error("Error eliminando entrada en AniList");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSingleAniListEntry(
|
||||||
|
token: string,
|
||||||
|
mediaId: number,
|
||||||
|
type: 'ANIME' | 'MANGA'
|
||||||
|
) {
|
||||||
|
const query = `
|
||||||
|
query ($mediaId: Int, $type: MediaType) {
|
||||||
|
MediaList(mediaId: $mediaId, type: $type) {
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const res = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: { mediaId, type }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
return json?.data?.MediaList || null;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
import { run } from "../shared/database";
|
import { run } from "../../shared/database";
|
||||||
|
|
||||||
async function anilist(fastify: FastifyInstance) {
|
async function anilist(fastify: FastifyInstance) {
|
||||||
fastify.get("/anilist", async (request, reply) => {
|
fastify.get("/anilist", async (request, reply) => {
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
// list.controller.ts
|
|
||||||
|
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import * as listService from './list.service';
|
import * as listService from './list.service';
|
||||||
|
|
||||||
// Tipos de solicitud asumidos:
|
|
||||||
// - UserRequest: Request con el objeto 'user' adjunto por el hook de autenticación.
|
|
||||||
interface UserRequest extends FastifyRequest {
|
interface UserRequest extends FastifyRequest {
|
||||||
user?: { id: number };
|
user?: { id: number };
|
||||||
}
|
}
|
||||||
@@ -19,14 +15,16 @@ interface UpsertEntryBody {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteEntryParams {
|
interface EntryParams {
|
||||||
entryId: string;
|
entryId: string;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SingleEntryQuery {
|
||||||
|
source: string;
|
||||||
|
entry_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /list
|
|
||||||
* Obtiene toda la lista del usuario autenticado.
|
|
||||||
*/
|
|
||||||
export async function getList(req: UserRequest, reply: FastifyReply) {
|
export async function getList(req: UserRequest, reply: FastifyReply) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -42,10 +40,42 @@ export async function getList(req: UserRequest, reply: FastifyReply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function getSingleEntry(req: UserRequest, reply: FastifyReply) {
|
||||||
* POST /list/entry
|
const userId = req.user?.id;
|
||||||
* Crea o actualiza una entrada de lista (upsert).
|
const { entryId } = req.params as EntryParams;
|
||||||
*/
|
const { source, entry_type } = req.query as SingleEntryQuery;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entryId || !source || !entry_type) {
|
||||||
|
return reply.code(400).send({ error: "Missing required identifier: entryId, source, or entry_type." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryIdentifier = entryId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const entry = await listService.getSingleListEntry(
|
||||||
|
userId,
|
||||||
|
entryIdentifier,
|
||||||
|
source,
|
||||||
|
entry_type
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
|
||||||
|
return reply.code(404).send({ found: false, message: "Entry not found in user list." });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { found: true, entry: entry };
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return reply.code(500).send({ error: "Failed to retrieve list entry" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
const body = req.body as UpsertEntryBody;
|
const body = req.body as UpsertEntryBody;
|
||||||
@@ -54,7 +84,6 @@ export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
return reply.code(401).send({ error: "Unauthorized" });
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// <--- NUEVO: Validación de entry_type
|
|
||||||
if (!body.entry_id || !body.source || !body.status || !body.entry_type) {
|
if (!body.entry_id || !body.source || !body.status || !body.entry_type) {
|
||||||
return reply.code(400).send({ error: "Missing required fields (entry_id, source, status, entry_type)." });
|
return reply.code(400).send({ error: "Missing required fields (entry_id, source, status, entry_type)." });
|
||||||
}
|
}
|
||||||
@@ -65,7 +94,7 @@ export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
entry_id: body.entry_id,
|
entry_id: body.entry_id,
|
||||||
external_id: body.external_id,
|
external_id: body.external_id,
|
||||||
source: body.source,
|
source: body.source,
|
||||||
entry_type: body.entry_type, // <--- NUEVO: Pasar entry_type
|
entry_type: body.entry_type,
|
||||||
status: body.status,
|
status: body.status,
|
||||||
progress: body.progress || 0,
|
progress: body.progress || 0,
|
||||||
score: body.score || null
|
score: body.score || null
|
||||||
@@ -80,25 +109,22 @@ export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /list/entry/:entryId
|
|
||||||
* Elimina una entrada de lista.
|
|
||||||
*/
|
|
||||||
export async function deleteEntry(req: UserRequest, reply: FastifyReply) {
|
export async function deleteEntry(req: UserRequest, reply: FastifyReply) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
const { entryId } = req.params as DeleteEntryParams;
|
const { entryId } = req.params as EntryParams;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return reply.code(401).send({ error: "Unauthorized" });
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const numericEntryId = parseInt(entryId, 10);
|
const entryIdentifier = entryId;
|
||||||
if (isNaN(numericEntryId)) {
|
|
||||||
|
if (!entryIdentifier) {
|
||||||
return reply.code(400).send({ error: "Invalid entry ID." });
|
return reply.code(400).send({ error: "Invalid entry ID." });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await listService.deleteListEntry(userId, numericEntryId);
|
const result = await listService.deleteListEntry(userId, entryIdentifier);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return { success: true, message: "Entry deleted successfully." };
|
return { success: true, message: "Entry deleted successfully." };
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as controller from './list.controller';
|
|||||||
|
|
||||||
async function listRoutes(fastify: FastifyInstance) {
|
async function listRoutes(fastify: FastifyInstance) {
|
||||||
fastify.get('/list', controller.getList);
|
fastify.get('/list', controller.getList);
|
||||||
|
fastify.get('/list/entry/:entryId', controller.getSingleEntry);
|
||||||
fastify.post('/list/entry', controller.upsertEntry);
|
fastify.post('/list/entry', controller.upsertEntry);
|
||||||
fastify.delete('/list/entry/:entryId', controller.deleteEntry);
|
fastify.delete('/list/entry/:entryId', controller.deleteEntry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
// list.service.ts (Actualizado)
|
import {queryAll, run, queryOne} from '../../shared/database';
|
||||||
|
|
||||||
import {queryAll, run} from '../../shared/database';
|
|
||||||
import {getExtension} from '../../shared/extensions';
|
import {getExtension} from '../../shared/extensions';
|
||||||
import * as animeService from '../anime/anime.service';
|
import * as animeService from '../anime/anime.service';
|
||||||
import * as booksService from '../books/books.service';
|
import * as booksService from '../books/books.service';
|
||||||
|
import * as aniListService from '../anilist/anilist.service';
|
||||||
|
|
||||||
// Define la interfaz de entrada de lista (sin external_id)
|
|
||||||
interface ListEntryData {
|
interface ListEntryData {
|
||||||
entry_type: any;
|
entry_type: any;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
entry_id: number; // ID de contenido de la fuente (AniList, MAL, o local)
|
entry_id: number;
|
||||||
source: string; // 'anilist', 'local', etc.
|
|
||||||
status: string; // 'COMPLETED', 'WATCHING', etc.
|
source: string;
|
||||||
|
|
||||||
|
status: string;
|
||||||
|
|
||||||
progress: number;
|
progress: number;
|
||||||
score: number | null;
|
score: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_DB = 'userdata';
|
const USER_DB = 'userdata';
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserta o actualiza una entrada de lista.
|
|
||||||
* Utiliza ON CONFLICT(user_id, entry_id) para el upsert.
|
|
||||||
*/
|
|
||||||
export async function upsertListEntry(entry: any) {
|
export async function upsertListEntry(entry: any) {
|
||||||
const {
|
const {
|
||||||
user_id,
|
user_id,
|
||||||
@@ -33,6 +30,29 @@ export async function upsertListEntry(entry: any) {
|
|||||||
score
|
score
|
||||||
} = entry;
|
} = entry;
|
||||||
|
|
||||||
|
if (source === 'anilist') {
|
||||||
|
|
||||||
|
const token = await getActiveAccessToken(user_id);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await aniListService.updateAniListEntry(token, {
|
||||||
|
mediaId: entry_id,
|
||||||
|
status,
|
||||||
|
progress,
|
||||||
|
score
|
||||||
|
});
|
||||||
|
|
||||||
|
return { changes: 0, external: true, anilistResult: result };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error actualizando AniList:", err);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
INSERT INTO ListEntry
|
INSERT INTO ListEntry
|
||||||
(user_id, entry_id, source, entry_type, status, progress, score, updated_at)
|
(user_id, entry_id, source, entry_type, status, progress, score, updated_at)
|
||||||
@@ -59,16 +79,13 @@ export async function upsertListEntry(entry: any) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await run(sql, params, USER_DB);
|
const result = await run(sql, params, USER_DB);
|
||||||
return { changes: result.changes, lastID: result.lastID };
|
return { changes: result.changes, lastID: result.lastID, external: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al guardar la entrada de lista:", error);
|
console.error("Error al guardar la entrada de lista:", error);
|
||||||
throw new Error("Error en la base de datos al guardar la entrada.");
|
throw new Error("Error en la base de datos al guardar la entrada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Recupera la lista completa de un usuario.
|
|
||||||
*/
|
|
||||||
export async function getUserList(userId: number): Promise<any> {
|
export async function getUserList(userId: number): Promise<any> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT * FROM ListEntry
|
SELECT * FROM ListEntry
|
||||||
@@ -77,11 +94,25 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Obtener la lista base de la DB
|
|
||||||
const dbList = await queryAll(sql, [userId], USER_DB) as ListEntryData[];
|
const dbList = await queryAll(sql, [userId], USER_DB) as ListEntryData[];
|
||||||
|
|
||||||
// 2. Crear un array de promesas para obtener los detalles de cada entrada concurrentemente
|
const connected = await isConnected(userId);
|
||||||
const enrichedListPromises = dbList.map(async (entry) => {
|
|
||||||
|
let finalList: ListEntryData[] = [...dbList];
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
const token = await getActiveAccessToken(userId);
|
||||||
|
|
||||||
|
const anilistEntries = await aniListService.getUserAniList(userId);
|
||||||
|
|
||||||
|
const localWithoutAnilist = dbList.filter(
|
||||||
|
entry => entry.source !== 'anilist'
|
||||||
|
);
|
||||||
|
|
||||||
|
finalList = [...anilistEntries, ...localWithoutAnilist];
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedListPromises = finalList.map(async (entry) => {
|
||||||
let contentDetails: any | null = null;
|
let contentDetails: any | null = null;
|
||||||
const id = entry.entry_id;
|
const id = entry.entry_id;
|
||||||
const source = entry.source;
|
const source = entry.source;
|
||||||
@@ -89,13 +120,12 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === 'ANIME') {
|
if (type === 'ANIME') {
|
||||||
// Lógica para ANIME
|
|
||||||
let anime: any;
|
let anime: any;
|
||||||
|
|
||||||
if (source === 'anilist') {
|
if (source === 'anilist') {
|
||||||
anime = await animeService.getAnimeById(id);
|
anime = await animeService.getAnimeById(id);
|
||||||
} else {
|
} else {
|
||||||
const ext = getExtension(source);
|
const ext = getExtension(source);
|
||||||
// Asegurar que id sea una cadena para getAnimeInfoExtension si el id es un número
|
|
||||||
anime = await animeService.getAnimeInfoExtension(ext, id.toString());
|
anime = await animeService.getAnimeInfoExtension(ext, id.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +136,12 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
} else if (type === 'MANGA' || type === 'NOVEL') {
|
} else if (type === 'MANGA' || type === 'NOVEL') {
|
||||||
// Lógica para MANGA, NOVEL y otros "books"
|
|
||||||
let book: any;
|
let book: any;
|
||||||
|
|
||||||
if (source === 'anilist') {
|
if (source === 'anilist') {
|
||||||
book = await booksService.getBookById(id);
|
book = await booksService.getBookById(id);
|
||||||
} else {
|
} else {
|
||||||
const ext = getExtension(source);
|
const ext = getExtension(source);
|
||||||
// Asegurar que id sea una cadena
|
|
||||||
const result = await booksService.getBookInfoExtension(ext, id.toString());
|
const result = await booksService.getBookInfoExtension(ext, id.toString());
|
||||||
book = result || null;
|
book = result || null;
|
||||||
}
|
}
|
||||||
@@ -120,42 +149,33 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
contentDetails = {
|
contentDetails = {
|
||||||
title: book?.title || 'Unknown Book Title',
|
title: book?.title || 'Unknown Book Title',
|
||||||
poster: book?.coverImage?.extraLarge || book?.image || '',
|
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||||
// Priorizar chapters, luego volumes * 10, sino 0
|
|
||||||
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (contentError) {
|
|
||||||
console.error(`Error fetching details for entry ${id} (${source}):`, contentError);
|
} catch {
|
||||||
contentDetails = {
|
contentDetails = {
|
||||||
title: 'Error Loading Details',
|
title: 'Error Loading Details',
|
||||||
poster: '/public/assets/placeholder.png',
|
poster: 'https://placehold.co/400x600?text=No+Cover',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Estandarizar y Combinar los datos.
|
|
||||||
|
|
||||||
let finalTitle = contentDetails?.title || 'Unknown Title';
|
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||||
let finalPoster = contentDetails?.poster || '/public/assets/placeholder.png';
|
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||||
|
|
||||||
// Aplanamiento del título (Necesario para Anilist que devuelve un objeto)
|
|
||||||
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||||
// Priorizar userPreferred, luego english, luego romaji, sino 'Unknown Title'
|
|
||||||
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Retornar el objeto combinado y estandarizado
|
|
||||||
return {
|
return {
|
||||||
...entry,
|
...entry,
|
||||||
// Datos estandarizados para el frontend:
|
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
poster: finalPoster,
|
poster: finalPoster,
|
||||||
total_episodes: contentDetails?.total_episodes, // Será undefined si es Manga/Novel
|
total_episodes: contentDetails?.total_episodes,
|
||||||
total_chapters: contentDetails?.total_chapters, // Será undefined si es Anime
|
total_chapters: contentDetails?.total_chapters,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Ejecutar todas las promesas y esperar el resultado
|
|
||||||
return await Promise.all(enrichedListPromises);
|
return await Promise.all(enrichedListPromises);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -164,10 +184,39 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function deleteListEntry(userId: number, entryId: string | number) {
|
||||||
* Elimina una entrada de lista por user_id y entry_id.
|
|
||||||
*/
|
const checkSql = `
|
||||||
export async function deleteListEntry(userId: number, entryId: number) {
|
SELECT source
|
||||||
|
FROM ListEntry
|
||||||
|
WHERE user_id = ? AND entry_id = ?;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existing = await queryOne(checkSql, [userId, entryId], USER_DB) as any;
|
||||||
|
|
||||||
|
if (existing?.source === 'anilist') {
|
||||||
|
const sql = `
|
||||||
|
SELECT access_token, anilist_user_id
|
||||||
|
FROM UserIntegration
|
||||||
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
|
`;
|
||||||
|
|
||||||
|
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
||||||
|
|
||||||
|
if (integration?.access_token) {
|
||||||
|
try {
|
||||||
|
await aniListService.deleteAniListEntry(
|
||||||
|
integration.access_token,
|
||||||
|
Number(entryId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, external: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error borrando en AniList:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
DELETE FROM ListEntry
|
DELETE FROM ListEntry
|
||||||
WHERE user_id = ? AND entry_id = ?;
|
WHERE user_id = ? AND entry_id = ?;
|
||||||
@@ -175,9 +224,189 @@ export async function deleteListEntry(userId: number, entryId: number) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await run(sql, [userId, entryId], USER_DB);
|
const result = await run(sql, [userId, entryId], USER_DB);
|
||||||
return { success: result.changes > 0, changes: result.changes };
|
return { success: result.changes > 0, changes: result.changes, external: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al eliminar la entrada de lista:", error);
|
console.error("Error al eliminar la entrada de lista:", error);
|
||||||
throw new Error("Error en la base de datos al eliminar la entrada.");
|
throw new Error("Error en la base de datos al eliminar la entrada.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSingleListEntry(
|
||||||
|
userId: number,
|
||||||
|
entryId: string | number,
|
||||||
|
source: string,
|
||||||
|
entryType: string
|
||||||
|
): Promise<any> {
|
||||||
|
|
||||||
|
const connected = await isConnected(userId);
|
||||||
|
|
||||||
|
if (source === 'anilist' && connected) {
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT access_token, anilist_user_id
|
||||||
|
FROM UserIntegration
|
||||||
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
|
`;
|
||||||
|
|
||||||
|
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
||||||
|
|
||||||
|
if (!integration) return null;
|
||||||
|
|
||||||
|
const aniEntry = await aniListService.getSingleAniListEntry(
|
||||||
|
integration.access_token,
|
||||||
|
Number(entryId),
|
||||||
|
entryType as any
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!aniEntry) return null;
|
||||||
|
|
||||||
|
let contentDetails: any = null;
|
||||||
|
|
||||||
|
if (entryType === 'ANIME') {
|
||||||
|
const anime:any = await animeService.getAnimeById(entryId);
|
||||||
|
|
||||||
|
contentDetails = {
|
||||||
|
title: anime?.title,
|
||||||
|
poster: anime?.coverImage?.extraLarge || anime?.image || '',
|
||||||
|
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const book: any = await booksService.getBookById(entryId);
|
||||||
|
|
||||||
|
contentDetails = {
|
||||||
|
title: book?.title,
|
||||||
|
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||||
|
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||||
|
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||||
|
|
||||||
|
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||||
|
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user_id: userId,
|
||||||
|
entry_id: Number(entryId),
|
||||||
|
source: 'anilist',
|
||||||
|
entry_type: entryType,
|
||||||
|
status: aniEntry.status,
|
||||||
|
progress: aniEntry.progress,
|
||||||
|
score: aniEntry.score,
|
||||||
|
title: finalTitle,
|
||||||
|
poster: finalPoster,
|
||||||
|
total_episodes: contentDetails?.total_episodes,
|
||||||
|
total_chapters: contentDetails?.total_chapters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT * FROM ListEntry
|
||||||
|
WHERE user_id = ? AND entry_id = ? AND source = ? AND entry_type = ?;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dbEntry = await queryAll(sql, [userId, entryId, source, entryType], USER_DB) as ListEntryData[];
|
||||||
|
|
||||||
|
if (!dbEntry || dbEntry.length === 0) return null;
|
||||||
|
|
||||||
|
const entry = dbEntry[0];
|
||||||
|
|
||||||
|
let contentDetails: any | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (entryType === 'ANIME') {
|
||||||
|
let anime: any;
|
||||||
|
|
||||||
|
if (source === 'anilist') {
|
||||||
|
anime = await animeService.getAnimeById(entryId);
|
||||||
|
} else {
|
||||||
|
const ext = getExtension(source);
|
||||||
|
anime = await animeService.getAnimeInfoExtension(ext, entryId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDetails = {
|
||||||
|
title: anime?.title,
|
||||||
|
poster: anime?.coverImage?.extraLarge || anime?.image || '',
|
||||||
|
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let book: any;
|
||||||
|
|
||||||
|
if (source === 'anilist') {
|
||||||
|
book = await booksService.getBookById(entryId);
|
||||||
|
} else {
|
||||||
|
const ext = getExtension(source);
|
||||||
|
book = await booksService.getBookInfoExtension(ext, entryId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDetails = {
|
||||||
|
title: book?.title,
|
||||||
|
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||||
|
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
contentDetails = {
|
||||||
|
title: 'Unknown',
|
||||||
|
poster: 'https://placehold.co/400x600?text=No+Cover',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||||
|
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||||
|
|
||||||
|
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||||
|
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
title: finalTitle,
|
||||||
|
poster: finalPoster,
|
||||||
|
total_episodes: contentDetails?.total_episodes,
|
||||||
|
total_chapters: contentDetails?.total_chapters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveAccessToken(userId: number): Promise<string | null> {
|
||||||
|
const sql = `
|
||||||
|
SELECT access_token, expires_at
|
||||||
|
FROM UserIntegration
|
||||||
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const integration = await queryOne(sql, [userId], USER_DB) as any | null;
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiryDate = new Date(integration.expires_at);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const fiveMinutes = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
if (expiryDate.getTime() < (now.getTime() + fiveMinutes)) {
|
||||||
|
|
||||||
|
console.log(`AniList token for user ${userId} expired or near expiry.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return integration.access_token;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al verificar la integración de AniList:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isConnected(userId: number): Promise<boolean> {
|
||||||
|
const token = await getActiveAccessToken(userId);
|
||||||
|
return !!token;
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ tag.src = "https://www.youtube.com/iframe_api";
|
|||||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||||
|
|
||||||
// Auth helpers
|
|
||||||
function getAuthToken() {
|
function getAuthToken() {
|
||||||
return localStorage.getItem('token');
|
return localStorage.getItem('token');
|
||||||
}
|
}
|
||||||
@@ -28,32 +27,46 @@ function getAuthHeaders() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if anime is in list
|
function getSimpleAuthHeaders() {
|
||||||
|
const token = getAuthToken();
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function checkIfInList() {
|
async function checkIfInList() {
|
||||||
|
|
||||||
|
const entryId = window.location.pathname.split('/').pop();
|
||||||
|
const source = extensionName || 'anilist';
|
||||||
|
const entryType = 'ANIME';
|
||||||
|
|
||||||
|
const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list`, {
|
const response = await fetch(fetchUrl, {
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const entry = data.results?.find(item =>
|
|
||||||
item.entry_id === parseInt(animeId) &&
|
|
||||||
item.source === (extensionName || 'anilist')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (entry) {
|
if (data.found && data.entry) {
|
||||||
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
currentListEntry = entry;
|
currentListEntry = data.entry;
|
||||||
updateAddToListButton();
|
} else {
|
||||||
|
isInList = false;
|
||||||
|
currentListEntry = null;
|
||||||
}
|
}
|
||||||
|
updateAddToListButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking list:', error);
|
console.error('Error checking single list entry:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update button state
|
|
||||||
function updateAddToListButton() {
|
function updateAddToListButton() {
|
||||||
const btn = document.getElementById('add-to-list-btn');
|
const btn = document.getElementById('add-to-list-btn');
|
||||||
if (isInList) {
|
if (isInList) {
|
||||||
@@ -68,13 +81,15 @@ function updateAddToListButton() {
|
|||||||
btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
||||||
} else {
|
} else {
|
||||||
btn.innerHTML = '+ Add to List';
|
btn.innerHTML = '+ Add to List';
|
||||||
|
btn.style.background = null;
|
||||||
|
btn.style.color = null;
|
||||||
|
btn.style.borderColor = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open add to list modal
|
|
||||||
function openAddToListModal() {
|
function openAddToListModal() {
|
||||||
if (isInList) {
|
if (isInList) {
|
||||||
// If already in list, open edit modal with current data
|
|
||||||
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
||||||
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
||||||
document.getElementById('modal-score').value = currentListEntry.score || '';
|
document.getElementById('modal-score').value = currentListEntry.score || '';
|
||||||
@@ -82,7 +97,7 @@ function openAddToListModal() {
|
|||||||
document.getElementById('modal-title').textContent = 'Edit List Entry';
|
document.getElementById('modal-title').textContent = 'Edit List Entry';
|
||||||
document.getElementById('modal-delete-btn').style.display = 'block';
|
document.getElementById('modal-delete-btn').style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
// New entry defaults
|
|
||||||
document.getElementById('modal-status').value = 'PLANNING';
|
document.getElementById('modal-status').value = 'PLANNING';
|
||||||
document.getElementById('modal-progress').value = 0;
|
document.getElementById('modal-progress').value = 0;
|
||||||
document.getElementById('modal-score').value = '';
|
document.getElementById('modal-score').value = '';
|
||||||
@@ -94,12 +109,10 @@ function openAddToListModal() {
|
|||||||
document.getElementById('modal-progress').max = totalEpisodes || 999;
|
document.getElementById('modal-progress').max = totalEpisodes || 999;
|
||||||
document.getElementById('add-list-modal').classList.add('active');}
|
document.getElementById('add-list-modal').classList.add('active');}
|
||||||
|
|
||||||
// Close modal
|
|
||||||
function closeAddToListModal() {
|
function closeAddToListModal() {
|
||||||
document.getElementById('add-list-modal').classList.remove('active');
|
document.getElementById('add-list-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to list
|
|
||||||
async function saveToList() {
|
async function saveToList() {
|
||||||
const status = document.getElementById('modal-status').value;
|
const status = document.getElementById('modal-status').value;
|
||||||
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
||||||
@@ -123,12 +136,12 @@ async function saveToList() {
|
|||||||
throw new Error('Failed to save entry');
|
throw new Error('Failed to save entry');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si la operación fue exitosa, actualizamos currentListEntry con el nuevo campo type
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
currentListEntry = {
|
currentListEntry = {
|
||||||
entry_id: parseInt(animeId),
|
entry_id: parseInt(animeId),
|
||||||
source: extensionName || 'anilist',
|
source: extensionName || 'anilist',
|
||||||
entry_type: 'ANIME', // <--- También se actualiza aquí
|
entry_type: 'ANIME',
|
||||||
|
|
||||||
status,
|
status,
|
||||||
progress,
|
progress,
|
||||||
score
|
score
|
||||||
@@ -142,14 +155,14 @@ async function saveToList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete from list
|
|
||||||
async function deleteFromList() {
|
async function deleteFromList() {
|
||||||
if (!confirm('Remove this anime from your list?')) return;
|
if (!confirm('Remove this anime from your list?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${animeId}`, {
|
const response = await fetch(`${API_BASE}/list/entry/${animeId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -167,7 +180,6 @@ async function deleteFromList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification
|
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
@@ -208,7 +220,7 @@ async function loadAnime() {
|
|||||||
const fetchUrl = extensionName
|
const fetchUrl = extensionName
|
||||||
? `/api/anime/${animeId}?source=${extensionName}`
|
? `/api/anime/${animeId}?source=${extensionName}`
|
||||||
: `/api/anime/${animeId}?source=anilist`;
|
: `/api/anime/${animeId}?source=anilist`;
|
||||||
const res = await fetch(fetchUrl);
|
const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -362,7 +374,6 @@ async function loadAnime() {
|
|||||||
|
|
||||||
renderEpisodes();
|
renderEpisodes();
|
||||||
|
|
||||||
// Check if in list after loading anime data
|
|
||||||
await checkIfInList();
|
await checkIfInList();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -458,7 +469,6 @@ searchInput.addEventListener('input', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal on outside click
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const modal = document.getElementById('add-list-modal');
|
const modal = document.getElementById('add-list-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -470,7 +480,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add animations
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const itemsPerPage = 12;
|
|||||||
let extensionName = null;
|
let extensionName = null;
|
||||||
let bookSlug = null;
|
let bookSlug = null;
|
||||||
|
|
||||||
// NUEVAS VARIABLES GLOBALES PARA LISTA
|
|
||||||
let currentBookData = null;
|
let currentBookData = null;
|
||||||
let isInList = false;
|
let isInList = false;
|
||||||
let currentListEntry = null;
|
let currentListEntry = null;
|
||||||
@@ -21,11 +20,6 @@ function getChaptersUrl(id, source = 'anilist') {
|
|||||||
return `/api/book/${id}/chapters?source=${source}`;
|
return `/api/book/${id}/chapters?source=${source}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// 1. FUNCIONES DE AUTENTICACIÓN Y LISTA
|
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
// Auth helpers
|
|
||||||
function getAuthToken() {
|
function getAuthToken() {
|
||||||
return localStorage.getItem('token');
|
return localStorage.getItem('token');
|
||||||
}
|
}
|
||||||
@@ -38,34 +32,53 @@ function getAuthHeaders() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if book is in list
|
function getSimpleAuthHeaders() {
|
||||||
|
const token = getAuthToken();
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBookEntryType(bookData) {
|
||||||
|
if (!bookData) return 'MANGA';
|
||||||
|
|
||||||
|
const format = bookData.format?.toUpperCase() || 'MANGA';
|
||||||
|
return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
|
||||||
|
}
|
||||||
|
|
||||||
async function checkIfInList() {
|
async function checkIfInList() {
|
||||||
|
if (!currentBookData) return;
|
||||||
|
|
||||||
|
const entryId = extensionName ? bookSlug : bookId;
|
||||||
|
const source = extensionName || 'anilist';
|
||||||
|
|
||||||
|
const entryType = getBookEntryType(currentBookData);
|
||||||
|
|
||||||
|
const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list`, {
|
const response = await fetch(fetchUrl, {
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const idToSearch = extensionName ? bookSlug : bookId;
|
|
||||||
|
|
||||||
const entry = data.results?.find(item =>
|
if (data.found && data.entry) {
|
||||||
item.entry_id === idToSearch &&
|
|
||||||
item.source === (extensionName || 'anilist')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (entry) {
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
currentListEntry = entry;
|
currentListEntry = data.entry;
|
||||||
updateAddToListButton();
|
} else {
|
||||||
|
isInList = false;
|
||||||
|
currentListEntry = null;
|
||||||
}
|
}
|
||||||
|
updateAddToListButton();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking list:', error);
|
console.error('Error checking single list entry:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update button state
|
|
||||||
function updateAddToListButton() {
|
function updateAddToListButton() {
|
||||||
const btn = document.getElementById('add-to-list-btn');
|
const btn = document.getElementById('add-to-list-btn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -83,22 +96,20 @@ function updateAddToListButton() {
|
|||||||
btn.onclick = openAddToListModal;
|
btn.onclick = openAddToListModal;
|
||||||
} else {
|
} else {
|
||||||
btn.innerHTML = '+ Add to Library';
|
btn.innerHTML = '+ Add to Library';
|
||||||
btn.style.background = null; // Restablecer estilos si no está en lista
|
btn.style.background = null;
|
||||||
btn.style.color = null;
|
btn.style.color = null;
|
||||||
btn.style.borderColor = null;
|
btn.style.borderColor = null;
|
||||||
btn.onclick = openAddToListModal;
|
btn.onclick = openAddToListModal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open add to list modal
|
|
||||||
function openAddToListModal() {
|
function openAddToListModal() {
|
||||||
if (!currentBookData) return;
|
if (!currentBookData) return;
|
||||||
|
|
||||||
// Obtener el total de capítulos/volúmenes
|
|
||||||
const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
|
const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
|
||||||
|
|
||||||
if (isInList) {
|
if (isInList) {
|
||||||
// If already in list, open edit modal with current data
|
|
||||||
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
||||||
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
||||||
document.getElementById('modal-score').value = currentListEntry.score || '';
|
document.getElementById('modal-score').value = currentListEntry.score || '';
|
||||||
@@ -106,7 +117,7 @@ function openAddToListModal() {
|
|||||||
document.getElementById('modal-title').textContent = 'Edit Library Entry';
|
document.getElementById('modal-title').textContent = 'Edit Library Entry';
|
||||||
document.getElementById('modal-delete-btn').style.display = 'block';
|
document.getElementById('modal-delete-btn').style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
// New entry defaults
|
|
||||||
document.getElementById('modal-status').value = 'PLANNING';
|
document.getElementById('modal-status').value = 'PLANNING';
|
||||||
document.getElementById('modal-progress').value = 0;
|
document.getElementById('modal-progress').value = 0;
|
||||||
document.getElementById('modal-score').value = '';
|
document.getElementById('modal-score').value = '';
|
||||||
@@ -115,7 +126,6 @@ function openAddToListModal() {
|
|||||||
document.getElementById('modal-delete-btn').style.display = 'none';
|
document.getElementById('modal-delete-btn').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajustar etiqueta de progreso según el formato
|
|
||||||
const progressLabel = document.getElementById('modal-progress-label');
|
const progressLabel = document.getElementById('modal-progress-label');
|
||||||
if (progressLabel) {
|
if (progressLabel) {
|
||||||
const format = currentBookData.format?.toUpperCase() || 'MANGA';
|
const format = currentBookData.format?.toUpperCase() || 'MANGA';
|
||||||
@@ -130,12 +140,10 @@ function openAddToListModal() {
|
|||||||
document.getElementById('add-list-modal').classList.add('active');
|
document.getElementById('add-list-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal
|
|
||||||
function closeAddToListModal() {
|
function closeAddToListModal() {
|
||||||
document.getElementById('add-list-modal').classList.remove('active');
|
document.getElementById('add-list-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to list
|
|
||||||
async function saveToList() {
|
async function saveToList() {
|
||||||
const status = document.getElementById('modal-status').value;
|
const status = document.getElementById('modal-status').value;
|
||||||
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
||||||
@@ -146,9 +154,7 @@ async function saveToList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determinar el tipo de entrada (MANGA o NOVEL basado en el formato)
|
const entryType = getBookEntryType(currentBookData);
|
||||||
const format = currentBookData.format?.toUpperCase() || 'MANGA';
|
|
||||||
const entryType = (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
|
|
||||||
const idToSave = extensionName ? bookSlug : bookId;
|
const idToSave = extensionName ? bookSlug : bookId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -180,7 +186,6 @@ async function saveToList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete from list
|
|
||||||
async function deleteFromList() {
|
async function deleteFromList() {
|
||||||
if (!confirm('Remove this book from your library?')) return;
|
if (!confirm('Remove this book from your library?')) return;
|
||||||
|
|
||||||
@@ -189,7 +194,8 @@ async function deleteFromList() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${idToDelete}`, {
|
const response = await fetch(`${API_BASE}/list/entry/${idToDelete}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -207,7 +213,6 @@ async function deleteFromList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification
|
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
@@ -232,11 +237,6 @@ function showNotification(message, type = 'info') {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// 2. FUNCIÓN PRINCIPAL DE CARGA (init)
|
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@@ -260,7 +260,7 @@ async function init() {
|
|||||||
extensionName || 'anilist'
|
extensionName || 'anilist'
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch(fetchUrl);
|
const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error || !data) {
|
if (data.error || !data) {
|
||||||
@@ -269,7 +269,7 @@ async function init() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentBookData = data; // <--- GUARDAR DATOS GLOBALES
|
currentBookData = data;
|
||||||
|
|
||||||
let title, description, score, year, status, format, chapters, poster, banner, genres;
|
let title, description, score, year, status, format, chapters, poster, banner, genres;
|
||||||
|
|
||||||
@@ -350,18 +350,13 @@ async function init() {
|
|||||||
|
|
||||||
loadChapters(idForFetch);
|
loadChapters(idForFetch);
|
||||||
|
|
||||||
await checkIfInList(); // <--- COMPROBAR ESTADO DE LA LISTA
|
await checkIfInList();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Metadata Error:", err);
|
console.error("Metadata Error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// 3. FUNCIONES DE CARGA Y RENDERIZADO DE CAPÍTULOS
|
|
||||||
// (Sin cambios, pero se incluyen para completar el script)
|
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function loadChapters(idForFetch) {
|
async function loadChapters(idForFetch) {
|
||||||
const tbody = document.getElementById('chapters-body');
|
const tbody = document.getElementById('chapters-body');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -375,7 +370,7 @@ async function loadChapters(idForFetch) {
|
|||||||
extensionName || 'anilist'
|
extensionName || 'anilist'
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch(fetchUrl);
|
const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
allChapters = data.chapters || [];
|
allChapters = data.chapters || [];
|
||||||
@@ -523,11 +518,6 @@ function openReader(bookId, chapterId, provider) {
|
|||||||
window.location.href = `/read/${p}/${c}/${bookId}${extension}`;
|
window.location.href = `/read/${p}/${c}/${bookId}${extension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// 4. LISTENERS Y ARRANQUE
|
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
// Close modal on outside click
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const modal = document.getElementById('add-list-modal');
|
const modal = document.getElementById('add-list-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -539,7 +529,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add animations (Copied from anime.js)
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
// API Configuration
|
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
let currentList = [];
|
let currentList = [];
|
||||||
let filteredList = [];
|
let filteredList = [];
|
||||||
let currentEditingEntry = null;
|
let currentEditingEntry = null;
|
||||||
|
|
||||||
// Get token from localStorage
|
|
||||||
function getAuthToken() {
|
function getAuthToken() {
|
||||||
return localStorage.getItem('token');
|
return localStorage.getItem('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create headers with auth token
|
|
||||||
function getAuthHeaders() {
|
function getAuthHeaders() {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
return {
|
return {
|
||||||
@@ -18,24 +15,39 @@ function getAuthHeaders() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on page load
|
function getSimpleAuthHeaders() {
|
||||||
|
const token = getAuthToken();
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await loadList();
|
await loadList();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================================
|
function getEntryLink(item) {
|
||||||
// FUNCIÓN: Poblar Filtro de Fuente (NUEVA LÓGICA)
|
const isAnime = item.entry_type?.toUpperCase() === 'ANIME';
|
||||||
// ==========================================================
|
const baseRoute = isAnime ? '/anime' : '/book';
|
||||||
|
const source = item.source || 'anilist';
|
||||||
|
|
||||||
|
if (source === 'anilist') {
|
||||||
|
|
||||||
|
return `${baseRoute}/${item.entry_id}`;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return `${baseRoute}/${source}/${item.entry_id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function populateSourceFilter() {
|
async function populateSourceFilter() {
|
||||||
const select = document.getElementById('source-filter');
|
const select = document.getElementById('source-filter');
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
// Opciones base
|
|
||||||
select.innerHTML = `
|
select.innerHTML = `
|
||||||
<option value="all">All Sources</option>
|
<option value="all">All Sources</option>
|
||||||
<option value="anilist">AniList</option>
|
<option value="anilist">AniList</option>
|
||||||
<option value="local">Local</option>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -44,13 +56,10 @@ async function populateSourceFilter() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const extensions = data.extensions || [];
|
const extensions = data.extensions || [];
|
||||||
|
|
||||||
// Añadir cada nombre de extensión como una opción
|
|
||||||
extensions.forEach(extName => {
|
extensions.forEach(extName => {
|
||||||
// Evitar duplicar 'anilist' o 'local'
|
|
||||||
if (extName.toLowerCase() !== 'anilist' && extName.toLowerCase() !== 'local') {
|
if (extName.toLowerCase() !== 'anilist' && extName.toLowerCase() !== 'local') {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = extName;
|
option.value = extName;
|
||||||
// Capitalizar el nombre
|
|
||||||
option.textContent = extName.charAt(0).toUpperCase() + extName.slice(1);
|
option.textContent = extName.charAt(0).toUpperCase() + extName.slice(1);
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
}
|
}
|
||||||
@@ -61,10 +70,8 @@ async function populateSourceFilter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Setup all event listeners
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// View toggle
|
|
||||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
||||||
@@ -79,13 +86,11 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filters
|
|
||||||
document.getElementById('status-filter').addEventListener('change', applyFilters);
|
document.getElementById('status-filter').addEventListener('change', applyFilters);
|
||||||
document.getElementById('source-filter').addEventListener('change', applyFilters);
|
document.getElementById('source-filter').addEventListener('change', applyFilters);
|
||||||
document.getElementById('type-filter').addEventListener('change', applyFilters);
|
document.getElementById('type-filter').addEventListener('change', applyFilters);
|
||||||
document.getElementById('sort-filter').addEventListener('change', applyFilters);
|
document.getElementById('sort-filter').addEventListener('change', applyFilters);
|
||||||
|
|
||||||
// Search
|
|
||||||
document.querySelector('.search-input').addEventListener('input', (e) => {
|
document.querySelector('.search-input').addEventListener('input', (e) => {
|
||||||
const query = e.target.value.toLowerCase();
|
const query = e.target.value.toLowerCase();
|
||||||
if (query) {
|
if (query) {
|
||||||
@@ -99,13 +104,11 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load list from API (MODIFICADO para incluir populateSourceFilter)
|
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
const loadingState = document.getElementById('loading-state');
|
const loadingState = document.getElementById('loading-state');
|
||||||
const emptyState = document.getElementById('empty-state');
|
const emptyState = document.getElementById('empty-state');
|
||||||
const container = document.getElementById('list-container');
|
const container = document.getElementById('list-container');
|
||||||
|
|
||||||
// Ejecutar la carga de extensiones antes de la lista principal
|
|
||||||
await populateSourceFilter();
|
await populateSourceFilter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -114,7 +117,7 @@ async function loadList() {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/list`, {
|
const response = await fetch(`${API_BASE}/list`, {
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -140,7 +143,6 @@ async function loadList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update statistics
|
|
||||||
function updateStats() {
|
function updateStats() {
|
||||||
const total = currentList.length;
|
const total = currentList.length;
|
||||||
const watching = currentList.filter(item => item.status === 'WATCHING').length;
|
const watching = currentList.filter(item => item.status === 'WATCHING').length;
|
||||||
@@ -153,14 +155,12 @@ function updateStats() {
|
|||||||
document.getElementById('planned-count').textContent = planning;
|
document.getElementById('planned-count').textContent = planning;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply filters and sorting
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const statusFilter = document.getElementById('status-filter').value;
|
const statusFilter = document.getElementById('status-filter').value;
|
||||||
const sourceFilter = document.getElementById('source-filter').value;
|
const sourceFilter = document.getElementById('source-filter').value;
|
||||||
const typeFilter = document.getElementById('type-filter').value;
|
const typeFilter = document.getElementById('type-filter').value;
|
||||||
const sortFilter = document.getElementById('sort-filter').value;
|
const sortFilter = document.getElementById('sort-filter').value;
|
||||||
|
|
||||||
// Filter
|
|
||||||
let filtered = [...filteredList];
|
let filtered = [...filteredList];
|
||||||
|
|
||||||
if (statusFilter !== 'all') {
|
if (statusFilter !== 'all') {
|
||||||
@@ -171,12 +171,10 @@ function applyFilters() {
|
|||||||
filtered = filtered.filter(item => item.source === sourceFilter);
|
filtered = filtered.filter(item => item.source === sourceFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrado por tipo
|
|
||||||
if (typeFilter !== 'all') {
|
if (typeFilter !== 'all') {
|
||||||
filtered = filtered.filter(item => (item.entry_type || 'ANIME') === typeFilter);
|
filtered = filtered.filter(item => (item.entry_type || 'ANIME') === typeFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort
|
|
||||||
switch (sortFilter) {
|
switch (sortFilter) {
|
||||||
case 'title':
|
case 'title':
|
||||||
filtered.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
filtered.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||||
@@ -196,7 +194,6 @@ function applyFilters() {
|
|||||||
renderList(filtered);
|
renderList(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render list items
|
|
||||||
function renderList(items) {
|
function renderList(items) {
|
||||||
const container = document.getElementById('list-container');
|
const container = document.getElementById('list-container');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -212,16 +209,15 @@ function renderList(items) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create individual list item (ACTUALIZADO para MANGA/NOVEL)
|
|
||||||
function createListItem(item) {
|
function createListItem(item) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'list-item';
|
div.className = 'list-item';
|
||||||
div.onclick = () => openEditModal(item);
|
|
||||||
|
const itemLink = getEntryLink(item);
|
||||||
|
|
||||||
const posterUrl = item.poster || '/public/assets/placeholder.png';
|
const posterUrl = item.poster || '/public/assets/placeholder.png';
|
||||||
const progress = item.progress || 0;
|
const progress = item.progress || 0;
|
||||||
|
|
||||||
// Determinar total de unidades basado en el tipo
|
|
||||||
const totalUnits = item.entry_type === 'ANIME' ?
|
const totalUnits = item.entry_type === 'ANIME' ?
|
||||||
item.total_episodes || 0 :
|
item.total_episodes || 0 :
|
||||||
item.total_chapters || 0;
|
item.total_chapters || 0;
|
||||||
@@ -229,7 +225,6 @@ function createListItem(item) {
|
|||||||
const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0;
|
const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0;
|
||||||
const score = item.score ? item.score.toFixed(1) : null;
|
const score = item.score ? item.score.toFixed(1) : null;
|
||||||
|
|
||||||
// Determinar la etiqueta de unidad (Episodes, Chapters, Volumes, etc.)
|
|
||||||
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
||||||
let unitLabel = 'units';
|
let unitLabel = 'units';
|
||||||
if (entryType === 'ANIME') {
|
if (entryType === 'ANIME') {
|
||||||
@@ -240,7 +235,6 @@ function createListItem(item) {
|
|||||||
unitLabel = 'chapters/volumes';
|
unitLabel = 'chapters/volumes';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajustar etiquetas de estado según el tipo (Watching/Reading)
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
'WATCHING': entryType === 'ANIME' ? 'Watching' : 'Reading',
|
'WATCHING': entryType === 'ANIME' ? 'Watching' : 'Reading',
|
||||||
'COMPLETED': 'Completed',
|
'COMPLETED': 'Completed',
|
||||||
@@ -250,27 +244,41 @@ function createListItem(item) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.png'">
|
<a href="${itemLink}" class="item-poster-link">
|
||||||
|
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.png'">
|
||||||
|
</a>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<h3 class="item-title">${item.title || 'Unknown Title'}</h3>
|
<div>
|
||||||
<div class="item-meta">
|
<a href="${itemLink}" style="text-decoration:none; color:inherit;">
|
||||||
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
|
<h3 class="item-title">${item.title || 'Unknown Title'}</h3>
|
||||||
<span class="meta-pill type-pill">${entryType}</span>
|
</a>
|
||||||
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
|
<div class="item-meta">
|
||||||
|
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
|
||||||
|
<span class="meta-pill type-pill">${entryType}</span>
|
||||||
|
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
|
||||||
<div class="progress-bar" style="width: ${progressPercent}%"></div>
|
<div>
|
||||||
</div>
|
<div class="progress-bar-container">
|
||||||
<div class="progress-text">
|
<div class="progress-bar" style="width: ${progressPercent}%"></div>
|
||||||
<span>${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel}</span> ${score ? `<span class="score-badge">⭐ ${score}</span>` : ''}
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span>${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel}</span> ${score ? `<span class="score-badge">⭐ ${score}</span>` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
|
||||||
|
<button class="edit-icon-btn" onclick="openEditModal(${JSON.stringify(item).replace(/"/g, '"')})">
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||||
|
<path d="M15.232 5.232l3.536 3.536m-2.036-5.808a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.536L15.232 5.232z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open edit modal (ACTUALIZADO para MANGA/NOVEL)
|
|
||||||
function openEditModal(item) {
|
function openEditModal(item) {
|
||||||
currentEditingEntry = item;
|
currentEditingEntry = item;
|
||||||
|
|
||||||
@@ -278,7 +286,6 @@ function openEditModal(item) {
|
|||||||
document.getElementById('edit-progress').value = item.progress || 0;
|
document.getElementById('edit-progress').value = item.progress || 0;
|
||||||
document.getElementById('edit-score').value = item.score || '';
|
document.getElementById('edit-score').value = item.score || '';
|
||||||
|
|
||||||
// Ajusta el texto del campo de progreso en el modal
|
|
||||||
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
||||||
const progressLabel = document.querySelector('label[for="edit-progress"]');
|
const progressLabel = document.querySelector('label[for="edit-progress"]');
|
||||||
if (progressLabel) {
|
if (progressLabel) {
|
||||||
@@ -291,23 +298,20 @@ function openEditModal(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establecer el max del progreso
|
|
||||||
const totalUnits = item.entry_type === 'ANIME' ?
|
const totalUnits = item.entry_type === 'ANIME' ?
|
||||||
item.total_episodes || 999 :
|
item.total_episodes || 999 :
|
||||||
item.total_chapters || 999;
|
item.total_chapters || 999;
|
||||||
document.getElementById('edit-progress').max = totalUnits;
|
document.getElementById('edit-progress').max = totalUnits;
|
||||||
|
|
||||||
|
document.getElementById('edit-modal').classList.add('active');
|
||||||
document.getElementById('edit-modal').style.display = 'flex';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close edit modal
|
|
||||||
function closeEditModal() {
|
function closeEditModal() {
|
||||||
currentEditingEntry = null;
|
currentEditingEntry = null;
|
||||||
document.getElementById('edit-modal').style.display = 'none';
|
|
||||||
|
document.getElementById('edit-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save entry changes
|
|
||||||
async function saveEntry() {
|
async function saveEntry() {
|
||||||
if (!currentEditingEntry) return;
|
if (!currentEditingEntry) return;
|
||||||
|
|
||||||
@@ -342,7 +346,6 @@ async function saveEntry() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete entry
|
|
||||||
async function deleteEntry() {
|
async function deleteEntry() {
|
||||||
if (!currentEditingEntry) return;
|
if (!currentEditingEntry) return;
|
||||||
|
|
||||||
@@ -353,7 +356,7 @@ async function deleteEntry() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${currentEditingEntry.entry_id}`, {
|
const response = await fetch(`${API_BASE}/list/entry/${currentEditingEntry.entry_id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -369,7 +372,6 @@ async function deleteEntry() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification (unchanged)
|
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
@@ -394,7 +396,6 @@ function showNotification(message, type = 'info') {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add keyframe animations (unchanged)
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
@@ -420,7 +421,6 @@ style.textContent = `
|
|||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
// Close modal on outside click (unchanged)
|
|
||||||
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'edit-modal') {
|
if (e.target.id === 'edit-modal') {
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const Gitea_OWNER = 'ItsSkaiya';
|
const Gitea_OWNER = 'ItsSkaiya';
|
||||||
const Gitea_REPO = 'WaifuBoard';
|
const Gitea_REPO = 'WaifuBoard';
|
||||||
const CURRENT_VERSION = 'v1.6.3';
|
const CURRENT_VERSION = 'v1.6.4';
|
||||||
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
|
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
let currentVersionDisplay;
|
let currentVersionDisplay;
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" id="add-to-list-btn" onclick="openAddToListModal()">+ Add to Library</button>
|
<button class="btn-secondary" id="add-to-list-btn" onclick="openAddToListModal()">+ Add to Library</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chapters-section">
|
<div class="chapters-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ body {
|
|||||||
padding-top: var(--nav-height);
|
padding-top: var(--nav-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar Styles */
|
|
||||||
.navbar {
|
.navbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--nav-height);
|
height: var(--nav-height);
|
||||||
@@ -127,14 +126,12 @@ body {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container */
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Section */
|
|
||||||
.header-section {
|
.header-section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
@@ -157,18 +154,19 @@ body {
|
|||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid rgba(255,255,255,0.05);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-lg);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
transition: transform 0.3s, border-color 0.3s;
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-5px);
|
||||||
border-color: var(--accent);
|
box-shadow: 0 10px 40px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
@@ -183,7 +181,6 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filters Section */
|
|
||||||
.filters-section {
|
.filters-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -261,7 +258,6 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading State */
|
|
||||||
.loading-state {
|
.loading-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -284,7 +280,6 @@ body {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty State */
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -304,10 +299,10 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List Grid View */
|
|
||||||
.list-grid {
|
.list-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,25 +312,33 @@ body {
|
|||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid rgba(255,255,255,0.05);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
transition: transform 0.3s, border-color 0.3s, box-shadow 0.3s;
|
||||||
transition: transform 0.3s, border-color 0.3s;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item:hover {
|
.list-item:hover {
|
||||||
transform: translateY(-8px);
|
transform: translateY(-5px);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .list-item {
|
.list-grid.list-view .list-item {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .list-item:hover {
|
.list-grid.list-view .list-item:hover {
|
||||||
transform: translateX(8px);
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-poster-link {
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-poster {
|
.item-poster {
|
||||||
@@ -353,12 +356,12 @@ body {
|
|||||||
|
|
||||||
.item-content {
|
.item-content {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .item-content {
|
.list-grid.list-view .item-content {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,24 +392,31 @@ body {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-pill {
|
||||||
background: rgba(139, 92, 246, 0.15);
|
background: rgba(139, 92, 246, 0.15);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-pill {
|
.source-pill {
|
||||||
background: rgba(168, 85, 247, 0.15);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
color: #a855f7;
|
color: var(--text-primary);
|
||||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
background: rgba(255,255,255,0.05);
|
background: rgba(255,255,255,0.05);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -419,10 +429,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
font-size: 0.8rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-badge {
|
.score-badge {
|
||||||
@@ -433,7 +444,22 @@ body {
|
|||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal */
|
.edit-btn-card {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, background 0.2s;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.edit-btn-card:hover {
|
||||||
|
background: #7c3aed;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -577,7 +603,16 @@ body {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
.modal-overlay {
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.modal-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
@@ -600,36 +635,49 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-grid {
|
.list-grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-pill {
|
.edit-icon-btn {
|
||||||
padding: 0.3rem 0.6rem;
|
position: absolute;
|
||||||
border-radius: 999px;
|
top: 1rem;
|
||||||
font-size: 0.75rem;
|
right: 1rem;
|
||||||
font-weight: 600;
|
z-index: 50;
|
||||||
text-transform: uppercase;
|
|
||||||
white-space: nowrap;
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s, background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilo para la píldora de tipo (opcional, para diferenciar) */
|
.list-item:hover .edit-icon-btn {
|
||||||
.type-pill {
|
opacity: 1;
|
||||||
background: rgba(255, 165, 0, 0.2); /* Naranja suave */
|
|
||||||
color: #ffb74d;
|
|
||||||
border: 1px solid rgba(255, 165, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Si usas la convención de colores de tu proyecto (e.g., violeta para extensión), puedes usar eso: */
|
.edit-icon-btn:hover {
|
||||||
.source-pill {
|
background: var(--accent);
|
||||||
background: rgba(139, 92, 246, 0.2);
|
border-color: var(--accent);
|
||||||
color: #a78bfa;
|
|
||||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ejemplo de color para la píldora de estado si no lo tienes */
|
.edit-btn-card {
|
||||||
.status-pill {
|
display: none;
|
||||||
background: rgba(34, 197, 94, 0.2); /* Verde de ejemplo */
|
}
|
||||||
color: #4ade80;
|
|
||||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
.item-poster-link {
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
<div id="list-container" class="list-grid"></div>
|
<div id="list-container" class="list-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-overlay" id="edit-modal" style="display: none;">
|
<div class="modal-overlay" id="edit-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="modal-close" onclick="closeEditModal()">✕</button>
|
<button class="modal-close" onclick="closeEditModal()">✕</button>
|
||||||
<h2 class="modal-title">Edit Entry</h2>
|
<h2 class="modal-title">Edit Entry</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user