better ui for anilist entries & fixes
This commit is contained in:
@@ -2,81 +2,192 @@ import { queryOne } from '../../shared/database';
|
|||||||
|
|
||||||
const USER_DB = 'userdata';
|
const USER_DB = 'userdata';
|
||||||
|
|
||||||
|
// Configuración de reintentos
|
||||||
|
const RETRY_CONFIG = {
|
||||||
|
maxRetries: 3,
|
||||||
|
initialDelay: 1000,
|
||||||
|
maxDelay: 5000,
|
||||||
|
backoffMultiplier: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper para hacer requests con reintentos y manejo de errores
|
||||||
|
async function fetchWithRetry(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit,
|
||||||
|
retries = RETRY_CONFIG.maxRetries
|
||||||
|
): Promise<Response> {
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
let delay = RETRY_CONFIG.initialDelay;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Si es rate limit (429), esperamos más tiempo
|
||||||
|
if (response.status === 429) {
|
||||||
|
const retryAfter = response.headers.get('Retry-After');
|
||||||
|
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : delay;
|
||||||
|
|
||||||
|
if (attempt < retries) {
|
||||||
|
console.warn(`Rate limited. Esperando ${waitTime}ms antes de reintentar...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
|
delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es un error de servidor (5xx), reintentamos
|
||||||
|
if (response.status >= 500 && attempt < retries) {
|
||||||
|
console.warn(`Error del servidor (${response.status}). Reintentando en ${delay}ms...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
|
||||||
|
if (attempt < retries && (
|
||||||
|
error instanceof Error && (
|
||||||
|
error.name === 'AbortError' ||
|
||||||
|
error.message.includes('fetch') ||
|
||||||
|
error.message.includes('network')
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
console.warn(`Error de conexión (intento ${attempt + 1}/${retries + 1}). Reintentando en ${delay}ms...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError || new Error('Request failed after all retries');
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserAniList(appUserId: number) {
|
export async function getUserAniList(appUserId: number) {
|
||||||
const sql = `
|
try {
|
||||||
SELECT access_token, anilist_user_id
|
const sql = `
|
||||||
FROM UserIntegration
|
SELECT access_token, anilist_user_id
|
||||||
WHERE user_id = ? AND platform = 'AniList';
|
FROM UserIntegration
|
||||||
`;
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
|
`;
|
||||||
|
|
||||||
const integration = await queryOne(sql, [appUserId], USER_DB) as any;
|
const integration = await queryOne(sql, [appUserId], USER_DB) as any;
|
||||||
if (!integration) return [];
|
if (!integration) return [];
|
||||||
|
|
||||||
const { access_token, anilist_user_id } = integration;
|
const { access_token, anilist_user_id } = integration;
|
||||||
|
if (!access_token || !anilist_user_id) return [];
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query ($userId: Int) {
|
query ($userId: Int) {
|
||||||
anime: MediaListCollection(userId: $userId, type: ANIME) {
|
anime: MediaListCollection(userId: $userId, type: ANIME) {
|
||||||
lists {
|
lists {
|
||||||
entries {
|
entries {
|
||||||
mediaId
|
mediaId
|
||||||
status
|
status
|
||||||
progress
|
progress
|
||||||
score
|
score
|
||||||
|
startedAt { year month day }
|
||||||
|
completedAt { year month day }
|
||||||
|
repeat
|
||||||
|
notes
|
||||||
|
private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manga: MediaListCollection(userId: $userId, type: MANGA) {
|
||||||
|
lists {
|
||||||
|
entries {
|
||||||
|
mediaId
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
startedAt { year month day }
|
||||||
|
completedAt { year month day }
|
||||||
|
repeat
|
||||||
|
notes
|
||||||
|
private
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manga: MediaListCollection(userId: $userId, type: MANGA) {
|
`;
|
||||||
lists {
|
|
||||||
entries {
|
const res = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
mediaId
|
method: 'POST',
|
||||||
status
|
headers: {
|
||||||
progress
|
'Authorization': `Bearer ${access_token}`,
|
||||||
score
|
'Content-Type': 'application/json',
|
||||||
}
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: { userId: anilist_user_id }
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`AniList API error: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json?.errors?.length) {
|
||||||
|
throw new Error(json.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromFuzzy = (d: any) => {
|
||||||
|
if (!d?.year) return null;
|
||||||
|
const m = String(d.month || 1).padStart(2, '0');
|
||||||
|
const day = String(d.day || 1).padStart(2, '0');
|
||||||
|
return `${d.year}-${m}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
start_date: fromFuzzy(entry.startedAt),
|
||||||
|
end_date: fromFuzzy(entry.completedAt),
|
||||||
|
repeat_count: entry.repeat || 0,
|
||||||
|
notes: entry.notes || null,
|
||||||
|
is_private: entry.private ? 1 : 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const res = await fetch('https://graphql.anilist.co', {
|
return result;
|
||||||
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();
|
return [
|
||||||
|
...normalize(json?.data?.anime?.lists, 'ANIME'),
|
||||||
const normalize = (lists: any[], type: 'ANIME' | 'MANGA') => {
|
...normalize(json?.data?.manga?.lists, 'MANGA')
|
||||||
const result: any[] = [];
|
];
|
||||||
|
} catch (error) {
|
||||||
for (const list of lists || []) {
|
console.error('Error fetching AniList data:', error);
|
||||||
for (const entry of list.entries || []) {
|
return [];
|
||||||
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: {
|
export async function updateAniListEntry(token: string, params: {
|
||||||
@@ -84,131 +195,318 @@ export async function updateAniListEntry(token: string, params: {
|
|||||||
status?: string | null;
|
status?: string | null;
|
||||||
progress?: number | null;
|
progress?: number | null;
|
||||||
score?: number | null;
|
score?: number | null;
|
||||||
|
start_date?: string | null; // YYYY-MM-DD
|
||||||
|
end_date?: string | null; // YYYY-MM-DD
|
||||||
|
repeat_count?: number | null;
|
||||||
|
notes?: string | null;
|
||||||
|
is_private?: boolean | number | null;
|
||||||
}) {
|
}) {
|
||||||
const mutation = `
|
try {
|
||||||
mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
|
if (!token) throw new Error('AniList token is required');
|
||||||
SaveMediaListEntry (
|
|
||||||
mediaId: $mediaId,
|
const mutation = `
|
||||||
status: $status,
|
mutation (
|
||||||
progress: $progress,
|
$mediaId: Int,
|
||||||
score: $score
|
$status: MediaListStatus,
|
||||||
|
$progress: Int,
|
||||||
|
$score: Float,
|
||||||
|
$startedAt: FuzzyDateInput,
|
||||||
|
$completedAt: FuzzyDateInput,
|
||||||
|
$repeat: Int,
|
||||||
|
$notes: String,
|
||||||
|
$private: Boolean
|
||||||
) {
|
) {
|
||||||
id
|
SaveMediaListEntry (
|
||||||
status
|
mediaId: $mediaId,
|
||||||
progress
|
status: $status,
|
||||||
score
|
progress: $progress,
|
||||||
|
score: $score,
|
||||||
|
startedAt: $startedAt,
|
||||||
|
completedAt: $completedAt,
|
||||||
|
repeat: $repeat,
|
||||||
|
notes: $notes,
|
||||||
|
private: $private
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
startedAt { year month day }
|
||||||
|
completedAt { year month day }
|
||||||
|
repeat
|
||||||
|
notes
|
||||||
|
private
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const toFuzzyDate = (dateStr?: string | null) => {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const [year, month, day] = dateStr.split('-').map(Number);
|
||||||
|
return { year, month, day };
|
||||||
|
};
|
||||||
|
|
||||||
|
const variables: any = {
|
||||||
|
mediaId: Number(params.mediaId),
|
||||||
|
status: params.status ?? undefined,
|
||||||
|
progress: params.progress ?? undefined,
|
||||||
|
score: params.score ?? undefined,
|
||||||
|
startedAt: toFuzzyDate(params.start_date),
|
||||||
|
completedAt: toFuzzyDate(params.end_date),
|
||||||
|
repeat: params.repeat_count ?? undefined,
|
||||||
|
notes: params.notes ?? undefined,
|
||||||
|
private: typeof params.is_private === 'boolean'
|
||||||
|
? params.is_private
|
||||||
|
: params.is_private === 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: mutation, variables }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
throw new Error(`AniList update failed: ${res.status} - ${errorText}`);
|
||||||
}
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
const variables: any = {
|
const json = await res.json();
|
||||||
mediaId: Number(params.mediaId),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params.status != null) variables.status = params.status;
|
if (json?.errors?.length) {
|
||||||
if (params.progress != null) variables.progress = params.progress;
|
throw new Error(`AniList GraphQL error: ${json.errors[0].message}`);
|
||||||
if (params.score != null) variables.score = params.score;
|
}
|
||||||
|
|
||||||
const res = await fetch('https://graphql.anilist.co', {
|
return json.data?.SaveMediaListEntry || null;
|
||||||
method: 'POST',
|
} catch (error) {
|
||||||
headers: {
|
console.error('Error updating AniList entry:', error);
|
||||||
'Authorization': `Bearer ${token}`,
|
throw error;
|
||||||
'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) {
|
export async function deleteAniListEntry(token: string, mediaId: number) {
|
||||||
|
if (!token) throw new Error("AniList token required");
|
||||||
|
|
||||||
const query = `
|
try {
|
||||||
query ($mediaId: Int) {
|
// 1️⃣ OBTENER VIEWER
|
||||||
MediaList(mediaId: $mediaId) {
|
const viewerQuery = `query { Viewer { id name } }`;
|
||||||
id
|
|
||||||
}
|
const vRes = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: viewerQuery }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vJson = await vRes.json();
|
||||||
|
const userId = vJson?.data?.Viewer?.id;
|
||||||
|
|
||||||
|
if (!userId) throw new Error("Invalid AniList token");
|
||||||
|
|
||||||
|
// 2️⃣ DETECTAR TIPO REAL DEL MEDIA
|
||||||
|
const mediaQuery = `
|
||||||
|
query ($id: Int) {
|
||||||
|
Media(id: $id) {
|
||||||
|
id
|
||||||
|
type
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const qRes = await fetch('https://graphql.anilist.co', {
|
const mTypeRes = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
'Accept': 'application/json',
|
||||||
body: JSON.stringify({ query, variables: { mediaId } }),
|
},
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
query: mediaQuery,
|
||||||
|
variables: { id: mediaId }
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const qJson = await qRes.json();
|
const mTypeJson = await mTypeRes.json();
|
||||||
const listEntryId = qJson?.data?.MediaList?.id;
|
const mediaType = mTypeJson?.data?.Media?.type;
|
||||||
|
|
||||||
if (!listEntryId) {
|
if (!mediaType) {
|
||||||
throw new Error("Entry not found or unauthorized to delete.");
|
throw new Error("Media not found in AniList");
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
|
||||||
mutation ($id: Int) {
|
|
||||||
DeleteMediaListEntry(id: $id) {
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3️⃣ BUSCAR ENTRY CON TIPO REAL
|
||||||
|
const listQuery = `
|
||||||
|
query ($userId: Int, $mediaId: Int, $type: MediaType) {
|
||||||
|
MediaList(userId: $userId, mediaId: $mediaId, type: $type) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const mRes = await fetch('https://graphql.anilist.co', {
|
const qRes = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
'Accept': 'application/json',
|
||||||
body: JSON.stringify({
|
},
|
||||||
query: mutation,
|
body: JSON.stringify({
|
||||||
variables: { id: listEntryId }
|
query: listQuery,
|
||||||
}),
|
variables: {
|
||||||
});
|
userId,
|
||||||
|
mediaId,
|
||||||
|
type: mediaType
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const mJson = await mRes.json();
|
const qJson = await qRes.json();
|
||||||
|
const listEntryId = qJson?.data?.MediaList?.id;
|
||||||
|
|
||||||
if (mJson?.errors?.length) {
|
if (!listEntryId) {
|
||||||
throw new Error("Error eliminando entrada en AniList");
|
throw new Error("Entry not found in user's AniList");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ BORRAR
|
||||||
|
const mutation = `
|
||||||
|
mutation ($id: Int) {
|
||||||
|
DeleteMediaListEntry(id: $id) {
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const delRes = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: mutation,
|
||||||
|
variables: { id: listEntryId }
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const delJson = await delRes.json();
|
||||||
|
|
||||||
|
if (delJson?.errors?.length) {
|
||||||
|
throw new Error(delJson.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("AniList DELETE failed:", err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getSingleAniListEntry(
|
export async function getSingleAniListEntry(
|
||||||
token: string,
|
token: string,
|
||||||
mediaId: number,
|
mediaId: number,
|
||||||
type: 'ANIME' | 'MANGA'
|
type: 'ANIME' | 'MANGA'
|
||||||
) {
|
) {
|
||||||
const query = `
|
try {
|
||||||
query ($mediaId: Int, $type: MediaType) {
|
if (!token) {
|
||||||
MediaList(mediaId: $mediaId, type: $type) {
|
throw new Error('AniList token is required');
|
||||||
status
|
|
||||||
progress
|
|
||||||
score
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
const res = await fetch('https://graphql.anilist.co', {
|
// 1️⃣ Obtener userId desde el token
|
||||||
method: 'POST',
|
const viewerRes = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Authorization': `Bearer ${token}`,
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
'Content-Type': 'application/json',
|
||||||
body: JSON.stringify({
|
'Accept': 'application/json',
|
||||||
query,
|
},
|
||||||
variables: { mediaId, type }
|
body: JSON.stringify({
|
||||||
})
|
query: `query { Viewer { id } }`
|
||||||
});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
const json = await res.json();
|
const viewerJson = await viewerRes.json();
|
||||||
return json?.data?.MediaList || null;
|
const userId = viewerJson?.data?.Viewer?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Failed to get AniList userId');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Query correcta con userId
|
||||||
|
const query = `
|
||||||
|
query ($mediaId: Int, $type: MediaType, $userId: Int) {
|
||||||
|
MediaList(mediaId: $mediaId, type: $type, userId: $userId) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
repeat
|
||||||
|
private
|
||||||
|
notes
|
||||||
|
startedAt { year month day }
|
||||||
|
completedAt { year month day }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const res = await fetchWithRetry('https://graphql.anilist.co', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: { mediaId, type, userId }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch entry: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json?.errors?.length) {
|
||||||
|
if (json.errors[0].status === 404) return null;
|
||||||
|
throw new Error(`GraphQL error: ${json.errors[0].message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = json?.data?.MediaList;
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry_id: mediaId,
|
||||||
|
source: 'anilist',
|
||||||
|
entry_type: type,
|
||||||
|
status: entry.status,
|
||||||
|
progress: entry.progress || 0,
|
||||||
|
score: entry.score ?? null,
|
||||||
|
|
||||||
|
start_date: entry.startedAt?.year
|
||||||
|
? `${entry.startedAt.year}-${String(entry.startedAt.month).padStart(2, '0')}-${String(entry.startedAt.day).padStart(2, '0')}`
|
||||||
|
: null,
|
||||||
|
|
||||||
|
end_date: entry.completedAt?.year
|
||||||
|
? `${entry.completedAt.year}-${String(entry.completedAt.month).padStart(2, '0')}-${String(entry.completedAt.day).padStart(2, '0')}`
|
||||||
|
: null,
|
||||||
|
|
||||||
|
repeat_count: entry.repeat || 0,
|
||||||
|
notes: entry.notes || null,
|
||||||
|
is_private: entry.private ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching single AniList entry:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getCachedExtension, cacheExtension, getCache, setCache, getExtensionTitle } from '../../shared/queries';
|
import { getCachedExtension, cacheExtension, getCache, setCache, getExtensionTitle } from '../../shared/queries';
|
||||||
import { queryOne, queryAll } from '../../shared/database';
|
import { queryOne, queryAll, run } from '../../shared/database';
|
||||||
import { getAllExtensions, getBookExtensionsMap } from '../../shared/extensions';
|
import { getAllExtensions, getBookExtensionsMap } from '../../shared/extensions';
|
||||||
import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types';
|
import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types';
|
||||||
|
|
||||||
@@ -62,7 +62,10 @@ const MEDIA_FIELDS = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export async function getBookById(id: string | number): Promise<Book | { error: string }> {
|
export async function getBookById(id: string | number): Promise<Book | { error: string }> {
|
||||||
const row = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]);
|
const row = await queryOne(
|
||||||
|
"SELECT full_data FROM books WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
return JSON.parse(row.full_data);
|
return JSON.parse(row.full_data);
|
||||||
@@ -70,6 +73,7 @@ export async function getBookById(id: string | number): Promise<Book | { error:
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[Book] Local miss for ID ${id}, fetching live...`);
|
console.log(`[Book] Local miss for ID ${id}, fetching live...`);
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query ($id: Int) {
|
query ($id: Int) {
|
||||||
Media(id: $id, type: MANGA) {
|
Media(id: $id, type: MANGA) {
|
||||||
@@ -90,13 +94,38 @@ export async function getBookById(id: string | number): Promise<Book | { error:
|
|||||||
|
|
||||||
const response = await fetch('https://graphql.anilist.co', {
|
const response = await fetch('https://graphql.anilist.co', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({ query, variables: { id: parseInt(id.toString()) } })
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: { id: parseInt(id.toString()) }
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.data && data.data.Media) {
|
|
||||||
return data.data.Media;
|
if (data?.data?.Media) {
|
||||||
|
const media = data.data.Media;
|
||||||
|
|
||||||
|
const insertSql = `
|
||||||
|
INSERT INTO books (id, title, updatedAt, full_data)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
updatedAt = EXCLUDED.updatedAt,
|
||||||
|
full_data = EXCLUDED.full_data;
|
||||||
|
`;
|
||||||
|
|
||||||
|
await run(insertSql, [
|
||||||
|
media.id,
|
||||||
|
media.title?.userPreferred || media.title?.romaji || media.title?.english || null,
|
||||||
|
media.updatedAt || Math.floor(Date.now() / 1000),
|
||||||
|
JSON.stringify(media)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return media;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Fetch error:", e);
|
console.error("Fetch error:", e);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import {FastifyReply, FastifyRequest} from 'fastify';
|
||||||
import * as listService from './list.service';
|
import * as listService from './list.service';
|
||||||
|
|
||||||
interface UserRequest extends FastifyRequest {
|
interface UserRequest extends FastifyRequest {
|
||||||
@@ -53,13 +53,11 @@ export async function getSingleEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
return reply.code(400).send({ error: "Missing required identifier: entryId, source, or entry_type." });
|
return reply.code(400).send({ error: "Missing required identifier: entryId, source, or entry_type." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryIdentifier = entryId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const entry = await listService.getSingleListEntry(
|
const entry = await listService.getSingleListEntry(
|
||||||
userId,
|
userId,
|
||||||
entryIdentifier,
|
entryId,
|
||||||
source,
|
source,
|
||||||
entry_type
|
entry_type
|
||||||
);
|
);
|
||||||
@@ -78,14 +76,16 @@ export async function getSingleEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
|
|
||||||
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 any;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return reply.code(401).send({ error: "Unauthorized" });
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
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)."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -97,7 +97,12 @@ export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
entry_type: body.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,
|
||||||
|
start_date: body.start_date || null,
|
||||||
|
end_date: body.end_date || null,
|
||||||
|
repeat_count: body.repeat_count ?? 0,
|
||||||
|
notes: body.notes || null,
|
||||||
|
is_private: body.is_private ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await listService.upsertListEntry(entryData);
|
const result = await listService.upsertListEntry(entryData);
|
||||||
@@ -112,24 +117,29 @@ export async function upsertEntry(req: UserRequest, reply: FastifyReply) {
|
|||||||
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 EntryParams;
|
const { entryId } = req.params as EntryParams;
|
||||||
|
const { source } = req.query as { source?: string }; // ✅ VIENE DEL FRONT
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return reply.code(401).send({ error: "Unauthorized" });
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryIdentifier = entryId;
|
if (!entryId || !source) {
|
||||||
|
return reply.code(400).send({ error: "Missing entryId or source." });
|
||||||
if (!entryIdentifier) {
|
|
||||||
return reply.code(400).send({ error: "Invalid entry ID." });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await listService.deleteListEntry(userId, entryIdentifier);
|
const result = await listService.deleteListEntry(
|
||||||
|
userId,
|
||||||
|
entryId,
|
||||||
|
source
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return { success: true, message: "Entry deleted successfully." };
|
return { success: true, external: result.external };
|
||||||
} else {
|
} else {
|
||||||
return reply.code(404).send({ error: "Entry not found or unauthorized to delete." });
|
return reply.code(404).send({
|
||||||
|
error: "Entry not found or unauthorized to delete."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -27,43 +27,60 @@ export async function upsertListEntry(entry: any) {
|
|||||||
entry_type,
|
entry_type,
|
||||||
status,
|
status,
|
||||||
progress,
|
progress,
|
||||||
score
|
score,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
repeat_count,
|
||||||
|
notes,
|
||||||
|
is_private
|
||||||
} = entry;
|
} = entry;
|
||||||
|
|
||||||
if (source === 'anilist') {
|
if (source === 'anilist') {
|
||||||
|
|
||||||
const token = await getActiveAccessToken(user_id);
|
const token = await getActiveAccessToken(user_id);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await aniListService.updateAniListEntry(token, {
|
const result = await aniListService.updateAniListEntry(token, {
|
||||||
mediaId: entry_id,
|
mediaId: entry_id,
|
||||||
status,
|
status,
|
||||||
progress,
|
progress,
|
||||||
score
|
score,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
repeat_count,
|
||||||
|
notes,
|
||||||
|
is_private
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return { changes: 0, external: true, anilistResult: result };
|
return { changes: 0, external: true, anilistResult: result };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error actualizando AniList:", 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,
|
||||||
|
start_date, end_date, repeat_count, notes, is_private,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT(user_id, entry_id) DO UPDATE SET
|
ON CONFLICT(user_id, entry_id) DO UPDATE SET
|
||||||
source = EXCLUDED.source,
|
source = EXCLUDED.source,
|
||||||
entry_type = EXCLUDED.entry_type,
|
entry_type = EXCLUDED.entry_type,
|
||||||
status = EXCLUDED.status,
|
status = EXCLUDED.status,
|
||||||
progress = EXCLUDED.progress,
|
progress = EXCLUDED.progress,
|
||||||
score = EXCLUDED.score,
|
score = EXCLUDED.score,
|
||||||
|
start_date = EXCLUDED.start_date,
|
||||||
|
end_date = EXCLUDED.end_date,
|
||||||
|
repeat_count = EXCLUDED.repeat_count,
|
||||||
|
notes = EXCLUDED.notes,
|
||||||
|
is_private = EXCLUDED.is_private,
|
||||||
updated_at = CURRENT_TIMESTAMP;
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -74,7 +91,12 @@ export async function upsertListEntry(entry: any) {
|
|||||||
entry_type,
|
entry_type,
|
||||||
status,
|
status,
|
||||||
progress,
|
progress,
|
||||||
score || null
|
score ?? null,
|
||||||
|
start_date || null,
|
||||||
|
end_date || null,
|
||||||
|
repeat_count ?? 0,
|
||||||
|
notes || null,
|
||||||
|
is_private ?? 0
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -101,8 +123,6 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
let finalList: ListEntryData[] = [...dbList];
|
let finalList: ListEntryData[] = [...dbList];
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
const token = await getActiveAccessToken(userId);
|
|
||||||
|
|
||||||
const anilistEntries = await aniListService.getUserAniList(userId);
|
const anilistEntries = await aniListService.getUserAniList(userId);
|
||||||
|
|
||||||
const localWithoutAnilist = dbList.filter(
|
const localWithoutAnilist = dbList.filter(
|
||||||
@@ -184,30 +204,22 @@ export async function getUserList(userId: number): Promise<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteListEntry(userId: number, entryId: string | number) {
|
export async function deleteListEntry(
|
||||||
|
userId: number,
|
||||||
|
entryId: string | number,
|
||||||
|
source: string
|
||||||
|
) {
|
||||||
|
|
||||||
const checkSql = `
|
if (source === 'anilist') {
|
||||||
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 token = await getActiveAccessToken(userId);
|
||||||
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 (token) {
|
||||||
|
|
||||||
if (integration?.access_token) {
|
|
||||||
try {
|
try {
|
||||||
await aniListService.deleteAniListEntry(
|
await aniListService.deleteAniListEntry(
|
||||||
integration.access_token,
|
token,
|
||||||
Number(entryId)
|
Number(entryId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, external: true };
|
return { success: true, external: true };
|
||||||
@@ -217,18 +229,14 @@ export async function deleteListEntry(userId: number, entryId: string | number)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ SOLO LOCAL
|
||||||
const sql = `
|
const sql = `
|
||||||
DELETE FROM ListEntry
|
DELETE FROM ListEntry
|
||||||
WHERE user_id = ? AND entry_id = ?;
|
WHERE user_id = ? AND entry_id = ?;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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, external: false };
|
||||||
return { success: result.changes > 0, changes: result.changes, external: false };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error al eliminar la entrada de lista:", error);
|
|
||||||
throw new Error("Error en la base de datos al eliminar la entrada.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSingleListEntry(
|
export async function getSingleListEntry(
|
||||||
@@ -238,19 +246,61 @@ export async function getSingleListEntry(
|
|||||||
entryType: string
|
entryType: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
|
||||||
const connected = await isConnected(userId);
|
// ✅ 1. BUSCAR PRIMERO EN TU BASE DE DATOS
|
||||||
|
const localSql = `
|
||||||
|
SELECT * FROM ListEntry
|
||||||
|
WHERE user_id = ? AND entry_id = ? AND source = ? AND entry_type = ?;
|
||||||
|
`;
|
||||||
|
|
||||||
if (source === 'anilist' && connected) {
|
const localResult = await queryAll(
|
||||||
|
localSql,
|
||||||
|
[userId, entryId, source, entryType],
|
||||||
|
USER_DB
|
||||||
|
) as any[];
|
||||||
|
|
||||||
|
if (localResult.length > 0) {
|
||||||
|
const entry = localResult[0];
|
||||||
|
|
||||||
|
const contentDetails: any =
|
||||||
|
entryType === 'ANIME'
|
||||||
|
? await animeService.getAnimeById(entryId).catch(() => null)
|
||||||
|
: await booksService.getBookById(entryId).catch(() => null);
|
||||||
|
|
||||||
|
let finalTitle = contentDetails?.title || 'Unknown';
|
||||||
|
let finalPoster = contentDetails?.coverImage?.extraLarge ||
|
||||||
|
contentDetails?.image ||
|
||||||
|
'https://placehold.co/400x600?text=No+Cover';
|
||||||
|
|
||||||
|
if (typeof finalTitle === 'object') {
|
||||||
|
finalTitle =
|
||||||
|
finalTitle.userPreferred ||
|
||||||
|
finalTitle.english ||
|
||||||
|
finalTitle.romaji ||
|
||||||
|
'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
title: finalTitle,
|
||||||
|
poster: finalPoster,
|
||||||
|
total_episodes: contentDetails?.episodes,
|
||||||
|
total_chapters: contentDetails?.chapters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source === 'anilist') {
|
||||||
|
|
||||||
|
const connected = await isConnected(userId);
|
||||||
|
if (!connected) return null;
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT access_token, anilist_user_id
|
SELECT access_token
|
||||||
FROM UserIntegration
|
FROM UserIntegration
|
||||||
WHERE user_id = ? AND platform = 'AniList';
|
WHERE user_id = ? AND platform = 'AniList';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
||||||
|
if (!integration?.access_token) return null;
|
||||||
if (!integration) return null;
|
|
||||||
|
|
||||||
const aniEntry = await aniListService.getSingleAniListEntry(
|
const aniEntry = await aniListService.getSingleAniListEntry(
|
||||||
integration.access_token,
|
integration.access_token,
|
||||||
@@ -258,118 +308,38 @@ export async function getSingleListEntry(
|
|||||||
entryType as any
|
entryType as any
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (!aniEntry) return null;
|
if (!aniEntry) return null;
|
||||||
|
|
||||||
let contentDetails: any = null;
|
const contentDetails: any =
|
||||||
|
entryType === 'ANIME'
|
||||||
|
? await animeService.getAnimeById(entryId).catch(() => null)
|
||||||
|
: await booksService.getBookById(entryId).catch(() => null);
|
||||||
|
|
||||||
if (entryType === 'ANIME') {
|
let finalTitle = contentDetails?.title || 'Unknown';
|
||||||
const anime:any = await animeService.getAnimeById(entryId);
|
let finalPoster = contentDetails?.coverImage?.extraLarge ||
|
||||||
|
contentDetails?.image ||
|
||||||
|
'https://placehold.co/400x600?text=No+Cover';
|
||||||
|
|
||||||
contentDetails = {
|
if (typeof finalTitle === 'object') {
|
||||||
title: anime?.title,
|
finalTitle =
|
||||||
poster: anime?.coverImage?.extraLarge || anime?.image || '',
|
finalTitle.userPreferred ||
|
||||||
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
|
finalTitle.english ||
|
||||||
};
|
finalTitle.romaji ||
|
||||||
} else {
|
'Unknown';
|
||||||
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 {
|
return {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
entry_id: Number(entryId),
|
...aniEntry,
|
||||||
source: 'anilist',
|
|
||||||
entry_type: entryType,
|
|
||||||
status: aniEntry.status,
|
|
||||||
progress: aniEntry.progress,
|
|
||||||
score: aniEntry.score,
|
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
poster: finalPoster,
|
poster: finalPoster,
|
||||||
total_episodes: contentDetails?.total_episodes,
|
total_episodes: contentDetails?.episodes,
|
||||||
total_chapters: contentDetails?.total_chapters,
|
total_chapters: contentDetails?.chapters,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const sql = `
|
return null;
|
||||||
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> {
|
export async function getActiveAccessToken(userId: number): Promise<string | null> {
|
||||||
|
|||||||
Binary file not shown.
@@ -34,8 +34,8 @@ function getSimpleAuthHeaders() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lógica de chequeo de lista corregida para usar /list/entry/{id} y esperar 'found'
|
||||||
async function checkIfInList() {
|
async function checkIfInList() {
|
||||||
|
|
||||||
const entryId = window.location.pathname.split('/').pop();
|
const entryId = window.location.pathname.split('/').pop();
|
||||||
const source = extensionName || 'anilist';
|
const source = extensionName || 'anilist';
|
||||||
const entryType = 'ANIME';
|
const entryType = 'ANIME';
|
||||||
@@ -51,8 +51,7 @@ async function checkIfInList() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.found && data.entry) {
|
if (data.found && data.entry) { // Esperamos 'found'
|
||||||
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
currentListEntry = data.entry;
|
currentListEntry = data.entry;
|
||||||
} else {
|
} else {
|
||||||
@@ -69,6 +68,8 @@ async function checkIfInList() {
|
|||||||
|
|
||||||
function updateAddToListButton() {
|
function updateAddToListButton() {
|
||||||
const btn = document.getElementById('add-to-list-btn');
|
const btn = document.getElementById('add-to-list-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
if (isInList) {
|
if (isInList) {
|
||||||
btn.innerHTML = `
|
btn.innerHTML = `
|
||||||
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -87,36 +88,73 @@ function updateAddToListButton() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Función openAddToListModal actualizada con todos los campos extendidos
|
||||||
function openAddToListModal() {
|
function openAddToListModal() {
|
||||||
if (isInList) {
|
// Referencias
|
||||||
|
const modalTitle = document.getElementById('modal-title');
|
||||||
|
const deleteBtn = document.getElementById('modal-delete-btn');
|
||||||
|
|
||||||
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
// Mapeo de prefijos (usamos 'entry-' para el HTML del modal extendido)
|
||||||
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
const statusEl = document.getElementById('entry-status');
|
||||||
document.getElementById('modal-score').value = currentListEntry.score || '';
|
const progressEl = document.getElementById('entry-progress');
|
||||||
|
const scoreEl = document.getElementById('entry-score');
|
||||||
|
const startDateEl = document.getElementById('entry-start-date');
|
||||||
|
const endDateEl = document.getElementById('entry-end-date');
|
||||||
|
const repeatCountEl = document.getElementById('entry-repeat-count');
|
||||||
|
const notesEl = document.getElementById('entry-notes');
|
||||||
|
const privateEl = document.getElementById('entry-is-private');
|
||||||
|
|
||||||
document.getElementById('modal-title').textContent = 'Edit List Entry';
|
|
||||||
document.getElementById('modal-delete-btn').style.display = 'block';
|
if (isInList && currentListEntry) {
|
||||||
|
statusEl.value = currentListEntry.status || 'PLANNING';
|
||||||
|
progressEl.value = currentListEntry.progress || 0;
|
||||||
|
scoreEl.value = currentListEntry.score || '';
|
||||||
|
|
||||||
|
// Campos extendidos
|
||||||
|
startDateEl.value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
|
||||||
|
endDateEl.value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
|
||||||
|
repeatCountEl.value = currentListEntry.repeat_count || 0;
|
||||||
|
notesEl.value = currentListEntry.notes || '';
|
||||||
|
privateEl.checked = currentListEntry.is_private === true || currentListEntry.is_private === 1;
|
||||||
|
|
||||||
|
modalTitle.textContent = 'Edit List Entry';
|
||||||
|
deleteBtn.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
|
// Valores por defecto
|
||||||
|
statusEl.value = 'PLANNING';
|
||||||
|
progressEl.value = 0;
|
||||||
|
scoreEl.value = '';
|
||||||
|
startDateEl.value = '';
|
||||||
|
endDateEl.value = '';
|
||||||
|
repeatCountEl.value = 0;
|
||||||
|
notesEl.value = '';
|
||||||
|
privateEl.checked = false;
|
||||||
|
|
||||||
document.getElementById('modal-status').value = 'PLANNING';
|
modalTitle.textContent = 'Add to List';
|
||||||
document.getElementById('modal-progress').value = 0;
|
deleteBtn.style.display = 'none';
|
||||||
document.getElementById('modal-score').value = '';
|
|
||||||
|
|
||||||
document.getElementById('modal-title').textContent = 'Add to List';
|
|
||||||
document.getElementById('modal-delete-btn').style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('modal-progress').max = totalEpisodes || 999;
|
progressEl.max = totalEpisodes || 999;
|
||||||
document.getElementById('add-list-modal').classList.add('active');}
|
document.getElementById('add-list-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
function closeAddToListModal() {
|
function closeAddToListModal() {
|
||||||
document.getElementById('add-list-modal').classList.remove('active');
|
document.getElementById('add-list-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Función saveToList actualizada con todos los campos extendidos
|
||||||
async function saveToList() {
|
async function saveToList() {
|
||||||
const status = document.getElementById('modal-status').value;
|
const status = document.getElementById('entry-status').value;
|
||||||
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
const progress = parseInt(document.getElementById('entry-progress').value) || 0;
|
||||||
const score = parseFloat(document.getElementById('modal-score').value) || null;
|
const scoreValue = document.getElementById('entry-score').value;
|
||||||
|
const score = scoreValue ? parseFloat(scoreValue) : null;
|
||||||
|
|
||||||
|
// Nuevos campos
|
||||||
|
const start_date = document.getElementById('entry-start-date').value || null;
|
||||||
|
const end_date = document.getElementById('entry-end-date').value || null;
|
||||||
|
const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
|
||||||
|
const notes = document.getElementById('entry-notes').value || null;
|
||||||
|
const is_private = document.getElementById('entry-is-private').checked;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry`, {
|
const response = await fetch(`${API_BASE}/list/entry`, {
|
||||||
@@ -128,7 +166,13 @@ async function saveToList() {
|
|||||||
entry_type: 'ANIME',
|
entry_type: 'ANIME',
|
||||||
status: status,
|
status: status,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
score: score
|
score: score,
|
||||||
|
// Campos extendidos
|
||||||
|
start_date: start_date,
|
||||||
|
end_date: end_date,
|
||||||
|
repeat_count: repeat_count,
|
||||||
|
notes: notes,
|
||||||
|
is_private: is_private
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,16 +180,10 @@ async function saveToList() {
|
|||||||
throw new Error('Failed to save entry');
|
throw new Error('Failed to save entry');
|
||||||
}
|
}
|
||||||
|
|
||||||
isInList = true;
|
const data = await response.json();
|
||||||
currentListEntry = {
|
|
||||||
entry_id: parseInt(animeId),
|
|
||||||
source: extensionName || 'anilist',
|
|
||||||
entry_type: 'ANIME',
|
|
||||||
|
|
||||||
status,
|
isInList = true;
|
||||||
progress,
|
currentListEntry = data.entry;
|
||||||
score
|
|
||||||
};
|
|
||||||
updateAddToListButton();
|
updateAddToListButton();
|
||||||
closeAddToListModal();
|
closeAddToListModal();
|
||||||
showNotification(isInList ? 'Updated successfully!' : 'Added to your list!', 'success');
|
showNotification(isInList ? 'Updated successfully!' : 'Added to your list!', 'success');
|
||||||
@@ -158,8 +196,11 @@ async function saveToList() {
|
|||||||
async function deleteFromList() {
|
async function deleteFromList() {
|
||||||
if (!confirm('Remove this anime from your list?')) return;
|
if (!confirm('Remove this anime from your list?')) return;
|
||||||
|
|
||||||
|
const source = extensionName || 'anilist';
|
||||||
|
const entryType = 'ANIME';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${animeId}`, {
|
const response = await fetch(`${API_BASE}/list/entry/${animeId}?source=${source}&entry_type=${entryType}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getSimpleAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function getBookEntryType(bookData) {
|
|||||||
return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
|
return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CORRECCIÓN: Usar el endpoint /list/entry/{id} y esperar 'found'
|
||||||
async function checkIfInList() {
|
async function checkIfInList() {
|
||||||
if (!currentBookData) return;
|
if (!currentBookData) return;
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ async function checkIfInList() {
|
|||||||
|
|
||||||
const entryType = getBookEntryType(currentBookData);
|
const entryType = getBookEntryType(currentBookData);
|
||||||
|
|
||||||
|
// URL CORRECTA: /list/entry/{id}?source={source}&entry_type={entryType}
|
||||||
const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
|
const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -64,6 +66,7 @@ async function checkIfInList() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// LÓGICA CORRECTA: Comprobar data.found
|
||||||
if (data.found && data.entry) {
|
if (data.found && data.entry) {
|
||||||
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
@@ -73,6 +76,11 @@ async function checkIfInList() {
|
|||||||
currentListEntry = null;
|
currentListEntry = null;
|
||||||
}
|
}
|
||||||
updateAddToListButton();
|
updateAddToListButton();
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
// Manejar 404 como 'no encontrado' si la API lo devuelve así
|
||||||
|
isInList = false;
|
||||||
|
currentListEntry = null;
|
||||||
|
updateAddToListButton();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking single list entry:', error);
|
console.error('Error checking single list entry:', error);
|
||||||
@@ -103,40 +111,71 @@ function updateAddToListButton() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REFACTORIZADO para usar la estructura del modal completo.
|
||||||
|
* Asume que el HTML usa IDs como 'entry-status', 'entry-progress', 'entry-score', etc.
|
||||||
|
*/
|
||||||
function openAddToListModal() {
|
function openAddToListModal() {
|
||||||
if (!currentBookData) return;
|
if (!currentBookData) return;
|
||||||
|
|
||||||
const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
|
const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
|
||||||
|
const entryType = getBookEntryType(currentBookData);
|
||||||
|
|
||||||
if (isInList) {
|
// Referencias a los elementos del nuevo modal (usando 'entry-' prefix)
|
||||||
|
const modalTitle = document.getElementById('modal-title');
|
||||||
|
const deleteBtn = document.getElementById('modal-delete-btn');
|
||||||
|
const progressLabel = document.getElementById('progress-label');
|
||||||
|
|
||||||
document.getElementById('modal-status').value = currentListEntry.status || 'PLANNING';
|
// **VERIFICACIÓN CRÍTICA**
|
||||||
document.getElementById('modal-progress').value = currentListEntry.progress || 0;
|
if (!modalTitle || !deleteBtn || !progressLabel) {
|
||||||
document.getElementById('modal-score').value = currentListEntry.score || '';
|
console.error("Error: Uno o más elementos críticos del modal (título, botón eliminar, o etiqueta de progreso) no se encontraron. Verifique los IDs en el HTML.");
|
||||||
|
return;
|
||||||
document.getElementById('modal-title').textContent = 'Edit Library Entry';
|
|
||||||
document.getElementById('modal-delete-btn').style.display = 'block';
|
|
||||||
} else {
|
|
||||||
|
|
||||||
document.getElementById('modal-status').value = 'PLANNING';
|
|
||||||
document.getElementById('modal-progress').value = 0;
|
|
||||||
document.getElementById('modal-score').value = '';
|
|
||||||
|
|
||||||
document.getElementById('modal-title').textContent = 'Add to Library';
|
|
||||||
document.getElementById('modal-delete-btn').style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressLabel = document.getElementById('modal-progress-label');
|
// --- Población de Datos ---
|
||||||
|
|
||||||
|
if (isInList && currentListEntry) {
|
||||||
|
// Datos comunes
|
||||||
|
document.getElementById('entry-status').value = currentListEntry.status || 'PLANNING';
|
||||||
|
document.getElementById('entry-progress').value = currentListEntry.progress || 0;
|
||||||
|
document.getElementById('entry-score').value = currentListEntry.score || '';
|
||||||
|
|
||||||
|
// Nuevos datos
|
||||||
|
// Usar formato ISO si viene como ISO, o limpiar si es necesario. Tu ejemplo JSON no tenía fechas.
|
||||||
|
document.getElementById('entry-start-date').value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
|
||||||
|
document.getElementById('entry-end-date').value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
|
||||||
|
document.getElementById('entry-repeat-count').value = currentListEntry.repeat_count || 0;
|
||||||
|
document.getElementById('entry-notes').value = currentListEntry.notes || '';
|
||||||
|
document.getElementById('entry-is-private').checked = currentListEntry.is_private === true || currentListEntry.is_private === 1;
|
||||||
|
|
||||||
|
modalTitle.textContent = 'Edit Library Entry';
|
||||||
|
deleteBtn.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Valores por defecto
|
||||||
|
document.getElementById('entry-status').value = 'PLANNING';
|
||||||
|
document.getElementById('entry-progress').value = 0;
|
||||||
|
document.getElementById('entry-score').value = '';
|
||||||
|
document.getElementById('entry-start-date').value = '';
|
||||||
|
document.getElementById('entry-end-date').value = '';
|
||||||
|
document.getElementById('entry-repeat-count').value = 0;
|
||||||
|
document.getElementById('entry-notes').value = '';
|
||||||
|
document.getElementById('entry-is-private').checked = false;
|
||||||
|
|
||||||
|
modalTitle.textContent = 'Add to Library';
|
||||||
|
deleteBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Configuración de Etiquetas y Máximo ---
|
||||||
|
|
||||||
if (progressLabel) {
|
if (progressLabel) {
|
||||||
const format = currentBookData.format?.toUpperCase() || 'MANGA';
|
if (entryType === 'MANGA') {
|
||||||
if (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') {
|
|
||||||
progressLabel.textContent = 'Chapters Read';
|
progressLabel.textContent = 'Chapters Read';
|
||||||
} else {
|
} else {
|
||||||
progressLabel.textContent = 'Volumes/Parts Read';
|
progressLabel.textContent = 'Volumes/Parts Read';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('modal-progress').max = totalUnits;
|
document.getElementById('entry-progress').max = totalUnits;
|
||||||
document.getElementById('add-list-modal').classList.add('active');
|
document.getElementById('add-list-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +183,23 @@ function closeAddToListModal() {
|
|||||||
document.getElementById('add-list-modal').classList.remove('active');
|
document.getElementById('add-list-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REFACTORIZADO para guardar TODOS los campos del modal.
|
||||||
|
*/
|
||||||
async function saveToList() {
|
async function saveToList() {
|
||||||
const status = document.getElementById('modal-status').value;
|
// Datos comunes
|
||||||
const progress = parseInt(document.getElementById('modal-progress').value) || 0;
|
const status = document.getElementById('entry-status').value;
|
||||||
const score = parseFloat(document.getElementById('modal-score').value) || null;
|
const progress = parseInt(document.getElementById('entry-progress').value) || 0;
|
||||||
|
const scoreValue = document.getElementById('entry-score').value;
|
||||||
|
const score = scoreValue ? parseFloat(scoreValue) : null;
|
||||||
|
|
||||||
|
// Nuevos datos
|
||||||
|
const start_date = document.getElementById('entry-start-date').value || null;
|
||||||
|
const end_date = document.getElementById('entry-end-date').value || null;
|
||||||
|
const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
|
||||||
|
const notes = document.getElementById('entry-notes').value || null;
|
||||||
|
const is_private = document.getElementById('entry-is-private').checked;
|
||||||
|
|
||||||
|
|
||||||
if (!currentBookData) {
|
if (!currentBookData) {
|
||||||
showNotification('Cannot save: Book data not loaded.', 'error');
|
showNotification('Cannot save: Book data not loaded.', 'error');
|
||||||
@@ -167,7 +219,13 @@ async function saveToList() {
|
|||||||
entry_type: entryType,
|
entry_type: entryType,
|
||||||
status: status,
|
status: status,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
score: score
|
score: score,
|
||||||
|
// Nuevos campos
|
||||||
|
start_date: start_date,
|
||||||
|
end_date: end_date,
|
||||||
|
repeat_count: repeat_count,
|
||||||
|
notes: notes,
|
||||||
|
is_private: is_private
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,8 +233,10 @@ async function saveToList() {
|
|||||||
throw new Error('Failed to save entry');
|
throw new Error('Failed to save entry');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
isInList = true;
|
isInList = true;
|
||||||
currentListEntry = { entry_id: idToSave, source: extensionName || 'anilist', entry_type: entryType, status, progress, score };
|
currentListEntry = data.entry; // Usar la respuesta del servidor si está disponible
|
||||||
updateAddToListButton();
|
updateAddToListButton();
|
||||||
closeAddToListModal();
|
closeAddToListModal();
|
||||||
showNotification(isInList ? 'Updated successfully!' : 'Added to your library!', 'success');
|
showNotification(isInList ? 'Updated successfully!' : 'Added to your library!', 'success');
|
||||||
@@ -186,16 +246,19 @@ async function saveToList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CORRECCIÓN: Usar el endpoint /list/entry/{id} con los parámetros correctos.
|
||||||
async function deleteFromList() {
|
async function deleteFromList() {
|
||||||
if (!confirm('Remove this book from your library?')) return;
|
if (!confirm('Remove this book from your library?')) return;
|
||||||
|
|
||||||
const idToDelete = extensionName ? bookSlug : bookId;
|
const idToDelete = extensionName ? bookSlug : bookId;
|
||||||
|
const source = extensionName || 'anilist';
|
||||||
|
const entryType = getBookEntryType(currentBookData); // Obtener el tipo de entrada
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${idToDelete}`, {
|
// URL CORRECTA para DELETE: /list/entry/{id}?source={source}&entry_type={entryType}
|
||||||
|
const response = await fetch(`${API_BASE}/list/entry/${idToDelete}?source=${source}&entry_type=${entryType}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getSimpleAuthHeaders()
|
headers: getSimpleAuthHeaders()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -519,6 +582,7 @@ function openReader(bookId, chapterId, provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// El ID del modal sigue siendo 'add-list-modal' para mantener la compatibilidad con el código original.
|
||||||
const modal = document.getElementById('add-list-modal');
|
const modal = document.getElementById('add-list-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.addEventListener('click', (e) => {
|
modal.addEventListener('click', (e) => {
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ 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;
|
||||||
|
const repeatCount = item.repeat_count || 0;
|
||||||
|
|
||||||
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
const entryType = (item.entry_type || 'ANIME').toUpperCase();
|
||||||
let unitLabel = 'units';
|
let unitLabel = 'units';
|
||||||
@@ -243,6 +244,15 @@ function createListItem(item) {
|
|||||||
'DROPPED': 'Dropped'
|
'DROPPED': 'Dropped'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extraInfo = [];
|
||||||
|
if (repeatCount > 0) {
|
||||||
|
extraInfo.push(`<span class="meta-pill repeat-pill">🔁 ${repeatCount}</span>`);
|
||||||
|
}
|
||||||
|
if (item.is_private) {
|
||||||
|
extraInfo.push('<span class="meta-pill private-pill">🔒 Private</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<a href="${itemLink}" class="item-poster-link">
|
<a href="${itemLink}" class="item-poster-link">
|
||||||
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.png'">
|
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.png'">
|
||||||
@@ -256,6 +266,7 @@ function createListItem(item) {
|
|||||||
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
|
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
|
||||||
<span class="meta-pill type-pill">${entryType}</span>
|
<span class="meta-pill type-pill">${entryType}</span>
|
||||||
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
|
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
|
||||||
|
${extraInfo.join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -282,9 +293,21 @@ function createListItem(item) {
|
|||||||
function openEditModal(item) {
|
function openEditModal(item) {
|
||||||
currentEditingEntry = item;
|
currentEditingEntry = item;
|
||||||
|
|
||||||
|
// Campos existentes
|
||||||
document.getElementById('edit-status').value = item.status;
|
document.getElementById('edit-status').value = item.status;
|
||||||
document.getElementById('edit-progress').value = item.progress || 0;
|
document.getElementById('edit-progress').value = item.progress || 0;
|
||||||
document.getElementById('edit-score').value = item.score || '';
|
// Asegura que el score se muestre si existe.
|
||||||
|
document.getElementById('edit-score').value = item.score !== null && item.score !== undefined ? item.score : '';
|
||||||
|
|
||||||
|
// Nuevos campos
|
||||||
|
// Usamos split('T')[0] para asegurar que solo se muestra la parte de la fecha (YYYY-MM-DD) si viene con formato DATETIME.
|
||||||
|
document.getElementById('edit-start-date').value = item.start_date?.split('T')[0] || '';
|
||||||
|
document.getElementById('edit-end-date').value = item.end_date?.split('T')[0] || '';
|
||||||
|
document.getElementById('edit-repeat-count').value = item.repeat_count || 0;
|
||||||
|
document.getElementById('edit-notes').value = item.notes || '';
|
||||||
|
// Maneja el booleano o el entero (1/0)
|
||||||
|
document.getElementById('edit-is-private').checked = item.is_private === 1 || item.is_private === true;
|
||||||
|
|
||||||
|
|
||||||
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"]');
|
||||||
@@ -315,9 +338,20 @@ function closeEditModal() {
|
|||||||
async function saveEntry() {
|
async function saveEntry() {
|
||||||
if (!currentEditingEntry) return;
|
if (!currentEditingEntry) return;
|
||||||
|
|
||||||
|
// Campos existentes
|
||||||
const status = document.getElementById('edit-status').value;
|
const status = document.getElementById('edit-status').value;
|
||||||
const progress = parseInt(document.getElementById('edit-progress').value) || 0;
|
const progress = parseInt(document.getElementById('edit-progress').value) || 0;
|
||||||
const score = parseFloat(document.getElementById('edit-score').value) || null;
|
// Usar null si el score está vacío
|
||||||
|
const scoreValue = document.getElementById('edit-score').value;
|
||||||
|
const score = scoreValue ? parseFloat(scoreValue) : null;
|
||||||
|
|
||||||
|
// Nuevos campos
|
||||||
|
const start_date = document.getElementById('edit-start-date').value || null;
|
||||||
|
const end_date = document.getElementById('edit-end-date').value || null;
|
||||||
|
const repeat_count = parseInt(document.getElementById('edit-repeat-count').value) || 0;
|
||||||
|
const notesValue = document.getElementById('edit-notes').value;
|
||||||
|
const notes = notesValue ? notesValue : null;
|
||||||
|
const is_private = document.getElementById('edit-is-private').checked;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry`, {
|
const response = await fetch(`${API_BASE}/list/entry`, {
|
||||||
@@ -329,7 +363,13 @@ async function saveEntry() {
|
|||||||
entry_type: currentEditingEntry.entry_type || 'ANIME',
|
entry_type: currentEditingEntry.entry_type || 'ANIME',
|
||||||
status: status,
|
status: status,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
score: score
|
score: score,
|
||||||
|
// Nuevos datos a enviar al backend
|
||||||
|
start_date: start_date,
|
||||||
|
end_date: end_date,
|
||||||
|
repeat_count: repeat_count,
|
||||||
|
notes: notes,
|
||||||
|
is_private: is_private
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,10 +394,13 @@ async function deleteEntry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/list/entry/${currentEditingEntry.entry_id}`, {
|
const response = await fetch(
|
||||||
method: 'DELETE',
|
`${API_BASE}/list/entry/${currentEditingEntry.entry_id}?source=${currentEditingEntry.source}`,
|
||||||
headers: getSimpleAuthHeaders()
|
{
|
||||||
});
|
method: 'DELETE',
|
||||||
|
headers: getSimpleAuthHeaders()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete entry');
|
throw new Error('Failed to delete entry');
|
||||||
@@ -372,6 +415,7 @@ async function deleteEntry() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 = `
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ async function ensureUserDataDB(dbPath) {
|
|||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
progress INTEGER NOT NULL DEFAULT 0,
|
progress INTEGER NOT NULL DEFAULT 0,
|
||||||
score INTEGER,
|
score INTEGER,
|
||||||
|
|
||||||
|
start_date DATE,
|
||||||
|
end_date DATE,
|
||||||
|
repeat_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
is_private BOOLEAN NOT NULL DEFAULT 0,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE (user_id, entry_id),
|
UNIQUE (user_id, entry_id),
|
||||||
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
||||||
@@ -89,8 +95,6 @@ async function ensureAnilistSchema(db) {
|
|||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
updatedAt INTEGER,
|
updatedAt INTEGER,
|
||||||
cache_created_at INTEGER DEFAULT 0,
|
|
||||||
cache_ttl_ms INTEGER DEFAULT 0,
|
|
||||||
full_data JSON
|
full_data JSON
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Synopsis Modal -->
|
|
||||||
<div class="modal-overlay" id="desc-modal">
|
<div class="modal-overlay" id="desc-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||||
@@ -32,40 +31,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add to List Modal -->
|
|
||||||
<div class="modal-overlay" id="add-list-modal">
|
<div class="modal-overlay" id="add-list-modal">
|
||||||
<div class="modal-content modal-list">
|
<div class="modal-content modal-list">
|
||||||
<button class="modal-close" onclick="closeAddToListModal()">✕</button>
|
<button class="modal-close" onclick="closeAddToListModal()">✕</button>
|
||||||
<h2 class="modal-title" id="modal-title">Add to List</h2>
|
<h2 class="modal-title" id="modal-title">Add to List</h2>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="modal-fields-grid">
|
||||||
<label>Status</label>
|
|
||||||
<select id="modal-status" class="form-input">
|
|
||||||
<option value="WATCHING">Watching</option>
|
|
||||||
<option value="COMPLETED">Completed</option>
|
|
||||||
<option value="PLANNING">Plan to Watch</option>
|
|
||||||
<option value="PAUSED">Paused</option>
|
|
||||||
<option value="DROPPED">Dropped</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Episodes Watched</label>
|
<label>Status</label>
|
||||||
<input type="number" id="modal-progress" class="form-input" min="0" placeholder="0">
|
<select id="entry-status" class="form-input">
|
||||||
</div>
|
<option value="WATCHING">Watching</option>
|
||||||
|
<option value="COMPLETED">Completed</option>
|
||||||
|
<option value="PLANNING">Plan to Watch</option>
|
||||||
|
<option value="PAUSED">Paused</option>
|
||||||
|
<option value="DROPPED">Dropped</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Your Score (0-10)</label>
|
<label>Episodes Watched</label>
|
||||||
<input type="number" id="modal-score" class="form-input" min="0" max="10" step="0.1" placeholder="Optional">
|
<input type="number" id="entry-progress" class="form-input" min="0" placeholder="0">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Your Score (0-10)</label>
|
||||||
|
<input type="number" id="entry-score" class="form-input" min="0" max="10" step="0.1" placeholder="Optional">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<div class="date-group">
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="entry-start-date">Start Date</label>
|
||||||
|
<input type="date" id="entry-start-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="entry-end-date">End Date</label>
|
||||||
|
<input type="date" id="entry-end-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="entry-repeat-count">Rewatch Count</label>
|
||||||
|
<input type="number" id="entry-repeat-count" class="form-input" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group notes-group">
|
||||||
|
<label for="entry-notes">Notes</label>
|
||||||
|
<textarea id="entry-notes" class="form-input notes-textarea" rows="4" placeholder="Personal notes..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="entry-is-private" class="form-checkbox">
|
||||||
|
<label for="entry-is-private">Mark as Private</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn-modal-secondary" onclick="closeAddToListModal()">Cancel</button>
|
|
||||||
<button class="btn-modal-danger" id="modal-delete-btn" onclick="deleteFromList()" style="display: none;">Remove</button>
|
|
||||||
<button class="btn-modal-primary" onclick="saveToList()">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-modal-danger" id="modal-delete-btn" onclick="deleteFromList()">Remove</button>
|
||||||
|
<button class="btn-modal-secondary" onclick="closeAddToListModal()">Cancel</button>
|
||||||
|
<button class="btn-modal-primary" onclick="saveToList()">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<title>WaifuBoard Book</title>
|
<title>WaifuBoard Book</title>
|
||||||
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/views/css/anime/home.css">
|
||||||
<link rel="stylesheet" href="/views/css/books/book.css">
|
<link rel="stylesheet" href="/views/css/books/book.css">
|
||||||
<link rel="stylesheet" href="/views/css/updateNotifier.css">
|
<link rel="stylesheet" href="/views/css/updateNotifier.css">
|
||||||
<link rel="stylesheet" href="/views/css/titlebar.css">
|
<link rel="stylesheet" href="/views/css/titlebar.css">
|
||||||
@@ -24,42 +25,70 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-overlay" id="add-list-modal">
|
<div class="modal-overlay" id="add-list-modal">
|
||||||
<div class="modal-content modal-list">
|
<div class="modal-content">
|
||||||
<button class="modal-close" onclick="closeAddToListModal()">✕</button>
|
<button class="modal-close" onclick="closeAddToListModal()">✕</button>
|
||||||
<h2 class="modal-title" id="modal-title">Add to Library</h2>
|
<h2 class="modal-title" id="modal-title">Add to Library</h2>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="modal-fields-grid">
|
||||||
<label>Status</label>
|
|
||||||
<select id="modal-status" class="form-input">
|
|
||||||
<option value="WATCHING">Reading</option>
|
|
||||||
<option value="COMPLETED">Completed</option>
|
|
||||||
<option value="PLANNING">Plan to Read</option>
|
|
||||||
<option value="PAUSED">Paused</option>
|
|
||||||
<option value="DROPPED">Dropped</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label id="modal-progress-label">Chapters Read</label>
|
<label for="entry-status">Status</label>
|
||||||
<input type="number" id="modal-progress" class="form-input" min="0" placeholder="0">
|
<select id="entry-status" class="form-input">
|
||||||
</div>
|
<option value="CURRENT">Reading</option>
|
||||||
|
<option value="COMPLETED">Completed</option>
|
||||||
|
<option value="PLANNING">Plan to Read</option>
|
||||||
|
<option value="PAUSED">Paused</option>
|
||||||
|
<option value="DROPPED">Dropped</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Your Score (0-10)</label>
|
<label for="entry-progress" id="progress-label">Chapters Read</label>
|
||||||
<input type="number" id="modal-score" class="form-input" min="0" max="10" step="0.1" placeholder="Optional">
|
<input type="number" id="entry-progress" class="form-input" min="0" max="0" placeholder="0">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="entry-score">Score (0-10)</label>
|
||||||
|
<input type="number" id="entry-score" class="form-input" min="0" max="10" step="0.1" placeholder="Optional">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width date-group">
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="entry-start-date">Start Date</label>
|
||||||
|
<input type="date" id="entry-start-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="entry-end-date">End Date</label>
|
||||||
|
<input type="date" id="entry-end-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="entry-repeat-count">Re-read Count</label>
|
||||||
|
<input type="number" id="entry-repeat-count" class="form-input" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group notes-group">
|
||||||
|
<label for="entry-notes">Notes</label>
|
||||||
|
<textarea id="entry-notes" class="form-input notes-textarea" rows="4" placeholder="Personal notes..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="entry-is-private" class="form-checkbox">
|
||||||
|
<label for="entry-is-private">Mark as Private</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn-modal-secondary" onclick="closeAddToListModal()">Cancel</button>
|
|
||||||
<button class="btn-modal-danger" id="modal-delete-btn" onclick="deleteFromList()" style="display: none;">Remove</button>
|
|
||||||
<button class="btn-modal-primary" onclick="saveToList()">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-danger" id="modal-delete-btn" onclick="deleteFromList()">Remove</button>
|
||||||
|
<button class="btn-secondary" onclick="closeAddToListModal()">Cancel</button>
|
||||||
|
<button class="btn-primary" onclick="saveToList()">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<a href="/books" class="back-btn">
|
<a href="/books" class="back-btn">
|
||||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
|
||||||
Back to Books
|
Back to Books
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
|
--bg-amoled: #0a0a0a; /* Añadido de list.css */
|
||||||
|
--bg-field: #0e0e0f; /* Añadido de list.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -271,92 +273,144 @@ body {
|
|||||||
.sidebar { display: none; }
|
.sidebar { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
/* --- ESTILOS DE MODAL (INCLUIDOS DE LIST.CSS) --- */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.9); /* Más oscuro */
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(10px);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
.modal-overlay.active {
|
.modal-overlay.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.modal-content {
|
|
||||||
background: #18181b;
|
@keyframes modalSlideUp {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
border-radius: 24px;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
padding: 2.5rem;
|
|
||||||
max-width: 650px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
||||||
transform: scale(0.95);
|
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
.modal-overlay.active .modal-content {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content.modal-list {
|
.modal-content {
|
||||||
max-width: 500px;
|
background: var(--bg-amoled); /* AMOLED */
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
max-width: 900px; /* Ancho para el grid de 3 columnas */
|
||||||
|
width: 95%;
|
||||||
|
padding: 0; /* padding se moverá al título y cuerpo */
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: modalSlideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.modal-list { /* Usamos esto para sobrescribir el ancho genérico del modal de descripción */
|
||||||
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1.5rem;
|
top: 1rem;
|
||||||
right: 1.5rem;
|
right: 1rem;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255,255,255,0.05);
|
||||||
border: none;
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: white;
|
color: white;
|
||||||
width: 32px;
|
width: 36px;
|
||||||
height: 32px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
z-index: 2001;
|
||||||
}
|
}
|
||||||
.modal-close:hover { background: rgba(255, 255, 255, 0.2); }
|
|
||||||
.modal-text {
|
.modal-close:hover {
|
||||||
line-height: 1.8;
|
background: var(--danger);
|
||||||
font-size: 1.1rem;
|
border-color: var(--danger);
|
||||||
color: #e4e4e7;
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 1.5rem 2rem 0.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
}
|
}
|
||||||
.modal-title { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.5rem; font-weight: 800; }
|
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
overflow-y: auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* GRUPO PRINCIPAL DE CAMPOS (GRID) */
|
||||||
|
.modal-fields-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.5rem 2rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
.form-group {
|
.form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grid layout overrides */
|
||||||
|
.form-group.notes-group {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date Input Styling */
|
||||||
|
.date-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.date-input-pair {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem; /* Tamaño más pequeño */
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
background: var(--bg-base);
|
background: var(--bg-field); /* Fondo específico para campos */
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.8rem 1rem;
|
||||||
@@ -372,10 +426,52 @@ body {
|
|||||||
box-shadow: 0 0 10px var(--accent-glow);
|
box-shadow: 0 0 10px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox Styling */
|
||||||
|
.form-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
background: var(--bg-base);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.form-checkbox:checked {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.form-checkbox:checked::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACCIONES (Barra inferior pegajosa) */
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.05);
|
||||||
|
background: var(--bg-amoled); /* Fondo para la barra sticky */
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary, .btn-modal-secondary, .btn-modal-danger {
|
.btn-modal-primary, .btn-modal-secondary, .btn-modal-danger {
|
||||||
@@ -385,15 +481,14 @@ body {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, opacity 0.2s;
|
transition: transform 0.2s, background 0.2s;
|
||||||
flex: 1;
|
flex: none; /* Asegura que no se estiren */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary {
|
.btn-modal-primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary:hover {
|
.btn-modal-primary:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
@@ -403,7 +498,6 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-secondary:hover {
|
.btn-modal-secondary:hover {
|
||||||
background: rgba(255,255,255,0.15);
|
background: rgba(255,255,255,0.15);
|
||||||
}
|
}
|
||||||
@@ -411,12 +505,34 @@ body {
|
|||||||
.btn-modal-danger {
|
.btn-modal-danger {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
color: white;
|
color: white;
|
||||||
|
margin-right: auto; /* Para alinear a la izquierda */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-danger:hover {
|
.btn-modal-danger:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Media Queries para el Modal */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.modal-content { max-width: 95%; }
|
||||||
|
.modal-fields-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.form-group.notes-group { grid-column: 1 / -1; }
|
||||||
|
.form-group.checkbox-group { grid-column: 1 / -1; align-self: auto; justify-content: flex-start; }
|
||||||
|
.modal-actions { padding: 1rem 1.5rem; }
|
||||||
|
.modal-title, .modal-body { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
.modal-content { margin: 0.5rem; width: auto; }
|
||||||
|
.modal-fields-grid { grid-template-columns: 1fr; gap: 1rem; padding-bottom: 0; }
|
||||||
|
.date-group { flex-direction: column; gap: 1rem; }
|
||||||
|
.form-group.notes-group, .form-group.checkbox-group { grid-column: auto; }
|
||||||
|
.modal-actions { flex-direction: column; align-items: stretch; }
|
||||||
|
.btn-modal-danger { margin-right: 0; order: 3; }
|
||||||
|
.btn-modal-secondary { order: 2; }
|
||||||
|
.btn-modal-primary { order: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.read-more-btn {
|
.read-more-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--bg-surface: #121215;
|
--bg-surface: #121215;
|
||||||
--bg-surface-hover: #1e1e22;
|
--bg-surface-hover: #1e1e22;
|
||||||
--accent: #8b5cf6;
|
--accent: #8b5cf6;
|
||||||
|
--accent-glow: rgba(139, 92, 246, 0.4); /* Añadido */
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--text-secondary: #a1a1aa;
|
--text-secondary: #a1a1aa;
|
||||||
--radius-md: 12px;
|
--radius-md: 12px;
|
||||||
@@ -11,6 +12,9 @@
|
|||||||
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
--glass-bg: rgba(20, 20, 23, 0.7);
|
--glass-bg: rgba(20, 20, 23, 0.7);
|
||||||
--nav-height: 80px;
|
--nav-height: 80px;
|
||||||
|
--danger: #ef4444; /* Añadido */
|
||||||
|
--bg-amoled: #0a0a0a; /* Añadido */
|
||||||
|
--bg-field: #0e0e0f; /* Añadido */
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; outline: none; }
|
* { box-sizing: border-box; outline: none; }
|
||||||
@@ -219,144 +223,325 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ==================================== */
|
/* ==================================== */
|
||||||
/* MODAL STYLES (Add to Library Modal) */
|
/* MODAL STYLES (REEMPLAZO POR MODAL COMPLETO) */
|
||||||
/* ==================================== */
|
/* ==================================== */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(10px);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
.modal-overlay.active {
|
.modal-overlay.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: #18181b;
|
background: var(--bg-amoled); /* Fondo AMOLED oscuro */
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 24px;
|
border-radius: var(--radius-lg);
|
||||||
padding: 2.5rem;
|
max-width: 900px; /* Ancho para el grid de 3 columnas */
|
||||||
max-width: 650px;
|
width: 95%;
|
||||||
width: 90%;
|
padding: 0;
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
|
||||||
transform: scale(0.95);
|
max-height: 90vh;
|
||||||
transition: transform 0.3s;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
.modal-overlay.active .modal-content {
|
animation: modalSlideUp 0.3s ease;
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content.modal-list {
|
@keyframes modalSlideUp {
|
||||||
max-width: 500px;
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1.5rem;
|
top: 1rem;
|
||||||
right: 1.5rem;
|
right: 1rem;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255,255,255,0.05);
|
||||||
border: none;
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: white;
|
color: white;
|
||||||
width: 32px;
|
width: 36px;
|
||||||
height: 32px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
z-index: 2001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 1.5rem 2rem 0.5rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
}
|
}
|
||||||
.modal-close:hover { background: rgba(255, 255, 255, 0.2); }
|
|
||||||
.modal-title { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.5rem; font-weight: 800; }
|
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
overflow-y: auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
/* GRUPO PRINCIPAL DE CAMPOS (GRID) */
|
||||||
|
.modal-fields-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.5rem 2rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group { /* Agregado para consistencia con list.css */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
|
||||||
background: var(--bg-base);
|
/* Ajustes de campos específicos */
|
||||||
|
.form-group.notes-group {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-direction: row; /* Necesario para la checkbox en línea */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date Input Styling */
|
||||||
|
.date-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input-pair {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Input Styling dentro del modal */
|
||||||
|
.modal-content .form-input {
|
||||||
|
background: var(--bg-field); /* Fondo específico para campos */
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary); /* Asegura color del texto */
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.8rem 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-family: inherit;
|
font-family: inherit; /* Asegura fuente consistente */
|
||||||
font-size: 1rem;
|
font-size: 1rem; /* Asegura tamaño de fuente */
|
||||||
transition: 0.2s;
|
transition: 0.2s; /* Asegura transiciones */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus {
|
.modal-content .form-input:focus {
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 10px rgba(139, 92, 246, 0.4); /* Usar el color acento */
|
box-shadow: 0 0 10px var(--accent-glow);
|
||||||
|
outline: none; /* Agregado */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select Styling */
|
||||||
|
.modal-content select.form-input {
|
||||||
|
/* Reiniciar la apariencia por defecto para selects, si es necesario */
|
||||||
|
-webkit-appearance: menulist;
|
||||||
|
appearance: menulist;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Checkbox Styling */
|
||||||
|
.form-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
background: var(--bg-base);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox:checked {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox:checked::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACCIONES (Barra inferior pegajosa) */
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.05);
|
||||||
|
background: var(--bg-amoled); /* Fondo para la barra sticky */
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary, .btn-modal-secondary, .btn-modal-danger {
|
/* Botones con nuevas clases para el modal */
|
||||||
|
.btn-primary, .btn-secondary, .btn-danger {
|
||||||
padding: 0.8rem 1.5rem;
|
padding: 0.8rem 1.5rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, opacity 0.2s;
|
transition: transform 0.2s, background 0.2s;
|
||||||
flex: 1;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary {
|
/* Re-estilizar btn-primary/secondary para el contexto del modal, pero manteniendo la base existente */
|
||||||
|
/* El .btn-primary global en book.css es blanco/negro, aquí es el color de acento/blanco para el modal */
|
||||||
|
|
||||||
|
.modal-actions .btn-primary { /* Asegura que el botón primario del modal use el color de acento */
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-primary:hover {
|
.modal-actions .btn-primary:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
|
/* No hace falta redefinir el hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-secondary {
|
.modal-actions .btn-secondary { /* Asegura que el botón secundario del modal use los estilos de blur */
|
||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1);
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-secondary:hover {
|
.modal-actions .btn-secondary:hover {
|
||||||
background: rgba(255,255,255,0.15);
|
background: rgba(255,255,255,0.15); /* Cambiado para ser coherente con list.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-danger {
|
.btn-danger {
|
||||||
background: #ef4444; /* Usar el color danger */
|
background: var(--danger);
|
||||||
color: white;
|
color: white;
|
||||||
|
margin-right: auto; /* Para alinear a la izquierda */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-modal-danger:hover {
|
.btn-danger:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Media Queries (Ajuste del Modal) --- */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.modal-content {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
.modal-fields-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.form-group.notes-group {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
align-self: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.modal-title, .modal-body {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
.modal-content {
|
||||||
|
margin: 0.5rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.modal-fields-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.date-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.form-group.notes-group,
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
margin-right: 0;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,14 @@
|
|||||||
--nav-height: 80px;
|
--nav-height: 80px;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
|
--bg-amoled: #0a0a0a;
|
||||||
|
--bg-field: #0e0e0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
margin: 0; /* Aseguramos que no haya margen en el body */
|
||||||
background-color: var(--bg-base);
|
background-color: var(--bg-base);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
@@ -134,6 +137,7 @@ body {
|
|||||||
|
|
||||||
.header-section {
|
.header-section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -161,12 +165,12 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 10px 40px var(--accent-glow);
|
box-shadow: 0 15px 35px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
@@ -181,15 +185,17 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Filtros mejorados --- */
|
||||||
.filters-section {
|
.filters-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
border: 1px solid rgba(255,255,255,0.05);
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
@@ -201,11 +207,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-group label {
|
.filter-group label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-select {
|
.filter-select {
|
||||||
@@ -217,6 +223,13 @@ body {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23a1a1aa'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.7rem center;
|
||||||
|
background-size: 1.2em;
|
||||||
|
padding-right: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-select:hover {
|
.filter-select:hover {
|
||||||
@@ -299,46 +312,52 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Diseño de Lista (Grid/List View) mejorado --- */
|
||||||
.list-grid {
|
.list-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
gap: 2rem;
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view {
|
.list-grid.list-view {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface-hover);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 0.3s, border-color 0.3s, box-shadow 0.3s;
|
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item:hover {
|
.list-item:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-8px);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
|
box-shadow: 0 15px 30px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .list-item {
|
.list-grid.list-view .list-item {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .list-item:hover {
|
.list-grid.list-view .list-item:hover {
|
||||||
transform: translateX(5px);
|
transform: none;
|
||||||
|
box-shadow: 0 4px 20px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-poster-link {
|
.item-poster-link {
|
||||||
display: block;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-poster {
|
.item-poster {
|
||||||
@@ -349,48 +368,60 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .item-poster {
|
.list-grid.list-view .item-poster {
|
||||||
width: 120px;
|
width: 100px;
|
||||||
height: 180px;
|
height: 150px;
|
||||||
aspect-ratio: auto;
|
aspect-ratio: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-content {
|
.item-content {
|
||||||
padding: 1rem;
|
padding: 1.2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.list-grid.list-view .item-content {
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-grid.list-view .item-content {
|
||||||
|
padding: 1rem 0;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.list-grid.list-view .item-content > div:first-child {
|
||||||
|
flex-basis: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
.item-title {
|
.item-title {
|
||||||
font-size: 1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-grid.list-view .item-title {
|
.list-grid.list-view .item-title {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-meta {
|
.item-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-pill {
|
.meta-pill {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
padding: 0.3rem 0.7rem;
|
padding: 0.25rem 0.6rem;
|
||||||
border-radius: 6px;
|
border-radius: 999px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@@ -413,12 +444,26 @@ body {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repeat-pill {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
.private-pill {
|
||||||
|
background: rgba(251, 191, 36, 0.15);
|
||||||
|
color: #facc15;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
background: rgba(255,255,255,0.05);
|
background: rgba(255,255,255,0.08);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
height: 8px;
|
height: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@@ -434,6 +479,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-badge {
|
.score-badge {
|
||||||
@@ -441,46 +487,86 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--success);
|
color: #facc15;
|
||||||
|
background: rgba(250, 204, 21, 0.1);
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn-card {
|
/* --- Botón de edición flotante --- */
|
||||||
background: var(--accent);
|
.edit-icon-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(18, 18, 21, 0.9);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5rem 1rem;
|
width: 40px;
|
||||||
border-radius: 999px;
|
height: 40px;
|
||||||
font-weight: 700;
|
border-radius: 50%;
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, background 0.2s;
|
display: flex;
|
||||||
margin-top: 1rem;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
.edit-btn-card:hover {
|
opacity: 0;
|
||||||
background: #7c3aed;
|
transition: opacity 0.3s, transform 0.2s, background 0.2s;
|
||||||
transform: scale(1.03);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-item:hover .edit-icon-btn {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon-btn:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-grid.list-view .edit-icon-btn {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.list-grid.list-view .list-item:hover .edit-icon-btn {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Modal de Edición Mejorado (Estilo Anilist + AMOLED) --- */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.8);
|
background: rgba(0,0,0,0.9);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(10px);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-amoled);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
max-width: 500px;
|
max-width: 900px;
|
||||||
width: 100%;
|
width: 95%;
|
||||||
padding: 2rem;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: modalSlideUp 0.3s ease;
|
animation: modalSlideUp 0.3s ease;
|
||||||
|
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalSlideUp {
|
@keyframes modalSlideUp {
|
||||||
@@ -510,6 +596,7 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
z-index: 2001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
@@ -519,14 +606,27 @@ body {
|
|||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
margin-bottom: 1.5rem;
|
padding: 1.5rem 2rem 0.5rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
overflow-y: auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GRUPO PRINCIPAL DE CAMPOS */
|
||||||
|
.modal-fields-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.5rem 2rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -535,14 +635,30 @@ body {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Column Span Overrides */
|
||||||
|
.form-group.notes-group {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
background: var(--bg-base);
|
background: var(--bg-field);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.8rem 1rem;
|
||||||
@@ -558,10 +674,73 @@ body {
|
|||||||
box-shadow: 0 0 10px var(--accent-glow);
|
box-shadow: 0 0 10px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CLAVE: Hace que la etiqueta de fecha esté encima del input */
|
||||||
|
.date-input-pair {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
background: var(--bg-base);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox:checked {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox:checked::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACCIONES (Barra inferior pegajosa) */
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.05);
|
||||||
|
background: var(--bg-amoled);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary, .btn-secondary, .btn-danger {
|
.btn-primary, .btn-secondary, .btn-danger {
|
||||||
@@ -571,8 +750,8 @@ body {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, opacity 0.2s;
|
transition: transform 0.2s, background 0.2s;
|
||||||
flex: 1;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -597,6 +776,7 @@ body {
|
|||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
color: white;
|
color: white;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
@@ -604,7 +784,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -613,65 +792,90 @@ body {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
/* --- Media Queries (Responsive) --- */
|
||||||
.navbar {
|
@media (max-width: 900px) {
|
||||||
padding: 0 1.5rem;
|
.modal-content {
|
||||||
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
|
.modal-fields-grid {
|
||||||
.container {
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-row {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
.form-group.notes-group {
|
||||||
.filters-section {
|
grid-column: 1 / -1;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
.form-group.checkbox-group {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
align-self: auto;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.modal-title, .modal-body {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
/* Layout de lista (card view) */
|
||||||
.list-grid {
|
.list-grid {
|
||||||
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
.list-grid.list-view .list-item {
|
||||||
gap: 1.5rem;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.list-grid.list-view .item-poster {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
.list-grid.list-view .item-content {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.list-grid.list-view .item-content > div:first-child {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
.list-grid.list-view .edit-icon-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(18, 18, 21, 0.8);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.edit-icon-btn {
|
/* Modal en móvil */
|
||||||
position: absolute;
|
.modal-content {
|
||||||
top: 1rem;
|
margin: 0.5rem;
|
||||||
right: 1rem;
|
width: auto;
|
||||||
z-index: 50;
|
}
|
||||||
|
.modal-fields-grid {
|
||||||
background: rgba(0, 0, 0, 0.6);
|
grid-template-columns: 1fr;
|
||||||
backdrop-filter: blur(5px);
|
gap: 1rem;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
padding-bottom: 0;
|
||||||
color: white;
|
}
|
||||||
|
.form-group.notes-group,
|
||||||
width: 36px;
|
.form-group.checkbox-group {
|
||||||
height: 36px;
|
grid-column: auto;
|
||||||
border-radius: 50%;
|
}
|
||||||
cursor: pointer;
|
.modal-actions {
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
justify-content: center;
|
}
|
||||||
|
.btn-danger {
|
||||||
opacity: 0;
|
margin-right: 0;
|
||||||
transition: opacity 0.3s, background 0.2s;
|
order: 3;
|
||||||
}
|
}
|
||||||
|
.btn-secondary {
|
||||||
.list-item:hover .edit-icon-btn {
|
order: 2;
|
||||||
opacity: 1;
|
}
|
||||||
}
|
.btn-primary {
|
||||||
|
order: 1;
|
||||||
.edit-icon-btn:hover {
|
}
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn-card {
|
.edit-btn-card {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<button class="nav-button" onclick="window.location.href='/marketplace'">Marketplace</button>
|
<button class="nav-button" onclick="window.location.href='/marketplace'">Marketplace</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mejorado el contenedor de usuario con dropdown más completo -->
|
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<div class="search-wrapper" style="visibility: hidden;">
|
<div class="search-wrapper" style="visibility: hidden;">
|
||||||
<svg class="search-icon" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="search-icon" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
@@ -182,7 +181,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h2>Your list is empty</h2>
|
<h2>Your list is empty</h2>
|
||||||
<p>Start adding anime to track your progress</p>
|
<p>Start adding anime to track your progress</p>
|
||||||
<button class="btn-primary" onclick="window.location.href='/'">Browse Anime</button>
|
<button class="btn-primary" onclick="window.location.href='/anime'">Browse Anime</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="list-container" class="list-grid"></div>
|
<div id="list-container" class="list-grid"></div>
|
||||||
@@ -194,33 +193,65 @@
|
|||||||
<h2 class="modal-title">Edit Entry</h2>
|
<h2 class="modal-title">Edit Entry</h2>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="modal-fields-grid">
|
||||||
<label>Status</label>
|
|
||||||
<select id="edit-status" class="form-input">
|
|
||||||
<option value="WATCHING">Watching</option>
|
|
||||||
<option value="COMPLETED">Completed</option>
|
|
||||||
<option value="PLANNING">Plan to Watch</option>
|
|
||||||
<option value="PAUSED">Paused</option>
|
|
||||||
<option value="DROPPED">Dropped</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-progress">Progress</label>
|
<label>Status</label>
|
||||||
<input type="number" id="edit-progress" class="form-input" min="0">
|
<select id="edit-status" class="form-input">
|
||||||
</div>
|
<option value="WATCHING">Watching</option>
|
||||||
|
<option value="COMPLETED">Completed</option>
|
||||||
|
<option value="PLANNING">Plan to Watch</option>
|
||||||
|
<option value="PAUSED">Paused</option>
|
||||||
|
<option value="DROPPED">Dropped</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Score (0-10)</label>
|
<label for="edit-progress">Progress</label>
|
||||||
<input type="number" id="edit-score" class="form-input" min="0" max="10" step="0.1">
|
<input type="number" id="edit-progress" class="form-input" min="0">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Score (0-10)</label>
|
||||||
|
<input type="number" id="edit-score" class="form-input" min="0" max="10" step="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<div class="date-group">
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="edit-start-date">Start Date</label>
|
||||||
|
<input type="date" id="edit-start-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="date-input-pair">
|
||||||
|
<label for="edit-end-date">End Date</label>
|
||||||
|
<input type="date" id="edit-end-date" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-repeat-count">Repeat Count</label>
|
||||||
|
<input type="number" id="edit-repeat-count" class="form-input" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group notes-group">
|
||||||
|
<label for="edit-notes">Notes</label>
|
||||||
|
<textarea id="edit-notes" class="form-input notes-textarea" rows="4" placeholder="Personal notes..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="edit-is-private" class="form-checkbox">
|
||||||
|
<label for="edit-is-private">Mark as Private</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
|
|
||||||
<button class="btn-danger" onclick="deleteEntry()">Delete</button>
|
|
||||||
<button class="btn-primary" onclick="saveEntry()">Save Changes</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
|
||||||
|
<button class="btn-danger" onclick="deleteEntry()">Delete</button>
|
||||||
|
<button class="btn-primary" onclick="saveEntry()">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user