@serifpersia/dango 3.3.7 → 3.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-WlhU88fn.js → AnimeCard-D4qo4289.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BCLagpbu.js → AnimeInfo-BdRRm4So.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-o0U_OXKP.js → AnimeInfoPage-DeJNAKMR.js} +2 -2
  5. package/client/dist/assets/{Asmr-ieGJ_BcQ.js → Asmr-D0EGRYsv.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-BwaoVski.js → AsmrResetProgressModal-Gs0GmVaT.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CXvBuY4l.js → AvSyncCalibrator-D8hqarVB.js} +1 -1
  8. package/client/dist/assets/{Home-DjLpvsp8.js → Home-BnBzjFkW.js} +1 -1
  9. package/client/dist/assets/Insights-i9QgY3nE.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-B2nhFFrc.js → LibraryListPage-D3hTqTRU.js} +1 -1
  11. package/client/dist/assets/{ListeningList-okIpsKCr.js → ListeningList-BgWsfu40.js} +1 -1
  12. package/client/dist/assets/{Manga-3u9nVNZC.js → Manga-Dn-CYins.js} +1 -1
  13. package/client/dist/assets/MangaInfoPage-EmQoK7Bv.js +1 -0
  14. package/client/dist/assets/MangaReadPage-xJB8K2YU.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-Ch8RjwbO.js → MangaResetProgressModal-Dian37PQ.js} +1 -1
  16. package/client/dist/assets/{Mature-pINwl5wj.js → Mature-DQtXKOXc.js} +1 -1
  17. package/client/dist/assets/{MediaCard-DDiYEAm8.js → MediaCard-DpKoY-1j.js} +1 -1
  18. package/client/dist/assets/{Music-B1swlVLI.js → Music-BP43Zqmq.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-CzH2VSPh.js → OptionListMenu-Bj9ebsW3.js} +1 -1
  20. package/client/dist/assets/{Player-SKPWb-o8.js → Player-O2Cy2GF1.js} +2 -2
  21. package/client/dist/assets/{PlayerSettings-jrmY95-i.js → PlayerSettings-DYZ272Yp.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-CcVP7Vi6.js → QueueOptionsButton-lv26-6jp.js} +1 -1
  23. package/client/dist/assets/{QueueRail-Cj-PU9EV.js → QueueRail-B7DeSFoA.js} +1 -1
  24. package/client/dist/assets/{Radio-CRlbaxQt.js → Radio-qQBYnVfY.js} +1 -1
  25. package/client/dist/assets/{ReadingList-wRtHGNo8.js → ReadingList-BT8kUjCB.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-BokfoYFB.js → ResetProgressModal-DWeiO-hs.js} +1 -1
  27. package/client/dist/assets/{Search-BOOBeTVA.js → Search-BkvPYN33.js} +1 -1
  28. package/client/dist/assets/{Settings-BgrcswJD.js → Settings-CyZEBF6S.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-C6S7OYHn.js → SkeletonGrid-B5loz7ii.js} +1 -1
  30. package/client/dist/assets/Trackers-JK-aXBYC.js +2 -0
  31. package/client/dist/assets/{Tv-ByVckozq.js → Tv-BwXWJxMy.js} +1 -1
  32. package/client/dist/assets/{TvInfo-fJ9SFMK8.js → TvInfo-Dzyq6ucH.js} +1 -1
  33. package/client/dist/assets/{TvPopup-yfH7p0y0.js → TvPopup-PlCU30FO.js} +1 -1
  34. package/client/dist/assets/{TvSearch-CBNWDByW.js → TvSearch-BYwCf5kO.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-BqjZ2-4A.js → TvWatchlist-BiJU6pKT.js} +1 -1
  36. package/client/dist/assets/{Watchlist-B_Q2hEiJ.js → Watchlist-BOEeGXH9.js} +1 -1
  37. package/client/dist/assets/{hls-kIF7bMpF.js → hls-C11FOWHj.js} +1 -1
  38. package/client/dist/assets/index-CtoKRNLc.js +178 -0
  39. package/client/dist/assets/{useAnimeInfoData-BKSrS8eN.js → useAnimeInfoData-kr3WOIWH.js} +1 -1
  40. package/client/dist/assets/{useAsmr-Byj8Qiwp.js → useAsmr-CB9THf0m.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-CLcWCCXf.js → useAsmrPopup-cT_Uu41e.js} +1 -1
  42. package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +1 -0
  43. package/client/dist/assets/{useMangaPopup-ZonOP5_c.js → useMangaPopup-PnmSctHu.js} +1 -1
  44. package/client/dist/assets/{useProviders-CymA42ZB.js → useProviders-B3VB7A9M.js} +1 -1
  45. package/client/dist/assets/{useTv-CPj6Cf42.js → useTv-BTUKMH7Q.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-COsyN8yu.js → useTvLibrary-C0sX-ilZ.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/controllers/data.controller.js +1 -0
  50. package/server/dist/controllers/insights.controller.js +3 -2
  51. package/server/dist/controllers/manga-library.controller.js +54 -0
  52. package/server/dist/controllers/watchlist.controller.js +13 -1
  53. package/server/dist/lib/anilist.js +36 -3
  54. package/server/dist/lib/mal.js +69 -0
  55. package/server/dist/lib/tracker/anilist-tracker.js +66 -0
  56. package/server/dist/lib/tracker/manga-sync.service.js +792 -0
  57. package/server/dist/repositories/manga.repository.js +17 -2
  58. package/server/dist/routes/manga-library.routes.js +2 -0
  59. package/server/dist/routes/tracker.routes.js +123 -0
  60. package/server/dist/sync.js +8 -1
  61. package/server/package.json +1 -1
  62. package/client/dist/assets/Insights-lQcQoeES.js +0 -1
  63. package/client/dist/assets/MangaInfoPage-Cat-B487.js +0 -1
  64. package/client/dist/assets/MangaReadPage-CyLRxzvl.js +0 -1
  65. package/client/dist/assets/Trackers-DLcTEJJW.js +0 -2
  66. package/client/dist/assets/index-C65i4HWv.js +0 -178
  67. package/client/dist/assets/useMangaLibrary-U7tUo8nx.js +0 -1
@@ -40,8 +40,8 @@ export const MangaLibraryRepository = {
40
40
  return row?.total || 0;
41
41
  },
42
42
  getIds: (db) => dbAll(db, 'SELECT id FROM manga_library').map((r) => r.id),
43
- upsert: (db, data) => dbRun(db, `INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, updatedAt)
44
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
43
+ upsert: (db, data) => dbRun(db, `INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
44
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
45
45
  ON CONFLICT(id) DO UPDATE SET
46
46
  provider = COALESCE(NULLIF(EXCLUDED.provider, ''), manga_library.provider),
47
47
  title = COALESCE(NULLIF(EXCLUDED.title, ''), manga_library.title),
@@ -50,6 +50,8 @@ export const MangaLibraryRepository = {
50
50
  author = COALESCE(NULLIF(EXCLUDED.author, ''), manga_library.author),
51
51
  altTitle = COALESCE(NULLIF(EXCLUDED.altTitle, ''), manga_library.altTitle),
52
52
  contentRating = COALESCE(EXCLUDED.contentRating, manga_library.contentRating),
53
+ anilistId = COALESCE(EXCLUDED.anilistId, manga_library.anilistId),
54
+ anilistIdSource = COALESCE(EXCLUDED.anilistIdSource, manga_library.anilistIdSource),
53
55
  updatedAt = strftime('%s', 'now')`, [
54
56
  data.id,
55
57
  data.provider,
@@ -60,7 +62,17 @@ export const MangaLibraryRepository = {
60
62
  data.author || null,
61
63
  data.altTitle || null,
62
64
  data.contentRating || null,
65
+ data.anilistId ?? null,
66
+ data.anilistIdSource ?? null,
63
67
  ]),
68
+ setAnilistId: (db, id, anilistId, source) => source
69
+ ? dbRun(db, 'UPDATE manga_library SET anilistId = ?, anilistIdSource = ? WHERE id = ?', [
70
+ anilistId,
71
+ source,
72
+ id,
73
+ ])
74
+ : dbRun(db, 'UPDATE manga_library SET anilistId = ? WHERE id = ?', [anilistId, id]),
75
+ getByAnilistId: (db, anilistId) => dbAll(db, 'SELECT * FROM manga_library WHERE anilistId = ?', [anilistId]),
64
76
  updateStatus: (db, id, status) => dbRun(db, "UPDATE manga_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id = ?", [status, id]),
65
77
  updateStatusMany: (db, ids, status) => {
66
78
  if (ids.length === 0)
@@ -69,6 +81,7 @@ export const MangaLibraryRepository = {
69
81
  dbRun(db, `UPDATE manga_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id IN (${placeholders})`, [status, ...ids]);
70
82
  },
71
83
  touchProgress: (db, id, progress) => dbRun(db, `UPDATE manga_library SET lastChapterId = ?, lastChapterNumber = ?, lastPage = ?, updatedAt = strftime('%s', 'now') WHERE id = ?`, [progress.chapterId, progress.chapterNumber, progress.page, id]),
84
+ setProgressPointer: (db, id, chapterNumber) => dbRun(db, `UPDATE manga_library SET lastChapterNumber = ?, updatedAt = strftime('%s', 'now') WHERE id = ?`, [chapterNumber, id]),
72
85
  delete: (db, id) => dbRun(db, 'DELETE FROM manga_library WHERE id = ?', [id]),
73
86
  deleteMany: (db, ids) => {
74
87
  if (ids.length === 0)
@@ -115,6 +128,8 @@ export const MangaProgressRepository = {
115
128
  mangaId,
116
129
  chapterId,
117
130
  ]),
131
+ deleteSyntheticChapters: (db, mangaId, keepChapterId) => dbRun(db, `DELETE FROM manga_progress WHERE mangaId = ? AND chapterId LIKE 'anilist:ch:%' AND chapterId != ?`, [mangaId, keepChapterId]),
132
+ moveSyntheticChapters: (db, fromMangaId, toMangaId) => dbRun(db, `UPDATE manga_progress SET mangaId = ?, updatedAt = strftime('%s', 'now') WHERE mangaId = ? AND chapterId LIKE 'anilist:ch:%'`, [toMangaId, fromMangaId]),
118
133
  getContinueReading: (db, limit) => {
119
134
  const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
120
135
  return dbAll(db, `SELECT p.mangaId as id,
@@ -6,9 +6,11 @@ export function createMangaLibraryRouter() {
6
6
  router.get('/manga/library', controller.getLibrary);
7
7
  router.get('/manga/library/ids', controller.getLibraryIds);
8
8
  router.get('/manga/library/check/:id', controller.checkLibrary);
9
+ router.get('/manga/library/entry/:id', controller.getEntry);
9
10
  router.post('/manga/library/add', controller.addToLibrary);
10
11
  router.post('/manga/library/remove', controller.removeFromLibrary);
11
12
  router.post('/manga/library/status', controller.updateStatus);
13
+ router.post('/manga/library/link', controller.linkAnilist);
12
14
  router.post('/manga/library/batch-status', controller.batchUpdateStatus);
13
15
  router.post('/manga/library/remove-many', controller.batchRemove);
14
16
  router.post('/manga/progress/remove-many', controller.batchRemoveProgress);
@@ -1,11 +1,38 @@
1
1
  import { Router } from 'express';
2
+ import multer from 'multer';
3
+ import { XMLParser } from 'fast-xml-parser';
2
4
  import { AniListTracker } from '../lib/tracker/anilist-tracker.js';
3
5
  import { syncAniList, importFromMalUsername, importFromUsername, } from '../lib/tracker/sync.service.js';
6
+ import { syncAniListManga, importFromUsernameManga, importMangaFromMalUsername, importMangaFromMalXmlItems, } from '../lib/tracker/manga-sync.service.js';
4
7
  import { SettingsRepository } from '../repositories/settings.repository.js';
5
8
  import { performWriteTransaction } from '../sync.js';
6
9
  const TOKEN_KEY = 'tracker_anilist_token';
7
10
  const USER_KEY = 'tracker_anilist_user';
8
11
  const CLIENT_ID_KEY = 'tracker_anilist_client_id';
12
+ const malMangaXmlParser = new XMLParser({
13
+ isArray: (name) => name === 'manga',
14
+ parseTagValue: false,
15
+ trimValues: true,
16
+ });
17
+ function mangaXmlText(value) {
18
+ if (Array.isArray(value))
19
+ return String(value[0] ?? '');
20
+ return String(value ?? '');
21
+ }
22
+ function mapMalMangaXmlStatus(status) {
23
+ switch (status) {
24
+ case 'Plan to Read':
25
+ return 'Planned';
26
+ case 'On Hold':
27
+ return 'On-Hold';
28
+ case 'Reading':
29
+ case 'Completed':
30
+ case 'Dropped':
31
+ return status;
32
+ default:
33
+ return 'Planned';
34
+ }
35
+ }
9
36
  export function createTrackerRouter() {
10
37
  const router = Router();
11
38
  router.get('/tracker/anilist/callback', (_req, res) => {
@@ -160,5 +187,101 @@ export function createTrackerRouter() {
160
187
  res.status(500).json({ error: message });
161
188
  }
162
189
  });
190
+ router.post('/tracker/manga/sync', async (req, res) => {
191
+ const { direction } = req.body ?? {};
192
+ try {
193
+ if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
194
+ return res.status(503).json({ error: 'Manga database is not ready' });
195
+ }
196
+ const syncDirection = direction === 'pull-only' ? 'pull-only' : 'two-way';
197
+ const result = await syncAniListManga(req.db, req.mangaDb, {
198
+ direction: syncDirection,
199
+ });
200
+ res.json({
201
+ success: true,
202
+ summary: result.summary,
203
+ details: result.details,
204
+ direction: syncDirection,
205
+ });
206
+ }
207
+ catch (err) {
208
+ const message = err instanceof Error ? err.message : 'Manga sync failed';
209
+ res.status(500).json({ error: message });
210
+ }
211
+ });
212
+ router.post('/tracker/manga/import', async (req, res) => {
213
+ const { username, erase } = req.body ?? {};
214
+ if (!username || typeof username !== 'string') {
215
+ return res.status(400).json({ error: 'Username is required' });
216
+ }
217
+ try {
218
+ if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
219
+ return res.status(503).json({ error: 'Manga database is not ready' });
220
+ }
221
+ const count = await importFromUsernameManga(req.db, req.mangaDb, username.trim(), erase === true);
222
+ res.json({ success: true, count });
223
+ }
224
+ catch (err) {
225
+ const message = err instanceof Error ? err.message : 'Import failed';
226
+ res.status(500).json({ error: message });
227
+ }
228
+ });
229
+ router.post('/tracker/manga/mal-import', async (req, res) => {
230
+ const { username, erase } = req.body ?? {};
231
+ if (!username || typeof username !== 'string') {
232
+ return res.status(400).json({ error: 'MAL username is required' });
233
+ }
234
+ try {
235
+ if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
236
+ return res.status(503).json({ error: 'Manga database is not ready' });
237
+ }
238
+ const result = await importMangaFromMalUsername(req.db, req.mangaDb, username.trim(), erase === true);
239
+ res.json({ success: true, ...result });
240
+ }
241
+ catch (err) {
242
+ const message = err instanceof Error ? err.message : 'Import failed';
243
+ if (message.includes('private') || message.includes('not found')) {
244
+ return res.status(404).json({ error: message });
245
+ }
246
+ if (message.includes('blocked') || message.includes('HTTP 429')) {
247
+ return res.status(429).json({ error: message });
248
+ }
249
+ res.status(500).json({ error: message });
250
+ }
251
+ });
252
+ router.post('/import/mal-xml-manga', multer().single('xmlfile'), async (req, res) => {
253
+ if (!req.file)
254
+ return res.status(400).json({ error: 'No file' });
255
+ const eraseRaw = req.body?.erase;
256
+ const erase = eraseRaw === true || eraseRaw === 'true' || eraseRaw === '1';
257
+ try {
258
+ if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
259
+ return res.status(503).json({ error: 'Manga database is not ready' });
260
+ }
261
+ let result;
262
+ try {
263
+ result = malMangaXmlParser.parse(req.file.buffer.toString());
264
+ }
265
+ catch {
266
+ return res.status(400).json({ error: 'Invalid XML' });
267
+ }
268
+ const mangaList = result?.myanimelist?.manga || [];
269
+ if (mangaList.length === 0) {
270
+ return res.status(400).json({ error: 'No manga found in XML' });
271
+ }
272
+ const items = mangaList.map((item) => ({
273
+ malId: parseInt(mangaXmlText(item.series_mangadb_id), 10) || 0,
274
+ title: mangaXmlText(item.series_title),
275
+ status: mapMalMangaXmlStatus(mangaXmlText(item.my_status)),
276
+ chapters: Math.max(parseInt(mangaXmlText(item.my_read_chapters), 10) || 0, 0),
277
+ }));
278
+ const { imported, skipped } = await importMangaFromMalXmlItems(req.db, req.mangaDb, items, erase);
279
+ res.json({ success: true, imported, skipped });
280
+ }
281
+ catch (err) {
282
+ const message = err instanceof Error ? err.message : 'Import failed';
283
+ res.status(500).json({ error: message });
284
+ }
285
+ });
163
286
  return router;
164
287
  }
@@ -713,7 +713,7 @@ export async function initializeMangaDatabase(dbPath) {
713
713
  db.run('PRAGMA temp_store = MEMORY;');
714
714
  db.run('PRAGMA mmap_size = 268435456;');
715
715
  db.run('PRAGMA foreign_keys = ON;');
716
- db.run(`CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT)`);
716
+ db.run(`CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)`);
717
717
  db.run(`CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))`);
718
718
  db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
719
719
  db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
@@ -724,6 +724,13 @@ export async function initializeMangaDatabase(dbPath) {
724
724
  if (!mangaColumns.some((c) => c.name === 'altTitle')) {
725
725
  db.run(`ALTER TABLE manga_library ADD COLUMN altTitle TEXT`);
726
726
  }
727
+ if (!mangaColumns.some((c) => c.name === 'anilistId')) {
728
+ db.run(`ALTER TABLE manga_library ADD COLUMN anilistId INTEGER`);
729
+ }
730
+ if (!mangaColumns.some((c) => c.name === 'anilistIdSource')) {
731
+ db.run(`ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT`);
732
+ }
733
+ db.run(`CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)`);
727
734
  const mangaProgressColumns = db.all(`PRAGMA table_info(manga_progress)`);
728
735
  if (!mangaProgressColumns.some((c) => c.name === 'title')) {
729
736
  db.run(`ALTER TABLE manga_progress ADD COLUMN title TEXT`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dango-server",
3
3
  "private": true,
4
- "version": "3.3.7",
4
+ "version": "3.3.8",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.12.0"
@@ -1 +0,0 @@
1
- import{h as G,T as i,d as R,u as e,L as W}from"./vendor-core-8R2fFbyN.js";import{b as j,u as P,a as O}from"./vendor-query-BXRECSI6.js";import{X as F,d as A,z as S,q as V,f as C}from"./index-C65i4HWv.js";import{I as o}from"./vendor-icons-CDMuNDeL.js";const Y="_header_1j07c_1",q="_fadeIn_1j07c_1",H="_personaBadge_1j07c_9",E="_personaInfo_1j07c_37",z="_personaLabel_1j07c_42",U="_personaValue_1j07c_51",K="_statsGrid_1j07c_57",Q="_slideUpIn_1j07c_1",J="_statCard_1j07c_65",X="_statIcon_1j07c_82",Z="_statInfo_1j07c_98",ee="_statValue_1j07c_103",ae="_statLabel_1j07c_112",te="_wideSection_1j07c_120",se="_sectionHeader_1j07c_132",ne="_yearSelect_1j07c_147",re="_heatmapLayout_1j07c_176",ce="_dayLabels_1j07c_183",oe="_heatmapWrapper_1j07c_195",le="_heatmapMonthLabels_1j07c_201",ie="_heatmapGrid_1j07c_212",de="_heatBox_1j07c_220",he="_hiddenBox_1j07c_229",me="_level0_1j07c_240",pe="_level1_1j07c_243",_e="_level2_1j07c_247",ue="_level3_1j07c_251",ge="_level4_1j07c_255",Ne="_heatmapFooter_1j07c_260",ve="_heatLegend_1j07c_267",ye="_chartsContainer_1j07c_277",be="_chartWrapper_1j07c_285",fe="_hourlyChart_1j07c_312",je="_hourlyBarContainer_1j07c_320",Se="_hourlyBar_1j07c_320",Ce="_hourlyLabel_1j07c_346",ke="_chartSubtext_1j07c_353",Le="_popScale_1j07c_365",we="_popTrack_1j07c_369",Be="_popThumb_1j07c_377",Ie="_popLabels_1j07c_389",De="_popDescription_1j07c_399",xe="_seasonalChart_1j07c_414",Te="_seasonalBarContainer_1j07c_422",Me="_seasonalBar_1j07c_422",$e="_seasonalLabel_1j07c_444",Ge="_genreList_1j07c_452",Re="_genreRow_1j07c_459",We="_genreInfo_1j07c_465",Pe="_genreName_1j07c_472",Oe="_genreCount_1j07c_479",Fe="_genreBarBg_1j07c_486",Ae="_genreBar_1j07c_486",Ve="_warningSection_1j07c_499",Ye="_warningHeader_1j07c_510",qe="_droppedGrid_1j07c_526",He="_droppedCard_1j07c_532",Ee="_droppedName_1j07c_549",ze="_droppedDate_1j07c_556",Ue="_genreGrid_1j07c_597",Ke="_genreCard_1j07c_609",Qe="_rankBadge_1j07c_618",Je="_rankNumber_1j07c_631",Xe="_genreTitle_1j07c_637",Ze="_genreStats_1j07c_645",ea="_posterRow_1j07c_653",aa="_miniPoster_1j07c_660",ta="_genreCardSkeleton_1j07c_674",sa="_pulse_1j07c_1",na="_skeletonRank_1j07c_682",ra="_skeletonTitle_1j07c_696",ca="_skeletonStats_1j07c_709",oa="_skeletonPosters_1j07c_721",la="_recSection_1j07c_752",ia="_recSubtext_1j07c_763",da="_recGrid_1j07c_769",ha="_recCard_1j07c_775",ma="_recCover_1j07c_790",pa="_recInfo_1j07c_798",_a="_recTitle_1j07c_805",ua="_recMatch_1j07c_816",ga="_recMeta_1j07c_822",Na="_recWhy_1j07c_827",va="_recNote_1j07c_832",ya="_recAddBtn_1j07c_838",a={header:Y,fadeIn:q,personaBadge:H,personaInfo:E,personaLabel:z,personaValue:U,statsGrid:K,slideUpIn:Q,statCard:J,statIcon:X,statInfo:Z,statValue:ee,statLabel:ae,wideSection:te,sectionHeader:se,yearSelect:ne,heatmapLayout:re,dayLabels:ce,heatmapWrapper:oe,heatmapMonthLabels:le,heatmapGrid:ie,heatBox:de,hiddenBox:he,level0:me,level1:pe,level2:_e,level3:ue,level4:ge,heatmapFooter:Ne,heatLegend:ve,chartsContainer:ye,chartWrapper:be,hourlyChart:fe,hourlyBarContainer:je,hourlyBar:Se,hourlyLabel:Ce,chartSubtext:ke,popScale:Le,popTrack:we,popThumb:Be,popLabels:Ie,popDescription:De,seasonalChart:xe,seasonalBarContainer:Te,seasonalBar:Me,seasonalLabel:$e,genreList:Ge,genreRow:Re,genreInfo:We,genreName:Pe,genreCount:Oe,genreBarBg:Fe,genreBar:Ae,warningSection:Ve,warningHeader:Ye,droppedGrid:qe,droppedCard:He,droppedName:Ee,droppedDate:ze,genreGrid:Ue,genreCard:Ke,rankBadge:Qe,rankNumber:Je,genreTitle:Xe,genreStats:Ze,posterRow:ea,miniPoster:aa,genreCardSkeleton:ta,pulse:sa,skeletonRank:na,skeletonTitle:ra,skeletonStats:ca,skeletonPosters:oa,recSection:la,recSubtext:ia,recGrid:da,recCard:ha,recCover:ma,recInfo:pa,recTitle:_a,recMatch:ua,recMeta:ga,recWhy:Na,recNote:va,recAddBtn:ya},Ca=()=>{G(()=>{document.title="Insights - dango"},[]);const{data:s,isLoading:k,isError:L}=j({queryKey:["insights"],queryFn:async()=>{const t=await fetch("/api/insights");if(!t.ok)throw new Error("Failed to fetch insights");return t.json()}}),{data:w,isLoading:p}=F(),{titlePreference:B}=A(),_=P(),{data:u}=j({queryKey:["insights-recommendations"],queryFn:async()=>{const t=await fetch("/api/insights/recommendations");if(!t.ok)throw new Error("Failed to fetch recommendations");return t.json()},staleTime:300*1e3,retry:1}),g=O({mutationFn:async t=>{await V("/api/watchlist/add",{method:"POST",body:JSON.stringify({id:String(t.id),name:t.title,thumbnail:t.cover||"",status:"Plan to Watch",type:t.format})})},onSuccess:()=>{_.invalidateQueries({queryKey:["watchlist"]}),_.invalidateQueries({queryKey:["insights-recommendations"]}),S.success("Added to Plan to Watch")},onError:()=>S.error("Could not add to watchlist")}),N=i(()=>[...u?.candidates||[]].sort((t,n)=>n.matchPct-t.matchPct).slice(0,6),[u]),v=t=>{switch(B){case"nativeName":return t.nativeName||t.name;case"englishName":return t.englishName||t.name;default:return t.name}},[d,I]=R(new Date().getFullYear()),D=i(()=>{if(!s?.activityGrid)return[new Date().getFullYear()];const t=new Set(s.activityGrid.map(n=>parseInt(n.day.split("-")[0])));return t.add(new Date().getFullYear()),Array.from(t).sort((n,r)=>r-n)},[s]),y=i(()=>{if(!s?.activityGrid)return[];const t=new Map(s.activityGrid.map(m=>[m.day,m.count])),n=[],r=new Date(d,0,1),l=new Date(d,11,31),h=r.getDay(),c=new Date(r);for(c.setDate(c.getDate()-h);(c<=l||c.getDay()!==0)&&!(c>l&&c.getDay()===0);){const m=c.getFullYear(),M=String(c.getMonth()+1).padStart(2,"0"),$=String(c.getDate()).padStart(2,"0"),b=`${m}-${M}-${$}`,f=c<r||c>l;n.push({date:b,count:f?0:t.get(b)||0,month:c.getMonth(),isOutOfRange:f}),c.setDate(c.getDate()+1)}return n},[s,d]),x=i(()=>s?.seasonality?.length?Math.max(...s.seasonality.map(t=>t.seconds),1):0,[s]),T=i(()=>s?.hourlyDist?.length?Math.max(...s.hourlyDist.map(t=>t.count),1):0,[s]);return k||p?e("div",{className:a.loading,children:"Analyzing your anime lifestyle..."}):L?e("div",{className:a.error,children:"Could not load insights. Data might be syncing."}):s?e("div",{className:"page-container",children:[e("div",{className:a.header,children:[e("h2",{className:"section-title",children:"Watch Insights"}),e("div",{className:a.personaBadge,children:[e(o,{name:"user-astronaut"}),e("div",{className:a.personaInfo,children:[e("span",{className:a.personaLabel,children:"Your Persona"}),e("span",{className:a.personaValue,children:s.persona})]})]})]}),e("div",{className:a.statsGrid,children:[e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(138, 79, 255, 0.2)",color:"var(--accent)"},children:e(o,{name:"clock"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.totalHours,"h"]}),e("span",{className:a.statLabel,children:"Watch Time"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(167, 139, 250, 0.2)",color:"#a78bfa"},children:e(o,{name:"bookmark"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.totalAnime}),e("span",{className:a.statLabel,children:"Total Anime"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(34, 197, 94, 0.2)",color:"#22c55e"},children:e(o,{name:"check-circle"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.totalEpisodes}),e("span",{className:a.statLabel,children:"Episodes Watched"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(34, 197, 94, 0.2)",color:"#22c55e"},children:e(o,{name:"check-circle"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.completionRate,"%"]}),e("span",{className:a.statLabel,children:"Completion Rate"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(45, 212, 191, 0.2)",color:"#2dd4bf"},children:e(o,{name:"tv"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.completedAnime}),e("span",{className:a.statLabel,children:"Completed Shows"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(239, 68, 68, 0.2)",color:"#ef4444"},children:e(o,{name:"fire"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.bingeFactor}),e("span",{className:a.statLabel,children:"Max Daily Binge"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(59, 130, 246, 0.2)",color:"#3b82f6"},children:e(o,{name:"history"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.avgSessionMinutes,"m"]}),e("span",{className:a.statLabel,children:"Avg. Session"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(251, 191, 36, 0.2)",color:"#fbbf24"},children:e(o,{name:"layer-group"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.avgCompletionDays,"d"]}),e("span",{className:a.statLabel,children:"Avg. Speed"})]})]})]}),e("div",{className:a.wideSection,children:[e("div",{className:a.sectionHeader,children:[e("h3",{children:"Activity"}),e("select",{className:a.yearSelect,value:d,onChange:t=>I(Number(t.currentTarget.value)),children:D.map(t=>e("option",{value:t,children:t},t))})]}),e("div",{className:a.heatmapLayout,children:[e("div",{className:a.dayLabels,children:[e("span",{style:{gridRow:2},children:"Mon"}),e("span",{style:{gridRow:4},children:"Wed"}),e("span",{style:{gridRow:6},children:"Fri"})]}),e("div",{className:a.heatmapWrapper,children:[e("div",{className:a.heatmapMonthLabels,children:(()=>{const t=[];let n=-1;return y.forEach((r,l)=>{if(r.month!==n&&!r.isOutOfRange){const h=Math.floor(l/7)+1;h>(t.length>0?t[t.length-1].props.style.gridColumn+2:-1)&&(t.push(e("span",{style:{gridColumn:h,whiteSpace:"nowrap"},children:new Date(0,r.month).toLocaleString("default",{month:"short"})},l)),n=r.month)}}),t})()}),e("div",{className:a.heatmapGrid,children:y.map((t,n)=>{let r=0;return t.count>0&&!t.isOutOfRange&&(r=1),t.count>2&&!t.isOutOfRange&&(r=2),t.count>5&&!t.isOutOfRange&&(r=3),t.count>10&&!t.isOutOfRange&&(r=4),e("div",{className:`${a.heatBox} ${a[`level${r}`]} ${t.isOutOfRange?a.hiddenBox:""}`,title:t.isOutOfRange?"":`${t.count} episodes on ${t.date}`},n)})}),e("div",{className:a.heatmapFooter,children:e("div",{className:a.heatLegend,children:[e("span",{children:"Less"}),e("div",{className:`${a.heatBox} ${a.level0}`}),e("div",{className:`${a.heatBox} ${a.level1}`}),e("div",{className:`${a.heatBox} ${a.level2}`}),e("div",{className:`${a.heatBox} ${a.level3}`}),e("div",{className:`${a.heatBox} ${a.level4}`}),e("span",{children:"More"})]})})]})]})]}),e("div",{className:a.chartsContainer,children:[e("div",{className:a.chartWrapper,children:[e("h3",{children:"Time Distribution (24h)"}),e("div",{className:a.hourlyChart,children:s.hourlyDist?.map((t,n)=>e("div",{className:a.hourlyBarContainer,children:[e("div",{className:a.hourlyBar,style:{height:`${t.count/T*100||2}%`,width:"100%",maxWidth:"12px"},title:`${t.count} watches at ${t.hour}:00`}),e("span",{className:a.hourlyLabel,style:{visibility:n%4===0?"visible":"hidden"},children:t.hour})]},n))}),e("div",{className:a.chartSubtext,children:(()=>{if(!s.hourlyDist?.length)return"Watch more to see your peak hours!";const t=parseInt([...s.hourlyDist].sort((n,r)=>r.count-n.count)[0].hour);return t>=6&&t<19?"You prefer daytime watching ☀️":"You're a confirmed Night Owl 🦉"})()})]}),e("div",{className:a.chartWrapper,children:[e("h3",{children:"Popularity Bias"}),e("div",{className:a.popScale,children:[e("div",{className:a.popTrack,children:e("div",{className:a.popThumb,style:{left:`${Math.max(0,Math.min(100,100-s.popularityScore*10))}%`}})}),e("div",{className:a.popLabels,children:[e("span",{children:"Mainstream"}),e("span",{children:"Underground"})]})]}),e("p",{className:a.popDescription,children:["Your taste score is ",e("strong",{children:[s.popularityScore,"/10"]}),".",s.popularityScore>=5?" You follow the big hits!":" You're a connoisseur of the obscure!"]})]}),e("div",{className:a.chartWrapper,children:[e("h3",{children:"Monthly Activity"}),e("div",{className:a.seasonalChart,children:s.seasonality?.map((t,n)=>e("div",{className:a.seasonalBarContainer,children:[e("div",{className:a.seasonalBar,style:{height:`${t.seconds/x*100||5}%`,width:"100%",maxWidth:"24px"},title:`${Math.round(t.seconds/3600)}h in ${new Date(0,n).toLocaleString("default",{month:"long"})}`}),e("span",{className:a.seasonalLabel,children:new Date(0,n).toLocaleString("default",{month:"short"})})]},n))})]})]}),e("div",{className:a.genreGrid,children:p?Array.from({length:4}).map((t,n)=>e("div",{className:a.genreCardSkeleton,children:[e("div",{className:a.skeletonRank}),e("div",{className:a.skeletonTitle}),e("div",{className:a.skeletonStats}),e("div",{className:a.skeletonPosters})]},n)):w?.map(t=>e("div",{className:a.genreCard,children:[e("div",{className:a.rankBadge,children:e("span",{className:a.rankNumber,children:t.rank})}),e("h3",{className:a.genreTitle,children:t.name}),e("div",{className:a.genreStats,children:[e("span",{children:[t.count," episodes"]}),e("span",{children:[t.meanScore.toFixed(1)," avg score"]}),e("span",{children:t.timeWatched})]}),e("div",{className:a.posterRow,children:t.topShows.map(n=>e("img",{src:C(n.thumbnail),alt:v(n),className:a.miniPoster,title:v(n),loading:"lazy",decoding:"async"},n.id))})]},t.name))}),N.length>0&&e("div",{className:a.recSection,children:[e("h3",{children:"Recommended For You"}),e("p",{className:a.recSubtext,children:"From your library taste — updates automatically as you watch"}),e("div",{className:a.recGrid,children:N.map(t=>e("div",{className:a.recCard,children:[t.cover&&e("img",{src:C(t.cover),alt:t.title,className:a.recCover,loading:"lazy",decoding:"async"}),e("div",{className:a.recInfo,children:[e(W,{to:`/anime/${t.id}`,className:a.recTitle,children:t.title}),e("span",{className:a.recMatch,children:[t.matchPct,"% match"]}),e("span",{className:a.recMeta,children:[t.year,t.format,t.episodes?`${t.episodes}ep`:null].filter(Boolean).join(" · ")}),e("span",{className:a.recWhy,children:["Shares ",t.sharedGenres.join(", "),t.exemplars.length>0&&` — like ${t.exemplars.join(", ")}`]}),t.kind==="continue"&&e("span",{className:a.recNote,children:"Continue — you watched the earlier part."}),t.kind==="sequel"&&t.prequelTitle&&e("span",{className:a.recNote,children:['Sequel — start with "',t.prequelTitle,'" first.']}),e("button",{className:a.recAddBtn,onClick:()=>g.mutate(t),disabled:g.isPending,children:"+ Plan to Watch"})]})]},t.id))})]}),s.droppedShows?.length>0&&e("div",{className:a.warningSection,children:[e("div",{className:a.warningHeader,children:[e(o,{name:"exclamation-triangle"}),e("h3",{children:"Dusty Watchlist (Inactive 90+ days)"})]}),e("div",{className:a.droppedGrid,children:s.droppedShows.map(t=>e("div",{className:a.droppedCard,children:[e("span",{className:a.droppedName,children:t.name}),e("span",{className:a.droppedDate,children:["Last watched: ",new Date(t.lastActivity).toLocaleDateString()]})]},t.id))})]})]}):null};export{Ca as default};
@@ -1 +0,0 @@
1
- import{u as e,n as E,o as z,a as D,d as Q,h as L,T as b,S as _}from"./vendor-core-8R2fFbyN.js";import{I as N}from"./vendor-icons-CDMuNDeL.js";import{d as j,B as I}from"./index-C65i4HWv.js";import{E as V}from"./ErrorMessage-z4f4WGgP.js";import{b as F,m as G,g as H,h as O,j as W,r as Y,k as q,l as J,n as K,M as X}from"./useMangaLibrary-U7tUo8nx.js";import{s as t}from"./Manga.module-BcfOdsNP.js";import"./vendor-query-BXRECSI6.js";const Z=({detail:r,altTitle:l,onBack:C,onOpenChapter:u,bookmarkButton:c,statusSelect:v,primaryAction:d,progressByChapter:y})=>{const i=[r.type,r.status,r.year?String(r.year):"",r.author?`by ${r.author}`:""].filter(Boolean);return e("div",{children:[e("button",{className:t.backBtn,onClick:C,children:[e(N,{name:"chevron-left",size:12})," Back to browse"]}),e("div",{className:t.detail,children:[r.cover&&e("img",{className:t.detailCover,src:F(r.provider,r.cover),alt:r.title,decoding:"async",draggable:!1}),e("div",{className:t.detailInfo,children:[e("h2",{className:t.detailTitle,children:r.title}),l&&e("p",{className:t.detailAltTitle,children:["Also known as: ",l]}),i.length>0&&e("div",{className:t.detailMeta,children:i.map(n=>e("span",{className:t.badge,children:n},n))}),r.genres&&r.genres.length>0&&e("div",{className:t.detailMeta,children:r.genres.map(n=>e("span",{className:t.badge,children:n},n))}),r.description&&e("p",{className:t.detailDesc,children:r.description}),(c||v||d)&&e("div",{className:t.detailActions,children:[d,c,v]})]})]}),e("h3",{className:t.detailTitle,style:{fontSize:"1rem"},children:["Chapters (",r.chapters.length,")"]}),r.chapters.length===0?e("p",{className:t.statusMsg,children:"No chapters found for this title on this provider."}):e("div",{className:t.chapterList,children:r.chapters.map(n=>{const a=y?.get(n.id),p=!!a&&a.pageCount>0&&a.page>=a.pageCount;return e("button",{className:`${t.chapterRow} ${p?t.chapterDone:""}`,onClick:()=>u(n),title:n.externalUrl?"External only — opens on source site":void 0,children:[e("span",{children:["Ch. ",n.number,n.title?` — ${n.title}`:""]}),e("span",{className:t.chapterMeta,children:[a&&a.pageCount>0?p?"read":`p. ${a.page}/${a.pageCount}`:"",n.group,n.externalUrl?"external":""].filter(Boolean).join(" · ")})]},n.id)})})]})};function le(){const{provider:r="",id:l=""}=E(),[C]=z(),u=D(),[c,v]=Q(()=>localStorage.getItem("agreedToViewMature")==="true");L(()=>{const s=()=>v(localStorage.getItem("agreedToViewMature")==="true");return window.addEventListener("focus",s),window.addEventListener("storage",s),()=>{window.removeEventListener("focus",s),window.removeEventListener("storage",s)}},[]);const d=C.get("rating")||"safe",y=c?d:"safe",i=r&&l?G(r,l):void 0,n=H(r||null,l||null,y,c),a=n.data,p=O(i),f=W(i),{titlePreference:w}=j(),m=b(()=>a&&{...a,title:Y(a,w)},[a,w]),M=q(),B=J(),P=K();L(()=>{m&&(document.title=`${m.title} - dango`)},[m]);const S=b(()=>{const s=new Map;for(const o of f.data?.progress??[])s.set(o.chapterId,o);return s},[f.data]),k=p.data?.inLibrary??!1,R=p.data?.status??"Reading",$=b(()=>(a?.chapters??[]).filter(s=>!s.externalUrl),[a]),g=b(()=>{if(!a)return null;const s=[...f.data?.progress??[]].sort((o,h)=>h.updatedAt-o.updatedAt);for(const o of s){const h=a.chapters.find(U=>U.id===o.chapterId);if(h&&!h.externalUrl)return h}return $[0]??null},[a,f.data,$]),x=(g?S.get(g.id):void 0)?`Continue Ch. ${g?.number}`:"Start Reading",T=s=>{if(s.externalUrl){window.open(s.externalUrl,"_blank","noopener,noreferrer");return}const o=new URLSearchParams;o.set("chapter",s.id),c&&d!=="safe"&&o.set("rating",d),u(`/manga/${r}/${encodeURIComponent(l)}/read?${o.toString()}`)},A=()=>{!a||!i||(k?B.mutate(i):M.mutate({provider:r,mangaId:l,title:a.title,cover:a.cover,author:a.author,altTitle:a.altTitle,contentRating:a.contentRating}))};return e("div",{className:t.page,children:n.isLoading?e("div",{"aria-hidden":!0,children:[e("div",{className:`${t.skeletonThumb} ${t.shimmer}`,style:{maxWidth:"12rem"}}),e("div",{className:`${t.skeletonLine} ${t.shimmer}`,style:{width:"40%"}}),e("div",{className:`${t.skeletonLine} ${t.shimmer}`,style:{width:"70%"}})]}):n.isError||!a||!m?e(_,{children:[e("button",{className:t.backBtn,onClick:()=>u("/manga"),children:[e(N,{name:"chevron-left",size:12})," Back to browse"]}),e(V,{message:"Failed to load this title. Please try again."})]}):e(Z,{detail:m,altTitle:a.altTitle!==m.title?a.altTitle:void 0,onBack:()=>u("/manga"),onOpenChapter:T,progressByChapter:S,primaryAction:g?e(I,{onClick:()=>g&&T(g),children:[e(N,{name:"book",size:13})," ",x]}):void 0,bookmarkButton:e(I,{variant:k?"secondary":"primary",onClick:A,disabled:M.isPending||B.isPending,children:[e(N,{name:"bookmark",size:13})," ",k?"Bookmarked":"Bookmark"]}),statusSelect:k?e("select",{className:t.detailSelect,value:R,onChange:s=>i&&P.mutate({id:i,status:s.target.value}),"aria-label":"Reading status",children:X.map(s=>e("option",{value:s,children:s},s))}):void 0})})}export{le as default};
@@ -1 +0,0 @@
1
- import{d as A,A as v,q,h as g,u as e,S as J,n as ie,o as ce,a as le,T as de}from"./vendor-core-8R2fFbyN.js";import{E as ge}from"./ErrorMessage-z4f4WGgP.js";import{I as C}from"./vendor-icons-CDMuNDeL.js";import{u as ue}from"./vendor-query-BXRECSI6.js";import{o as he,p as me,m as pe,g as fe,r as ve,j as be,q as Ne}from"./useMangaLibrary-U7tUo8nx.js";import{s as n}from"./Manga.module-BcfOdsNP.js";import{d as ye}from"./index-C65i4HWv.js";const Y="mangaReadMode",G="mangaFitMode";function we(){try{return localStorage.getItem(Y)==="page"?"page":"strip"}catch{return"strip"}}function Se(){try{return localStorage.getItem(G)==="height"?"height":"width"}catch{return"width"}}const Ie=({provider:p,mangaTitle:b,chapter:u,chapters:R,onBack:$,onOpenChapter:f,initialPage:l=0,onProgress:z,savedPage:w=0})=>{const[i,s]=A(we),[B,F]=A(Se),[d,x]=A(0),[h,S]=A(new Set),[D,N]=A(0),y=v(null),c=v([]),O=ue(),{data:T,isLoading:K,isError:U}=he(p,u.id),t=T?.pages??[],o=t.map(a=>me(p,a)),M=v(0),ee=v(0),L=v(!1);ee.current=t.length;const _=R.findIndex(a=>a.id===u.id),I=_>0?[...R.slice(0,_)].reverse().find(a=>!a.externalUrl)??null:null,k=_>=0?R.slice(_+1).find(a=>!a.externalUrl)??null:null,P=q(a=>{L.current=!0,x(r=>{const m=r+a;return m<0?(I&&f(I),0):m>=t.length?(k&&f(k),r):m})},[t.length,I,k,f]);g(()=>{L.current=!1,M.current=Math.max(0,l),x(Math.max(0,l)),S(new Set),c.current=[],l<=0&&window.scrollTo(0,0)},[u.id,l]);const Q=v(z);Q.current=z;const W=v("");g(()=>{M.current=d,t.length!==0&&Q.current?.(Math.min(d+1,t.length),t.length)},[d,t.length]),g(()=>{l<=0||L.current||M.current===0&&(L.current=!1,M.current=l,x(l))},[l]),g(()=>{if(i!=="strip"||t.length===0||l<=0)return;const a=`${u.id}:${l}`;if(W.current===a)return;W.current=a;const r=window.setTimeout(()=>{c.current[Math.min(l,t.length-1)]?.scrollIntoView({block:"start"})},150);return()=>window.clearTimeout(r)},[i,t.length,l,u.id]),g(()=>{document.title=`${b} Ch. ${u.number} - dango`},[b,u.number]),g(()=>{try{localStorage.setItem(Y,i)}catch{}},[i]),g(()=>{try{localStorage.setItem(G,B)}catch{}},[B]),g(()=>{i==="page"&&window.scrollTo(0,0)},[i]),g(()=>{if(i!=="page"||t.length===0)return;const a=r=>{r.key==="ArrowRight"||r.key===" "?(r.preventDefault(),P(1)):r.key==="ArrowLeft"&&(r.preventDefault(),P(-1))};return window.addEventListener("keydown",a),()=>window.removeEventListener("keydown",a)},[i,t.length,P]),g(()=>{if(i!=="strip"||t.length===0)return;const a=new IntersectionObserver(r=>{for(const m of r)if(m.isIntersecting){const j=Number(m.target.dataset.pageIndex);Number.isNaN(j)||(j!==M.current&&(L.current=!0),x(j))}},{rootMargin:"-45% 0px -45% 0px",threshold:0});return c.current.forEach(r=>r&&a.observe(r)),()=>a.disconnect()},[i,t.length,T]);const te=a=>{y.current=a.touches[0].clientX},ne=a=>{if(y.current===null)return;const r=a.changedTouches[0].clientX-y.current;y.current=null,!(Math.abs(r)<50)&&P(r<0?1:-1)},V=a=>{a&&f(a)},Z=a=>S(r=>{if(r.has(a))return r;const m=new Set(r);return m.add(a),m}),ae=a=>{S(r=>{const m=new Set(r);return m.delete(a),m}),N(r=>r+1)},re=()=>{S(new Set),N(a=>a+1),O.invalidateQueries({queryKey:["mangaPages",p,u.id]})},se=t.length>0&&h.size>=t.length,H=a=>D>0?`${a}&n=${D}`:a,X=a=>e("div",{className:n.deadPage,role:"alert",children:[e("span",{children:["Page ",a+1," failed to load"]}),e("button",{className:n.pageBtn,onClick:()=>ae(a),children:[e(C,{name:"sync-alt",size:12})," Retry"]})]}),E=B==="height",oe=w>0?w-1:-1;return e("div",{className:n.readerShell,children:[e("div",{className:n.readerBar,children:[e("button",{className:n.readerIconBtn,onClick:$,"aria-label":"Back to chapters",children:e(C,{name:"chevron-left",size:14})}),e("span",{className:n.readerTitle,children:[b," — Ch. ",u.number,u.title?` ${u.title}`:""]}),t.length>0&&e("span",{className:n.readerCounter,children:[Math.min(d+1,t.length)," / ",t.length]}),e("div",{className:n.readerSegment,role:"group","aria-label":"Reading mode",children:[e("button",{type:"button","aria-pressed":i==="strip",className:i==="strip"?n.readerSegmentActive:"",onClick:()=>s("strip"),children:"Strip"}),e("button",{type:"button","aria-pressed":i==="page",className:i==="page"?n.readerSegmentActive:"",onClick:()=>s("page"),children:"Page"})]}),e("div",{className:n.readerSegment,role:"group","aria-label":"Fit mode",children:[e("button",{type:"button","aria-pressed":!E,className:E?"":n.readerSegmentActive,onClick:()=>F("width"),title:"Fit width — best for portrait / phones",children:"Wide"}),e("button",{type:"button","aria-pressed":E,className:E?n.readerSegmentActive:"",onClick:()=>F("height"),title:"Fit height — best for landscape / desktop",children:"Tall"})]}),e("button",{className:n.readerIconBtn,disabled:!I,onClick:()=>V(I),"aria-label":"Previous chapter",children:e(C,{name:"chevron-left",size:14})}),e("button",{className:n.readerIconBtn,disabled:!k,onClick:()=>V(k),"aria-label":"Next chapter",children:e(C,{name:"chevron-right",size:14})})]}),U?e("p",{className:n.statusMsg,children:"Failed to load pages. Please try again."}):K?e("div",{className:n.readerPages,"aria-hidden":!0,children:Array.from({length:3}).map((a,r)=>e("div",{className:`${n.skeletonThumb} ${n.shimmer}`,style:{width:"100%",maxWidth:"56rem",aspectRatio:"3 / 4"}},r))}):t.length===0?e("p",{className:n.statusMsg,children:"No pages found for this chapter."}):se?e("div",{className:n.failedBox,children:[e("p",{className:n.statusMsg,children:"Page images failed to load (expired or blocked links)."}),e("button",{className:n.pageBtn,onClick:re,children:[e(C,{name:"sync-alt",size:12})," Reload pages"]})]}):i==="strip"?e("div",{className:`${n.readerPages} ${E?n.readerPagesTall:""}`,children:o.map((a,r)=>e("div",{ref:m=>{c.current[r]=m},"data-page-index":r,className:n.readerPageWrap,children:[r===oe&&e("div",{className:n.saveMarker,role:"note","aria-label":"Saved reading position",children:[e(C,{name:"bookmark",size:12}),e("span",{children:["Saved · p. ",w]})]}),h.has(r)?X(r):e("img",{className:n.readerPage,src:H(a),alt:`Page ${r+1}`,loading:r<3?"eager":"lazy",decoding:"async",draggable:!1,onError:()=>Z(r)})]},`${r}-${t[r].slice(-32)}`))}):e(J,{children:[e("div",{className:`${n.pageView} ${E?n.pageViewTall:""}`,onTouchStart:te,onTouchEnd:ne,children:[e("button",{className:`${n.tapZone} ${n.tapZoneLeft}`,onClick:()=>P(-1),"aria-label":"Previous page",children:e("span",{className:n.tapChevron,children:e(C,{name:"chevron-left",size:16})})}),h.has(d)?X(d):e("img",{className:n.pageImage,src:H(o[d]),alt:`Page ${d+1} of ${t.length}`,decoding:"async",draggable:!1,onError:()=>Z(d)},`${d}-${t[d].slice(-32)}`),e("button",{className:`${n.tapZone} ${n.tapZoneRight}`,onClick:()=>P(1),"aria-label":"Next page",children:e("span",{className:n.tapChevron,children:e(C,{name:"chevron-right",size:16})})})]}),e("nav",{className:n.pagination,"aria-label":"Page navigation",children:[e("button",{className:n.pageBtn,disabled:d<=0,onClick:()=>P(-1),children:"Previous page"}),e("span",{className:n.pageIndicator,children:[d+1," / ",t.length]}),e("button",{className:n.pageBtn,disabled:d>=t.length-1,onClick:()=>P(1),children:"Next page"})]})]}),(I||k)&&e("nav",{className:n.pagination,children:[e("button",{className:n.pageBtn,disabled:!I,onClick:()=>V(I),children:"Previous chapter"}),e("button",{className:n.pageBtn,disabled:!k,onClick:()=>V(k),children:"Next chapter"})]})]})},ke=800,Pe=5e3;function Be(){const{provider:p="",id:b=""}=ie(),[u]=ce(),R=le(),$=u.get("chapter")||"",f=u.get("rating")||"safe",l=typeof localStorage<"u"&&localStorage.getItem("agreedToViewMature")==="true",z=l?f:"safe",w=p&&b?pe(p,b):"",i=fe(p||null,b||null,z,l),s=i.data,{titlePreference:B}=ye(),F=s?ve(s,B):"Manga",d=be(w||void 0),x=Ne(),h=s&&$?s.chapters.find(t=>t.id===$)??null:null,S=de(()=>{const t=(d.data?.progress??[]).find(o=>o.chapterId===$);return t&&t.page>0?t.page:0},[d.data,$]),D=S>1?S-1:0,N=v(null),y=v(null),c=q(()=>{N.current&&(clearTimeout(N.current),N.current=null);const t=y.current;y.current=null,t&&w&&x.mutate({mangaId:w,chapterId:t.chapterId,chapterNumber:t.chapterNumber,page:t.page,pageCount:t.pageCount,title:s?.title??null,cover:s?.cover??null,provider:p||null,altTitle:s?.altTitle??null,contentRating:s?.contentRating??null})},[w,x,s?.title,s?.cover,s?.altTitle,s?.contentRating,p]),O=q((t,o)=>{h&&(y.current={chapterId:h.id,chapterNumber:h.number,page:t,pageCount:o},N.current&&clearTimeout(N.current),N.current=setTimeout(c,ke))},[h,c]);g(()=>{c()},[$,c]),g(()=>()=>c(),[c]),g(()=>{const t=window.setInterval(()=>{y.current&&c()},Pe);return()=>window.clearInterval(t)},[c]),g(()=>{const t=()=>{document.visibilityState==="hidden"&&c()},o=()=>c();return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",o),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",o)}},[c]);const T=v("");T.current||(T.current=`manga-${Date.now()}-${Math.random().toString(36).slice(2)}`),g(()=>{if(!s||!h)return;const t=s.contentRating==="erotica"||s.contentRating==="pornographic"||s.type==="doujinshi",o=()=>{fetch("/api/discord/manga",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:s.title,chapterLabel:`Chapter ${h.number}`,isPlaying:!0,thumbnail:s.cover||"",thumbnails:[s.cover].filter(Boolean),isAdult:t,sessionId:T.current})}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:T.current})}).catch(()=>{})};o();const M=window.setInterval(o,6e4);return()=>window.clearInterval(M)},[s,h]);const K=t=>{if(c(),t.externalUrl){window.open(t.externalUrl,"_blank","noopener,noreferrer");return}const o=new URLSearchParams;o.set("chapter",t.id),l&&f!=="safe"&&o.set("rating",f),R(`/manga/${p}/${encodeURIComponent(b)}/read?${o.toString()}`)},U=()=>{c();const t=new URLSearchParams;l&&f!=="safe"&&t.set("rating",f);const o=t.toString()?`?${t.toString()}`:"";R(`/manga/${p}/${encodeURIComponent(b)}${o}`)};return e("div",{className:n.page,children:i.isLoading?e("div",{className:n.readerPages,"aria-hidden":!0,children:Array.from({length:3}).map((t,o)=>e("div",{className:`${n.skeletonThumb} ${n.shimmer}`,style:{width:"100%",maxWidth:"56rem",aspectRatio:"3 / 4"}},o))}):!s||!h?e(J,{children:[e("button",{className:n.backBtn,onClick:U,children:"Back to title"}),e(ge,{message:"Failed to load this chapter. Please try again."})]}):e(Ie,{provider:p,mangaTitle:F,chapter:h,chapters:s.chapters,onBack:U,onOpenChapter:K,initialPage:D,onProgress:O,savedPage:S},h.id)})}export{Be as default};
@@ -1,2 +0,0 @@
1
- import{d as a,h as me,g as ce,A as le,q as je,u as e,S as I}from"./vendor-core-8R2fFbyN.js";import{u as ue,b as de,a as R}from"./vendor-query-BXRECSI6.js";import{z as s,W as Se,B as pe}from"./index-C65i4HWv.js";import{I as h}from"./vendor-icons-CDMuNDeL.js";function Ce(){const B=ue(),[r,m]=a(!1);return me(()=>{const g=new URLSearchParams(window.location.search),u=g.get("anilist");if(u){const i=g.get("user"),b=g.get("reason");window.history.replaceState(null,"",window.location.pathname),u==="success"?(s.success(i?`Connected to AniList as ${decodeURIComponent(i)}`:"Connected to AniList"),B.invalidateQueries({queryKey:["trackerStatus"]})):s.error(b?decodeURIComponent(b):"AniList authentication failed");return}const w=new URLSearchParams(window.location.hash.substring(1)),c=w.get("access_token"),D=g.get("code");if(!c&&!D){const i=w.get("error");i&&(s.error(`AniList error: ${i}`),window.history.replaceState(null,"",window.location.pathname));return}const j=window.location.origin+window.location.pathname;window.history.replaceState(null,"",window.location.pathname),m(!0),s("Connecting to AniList..."),fetch("/api/tracker/anilist/auth",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c?{token:c}:{code:D,redirectUri:j})}).then(async i=>{const b=await i.json().catch(()=>({}));if(!i.ok)throw new Error(b.error||"Authentication failed");s.success(`Connected to AniList as ${b.user?.name??"AniList user"}`),B.invalidateQueries({queryKey:["trackerStatus"]})}).catch(i=>{s.error(i instanceof Error?i.message:"Failed to authenticate with AniList")}).finally(()=>{m(!1)})},[B]),r}const Te="_header_j1lbh_1",xe="_fadeIn_j1lbh_1",Le="_pageTitle_j1lbh_6",Ae="_pageSubtitle_j1lbh_14",Pe="_importCard_j1lbh_20",Ee="_slideUpIn_j1lbh_1",Be="_cardHeader_j1lbh_37",De="_uploadArea_j1lbh_55",Me="_fileInputWrapper_j1lbh_59",Oe="_fileInput_j1lbh_59",Fe="_fileDisplay_j1lbh_87",$e="_fileIcon_j1lbh_95",Ue="_fileName_j1lbh_100",Re="_browseButton_j1lbh_109",qe="_optionsArea_j1lbh_120",Ke="_checkboxLabel_j1lbh_128",He="_checkbox_j1lbh_128",Qe="_checkboxCustom_j1lbh_141",Ge="_optionText_j1lbh_169",We="_optionTitle_j1lbh_175",ze="_optionDesc_j1lbh_181",Je="_actions_j1lbh_187",Xe="_importBtn_j1lbh_195",Ve="_cancelBtn_j1lbh_201",Ye="_statusMessage_j1lbh_215",Ze="_success_j1lbh_225",en="_error_j1lbh_231",nn="_progressSection_j1lbh_237",tn="_progressBar_j1lbh_242",sn="_progressFill_j1lbh_250",an="_progressInfo_j1lbh_257",on="_progressCount_j1lbh_264",rn="_progressTitle_j1lbh_271",cn="_sourceBadge_j1lbh_289",ln="_anilist_j1lbh_301",dn="_kitsu_j1lbh_307",pn="_offline_j1lbh_313",hn="_skipped_j1lbh_319",mn="_cardTitleRow_j1lbh_324",un="_anilistIcon_j1lbh_334",_n="_malIcon_j1lbh_340",gn="_badgeOnline_j1lbh_346",bn="_badgeOffline_j1lbh_358",fn="_connectedProfile_j1lbh_370",yn="_avatar_j1lbh_379",wn="_connectedInfo_j1lbh_386",Nn="_connectedName_j1lbh_394",kn="_connectedHint_j1lbh_400",vn="_loginPrompt_j1lbh_405",In="_loginSection_j1lbh_413",jn="_fieldLabel_j1lbh_419",Sn="_helpText_j1lbh_438",Cn="_loginText_j1lbh_455",Tn="_primaryBtn_j1lbh_464",xn="_secondaryBtn_j1lbh_465",Ln="_syncBtn_j1lbh_466",An="_divider_j1lbh_518",Pn="_actionSection_j1lbh_524",En="_inputGroup_j1lbh_538",Bn="_input_j1lbh_538",Dn="_syncSummary_j1lbh_559",Mn="_syncErrors_j1lbh_565",On="_spin_j1lbh_569",Fn="_radioGroup_j1lbh_598",$n="_radioLabel_j1lbh_604",Un="_syncHint_j1lbh_642",Rn="_importLoading_j1lbh_649",n={header:Te,fadeIn:xe,pageTitle:Le,pageSubtitle:Ae,importCard:Pe,slideUpIn:Ee,cardHeader:Be,uploadArea:De,fileInputWrapper:Me,fileInput:Oe,fileDisplay:Fe,fileIcon:$e,fileName:Ue,browseButton:Re,optionsArea:qe,checkboxLabel:Ke,checkbox:He,checkboxCustom:Qe,optionText:Ge,optionTitle:We,optionDesc:ze,actions:Je,importBtn:Xe,cancelBtn:Ve,statusMessage:Ye,success:Ze,error:en,progressSection:nn,progressBar:tn,progressFill:sn,progressInfo:an,progressCount:on,progressTitle:rn,sourceBadge:cn,anilist:ln,kitsu:dn,offline:pn,skipped:hn,cardTitleRow:mn,anilistIcon:un,malIcon:_n,badgeOnline:gn,badgeOffline:bn,connectedProfile:fn,avatar:yn,connectedInfo:wn,connectedName:Nn,connectedHint:kn,loginPrompt:vn,loginSection:In,fieldLabel:jn,helpText:Sn,loginText:Cn,primaryBtn:Tn,secondaryBtn:xn,syncBtn:Ln,divider:An,actionSection:Pn,inputGroup:En,input:Bn,syncSummary:Dn,syncErrors:Mn,spin:On,radioGroup:Fn,radioLabel:$n,syncHint:Un,importLoading:Rn},W="tracker_anilist_client_id",he="49887".trim(),Gn=()=>{const{setIsOpen:B}=Se(),r=ue();ce.useEffect(()=>{document.title="Trackers - dango"},[]);const[m,g]=a(""),[u,w]=a("username"),[c,D]=a(null),[j,i]=a(""),b=Ce(),{data:z,isLoading:_e}=de({queryKey:["trackerStatus"],queryFn:async()=>{const t=await fetch("/api/tracker/status");if(!t.ok)throw new Error("Failed to load tracker status");return t.json()}}),{data:N}=de({queryKey:["anilistClientId"],queryFn:async()=>(await(await fetch(`/api/settings?key=${W}`)).json()).value||""}),J=le(!1);ce.useEffect(()=>{J.current||N!==void 0&&(N&&i(N),J.current=!0)},[N]);const S=z?.anilist?.connected??!1;me(()=>{S&&w("sync")},[S]);const M=z?.anilist?.user??null,q=!!he,ge=(N||"").trim(),be=async()=>{const t=j.trim(),o=t||he;if(!o){s.error("Enter your AniList client ID first");return}const _=async(y,Q)=>{if(!(await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:y,value:Q})})).ok)throw new Error(`Failed to save ${y}`)};try{t?t!==N&&await _(W,t):q&&ge&&await _(W,""),r.invalidateQueries({queryKey:["anilistClientId"]})}catch(y){s.error(y.message);return}const p=window.location.origin+window.location.pathname,A=encodeURIComponent(p);window.location.href=`https://anilist.co/api/v2/oauth/authorize?client_id=${encodeURIComponent(o)}&response_type=token&state=${A}`},X=R({mutationFn:async()=>{const t=await fetch("/api/tracker/anilist/disconnect",{method:"POST"});if(!t.ok)throw new Error("Failed to disconnect");return t.json()},onSuccess:()=>{r.invalidateQueries({queryKey:["trackerStatus"]}),s.success("Disconnected from AniList")},onError:t=>s.error(t.message)}),O=R({mutationFn:async()=>{const t=await fetch("/api/tracker/sync",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:"anilist"})}),o=await t.json();if(!t.ok)throw new Error(o.error||"Sync failed");return o.summary},onSuccess:t=>{r.invalidateQueries({queryKey:["watchlist"]}),r.invalidateQueries({queryKey:["allContinueWatching"]}),D(t),s.success(`Sync complete — pushed ${t.pushed}, pulled ${t.pulled}, merged ${t.merged}, unchanged ${t.unchanged}`,{duration:6e3})},onError:t=>s.error(t.message)}),k=R({mutationFn:async()=>{if(!m.trim())throw new Error("Please enter a username");const t=await fetch("/api/tracker/anilist/import",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:m.trim(),erase:f})}),o=await t.json();if(!t.ok)throw new Error(o.error||"Import failed");return o.count},onSuccess:t=>{r.invalidateQueries({queryKey:["watchlist"]}),r.invalidateQueries({queryKey:["allContinueWatching"]}),s.success(`Imported ${t} anime entries from AniList`),g("")},onError:t=>s.error(t.message)}),[C,V]=a(""),d=R({mutationFn:async t=>{if(!C.trim())throw new Error("Please enter a MAL username");const o=await fetch("/api/tracker/mal/import",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:C.trim(),erase:t.erase,useOfflineDb:t.useOfflineDb,skipFallback:t.skipFallback})}),_=await o.json();if(!o.ok)throw new Error(_.error||"Import failed");return _.count},onSuccess:t=>{r.invalidateQueries({queryKey:["watchlist"]}),r.invalidateQueries({queryKey:["allContinueWatching"]}),s.success(`Imported ${t} anime entries from MAL`),V("")},onError:t=>s.error(t.message)}),[f,Y]=a(!1),[T,fe]=a(!0),[x,ye]=a(!1),[Z,ee]=a(""),[v,ne]=a(!1),[l,te]=a(null),[K,se]=a(null),[ae,F]=a(""),H=le(null),we=t=>{t.target.files&&t.target.files.length>0?ee(t.target.files[0].name):ee("")},Ne=je(async()=>{const t=document.getElementById("malFile");if(!t.files||t.files.length===0){F("Please select a file first.");return}const o=t.files[0];ne(!0),te(null),se(null),F("");const _=new AbortController;H.current=_;try{const p=new FormData;p.append("xmlfile",o),p.append("erase",String(f)),p.append("useOfflineDb",String(T)),p.append("skipFallback",String(x));const A=await fetch("/api/import/mal-xml",{method:"POST",body:p,signal:_.signal});if(!A.ok){const G=await A.text();let P="Failed to import";try{P=JSON.parse(G).error||P}catch{}throw new Error(P)}const y=A.body?.getReader();if(!y)throw new Error("No response stream");const Q=new TextDecoder;let $="",U="";for(;;){const{done:G,value:P}=await y.read();if(G)break;$+=Q.decode(P,{stream:!0});const oe=$.split(`
2
- `);$=oe.pop()||"";for(const E of oe)if(E.startsWith("event: "))U=E.slice(7).trim();else if(E.startsWith("data: ")){const Ie=E.slice(6);try{const re=JSON.parse(Ie);U==="progress"?te(re):U==="complete"&&se(re)}catch{}}else E===""&&(U="")}r.invalidateQueries({queryKey:["watchlist"]})}catch(p){p instanceof DOMException&&p.name==="AbortError"?F("Import cancelled."):F(p.message)}finally{ne(!1),H.current=null}},[f,T,x,r]),ke=()=>{H.current?.abort(),fetch("/api/import/mal-xml/cancel",{method:"POST"}).catch(()=>{})},ve=l?Math.round(l.current/l.total*100):0,[L,ie]=a("username");return e("div",{className:"page-container",children:[e("div",{className:n.header,children:[e("h1",{className:n.pageTitle,children:"Trackers"}),e("p",{className:n.pageSubtitle,children:"Import & sync your anime watchlists with tracking services"})]}),e("div",{className:n.importCard,children:[e("div",{className:n.cardHeader,children:e("div",{className:n.cardTitleRow,children:[e(h,{name:"anilist",className:n.anilistIcon}),e("h3",{children:"AniList"}),_e?null:S?e("span",{className:n.badgeOnline,children:"Connected"}):e("span",{className:n.badgeOffline,children:"Not connected"})]})}),S&&M?e("div",{className:n.connectedProfile,children:[M.avatar&&e("img",{src:M.avatar,alt:"Avatar",className:n.avatar,loading:"lazy",decoding:"async"}),e("div",{className:n.connectedInfo,children:[e("span",{className:n.connectedName,children:M.name}),e("span",{className:n.connectedHint,children:"2-way sync is ready"})]}),e("button",{className:n.secondaryBtn,onClick:()=>X.mutate(),disabled:X.isPending,children:[e(h,{name:"sign-out-alt"})," Disconnect"]})]}):e("div",{className:n.loginSection,children:[e("p",{className:n.loginText,children:["Log in with AniList to enable bidirectional progress & status synchronization.",b&&e("span",{style:{marginLeft:8,color:"var(--accent)"},children:"Finishing login…"})]}),e("div",{className:n.fieldLabel,children:["AniList client ID — defaults to Dango's app"," ",e("a",{href:"https://anilist.co/settings/developer",target:"_blank",rel:"noopener noreferrer",children:["create your own app ",e(h,{name:"external-link-alt",size:9})]})]}),e("div",{className:n.inputGroup,children:[e("input",{type:"text",placeholder:"Client ID",value:j,onChange:t=>i(t.target.value),className:n.input}),e("button",{className:n.primaryBtn,onClick:be,disabled:!(j.trim()||q),children:[e(h,{name:"external-link-alt"})," Connect"]})]}),e("p",{className:n.helpText,children:q?e(I,{children:["By default uses Dango's client. To use your own AniList app: create it at"," ",e("a",{href:"https://anilist.co/settings/developer",target:"_blank",rel:"noopener noreferrer",children:"anilist.co/settings/developer"}),", set its ",e("strong",{children:"Redirect URL"})," to"," ",e("code",{children:window.location.port==="5173"?`${window.location.protocol}//${window.location.hostname}:3000/api/tracker/anilist/callback`:`${window.location.origin}/api/tracker/anilist/callback`}),", then paste only the ",e("strong",{children:"Client ID"})," above (leave empty to use Dango's)."]}):e(I,{children:["No shipped client in this build — create your own app at"," ",e("a",{href:"https://anilist.co/settings/developer",target:"_blank",rel:"noopener noreferrer",children:"anilist.co/settings/developer"}),", set ",e("strong",{children:"Redirect URL"})," to"," ",e("code",{children:window.location.port==="5173"?`${window.location.protocol}//${window.location.hostname}:3000/api/tracker/anilist/callback`:`${window.location.origin}/api/tracker/anilist/callback`}),", then paste the ",e("strong",{children:"Client ID"})," above."]})})]}),S&&e(I,{children:[e("hr",{className:n.divider}),e("div",{className:n.radioGroup,children:[e("label",{className:n.radioLabel,children:[e("input",{type:"radio",name:"anilistMode",value:"username",checked:u==="username",onChange:()=>w("username")}),e("span",{children:"Quick Import"})]}),e("label",{className:n.radioLabel,children:[e("input",{type:"radio",name:"anilistMode",value:"sync",checked:u==="sync",onChange:()=>w("sync")}),e("span",{children:"Two-way Sync"})]})]}),u==="username"?e("div",{className:n.inputGroup,children:e("input",{type:"text",placeholder:"Enter AniList username",value:m,onChange:t=>g(t.target.value),className:n.input,disabled:k.isPending,onKeyDown:t=>{t.key==="Enter"&&m.trim()&&k.mutate()}})}):e("p",{className:n.syncHint,children:"Merges progress non-destructively: pushes shows and episodes you watched on dango to AniList, and pulls anything you updated on AniList into dango."}),u==="username"&&e("div",{className:n.optionsArea,children:e("label",{className:n.checkboxLabel,children:[e("input",{type:"checkbox",checked:f,onChange:t=>Y(t.target.checked),className:n.checkbox,disabled:k.isPending}),e("span",{className:n.checkboxCustom}),e("div",{className:n.optionText,children:[e("span",{className:n.optionTitle,children:"Erase current watchlist"}),e("span",{className:n.optionDesc,children:"Delete existing watchlist before importing."})]})]})}),u==="username"?e("button",{className:n.syncBtn,onClick:()=>k.mutate(),disabled:!m.trim()||k.isPending,children:[e(h,{name:"download"}),k.isPending?"Importing...":"Start Import"]}):e(I,{children:[e("button",{className:n.syncBtn,onClick:()=>O.mutate(),disabled:O.isPending,children:[e(h,{name:"sync-alt",className:O.isPending?n.spin:""}),O.isPending?"Syncing...":"Start Sync"]}),c&&e("div",{className:n.syncSummary,children:["Pushed: ",c.pushed," · Pulled: ",c.pulled," · Merged:"," ",c.merged," · Unchanged: ",c.unchanged,c.errors.length>0&&e("span",{className:n.syncErrors,children:[" ","· ",c.errors.length," error",c.errors.length>1?"s":""]})]})]}),e("p",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)",marginTop:"8px"},children:["AniList ",e("code",{children:"Rewatching (REPEATING)"})," is not tracked — entries with this status are ignored."]})]})]}),e("div",{className:n.importCard,children:[e("div",{className:n.cardHeader,children:e("div",{className:n.cardTitleRow,children:[e(h,{name:"myanimelist",className:n.malIcon}),e("h3",{children:"MyAnimeList"})]})}),e("div",{className:n.radioGroup,children:[e("label",{className:n.radioLabel,children:[e("input",{type:"radio",name:"malMode",value:"username",checked:L==="username",onChange:()=>ie("username")}),e("span",{children:"Username"})]}),e("label",{className:n.radioLabel,children:[e("input",{type:"radio",name:"malMode",value:"xml",checked:L==="xml",onChange:()=>ie("xml")}),e("span",{children:"XML File"})]})]}),L==="username"?e(I,{children:[e("div",{className:n.inputGroup,children:e("input",{type:"text",placeholder:"Enter MAL username",value:C,onChange:t=>V(t.target.value),className:n.input,disabled:d.isPending,onKeyDown:t=>{t.key==="Enter"&&C.trim()&&d.mutate({erase:f,useOfflineDb:T,skipFallback:x})}})}),d.isPending&&e("div",{className:n.importLoading,children:[e(h,{name:"sync-alt",className:n.spin}),e("span",{children:"Importing from MAL..."})]})]}):e("div",{className:n.uploadArea,children:e("div",{className:n.fileInputWrapper,children:[e("input",{type:"file",id:"malFile",accept:".xml,application/xml",className:n.fileInput,onChange:we,disabled:v}),e("div",{className:n.fileDisplay,children:[e(h,{name:"file-alt",className:n.fileIcon}),e("span",{className:n.fileName,children:Z||"Choose XML file..."})]}),e("label",{htmlFor:"malFile",className:n.browseButton,children:"Browse"})]})}),e("div",{className:n.optionsArea,children:[e("label",{className:n.checkboxLabel,children:[e("input",{type:"checkbox",checked:f,onChange:t=>Y(t.target.checked),className:n.checkbox,disabled:v||d.isPending}),e("span",{className:n.checkboxCustom}),e("div",{className:n.optionText,children:[e("span",{className:n.optionTitle,children:"Erase current watchlist"}),e("span",{className:n.optionDesc,children:"Delete existing watchlist before importing."})]})]}),e("label",{className:n.checkboxLabel,children:[e("input",{type:"checkbox",checked:T,onChange:t=>fe(t.target.checked),className:n.checkbox,disabled:v||d.isPending}),e("span",{className:n.checkboxCustom}),e("div",{className:n.optionText,children:[e("span",{className:n.optionTitle,children:"Use offline database (fast)"}),e("span",{className:n.optionDesc,children:"Match by MAL ID locally first — instant and avoids AniList rate limits."})]})]}),e("label",{className:n.checkboxLabel,children:[e("input",{type:"checkbox",checked:x,onChange:t=>ye(t.target.checked),className:n.checkbox,disabled:v||d.isPending}),e("span",{className:n.checkboxCustom}),e("div",{className:n.optionText,children:[e("span",{className:n.optionTitle,children:"Skip online fallback"}),e("span",{className:n.optionDesc,children:"Only import offline matches. Unmatched titles are skipped without API calls."})]})]})]}),L==="username"?e("button",{className:n.syncBtn,onClick:()=>d.mutate({erase:f,useOfflineDb:T,skipFallback:x}),disabled:!C.trim()||d.isPending,children:[e(h,{name:"download"}),d.isPending?"Importing...":"Start Import"]}):e("div",{className:n.actions,children:v?e(pe,{onClick:ke,className:n.cancelBtn,children:"Cancel Import"}):e(pe,{onClick:Ne,className:n.importBtn,disabled:!Z,children:[e(h,{name:"upload"})," Start Import"]})}),L==="username"&&d.isError&&e("div",{className:`${n.statusMessage} ${n.error}`,children:d.error.message}),v&&l&&e("div",{className:n.progressSection,children:[e("div",{className:n.progressBar,children:e("div",{className:n.progressFill,style:{width:`${ve}%`}})}),e("div",{className:n.progressInfo,children:[e("span",{className:n.progressCount,children:[l.current," / ",l.total]}),e("span",{className:n.progressTitle,children:l.found?e(I,{children:[l.matchedTitle,e("span",{className:`${n.sourceBadge} ${n[l.source||"anilist"]}`,children:l.source==="kitsu"?"Kitsu":l.source==="offline"?"Offline DB":"AniList"})]}):e("span",{className:n.skipped,children:["Skipped: ",l.title]})})]})]}),K&&e("div",{className:`${n.statusMessage} ${n.success}`,children:["Import complete! Imported: ",K.imported,", Skipped: ",K.skipped,"."]}),ae&&e("div",{className:`${n.statusMessage} ${n.error}`,children:ae})]})]})};export{Gn as default};