Compare commits

...

701 Commits

Author SHA1 Message Date
Rishi Ghan
4b8693fe68 Made manual cv search form collapsible 2026-04-16 12:07:22 -04:00
e0a383042e Added caveman skills for personal use 2026-04-16 12:06:02 -04:00
eb9070966a Fixes to bleed-off in PullList 2026-04-16 11:42:44 -04:00
00adbb2c4a Removed react-redux from project 2026-04-15 20:15:19 -04:00
Rishi Ghan
3ea9b83ed9 Massive ts error cleanup 2026-04-15 13:30:28 -04:00
Rishi Ghan
0c363dd8ae Consolidating types across the project 2026-04-15 12:22:20 -04:00
Rishi Ghan
4514f578ae Refactored Import.tsx 2026-04-15 11:53:53 -04:00
Rishi Ghan
2dc38b6c95 JsDoc across all project files 2026-04-15 11:31:52 -04:00
Rishi Ghan
6deab0b87e Refactoring the RealTimeImportStats 2026-04-15 10:54:46 -04:00
Rishi Ghan
81f4654b50 Fixes to the import error checks 2026-04-14 17:27:49 -04:00
Rishi Ghan
4e53f23e79 Fixing up build errors 2026-04-14 12:51:29 -04:00
Rishi Ghan
91e99c50d9 Troubleshooting vite and fonts 2026-04-14 12:02:47 -04:00
733a453352 👹 Metadata Reconciler WIP 2026-04-13 22:18:51 -04:00
3d88920f39 Cleanup 2026-04-13 20:31:24 -04:00
0949ebc637 📃 WIP bottom sheet 2026-04-03 10:55:10 -04:00
3e045f4c10 📃 Added a bottom sheet to Metadata info 2026-04-02 23:44:03 -04:00
17db1e64e1 🔨 Added JsDoc, WIP metadata reconciliation 2026-04-02 18:29:40 -04:00
d7ab553120 🔨 Clearning up the VolumeInformation tab 2026-04-02 13:07:13 -04:00
91592019c4 📗 Updated README 2026-03-26 20:55:23 -04:00
0e8f63101c 🤺 ComicVine matching in drawer UX fixed 2026-03-26 20:50:27 -04:00
4e2cad790b 🕶Added visibility guards to volumes, stats and wanted on dashboard 2026-03-09 23:11:15 -04:00
ba1b5bb965 🔨 Fix for showing accurate import counts 2026-03-09 22:54:47 -04:00
8546641152 🖌 Icon fixes 2026-03-09 22:31:59 -04:00
867935be39 📛 Added missing files to lib stats on Dashboard 2026-03-09 21:54:21 -04:00
d506cf8ba8 🔢 Fix for filesize on disk on Dashboard 2026-03-09 21:33:21 -04:00
71d7034d01 🎫 Fixed download badges in T2Table 2026-03-09 20:39:58 -04:00
a217d447fa 🔨 Added missing file statuses on Dashboard and Library 2026-03-09 19:55:16 -04:00
20336e5569 🔍 Missing files statuses in the UI 2026-03-09 17:10:18 -04:00
8913e9cd99 🔨 ComicDetail grqphQL refactor 2026-03-09 11:44:24 -04:00
c392333170 🛠 Missing files logic 2026-03-07 21:52:26 -05:00
e083c18c0e 🔨 Removed useless build-deps 2026-03-06 14:28:03 -05:00
7818c6f290 🛠 Build optimizations (chunking and lazy-loading) 2026-03-06 12:37:45 -05:00
46e683859e 🔨 Fixed warnings in yarn dev script 2026-03-06 10:50:19 -05:00
a45eae2604 Removed redundant server code 2026-03-05 22:18:20 -05:00
a0d971e010 🔨 Fixed the status updates on Import 2026-03-05 21:25:01 -05:00
aec989d021 🏗 Added a real time import stats panel 2026-03-05 12:39:16 -05:00
2b4ee716e3 🪓 Removed apollo-client 2026-03-05 11:55:46 -05:00
ec52906eca 🔨 Fixed timestamps 2026-03-05 11:48:43 -05:00
07f5e6efe6 ⬇ Import flow graph-qlified 2026-03-05 11:41:10 -05:00
5d18bd1e43 📔 JsDoc for index.ts 2026-03-05 11:00:56 -05:00
74c0d6513c 🐘 Massive refactor for graphql changes 2026-03-04 23:42:50 -05:00
4b8d7b5905 ✂ Refactoring ComicDetail to make it... readable? 2026-02-26 14:06:37 -05:00
92992449a9 📃 Fixed comicvine matching UI 2026-02-26 13:51:53 -05:00
59afeded6a 🔨 Fixes to ComicDetail container 2026-02-25 20:52:40 -05:00
f9aac5e19f 🎠 Carousels on Wanted and Volumes 2026-02-25 20:33:33 -05:00
a8ae4130a6 🔨 Fixes for RecentlyImported Section 2026-02-25 20:27:12 -05:00
6f781af381 📏 Alignment fixes 2026-02-25 20:15:29 -05:00
c005d118ac 🎨 Styling fixes in wake of tailwindv4 2026-02-25 17:38:27 -05:00
4498830e29 🛻 Upgraded deps, fixed issues 2026-02-25 16:57:46 -05:00
e113066094 🤡 Unit testing scaffold 2026-02-25 09:18:28 -05:00
0af9482be9 📝 State issue fixes 2026-02-24 15:48:38 -05:00
37a2d0c75b 📏 Fixes for responsiveness 2026-02-17 20:29:03 -05:00
b47b38cc8d 🔨 Fixes for broken cover images in RI 2026-02-17 12:07:34 -05:00
dependabot[bot]
5c99cfb28b Bump react-router from 7.5.2 to 7.12.0 (#145)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.5.2 to 7.12.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 21:42:24 -05:00
dependabot[bot]
880b6c44ff Bump lodash from 4.17.21 to 4.17.23 (#146)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 21:42:11 -05:00
b0c8c295c7 🔧 Fixed embla carousel bleed-off 2026-02-03 20:58:36 -05:00
d39e2cdca1 🛠 Switched to embla and made carousel bleed off page 2026-01-30 17:03:23 -05:00
dependabot[bot]
1a6154e0b4 Bump storybook from 7.6.10 to 7.6.21 (#142)
Bumps [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) from 7.6.10 to 7.6.21.
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.6.21/code/core)

---
updated-dependencies:
- dependency-name: storybook
  dependency-version: 7.6.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 16:10:16 -05:00
dependabot[bot]
d8e110e2e7 Bump axios-cache-interceptor from 1.5.1 to 1.11.1 (#143)
Bumps [axios-cache-interceptor](https://github.com/arthurfiorette/axios-cache-interceptor) from 1.5.1 to 1.11.1.
- [Release notes](https://github.com/arthurfiorette/axios-cache-interceptor/releases)
- [Changelog](https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/arthurfiorette/axios-cache-interceptor/compare/v1.5.1...v1.11.1)

---
updated-dependencies:
- dependency-name: axios-cache-interceptor
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 16:10:01 -05:00
dependabot[bot]
9189386593 Bump qs from 6.13.0 to 6.14.1 (#144)
Bumps [qs](https://github.com/ljharb/qs) from 6.13.0 to 6.14.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 16:09:49 -05:00
a10cb07d67 🔧 Fixed the dark mode switch 2025-12-02 17:05:58 -05:00
dependabot[bot]
2a83855115 Bump tar-fs from 2.1.3 to 2.1.4 (#140) 2025-11-21 08:25:27 -05:00
dependabot[bot]
58b60a38b4 Bump vite from 5.4.20 to 5.4.21 (#141) 2025-11-21 08:25:10 -05:00
dependabot[bot]
c339dc9df1 Bump vite from 5.4.19 to 5.4.20 (#138)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 5.4.20.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 18:16:31 -04:00
dependabot[bot]
d8bcad88b7 Bump axios from 1.8.2 to 1.12.0 (#139)
Bumps [axios](https://github.com/axios/axios) from 1.8.2 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.2...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 18:16:17 -04:00
dependabot[bot]
ff1742998a Bump form-data from 4.0.0 to 4.0.4 (#137) 2025-09-06 18:43:09 -04:00
dependabot[bot]
de1fb349f6 Bump cross-spawn from 7.0.3 to 7.0.6 (#124)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 13:47:30 -04:00
dependabot[bot]
698e2a89da Bump store2 from 2.14.2 to 2.14.4 (#126)
Bumps [store2](https://github.com/nbubna/store) from 2.14.2 to 2.14.4.
- [Commits](https://github.com/nbubna/store/compare/2.14.2...2.14.4)

---
updated-dependencies:
- dependency-name: store2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 13:47:14 -04:00
dependabot[bot]
0597180637 Bump @babel/runtime from 7.23.9 to 7.26.10 (#129)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.23.9 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 13:47:03 -04:00
dependabot[bot]
422b16abf8 Bump @babel/runtime-corejs3 from 7.23.9 to 7.26.10 (#130)
Bumps [@babel/runtime-corejs3](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs3) from 7.23.9 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime-corejs3)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs3"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 13:46:51 -04:00
80f0ced0b0 🐳 Added Docker vars in the UI 2025-07-14 12:06:04 -04:00
dependabot[bot]
0def39cd73 Bump @babel/helpers from 7.23.9 to 7.27.4 (#136) 2025-06-04 07:06:07 -04:00
dependabot[bot]
1c60837ec9 Bump react-router from 6.21.3 to 7.5.2 (#132) 2025-06-04 07:05:53 -04:00
dependabot[bot]
d42a700cec Bump axios from 1.7.4 to 1.8.2 (#134) 2025-06-04 07:05:40 -04:00
dependabot[bot]
40fd8ede5d Bump vite from 5.4.12 to 5.4.19 (#135) 2025-06-04 07:05:28 -04:00
dependabot[bot]
24b09c9c5e Bump tar-fs from 2.1.1 to 2.1.3 (#133) 2025-06-04 07:03:29 -04:00
c176dab78b 🧾 Modernizing the table 2025-06-03 22:41:33 -04:00
c0b189c9e6 🧾 Modernizing tables 2025-06-03 22:33:49 -04:00
f3333b5c2c ⬇️ Fixing AirDC++ download integration 2025-06-03 21:56:55 -04:00
2ce90d94c0 🧦 Refactored socket store in zustand 2025-05-18 18:02:21 -04:00
0e445ba3d4 🔧 Fixed LoCG pull list scraping 2025-05-06 18:17:30 -04:00
9c5fb93d5b 🐳 Updated Dockerfile 2025-02-25 16:35:39 -05:00
338a46224d 🔧 Added an interface for ComicDetail 2025-02-20 17:34:21 -05:00
dependabot[bot]
0615d08e7d Bump vite from 5.2.14 to 5.4.12 (#127)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.14 to 5.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 16:29:57 -05:00
21a127509b Airdcpp regression (#123)
* 🔧 Fixing broken AirDCPP search

* 🔧 Fixing broken DC++ downloads

* 🔧 Todo to move method out to core service

* 🔧 Fixing the DC++ download bundles

* 🔧 Bundles endpoint integration

* 🔧 Fixed the download bundles page

*  Added an active hub badge to DC++ search

* 🔧 Fixing autodownload functionality

* 🔧 Fixed PullList source

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2025-02-17 16:27:48 -05:00
dependabot[bot]
e92b86792e Bump axios from 1.6.8 to 1.7.4 (#121)
Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 12:38:25 -04:00
dependabot[bot]
0f9bbd8dc1 Bump micromatch from 4.0.5 to 4.0.8 (#115)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-13 23:45:14 -04:00
dependabot[bot]
2786f0e2a4 Bump vite from 5.2.7 to 5.2.14 (#116)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.7 to 5.2.14.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.2.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-13 23:45:03 -04:00
dependabot[bot]
8dcf643444 Bump express from 4.19.2 to 4.20.0 (#118)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.20.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-13 23:44:47 -04:00
628e1f72e2 🖌️ Prettifying the form 2024-07-14 09:45:07 -04:00
f4c498bce3 🔧 Fixed caching issue with DC++ Hubs settings page 2024-07-14 00:51:08 -04:00
dependabot[bot]
bf406c8b6b Bump braces from 3.0.2 to 3.0.3 (#113)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 22:11:31 -04:00
dependabot[bot]
786ced6c21 Bump ws from 6.2.2 to 6.2.3 (#114)
Bumps [ws](https://github.com/websockets/ws) from 6.2.2 to 6.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/6.2.2...6.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 22:11:23 -04:00
c6c3f2eaf7 🗒️ Added scaffold for the toast notification 2024-07-02 22:10:16 -04:00
e2f1d5a307 🏗️ Added react-toastify 2024-06-13 14:09:15 -04:00
2879df114d 🔧 kafka-powered search + download 2024-06-03 17:12:49 -04:00
2c66e2f6af 🐳 Explicitly installing sass 2024-05-17 10:36:41 -04:00
217df2f899 Comicvine integration improvements I (#112)
* ️ Refactored VolumeDetail page to use react-query

* 🎨 Added some icons to tabs

* 📚 Wired up story arc fetching

*  Added status checks

* 🍇 Added some integration for issues

* 🔍 Improvements to CV search results

* 🔍 Refining CV search UX

* 🌍 Added i18n lib

* 🔍 CV search metadata wrangling

* 🔧 Refactored Wanted component

Included # of issues in a wanted volume

* 🔧 Refactoring DC++ search/download

* 🔧 Refactored AirDC++ init in store

* 🏗️ Automatic downloads WIP

* 🏗️ Modified the Dockerfile
2024-05-17 10:04:06 -04:00
9ab15df0a8 Comicvine integration improvements (#109)
* ️ Refactored VolumeDetail page to use react-query

* 🎨 Added some icons to tabs

* 📚 Wired up story arc fetching

*  Added status checks

* 🍇 Added some integration for issues

* 🔍 Improvements to CV search results

* 🔍 Refining CV search UX

* 🌍 Added i18n lib

* 🔍 CV search metadata wrangling

* 🔧 Refactored Wanted component

Included # of issues in a wanted volume

* 🔧 Refactoring DC++ search/download

* 🔧 Refactored AirDC++ init in store

* 🏗️ Automatic downloads WIP

* 🏗️ Modified the Dockerfile
2024-05-11 18:51:28 -04:00
dependabot[bot]
f57bd35cd4 Bump ejs from 3.1.9 to 3.1.10 (#111)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-09 13:50:12 -04:00
dependabot[bot]
56c9fb03ac Bump tar from 6.2.0 to 6.2.1 (#110)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 22:48:46 -04:00
dependabot[bot]
26c9c1e562 Bump follow-redirects from 1.15.5 to 1.15.6 (#106)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 22:48:33 -04:00
dependabot[bot]
51e18c65d1 Bump express from 4.18.2 to 4.19.2 (#107)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 22:48:23 -04:00
dependabot[bot]
b82d5fd350 Bump vite from 5.0.12 to 5.0.13 (#108)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.0.13.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.13/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 22:48:03 -04:00
9a3ccba719 🪢 Wiring up to addTorrent endpoint (#105)
* 🪢 Wiring up to addTorrent endpoint

* 🧲 Added a torrent download sub-panel

* 🧲 Fixed the auto-population of search box

* 🧲 Added downloads panel

* 🧲 Surfacing torrent progress in UI via scheduled job

* 🧲 Added visual indicators of torrent progress

* 💅🏼 Formatting improvements

* 💅🏼 Formatting tweaks to tabs
2024-03-30 21:41:05 -04:00
dependabot[bot]
c89e4af328 Bump es5-ext from 0.10.62 to 0.10.64 (#104)
Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.64.
- [Release notes](https://github.com/medikoo/es5-ext/releases)
- [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md)
- [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.64)

---
updated-dependencies:
- dependency-name: es5-ext
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 22:31:32 -05:00
df4cecedf0 📝 Added captions to screenshots 2024-02-23 16:59:31 -05:00
3dee53f33f 📝 Updated README 2024-02-23 16:52:45 -05:00
ef05dee600 Removed pngs 2024-02-23 16:42:37 -05:00
2b6bce4731 Merge branch 'main' of https://github.com/rishighan/threetwo 2024-02-23 16:41:21 -05:00
fc4c5c61e2 🌇 Updated screenshots 2024-02-23 16:41:14 -05:00
ae04260f69 Updated .gitignore to ignore storybook static assets (#102)
*  Updated .gitignore to ignore storybook static assets

* 🏗️ Fixed cover-only config for CV detail panel

* 🏗️ Added stats to dashboard

* 🐯 Prowlarr integration WIP

* 🐯 Prowlarr settings form scaffold

* 🧲 Added a form for torrent search

* 🐯 Mocked a prowlarr call
2024-02-23 16:29:13 -05:00
dependabot[bot]
ad5fc0b8b3 Bump ip from 2.0.0 to 2.0.1 (#103)
Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1.
- [Commits](https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 09:07:19 -06:00
c03f706e9d Dark mode 2 (#100)
* 🗂️ Added tab icons and styles

* 🪑 Styled download panel table

* 🪑 Cleaned up the DC++ search results table

* 🪑 Many changes to DC++ downloads table

* 🏗️ Wired up search with RQ

* 🏗️ Changes to ComicDetail section

* 🔧 Fixing table styes

* 🏗 Fixed the archive ops panel

* 🔧 Tweaked Archive ops further

* 🃏 Styling the action menu

* 🧩 CV Match panel refactor WIP

* 🏗️ Refactored the action menu

* 🤼 Cleaning up CV match panel

* 🏗️ Refactored the scored matches

* 🤼 Revamped CV match panel UX

* 🖌️ Styling tweaks to the side panel

* 🖌️ Cleaned up the form

* 🏗️ Refactored the search form

* 🤐 Added a uncompress indicator

* 🏗️ Fix for encoding # in URIs

* 🔧 Fixed # symbol handling in URLs

* 🔧 Started work on Edit Metadata panel

* 🔎 Added a check for existing uncompressed archives

* 🏗️ Settings styling tweaks

* 🏗️ Fixed invalidation of archiveOps

* 🏗️ Fixed an invalidation query on DC++ download panel

* 🏗️ Fixed CV-sourced Volume info panel

* 🏗️ Fixed volume group card stacks on Dashboard

* 🔍 Fixing CV search page

* 🏗️ Refactoring Volume groups and wanted panel

* 🏗️ Cleaning up useless files

* 🛝 Added keen-slider for pull list

* 🏗️ Abstracted heading/subheading into Header

* 🏗️ Continued refactoring of PullList, Volumes etc.

* 📆 Wired up the datepicker to LoCG pull list
2024-02-06 05:58:56 -05:00
4f49e538a8 Dark mode refactor (#98)
* 🏗️ Acquisition Panel refactor WIP

* 🔧 Formatted the search query box

* 🔧 Implementing download method

* 🏗️ Refactored the AirDC++ download panel

* 🌜 Initial Dark Mode support

* 🌜 Trying dark mode on the react-select

* Update App.scss

* 🏗️ Migrating Navbar to TailwindCSS

* 🖼️ Added solar icons

* 🔧 Added solar icons

* 🔧 Added code for dark mode toggle

* 🏗️ Wiring up the dark mode toggle

* 🌜 Added Dark mode to the body

* 🏗️ Building out the import page

* 🪑 Cleaning up the table styles

* 🏗️ Cleaned up past imports table

* 🏗️ Refactored Import socket events

* 🏗️ Refactored the card grid on dashboard

* 🏗️ Building variants for Cards

* 🏗️ Added a horizontal medium variant

* 🏗️ Cleaning up forms and cards

* 🔧 Styling form inputs

* 🏗️ Form refactor

* 🔠 Added a monospace font

* 🪑 Refactoring the table

* 🧹 Formatting in connection confirmation panels

* 🏗️ Refactoring table for library

* 🏗️ Added icons and details to metadata

* 🏗️ Cleaned the table further

* 🏗️ Fixed fonts, and comic detail page first draft

*  Removing yarn.lockfile
2023-12-21 16:23:29 -05:00
dependabot[bot]
68442894d0 Bump vite from 5.0.2 to 5.0.5 (#99)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.2 to 5.0.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 22:41:14 -06:00
dba520b4c1 🧸 Zustand and Tanstack Query (#96)
* ↪️ Removed node-sass, added sass

* 🏗️ Refactoring Navbar to read from zustand store

* ⬆️ Bumped deps

* 🏗️ Refactored AirDC++ session status indicator

* 🏗️ Refactored Import page to read from global state

* 🏗 Wired up the event emit correctly

* 🏗️ Added import queue related state

* 🏗 Implemented setQueueAction

* 🏗️ Wired up job queue control methods

* 🏗️ Added null check and removed useless deps

* 🏗️ Refactored the Import page

* ↪️ Added cache invalidation to job statistics query

* 🏗️ Refactoring the Library page

* 🏗️ Fixed pagination and disabled states

* ✏️ Changed page to offset

To better reflect what we are doing with the pagination controls

* 🏗️ Refactoring ComicDetail page and its children

* 🏗️ Refactored ComicDetailContainer with useQuery

* 🔧 Fixed the error check on Library page

* 🏗️ Refactoring AcquisitionPanel

* 🏗️ Refactoring the AirDC++ Forms

* 🦃 Thanksgiving Day bug fixes

* ⬆️ Bumped up Vite to 5.0

* 🔧 Refactoring AcquisitionPanel

* 🏗️ Wiring up the DC++ search method

* 🏗️ Refactoring AirDC++ search method

* 🔎 Added some validation to ADC++ Hubs settings form

* 🏗️ Fixed the ADC++ search results

* 🏗️ Cleanup of the search results pane
2023-11-28 22:54:45 -05:00
ef75dad4e2 Adding zustand and react-query (Settings Page) (#95)
* 🌊 qBittorrent settings scaffold

* 🔧 Added scaffold for the qBittorrent connection form

* 🔧 Some refactoring

* 🔧 Cleaned up folder structure

* 🔧 Fixed broken paths

* 🔧 Cleaned up Search and Import component hierarchy

* 🔧 More path fixes

* 🔧 Tooling changes

* 📝 Qbittorrent form scaffold

* ⬆️ Bumped @dnd-kit deps

* 🧑🏼‍🔧 Fixed the hostname regex

* 🏗️ Adding fields to the settings form

* 🔧 Formatting and more layout changes

* 🔧 Added Prowlarr settings items in JSON

* 📝 Purified Card Component

* 📝 Abstracted connection form into a component

* 🏗️ Reorganized tabs

* Migrating from Redux to RTK-query

* ⬇️ Fetched qBittorrent settings

* 🏗️ Trying out react-query

* 🧩 Added react-query query to qBittorrentSettings page

* 📝 qbittorrent form RU actions first draft

* 🏗️ Added loading state check

* 🏗 Added error check state

* 🏗️ Refactored AirDCPP context using react-query

* 🏗️ Refactoring AirDCPP Settings Form with react-query

* 🔧 Removed context

* 🔧 Removing context from AirDCPP settings page

* 🔧 Fixed early init error on the store

* 🐛 Debugging AirDCPP Settings Form page

* 🧸 Zustand-ified AirDCPP Form

*  AirDCPP code cleaned up from App.tsx

*  Re-added yarn.lock
2023-11-07 11:56:29 -06:00
8bebffd95e 🌊 qBittorrent Settings Scaffold (#90)
* 🌊 qBittorrent settings scaffold

* 🔧 Added scaffold for the qBittorrent connection form

* 🔧 Some refactoring

* 🔧 Cleaned up folder structure

* 🔧 Fixed broken paths

* 🔧 Cleaned up Search and Import component hierarchy

* 🔧 More path fixes

* 🔧 Tooling changes

* 📝 Qbittorrent form scaffold

* ⬆️ Bumped @dnd-kit deps

* 🧑🏼‍🔧 Fixed the hostname regex

* 🏗️ Adding fields to the settings form

* 🔧 Formatting and more layout changes

* 🔧 Added Prowlarr settings items in JSON

* 📝 Purified Card Component

* 📝 Abstracted connection form into a component

* 🏗️ Reorganized tabs

* Migrating from Redux to RTK-query

* ⬇️ Fetched qBittorrent settings

* 🏗️ Trying out react-query

* 🧩 Added react-query query to qBittorrentSettings page

* 📝 qbittorrent form RU actions first draft

* 🏗️ Added loading state check

* 🏗 Added error check state

* 🏗️ Refactored AirDCPP context using react-query

* 🏗️ Refactoring AirDCPP Settings Form with react-query

* 🔧 Removed context

* 🔧 Removing context from AirDCPP settings page

* 🔧 Fixed early init error on the store

* 🐛 Debugging AirDCPP Settings Form page

* 🧸 Zustand-ified AirDCPP Form

*  AirDCPP code cleaned up from App.tsx

*  Re-added yarn.lock
2023-11-07 11:46:08 -06:00
dependabot[bot]
1bd3d611e4 Bump postcss from 8.4.29 to 8.4.31 (#93) 2023-10-19 08:37:49 -05:00
dependabot[bot]
825782fe13 Bump @babel/traverse from 7.22.11 to 7.23.2 (#94) 2023-10-19 08:37:41 -05:00
32f4055daa Import queue progress (#87)
* 🚥 Added service status panel scaffold

* 🐂 Support for showing import progress

* 🐂 Support for session-tracking

* 🔧 Tooling for resumable socket.io sessions

* 🧹 Minor change in socket.io connection code

* 🔧 Refactoring the Import Page

* 📝 Added more details to import statuses

* 🐂 Queue pause/resume functionality

* 🐂 Queue drain event reducer

* 🐂 Queue controls

* 🔧 Hardening the import UX

* 🔀 Bumped deps

* 🔧 Fixed the airdcpp-apisocket version

* ⛑️ Removed useless deps

* 🪡 Fixed margin on the comicinfo.xml panel on the library page

* 🏗️ Scaffold for job results

* 🔢 Removed the useless LS_IMPORT event

* 🔧 Wired up jobStatistics call

* 🧹 Cleaning up the tabulated job results

* 🔧 More finishing touches to Import UX

* 🔧 Added a console log for debugging purposes

---------

Co-authored-by: Rishi Ghan <hghan@apple.com>
2023-08-30 13:49:58 -04:00
dependabot[bot]
c20f24b1a2 Bump word-wrap from 1.2.3 to 1.2.4 (#88)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-11 14:11:53 -04:00
dependabot[bot]
e4fc28f698 Bump tough-cookie from 4.0.0 to 4.1.3 (#85)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 10:52:32 -07:00
dependabot[bot]
d6c183339f Bump semver from 5.7.1 to 5.7.2 (#86)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 10:52:20 -07:00
dependabot[bot]
0eba47e20f Bump vite from 4.2.0 to 4.2.3 (#82) 2023-06-05 22:37:43 -04:00
928bfd573e 🔧 Fix for the hostname env var when running in Docker (#81) 2023-05-16 12:41:57 -04:00
b8fb179ac6 🔧 Miscellaneous Fixes (#79)
* ️ Config key to expose hostname in a Docker context

* 🔧 Miscellaneous Fixes
2023-05-15 16:16:26 -04:00
94692bb6d4 71 dockerfile update (#75)
* 🐳 Fixes to the Dockerfile

* 🚢 Updated port number for UI

*  Removed the param serialization from axios calls
2023-05-15 16:13:40 -04:00
4b795aca5d ️ Config key to expose hostname in a Docker context (#77) 2023-04-27 11:38:50 -04:00
fbf6bed4fe 🐳 Fixes to the Dockerfile (#74) 2023-03-31 15:09:23 -04:00
Lars Gohr
18b18c3d81 Updated elgohr/Publish-Docker-Github-Action to a supported version (v5) (#73) 2023-03-30 21:00:12 -04:00
a939bf4c71 🐛UI bugs (#68)
* 🔧 Updated date for PullList on Dashboard

* 🔧 Fixes for broken image paths

* ⬆️ Bumped deps, removed useless ones
2023-03-24 00:00:48 -04:00
0a48ecbb2c 📗 Upgraded Storybook to v7 rc (#69) 2023-03-15 16:43:16 -04:00
c5dd1abcdd 🖼️ Relocating screenshots (#70)
* 🔧 Updated date for PullList on Dashboard

* 🔧 Fixes for broken image paths

* 🖼️ Updated screenshot links in README
2023-03-15 14:04:49 -04:00
79f9b22fad Elasticsearch upgrade fix (#67)
* 🔧 Fixed the response object in reducers and components https://github.com/rishighan/threetwo/issues/64

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* #️⃣ Added a key prop to MetadataPanel in global search results

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed DOMNesting issues

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed the response in wanted reducer action

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

*  Committing the reducer

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixes for DOMNesting issues on the Downloads page

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-13 11:25:15 -04:00
dependabot[bot]
e291b4806b Bump http-cache-semantics from 4.1.0 to 4.1.1 (#65)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-02 21:47:16 -05:00
ff7b7c9207 Fix for elasticsearch upgrade breakage (#66)
* 🔧 Fixed the response object in reducers and components https://github.com/rishighan/threetwo/issues/64

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* #️⃣ Added a key prop to MetadataPanel in global search results

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed DOMNesting issues

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed the response in wanted reducer action

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

*  Committing the reducer

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixes for DOMNesting issues on the Downloads page

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-02 00:32:43 -05:00
2d61fa1436 Changing the build system to Vite (#62)
* 🔧 Updated date for PullList on Dashboard

* ️ Added Vite config and removed useless files

* 👷🏽 Updated build command

*  Removed useless deps

* 🔧 Cleaned up package.json and bumped airdcpp-apisocket

* 🔧 Updated some packages and deps

* ⬆️ Bumped some deps

* 🔧 Fixed typo in package.json

* 🔧 Fix for broken paths https://github.com/rishighan/threetwo/issues/63

* 🔧 Fixed broken path and npm script

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-01 23:31:22 -05:00
b12849efda 🔧 Fixed the path to funding.yml (#61) 2023-01-28 09:42:39 -08:00
fb2c969c00 💵 Added a funding page (#60) 2023-01-28 09:28:22 -08:00
d437fddd1a Merge branch 'master' of https://github.com/rishighan/threetwo 2023-01-21 18:31:00 -08:00
8dd68e9d54 AirDC++ Socket Status (#58) 2023-01-21 02:29:32 -08:00
dependabot[bot]
a8b52d0ac6 Bump json5 from 1.0.1 to 1.0.2 (#56)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-08 08:49:09 -08:00
719ebe7c6e ThreeTwo favicon (#54)
* 🔧 Fixed empty library state with explanation

* ️ Added a status indicator for the AirDC++ socket connection

* 🖼️ Fixed the ThreeTwo favico
2023-01-01 17:14:27 -08:00
ddef87ea29 AirDC++ Connection Status (#53)
* 🔧 Fixed empty library state with explanation

* ️ Added a status indicator for the AirDC++ socket connection
2022-12-30 22:39:38 -08:00
71bad167ab 🔧 Library zero state (#52)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README

* 🏗️ Massive refactor around archive uncompression for reading/analysis

* 🔧 Tweaked state vars for reading and analysis

* 🏗️ Refactor to support DC++ and socket.io integration

This refactor covers the following workflows:

1. Adding a comic from LOCG or ComicVine adds it to the wanted list
2. Downloading that comic from DC++ correctly adds download metadata to the corresponding comic object in mongo
3. Successful download triggers automatic import to library and cover extraction, metadata application
2022-12-29 15:55:33 -08:00
df1fbc7dd3 🔧 Fixed empty library state with explanation 2022-12-29 23:33:23 +00:00
789e5b9518 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-12-21 21:49:42 -08:00
ce6653b5d7 🏗️ Applying the refactor patc 2022-12-21 21:49:12 -08:00
d065225d8e 🏗️ Refactoring archive uncompression for "Read Comic" and "Analysis" user flows (#46)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README

* 🏗️ Massive refactor around archive uncompression for reading/analysis

* 🔧 Tweaked state vars for reading and analysis

* 🏗️ Refactor to support DC++ and socket.io integration

This refactor covers the following workflows:

1. Adding a comic from LOCG or ComicVine adds it to the wanted list
2. Downloading that comic from DC++ correctly adds download metadata to the corresponding comic object in mongo
3. Successful download triggers automatic import to library and cover extraction, metadata application
2022-12-21 21:17:38 -08:00
dependabot[bot]
f854ff9cc6 Bump decode-uri-component from 0.2.0 to 0.2.2 (#45)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 20:35:38 -08:00
815444a973 🔧 Refactoring the uncompression methods for "Read Comic" and "Uncompress Archive" operations (#43)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README
2022-12-06 14:08:53 -08:00
1cbf53be98 🖼️ Adding screenshots as of December 2022 (#44) 2022-12-06 14:07:44 -08:00
0e73f9dad1 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-11-24 22:00:25 -08:00
a15168a6be 📝 Added JSDoc to extractComicArchive method 2022-11-24 21:51:49 -08:00
dependabot[bot]
67079f0cb4 Bump engine.io from 6.2.0 to 6.2.1 (#42)
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 10:03:42 -08:00
ef5af01e33 ✏️ Code cleanup and adding jsDoc 2022-11-19 11:23:44 -08:00
5b6c9c8ffc ✏️ Added jsDoc for some components 2022-11-17 15:55:52 -08:00
4556778a47 🔧 Tweaking layout of the CV manual search form 2022-11-17 14:06:26 -08:00
af5f443cbe 🔧 Fixing CV manual search form 2022-11-17 13:46:20 -08:00
7babf9f73d 🔧 🏗️ Massive tables refactor
Abstracted a table component that can be configured to display issues, volumes or pull list items
2022-11-15 17:22:50 -08:00
1e39daeda2 🔧 Fixed wanted comics table data source 2022-11-08 10:05:30 -08:00
e3cea24615 🔧 Using the refactored T2Table for Wanted Comics 2022-11-07 21:15:02 -08:00
f60c9e4e67 🔧 Refactored the T2Table component with the new pagination controls 2022-11-07 20:55:58 -08:00
27e6f26331 ✏️ Changed the hard-coded date of the pull list 2022-11-07 08:06:09 -08:00
1f5502ce23 👁️ Added grid and tabular view buttons 2022-11-05 09:28:54 -07:00
3cb9588bbf 🎨 Styling tweaks to pagination controls 2022-11-03 21:40:23 -07:00
b1fb256189 🔧 Streamlined search and pagination controls on Library page 2022-11-03 12:47:31 -07:00
74ea2742f0 🔧 Fixes for controlled pagination on react-table 2022-11-01 22:21:58 -07:00
151c6ec314 🔎 Fixed search bar on Library page 2022-10-29 15:47:27 -07:00
c6a3be968a 🎨 Tweaking styling of the library table 2022-10-28 21:46:16 -07:00
ff5ce10e17 🔧 Library page table pagination 2022-10-27 23:06:40 -07:00
63e96bf96e 🚏 Added a path to comic detail row on Library page 2022-10-17 11:30:30 -07:00
b699a90a00 🕵🏼 Added manual search form for CV matching 2022-09-28 09:51:48 -07:00
eda11d3537 🐛 Fixed bug on the AirDC++ settings page 2022-09-19 15:41:32 -07:00
262f8d49d7 🔼 Bumped up react-final-form 2022-09-12 12:50:30 -07:00
6b1bb02d57 📝 Added a port number field to AirDC++ settings form 2022-09-08 10:35:30 -07:00
8d114ff04d 🎨 Minor prettification of bundleID for downloads 2022-08-20 09:49:58 -07:00
9bbf2efc3c 🪢 Unwrangling state vars for global search vs local 2022-08-18 00:10:31 -07:00
5f59456c8b 🔧 Fixed publisher name in Library table 2022-08-17 22:25:48 -07:00
ea366d1888 🔧 Fixes for Wanted comics table 2022-08-17 20:48:10 -07:00
014ea27752 🪢 Wiring up download complete action -> import 2022-08-07 22:35:56 -07:00
580d19e8a4 🔽 Downloads section building out 2022-07-30 21:38:56 -07:00
f146dfdd0b 📝 Tabulated downloads per comic 2022-07-28 08:59:30 -07:00
602adf8775 🔧 Refactoring the way bundles are saved 2022-07-27 22:49:00 -07:00
fb40fe86b5 🔽 Wiring up downloads API calls and actions 2022-07-26 22:26:24 -07:00
a3aa46bca3 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-07-24 22:20:31 -07:00
365fa2e115 🔎 Integration with DC++ downloads endpoint 2022-07-24 22:20:25 -07:00
d2de78968d Merge pull request #38 from rishighan/dependabot/npm_and_yarn/moment-2.29.4
Bump moment from 2.29.3 to 2.29.4
2022-07-24 22:00:50 -07:00
dependabot[bot]
684730f186 Bump moment from 2.29.3 to 2.29.4
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-25 04:59:52 +00:00
fad18bc270 Merge pull request #37 from rishighan/dependabot/npm_and_yarn/terser-4.8.1
Bump terser from 4.8.0 to 4.8.1
2022-07-24 21:59:02 -07:00
dependabot[bot]
67d66cefcf Bump terser from 4.8.0 to 4.8.1
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 06:03:04 +00:00
f865feeeb2 ⤵️ Downloads page scaffold 2022-07-10 21:11:56 -07:00
23a0d2c231 🧩 MetadataPanel story added 2022-07-06 16:07:48 -07:00
2ce7ad2297 🌁 Added a story for Card 2022-07-06 10:40:33 -07:00
a10c8b7745 📘Storybook configuration added 2022-07-05 20:56:23 -07:00
181de14f26 ✏️ Updated README 2022-07-05 11:16:58 -07:00
2ab27926f6 🧩 react-table 8.1.1 breaking changes fixes 2022-07-03 21:59:59 -07:00
a4beae5d95 🔼 Bumped react-table to 8.1.1 2022-07-02 13:44:33 -07:00
ff63944810 🔧 Tweaking the masonry breakpoints 2022-06-30 08:37:35 -07:00
ab6585ca0c 🏀 Added debounce to global searchbar 2022-06-27 15:14:53 -07:00
44cf66b6c5 📥 Download nav statbar indicator 2022-06-26 11:16:11 -07:00
2dea9e1096 📥 Refactoring download tick panel 2022-06-23 11:11:45 -07:00
d2dbb133b3 🔧 Fixing state and context issues WIP for ADCPP socket 2022-06-21 14:39:37 -07:00
702f4e62a2 🔧 Fixing the AirDCPP socket issue on AcquisitionPanel on ComicDetails page 2022-06-21 12:43:35 -07:00
34e475d3cc 🔧 Fix for save/delete functionality on AirDCPP settings form 2022-06-20 00:34:46 -07:00
9cec1c40a8 🏗 WIP fixes for AirDCPP socket connection Part Deux 2022-06-17 22:45:02 -07:00
2244d2f512 🔧 More fixes for the react-refresh plugin bugs 2022-06-14 20:25:26 -07:00
a46eebb043 🔧 Fix for react-refresh plugin in production 2022-06-14 13:04:29 -07:00
7a3e0def34 🏗 Actions, reducers for Downloads 2022-06-13 14:56:15 -07:00
15c0840c63 📜 Added configuration for jsdoc + better-docs 2022-06-12 22:54:36 -07:00
f308ec0f01 🔧 Various AirDCPPSocketContext-related fixes 2022-06-10 18:39:27 -07:00
a73250d99c 🏗 Refactoring AirDCPPSocket init and download handling 2022-06-09 00:55:39 -07:00
2943000db3 🔧 Adding a download status to navbar WIP 2022-06-05 10:31:06 -07:00
5735f09431 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-06-03 09:41:45 -07:00
8ce44daf9a ⤵️ Downloads page WIP 2022-06-03 09:41:40 -07:00
6b5128ac30 Merge pull request #30 from rishighan/dependabot/npm_and_yarn/npm-8.11.0
Bump npm from 7.24.2 to 8.11.0
2022-06-02 08:50:01 -07:00
dependabot[bot]
6e7b489836 Bump npm from 7.24.2 to 8.11.0
Bumps [npm](https://github.com/npm/cli) from 7.24.2 to 8.11.0.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v7.24.2...v8.11.0)

---
updated-dependencies:
- dependency-name: npm
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-02 15:49:18 +00:00
ad07d85944 🖼 Added descriptive icons next to dashboard headers 2022-06-02 01:06:21 -07:00
32156a4efc Merge branch 'master' of https://github.com/rishighan/threetwo 2022-06-01 23:17:59 -07:00
aeb6e5225d 🔎 CV search results prettification 2022-06-01 23:17:53 -07:00
94eaf8d146 Merge pull request #29 from rishighan/dependabot/npm_and_yarn/sharp-0.30.5 2022-06-01 16:54:39 -07:00
dependabot[bot]
cbb579dda4 Bump sharp from 0.28.3 to 0.30.5
Bumps [sharp](https://github.com/lovell/sharp) from 0.28.3 to 0.30.5.
- [Release notes](https://github.com/lovell/sharp/releases)
- [Changelog](https://github.com/lovell/sharp/blob/main/docs/changelog.md)
- [Commits](https://github.com/lovell/sharp/compare/v0.28.3...v0.30.5)

---
updated-dependencies:
- dependency-name: sharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 23:48:16 +00:00
f6d86199dd 🔧 Tweaking appearance of search results 2022-05-29 09:40:46 -07:00
4ec45352e9 🔎 Glorious universal library search first draft 2022-05-26 22:52:07 -07:00
a2044941a6 Pruned useless routing scaffold 2022-05-25 10:39:11 -07:00
5596da2f2f Removed OPDS routes from the UI facade 2022-05-25 09:08:46 -07:00
Rishi Ghan
794a34d7f2 🔀 Re-arranged some nav items 2022-05-24 15:34:14 -07:00
Rishi Ghan
aba16c3708 🔎 Adding universal Library search bar WIP 2022-05-24 15:29:36 -07:00
Rishi Ghan
d50a5ada02 🔧 Moved metadata panels to its own component 2022-05-24 14:03:07 -07:00
a156f07598 🔧 Metadata Panel WIP 2 2022-05-24 11:06:55 -07:00
33976f0f63 🎛 Metadata panel WIP 2022-05-23 23:01:20 -07:00
d80c672cd1 🚛 Massive refactor of dashboard 2022-05-23 22:01:51 -07:00
f0505d7428 🖼 Added a League of Comic Geeks logo 2022-05-22 01:55:51 -07:00
3173cbf873 🔧 Fix for https://github.com/rishighan/threetwo/issues/10 2022-05-19 22:57:26 -07:00
9be9750d7b 🔧 Fixed the tab-switching bug 2022-05-17 10:44:06 -07:00
6c673dff2b 🔧 Debugging 2022-05-17 10:31:34 -07:00
45be01a140 🕷 Gnarly tab switch bug regressed 2022-05-17 08:56:12 -07:00
1a6e28e55d 🔧 Cleaned up the reference card UI 2022-05-15 23:04:30 -07:00
b754b75eb6 🌁 Added reference card for dc++ downloads 2022-05-13 22:21:40 -07:00
8f79da2eab Removed useless imports from ComicDetail 2022-05-12 21:55:30 -07:00
Rishi Ghan
2b74aaa8f5 🔧 Some spacing tweaks 2022-05-12 14:21:23 -07:00
Rishi Ghan
a64a8b5410 🔧 Fixed a nagging default active tab issue on comic detail page 2022-05-12 14:12:59 -07:00
6b508d4c36 🔫 Tweaking the trigger for download complete action 2022-05-11 23:40:10 -07:00
Rishi Ghan
30ee5e4a67 🔧 Search page refactoring 2022-05-10 14:57:46 -07:00
Rishi Ghan
5e7496028f 🔧 Wiring up the modal and the comic viewer 2022-05-05 13:56:07 -07:00
a0d29b5086 🏗 Comic viewer scaffold WIP 2022-05-04 22:47:08 -07:00
Rishi Ghan
349e74b123 👓 Fixed histogram scaling on retina displays 2022-05-03 14:06:55 -07:00
c3d8a3db74 📏 Adjusted histogram height 2022-05-03 09:42:24 -07:00
bb54f3e107 🔧 Socket.io-zation of the dashboard 2022-05-01 16:45:58 -07:00
1fab50a92a 🔧 Refactored bulk unzip endpoint wired up 2022-04-28 22:37:31 -07:00
e243d7c795 🔧 Building out full uncompression integration 2022-04-28 08:17:41 -07:00
Rishi Ghan
ee5ba474ee 🔧 Rewiring for the new uncompression endpoint 2022-04-26 14:51:28 -07:00
52daf4781b 🔧 Refactoring 2022-04-25 12:50:54 -07:00
2b4f56d51c 🔧 Refactoring to hook on to the file completed event 2022-04-25 12:13:11 -07:00
544d359501 🔧 Refactoring AirDC++ downloads 2022-04-22 09:13:44 -07:00
Rishi Ghan
a40b08c990 ✍🏽 Manual search override for AirDC++ query 2022-04-19 14:36:21 -07:00
ab19f37007 🖼 UX tweaks on Acquisition Panel 2022-04-19 07:54:06 -07:00
d17f49baf4 🔎 DC++ manual search scaffold 2022-04-19 01:01:34 -07:00
253c7357a0 🔧 Fixed some issues with axios cached instance 2022-04-18 12:08:33 -07:00
423aacca2a Merge branch 'master' of https://github.com/rishighan/threetwo
# Conflicts:
#	yarn.lock
2022-04-18 10:51:56 -07:00
90889ea2f1 🔧 Updated yarn.lock 2022-04-18 10:51:35 -07:00
8927856116 Reverted the changes 2022-04-18 09:39:25 -07:00
9d4faccb35 ✏️ Editing workflow II 2022-04-18 09:14:55 -07:00
241be87ec0 ✏️ Editing workflow 2022-04-18 09:13:29 -07:00
2cdf81fc04 🔼 Updated Github docker workflow to v4 2022-04-18 09:00:38 -07:00
f4b2ae2bff Merge pull request #28 from rishighan/dependabot/npm_and_yarn/async-2.6.4
Bump async from 2.6.3 to 2.6.4
2022-04-18 08:38:06 -07:00
dependabot[bot]
f4f39d4ec9 Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-18 15:37:09 +00:00
32125c074d Merge branch 'master' of https://github.com/rishighan/threetwo
# Conflicts:
#	yarn.lock
2022-04-18 08:36:15 -07:00
31fd22a291 👂🏼Added an event listener for ADCPP queue file status 2022-04-18 08:34:44 -07:00
3d1a664d49 Merge pull request #27 from rishighan/dependabot/npm_and_yarn/urijs-1.19.11
Bump urijs from 1.19.10 to 1.19.11
2022-04-13 14:38:57 -07:00
dependabot[bot]
9e9c2849b5 Bump urijs from 1.19.10 to 1.19.11
Bumps [urijs](https://github.com/medialize/URI.js) from 1.19.10 to 1.19.11.
- [Release notes](https://github.com/medialize/URI.js/releases)
- [Changelog](https://github.com/medialize/URI.js/blob/gh-pages/CHANGELOG.md)
- [Commits](https://github.com/medialize/URI.js/compare/v1.19.10...v1.19.11)

---
updated-dependencies:
- dependency-name: urijs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-13 00:15:45 +00:00
ce683feb11 🔢 Added issue number to the issues contained in a volume 2022-04-12 08:35:22 -07:00
2990fa354f 🖼 UX work on volume details page 2022-04-11 14:30:34 -07:00
f94540616d 🔧 Tweaked the issues-for-series call 2022-04-11 14:18:47 -07:00
312ee93781 🏗 Added a downloads navbar link 2022-04-10 00:49:12 -07:00
771400fdf3 🏗 Scaffold for tabulated volumes page WIP 2022-04-10 00:31:35 -07:00
6a8d729ad9 🔧 Fixes 2022-04-09 21:55:04 -07:00
6d89b94425 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-04-09 21:54:58 -07:00
115571e297 🔧 Fixed issues with the Wanted table view 2022-04-09 21:54:52 -07:00
93bd0f949c Merge pull request #26 from rishighan/dependabot/npm_and_yarn/moment-2.29.2
Bump moment from 2.29.1 to 2.29.2
2022-04-09 08:31:00 -07:00
dependabot[bot]
ec5427f53b Bump moment from 2.29.1 to 2.29.2
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 10:51:24 +00:00
99f6991896 🔧 Added a call to fetch volumes 2022-04-09 01:46:08 -07:00
1f26fe5cfa 🔧 Fixed some glaring errors 2022-04-08 23:50:55 -07:00
c4f46cc727 🏗 WIP scaffold for Volumes tabulated view 2022-04-08 22:47:57 -07:00
a3f076add3 🖼 Building out the tabulated Wanted Comics page 2022-04-07 16:12:34 -07:00
c91f64239a 🏗 Scaffold for Wanted list table 2022-04-07 01:03:46 -07:00
0a315b1ef9 🧹 Cleaned up the deck/description for CV sourced volumes 2022-04-06 23:08:53 -07:00
81bdaefdd1 📋 UI improvements in the Library section 2022-04-06 11:00:16 -07:00
0cd142153c 🖼 Some UI fixes 2022-04-06 00:10:36 -07:00
0d0fd948b5 🔧 Some UI flourishes 2022-04-05 01:18:03 -07:00
89ca89752c 🤯 Added ES index delete option in the UI 2022-04-04 22:57:02 -07:00
ff47e3d590 🔧 Fix for AirDC++ search result not downloading 2022-04-03 22:24:31 -07:00
5bf9e88b41 🔼 Upgraded to react 18 and react-dom 18 2022-03-30 11:01:03 -07:00
c208cbb76f ✏️ Added comics and userdata folder to dockerignore 2022-03-29 13:45:59 -07:00
3d2fe78657 🔧 Updated README 2022-03-28 20:32:41 -07:00
ddd9fe958e Removed docker-compose related files 2022-03-28 20:27:21 -07:00
55f4e0ccec Removed reference to a useless logger dep 2022-03-28 10:38:53 -07:00
6dcdbd9227 🔧 Removed package-lock.json 2022-03-27 23:17:37 -07:00
7e7e042591 🔧 Adjusting paths 2022-03-27 23:13:47 -07:00
5aef96a5fb 🔧 Updated Dockerfile 2022-03-27 22:45:53 -07:00
36f743362c 🔼 Bumped up webpack, removed some useless deps 2022-03-26 22:44:00 -07:00
a0292d688a Merge pull request #23 from rishighan/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-03-26 22:42:30 -07:00
dependabot[bot]
aece7fc7d1 Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-27 05:35:49 +00:00
8a79f6925a 🔼 Bumped webpack-dev-server 2022-03-26 22:34:50 -07:00
7b1dc56dbb 🔽 Weekly Pull List dedicated tablulated page 2022-03-26 22:28:22 -07:00
03b982858d 🥡 Started making react-table re-usable 2022-03-26 11:27:41 -07:00
089078fda0 💅🏽 react-table UI polishes 2022-03-20 23:33:16 -07:00
2c8e339e3b 💅🏽 Refactoring UX on Library page 2022-03-20 02:31:25 -07:00
4e2a91a1ce 🤳🏼 Updated icon for ComicInfo.xml file 2022-03-18 23:17:50 -07:00
b1b4070867 💅🏽 Beautifying the import UI 2022-03-18 09:35:26 -07:00
f6ca91f530 ⏯ Added queue pause/resume controls in UI 2022-03-17 17:06:13 -07:00
f28fbfa938 🔧 Tweaking the library table list 2022-03-11 02:11:44 -08:00
d6782bcf5d Merge pull request #22 from rishighan/dependabot/npm_and_yarn/urijs-1.19.10 2022-03-08 16:46:09 -08:00
dependabot[bot]
257edbc8de Bump urijs from 1.19.9 to 1.19.10
Bumps [urijs](https://github.com/medialize/URI.js) from 1.19.9 to 1.19.10.
- [Release notes](https://github.com/medialize/URI.js/releases)
- [Changelog](https://github.com/medialize/URI.js/blob/gh-pages/CHANGELOG.md)
- [Commits](https://github.com/medialize/URI.js/compare/v1.19.9...v1.19.10)

---
updated-dependencies:
- dependency-name: urijs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-08 23:42:53 +00:00
b237d2a32d 🔧 Fixing up the UI on the library page 2022-03-08 00:54:42 -08:00
bb3e01ca24 🔎 Wiring up the Library page for elasticsearch-powered, search 2022-03-07 03:05:22 -08:00
a7a536c647 Merge pull request #21 from rishighan/dependabot/npm_and_yarn/urijs-1.19.9
Bump urijs from 1.19.8 to 1.19.9
2022-03-05 00:16:20 -08:00
cf167be8da 🔧 Fixed object orchestration for volumes in UI 2022-03-04 15:43:27 -08:00
dependabot[bot]
c5e369f42b Bump urijs from 1.19.8 to 1.19.9
Bumps [urijs](https://github.com/medialize/URI.js) from 1.19.8 to 1.19.9.
- [Release notes](https://github.com/medialize/URI.js/releases)
- [Changelog](https://github.com/medialize/URI.js/blob/gh-pages/CHANGELOG.md)
- [Commits](https://github.com/medialize/URI.js/compare/v1.19.8...v1.19.9)

---
updated-dependencies:
- dependency-name: urijs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-03 23:33:49 +00:00
103cc7fa91 🙋🏽‍♂️ Wanted comics section on dashboard 2022-03-03 14:57:22 -08:00
9ec5040bd7 🦟 Fixed 404s upon page refresh 2022-03-01 23:01:57 -08:00
769e2e3edc 🔼 Upgraded to react-router v6 2022-03-01 15:20:17 -08:00
9b8f66c8b2 🔧 Reworked metadata UX 2022-02-28 22:05:52 -08:00
6094c2489c 📉 Added a comicinfo.xml stat data point 2022-02-28 13:57:25 -08:00
ffb5d73ab8 🔧 Added visibility filters to the metadata tabs on comic details page 2022-02-28 13:42:02 -08:00
e4b04c51eb 💽 ComicInfo.xml conversion and import wiring up 2022-02-27 23:48:25 -08:00
50f59f9493 Merge pull request #20 from rishighan/dependabot/npm_and_yarn/url-parse-1.5.10
Bump url-parse from 1.5.7 to 1.5.10
2022-02-25 22:39:37 -08:00
dependabot[bot]
afb015fd7b Bump url-parse from 1.5.7 to 1.5.10
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 04:41:34 +00:00
b80e9acbcc Merge pull request #19 from rishighan/dependabot/npm_and_yarn/urijs-1.19.8 2022-02-25 20:40:53 -08:00
dependabot[bot]
0e41b16cb2 Bump urijs from 1.19.7 to 1.19.8
Bumps [urijs](https://github.com/medialize/URI.js) from 1.19.7 to 1.19.8.
- [Release notes](https://github.com/medialize/URI.js/releases)
- [Changelog](https://github.com/medialize/URI.js/blob/gh-pages/CHANGELOG.md)
- [Commits](https://github.com/medialize/URI.js/compare/v1.19.7...v1.19.8)

---
updated-dependencies:
- dependency-name: urijs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 03:29:38 +00:00
262a31e3a5 🔧 Testing a different date 2022-02-24 00:16:28 -08:00
822e43810c Merge pull request #18 from rishighan/dependabot/npm_and_yarn/url-parse-1.5.7
Bump url-parse from 1.5.4 to 1.5.7
2022-02-18 23:45:22 -08:00
dependabot[bot]
758bb4e67e Bump url-parse from 1.5.4 to 1.5.7
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.4 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.4...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-19 07:37:54 +00:00
573df70240 🔧 Tweaking the dashboard 2022-02-17 01:05:50 -08:00
fe1b55a35d 🔧 Stats UI cleanup 2022-02-16 09:34:43 -08:00
36f41ef168 ✏️ Fixes to README 2022-02-15 22:04:19 -08:00
3911a54e94 ✏️ Updated client folder README 2022-02-15 21:58:30 -08:00
829bc488c8 🔧 Added checks to stats modules 2022-02-15 21:25:17 -08:00
635e70ba81 🎠 Weekly Pull list carousel first draft 2022-02-15 15:31:35 -08:00
8ff12e5e02 Merge pull request #17 from rishighan/dependabot/npm_and_yarn/follow-redirects-1.14.8
Bump follow-redirects from 1.14.7 to 1.14.8
2022-02-14 18:18:27 -08:00
dependabot[bot]
82719d24b8 Bump follow-redirects from 1.14.7 to 1.14.8
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 06:25:01 +00:00
1fbf677806 🔧 Added caching to pull list call to CV 2022-02-13 22:24:16 -08:00
ce392ec13e 🔧 Refactored the volumeGroup to use volume info from mongo 2022-02-12 19:41:47 -08:00
d1a0bc7d46 🌁 Added an icon to indicate missing file 2022-02-12 12:55:49 -08:00
033c59e2d0 📊 Added a scaffold for stats 2022-02-10 21:44:06 -08:00
df2d336b48 🌈 Color histograms for images, along with stats 2022-02-10 01:22:56 -08:00
7a10fb4d35 Merge pull request #16 from rishighan/dependabot/npm_and_yarn/simple-get-3.1.1
Bump simple-get from 3.1.0 to 3.1.1
2022-02-08 22:15:35 -08:00
1bbf5db6a8 🎨 UX improvements 2022-02-08 20:50:45 -08:00
9ff048d541 🔬 Wiring up the image analysis UI with the endpoint 2022-02-08 14:48:24 -08:00
8fdd8d0226 🪢 Wired up the updated Elasticsearch call to the potential matches page 2022-02-07 09:55:21 -08:00
dependabot[bot]
2738ae54f1 Bump simple-get from 3.1.0 to 3.1.1
Bumps [simple-get](https://github.com/feross/simple-get) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/feross/simple-get/releases)
- [Commits](https://github.com/feross/simple-get/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: simple-get
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 07:13:33 +00:00
cf1fe451c9 🔎 Wiring up the refactored elasticsearch endpoint 2022-02-06 23:12:39 -08:00
c6265599de 🔧 Renamed IMPORT_* to LIBRARY_* 2022-02-03 00:19:32 -08:00
bcfc174829 🔎 Scaffold for ES-powered issue matching in library first draft 2022-02-02 18:17:52 -08:00
1d317abbdb 🤼‍♀️ Wiring up ES-powered issue detection 2022-02-01 23:19:43 -08:00
d819bac7f2 🔧 Bumped threetwo-ui-typings to 1.0.12 2022-02-01 11:04:09 -08:00
3cedb9238b 🔧 Abstracted filename-parser into its own npm package 2022-01-31 08:36:29 -08:00
27bd383f00 🧮 Using Masonry to grid out the issue covers in VolumeDetails 2022-01-30 02:21:47 -08:00
e9e7ff7e5f 🏗 Building out the VolumeDetail page scaffold 2022-01-29 15:55:45 -08:00
8f83a0f94f 🔧 Integrating the auto-watched files from threetwo-core-service 2022-01-26 10:11:03 -08:00
8e5ff81d5f ✏️ Updated README to reflect new service names 2022-01-24 13:22:39 -08:00
30733dc62d 🐳 Consolidating env vars in a single file 2022-01-24 00:40:34 -08:00
ca6b030746 🔧 Removed extra line 2022-01-19 21:12:51 -08:00
3876037a7f Merge branch 'master' of https://github.com/rishighan/threetwo
# Conflicts:
#	yarn.lock
2022-01-18 21:46:01 -08:00
7769ed2776 🦭 Still updating Dockerfile 2022-01-18 21:45:24 -08:00
61556c958b Merge pull request #15 from rishighan/dependabot/npm_and_yarn/engine.io-5.2.1
Bump engine.io from 5.2.0 to 5.2.1
2022-01-16 14:33:24 -08:00
dependabot[bot]
acffccfc18 Bump engine.io from 5.2.0 to 5.2.1
Bumps [engine.io](https://github.com/socketio/engine.io) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/5.2.1/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/5.2.0...5.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-15 19:41:38 +00:00
1722b7c812 Merge pull request #14 from rishighan/dependabot/npm_and_yarn/follow-redirects-1.14.7
Bump follow-redirects from 1.14.4 to 1.14.7
2022-01-15 11:40:49 -08:00
dependabot[bot]
75bbc9851b Bump follow-redirects from 1.14.4 to 1.14.7
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.4 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.4...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-15 09:36:25 +00:00
762db13b4f 🔧 Updating the Dockerfile for Threetwo 2022-01-12 22:57:15 -08:00
fe3e294584 🔧 Refactoring docker-compose in preparation for single-source install 2022-01-11 18:06:43 -08:00
ebd7891cdf ✏️ Fixed type in threetwo-core-service 2022-01-09 23:21:42 -08:00
de0c5a462a ✏️ Updated link to threetwo-metadata-service 2022-01-09 15:51:17 -08:00
a9df8db867 ✏️ Updated threetwo-core-services link in description 2022-01-08 22:14:39 -08:00
a411b696f5 🔧 Added a bunch of fields to the metadata panel 2022-01-06 22:51:24 -08:00
7bb5669c52 🔧 Fixed value update issue in the react-select dropdowns 2022-01-06 15:15:45 -08:00
0bfe580389 🔧 Added __str__ key to the series AsyncSelectPaginate populator 2022-01-06 05:17:09 -08:00
ea6b2013f5 🔧 Generecized the AsyncSelectPaginate component 2022-01-06 05:12:11 -08:00
68a0c816ec 🧑‍🤝‍🧑 Added team credits to Metadata/Metron form 2022-01-05 14:50:04 -08:00
e4826c3272 🔧 Functioning select with Metron API pre-fetching and autosuggest with pagination 2022-01-04 20:29:44 -08:00
8644b79b75 🧽 Scrubbed secret for Metron 2022-01-04 08:00:14 -08:00
2c0664506e 🦸🏻 Integrating with Metron service 2022-01-04 07:54:18 -08:00
ac563b9ce9 🖌 Beautification, added a description to CV-scraped results 2022-01-02 01:14:37 -08:00
296ebafd5f 🖌 Beautifying CV matches WIP 2022-01-02 00:29:43 -08:00
cc317196ba 🔧 Formatting the new volume matches we get in CV-scraped results 2022-01-01 17:00:26 -08:00
a964ffbf07 🔧 Wiring up the updated CV scraper call 2021-12-31 15:34:53 -08:00
41918daafa ⚙️ Added a year extractor regex in filenameparser 2021-12-20 14:57:01 -08:00
3017920fb7 🏗 Building out the metadata form 2021-12-10 11:30:54 -08:00
1bf4fd3423 🔑 Added the password back 2021-12-07 20:10:35 -08:00
3ee2527b75 Removed elastic password from compose config 2021-12-07 11:37:52 -08:00
9b65c0a97a ✏️ Edit Metadata drawer scaffold 2021-12-07 09:19:32 -08:00
03c24328ed 🔧 Fixed a long standing issue with CV-sourced comics, which should not be scraped again 2021-12-06 14:45:16 -08:00
1b57092d3b 🔧 Fixed jankiness with Downloads Panel 2021-12-06 08:42:24 -08:00
e6952b44bf 🧱 Broke out more components out of ComicDetail page 2021-12-05 10:22:18 -08:00
1974e8d857 🔧 Refactoring ComicDetail and re-organizing folder structure 2021-12-03 23:39:08 -08:00
2656646412 🔧 Breaking apart ComicDetail page 2021-12-03 09:13:18 -08:00
f371858a0c 🧱 Broke Library into components 2021-12-02 16:07:47 -08:00
ffeb434075 Merge branch 'master' of https://github.com/rishighan/threetwo 2021-12-01 14:37:40 -08:00
edf49527a0 🚽 Wiring up the flushDb call 2021-12-01 14:37:29 -08:00
e197143498 🔧 Support for refactored import process WIP 2021-12-01 14:06:41 -08:00
69ccbd3b55 💬 Updated Discord Link 2021-11-26 01:19:12 -08:00
2cc9aee22e Removed nginx container 2021-11-26 01:13:21 -08:00
0c01d11b44 🔧 Several CORS related fixes 2021-11-25 23:47:00 -08:00
95fe37e542 🚒 Trying out nginx for CORS mitigation 2021-11-24 23:15:56 -08:00
25d68edbc4 🔧 Fixed a port check 2021-11-24 15:54:09 -08:00
2d75ba25bd 🔧 Commented out elasticsearch 2021-11-24 13:01:49 -08:00
485273da21 🔧 CORS proxy server 2021-11-24 10:23:48 -08:00
ce5ad3575e 🔧 Fixed an issue with AirDC++ Socket creation 2021-11-24 08:26:04 -08:00
bde76a81b7 🔧 Fixes to docker-compose config 2021-11-24 07:38:57 -08:00
2941559a18 Removed NATS from the docker-compose config and service .env files 2021-11-24 00:11:20 -08:00
5374f38b62 🐳 Added docker-compose definition for settings service 2021-11-23 18:36:48 -08:00
84f242184e 🔧 Genericized AirDC++ settings
1. You can now enter your AirDC++ client settings in the settings menu and the UI will read from them
2. Hubs can also be selected for search
2021-11-23 17:26:48 -08:00
610038247f 🔧 Fixed positioning of the delete button 2021-11-20 22:52:00 -08:00
5251803233 🔄 Moved socket init code higher up into ComicDetail parent component 2021-11-20 02:10:52 -08:00
725c156e88 ⚙️Settings-driven AirDC++ configuration first draft 2021-11-19 13:49:39 -08:00
2104f12e8f 🔢 Bumped up ui-typings one minor version 2021-11-18 14:19:42 -08:00
ba6acaac08 🔧 Created context for AirDC++ socket and refactored all actions 2021-11-18 14:15:48 -08:00
925008bdcb 🔧 AirDC++ Settings wired up (first draft) 2021-11-17 22:09:48 -08:00
6e1b431600 🔌 AirDCPP socket refactoring 2021-11-16 21:47:58 -08:00
b34c985ff4 🏗 Wiring up Settings form 2021-11-15 21:27:04 -08:00
305c172be7 🏗 Wiring up the AirDCPP settings call 2021-11-14 21:43:47 -08:00
ced3457ea2 🖊 Form for AirDCPP settings 2021-11-12 18:54:17 -08:00
ae3711f321 🎆 Fixes to fontawesome icons 2021-11-11 20:40:01 -08:00
4f95a9712d 🍚 Sticky header WIP 2021-11-11 15:16:53 -08:00
245dd7a7c1 🍚 Sticky header for Library table 2021-11-10 21:39:08 -08:00
9e39448545 🪢Removed traefik 2021-11-10 14:07:42 -08:00
c5845a55cb 🔧 Different docompose config 2021-11-10 09:01:35 -08:00
68fcd4c3ce 🔢 Fixed app-search version number 2021-11-10 08:51:39 -08:00
245d9e9050 🔧 OSS ES docker compose config 2021-11-10 08:49:01 -08:00
7c368b9e54 🔧 Trying a different ES configuration 2021-11-10 08:34:19 -08:00
5545d8bbf5 🇵🇹 Fixed port mapping 2021-11-09 22:05:54 -08:00
e42e67d9ab 🔄 Correcting version number 2021-11-09 21:55:29 -08:00
0cf76193e4 🔢 Added a version number to elk stack 2021-11-09 21:54:25 -08:00
4cc8e92f95 🪢 Added elasticsearch to the docker-compose stack 2021-11-09 21:48:52 -08:00
6babcc4c6a 🛣 Fixed path mapping 2021-11-08 12:37:00 -08:00
b7ad2d1634 🔄 Moved volume mapping to services from the API containers 2021-11-08 12:22:34 -08:00
9d6a43a7aa Removed references to COMICVINE_API_KEY from docompose 2021-11-08 12:14:02 -08:00
b13354c9f7 🏝Added env vars for service-specific env files 2021-11-08 12:04:15 -08:00
b98ad28fbe 🪲 Trying to get to the bottom of the env var issue 2021-11-07 22:35:24 -08:00
e4750cea2f Revert "🔧 Consolidated env files for services"
This reverts commit ab09335cfb.
2021-11-07 16:37:52 -08:00
ab09335cfb 🔧 Consolidated env files for services 2021-11-07 16:32:54 -08:00
feb4bbc3b6 😬 Added braces to env var 2021-11-07 10:03:53 -08:00
10e5ca3245 🔑 Added a CV_API_KEY interpolation in the docker-compose config 2021-11-06 22:18:17 -07:00
4a108987a8 Removed API key 2021-11-06 13:41:51 -07:00
10b284c96c 🔑 Added CV API KEY to the respective env files for the containers 2021-11-05 22:35:40 -07:00
59f5f42107 🔑 Added CV Key var to importapi 2021-11-05 20:20:10 -07:00
dbd133ed8d Added it back 2021-11-04 17:39:46 -07:00
bb10b4a364 Removed the key from docker-compose 2021-11-04 17:36:35 -07:00
beb39fbf31 Removed the hard-coded hostname 2021-11-04 17:25:28 -07:00
1e74363411 🔑 Added the CV API KEY to comicvine-service container 2021-11-04 16:22:12 -07:00
3f92b36d55 🔑 Added Comic Vine key 2021-11-04 16:16:48 -07:00
5b59758654 ✏️ Fixed an egregious typo 2021-11-04 14:48:36 -07:00
8f23b5a251 🔧 Hard-coded the socket host 2021-11-04 10:20:13 -07:00
0d5f6d9228 🔧 Added an env_file key 2021-11-04 10:11:34 -07:00
f38fe2ded3 🔧 Added a list file 2021-11-04 10:09:34 -07:00
b8c932b09f 🔧 Added the ENV var to the Dockerfile 2021-11-03 23:05:54 -07:00
c7a525da0a Deleted docker-compose.env file 2021-11-03 22:58:06 -07:00
e46c5e02b2 🔧 Fixes to env var substitution 2021-11-03 22:53:39 -07:00
fd948bd2eb 🥨 A roundabout way to pass env var 2021-11-03 22:45:34 -07:00
3da522c9c0 Reverting back to a key 2021-11-03 18:29:25 -07:00
0ee372fc5d 🔧 Added entrypoint to Dockerfile 2021-11-03 18:23:45 -07:00
e1635ff258 💿 Trying another approach to passing env var in docker-compose 2021-11-03 18:05:24 -07:00
5ce04317a6 Reverting change 2021-11-03 15:43:28 -07:00
4993269540 🏝 Added env file path for threetwo container 2021-11-03 15:40:28 -07:00
f1f4375af2 🎀 Trying bind volumes in docker-compose 2021-11-03 15:38:32 -07:00
d1e81b24e4 ✏️ Changes 2021-11-03 13:34:33 -07:00
64f8cbeadb 🔧 Fixed a path 2021-11-03 13:10:20 -07:00
23a5cb383e 🔧 Testing a compose config with vars 2021-11-03 13:06:33 -07:00
a383737535 💿 Volume mapping in docker-compose configuration 2021-11-03 12:58:00 -07:00
d27f4dbe06 💿 Volumes configuration for unraid VM 2021-11-03 08:49:57 -07:00
51117c2785 🔧 Removed quotes from volume mappings 2021-11-02 01:21:33 -07:00
d2d21fc4f8 🔧 Testing changes to volume paths 2021-11-02 01:14:48 -07:00
c7286f827b 🚴🏽 Added external: true to volume definitions in docker-compose 2021-11-02 00:32:46 -07:00
25bd822b46 💾 Fixed volume mappings 2021-11-02 00:22:57 -07:00
f41cfd505e 🙅🏽‍♂️Added Nova configuration to gitignore 2021-11-02 00:09:12 -07:00
c6e755eca6 🐳 Volume mapping in docker-compose first draft 2021-11-02 00:08:31 -07:00
4209012298 🔧 Fixed the process env var 2021-11-01 16:28:04 -07:00
95a5a9bfa6 🔧 Fixed a key in docker-compose yml 2021-11-01 16:21:16 -07:00
6e3594f71b 🔧 Fixes 2021-11-01 16:11:25 -07:00
50c299a8c4 🔧 Hostname fix for the 4th time 2021-11-01 15:29:00 -07:00
616e3e5866 🐳 Changed the name of the ENV_VAR pointing to underlying host's name 2021-11-01 10:37:10 -07:00
efe5416e5f 🐳 Removed the duplicated env var 2021-11-01 09:35:33 -07:00
d90bf44210 🐳 Added a DOCKER_HOST env var 2021-11-01 09:24:31 -07:00
0a11a1862d Removed comments from docker-compose config 2021-11-01 09:12:28 -07:00
5557b2a119 🔧 Testing out a potential solution 2021-11-01 08:24:24 -07:00
95ee4d7a40 Reinstating socket connection to "rook" 2021-11-01 01:32:02 -07:00
195d9431c4 ✏️Editing socket client config 2021-11-01 01:10:29 -07:00
464e7da826 🔧 Webpack fixes 2021-11-01 00:58:58 -07:00
9003124032 🔧 Fixes 2021-11-01 00:58:33 -07:00
bc053c3862 🔧 Referring to the hostname for posterity 2021-11-01 00:42:05 -07:00
4e960d272e 🔧 Switching to container name for socket host 2021-11-01 00:26:29 -07:00
434fbd782f 🔧 Another socket host hack 2021-11-01 00:24:13 -07:00
1576ada32c 🔧 Hack for hostname 2021-11-01 00:22:04 -07:00
08811e5128 🔧 WIP fixing the socket errors in docker-compose on portainer 2021-11-01 00:08:47 -07:00
124a88cb2c 🔧 Debugging port issues with Portainer deploys 2021-10-31 23:36:50 -07:00
5964221c61 🔧 Fixes to Library 2021-10-31 17:36:38 -07:00
cdbe39c0c7 🔧 Fixes to fix the way rawFileDetails object is accessed 2021-10-31 17:33:25 -07:00
a18705dab8 🔧 Troubleshooting socket.io in docker-compose part 2 2021-10-31 09:41:41 -07:00
8fc8bf7248 🔧 Testing variations for troubleshooting socket.io issues 2021-10-30 19:04:49 -07:00
843dee30a4 🔧 AirDCPP Settings form WIP 2021-10-29 00:11:51 -07:00
2f859a7250 🐳 Removed the network mode key from docker-compose config 2021-10-28 23:19:41 -07:00
2e5214338c 🔧 Fixed typo 2021-10-28 17:06:06 -07:00
2406c68216 🔧 Updated LIBRARY_SOCKET_HOST 2021-10-28 11:54:20 -07:00
086cdb3817 🐳 Added libraryqueue to docker-compose 2021-10-28 06:29:04 -07:00
e70994c032 🔧 Fixed some static paths 2021-10-27 22:04:01 -07:00
f5879e25fe 🔧 Fixed typo 2021-10-27 21:04:46 -07:00
881b19fb18 🔧 Changing up the dist path 2021-10-27 21:02:04 -07:00
56449b3ef6 🔧 Used the right constant for socket URI 2021-10-27 19:02:57 -07:00
acca2037b4 🔧 Troubleshooting hostname issues 2021-10-27 18:44:56 -07:00
e1504c2f11 📋 Added Redis environment variable to docker-compose config 2021-10-27 14:17:21 -07:00
56d22a28a0 🔧 Changes to support BullMQ on the service layer
1. Removed socket connection from context
2. Added Redux middleware to persist socket connection
3. Removed amqplib and RabbitMQ support
4. Removed RabbitMQ from docker-compose configuration
5. Removed a proxy route to IMS from the facade
6. Refactored file actions to support the new way of socket event emitting and listening
2021-10-27 07:46:21 -07:00
8fbea27fb2 🔧 Trying out some refactoring on socket connection to Import service 2021-10-23 23:32:25 -07:00
d520f897de 🔧 Fixed a typo 2021-10-22 21:55:07 -07:00
0df9b48703 ✏️ Another typo 2021-10-21 22:07:03 -07:00
48213592f0 ✏️ Fixed a typo in rabbitMQ env var 2021-10-21 22:06:29 -07:00
824ff7ff9e 🐇 RabbitMQ connection string changes 2021-10-21 22:05:28 -07:00
1ffc725252 🐇 Added an env variable for RabbitMQ hostname inside the docker container 2021-10-21 21:03:18 -07:00
5f0e2ec06f 🔧 Added a DOCKER_HOST env var to import service 2021-10-21 16:15:09 -07:00
3983466105 Removed a useless file 2021-10-21 15:26:42 -07:00
116cbf2a62 🍬 Removed hardcoded reference to import-service
TODO: Remove the repetitive use of thumbnail generating logic
2021-10-21 12:03:58 -07:00
419962c756 Removed a funky route from the frontend API 2021-10-21 01:16:22 -07:00
31a72e4b6f 🔧 Removed one more localhost reference 2021-10-20 23:28:42 -07:00
8881576296 🚦 Added traefik configuration for comicvine-service container 2021-10-20 14:46:19 -07:00
7531d56c6a 🔧 Added a hostname generator 2021-10-20 14:38:12 -07:00
f341529e6c 🔧 Fixing a lot of static path references in actions 2021-10-20 14:13:33 -07:00
261e3ee882 🔧 Adding docker host to import service base URI 2021-10-20 09:11:33 -07:00
705ccaa51d 🔧 Passing just the hostname and not port 2021-10-20 07:55:07 -07:00
343dbf075d 🚦Added traefik configuration for Import service 2021-10-20 07:52:42 -07:00
fda7a7e36e 👀 Adding $HOSTNAME 2021-10-19 21:39:56 -07:00
5dbe7afd5e Reverting the previous change 2021-10-19 21:19:59 -07:00
7b10f28ee0 🍃 Added socket host ENV var to Dockerfile 2021-10-19 21:12:48 -07:00
be370d2a46 🥨 Added a gnarly entrypoint for resolving hostname 2021-10-19 14:58:22 -07:00
eed908b9cf 🔧 Actual change 2021-10-19 14:45:03 -07:00
a51d7a602c 🔧 Turn DOCKER_SOCKET_HOST into an interpolation 2021-10-19 14:44:47 -07:00
9024c3cce9 🔧 Added process.env support via Webpack 2021-10-19 14:32:47 -07:00
c1e9a30419 📦 Added a DefinePlugin process var in Webpack 2021-10-19 12:55:13 -07:00
b1ff7e78ba 🔧 Fixed an erroneous use of process 2021-10-19 12:46:53 -07:00
c3a91ef39f 🔧 Interpolating hostname of Docker Host 2021-10-19 12:40:13 -07:00
98e49a6e73 🔧 Trying out a docker container name as socket hostname 2021-10-19 10:21:24 -07:00
2cc8f9da87 🔧 changed the protocol for socket connection 2021-10-19 10:07:14 -07:00
984e0c4bb1 🔧 Modified the socket base hostname to 0.0.0.0 2021-10-19 09:56:30 -07:00
04968a6031 🔧 Enabled rabbitmq 2021-10-19 09:50:57 -07:00
41ab6fb9d5 🔧 Fixing a container name in docker-compose for traefik 2021-10-19 09:47:23 -07:00
d5c9d842a3 🔧 More shenanigans 2021-10-19 09:42:35 -07:00
857e8b58e0 🔧 Updates to loadbalancer port 2021-10-19 09:35:46 -07:00
eaf5616fea ✏️ Added a .localhost TLD for threetwo-ui 2021-10-19 09:28:25 -07:00
918b4dd47d 🔧 Debugging docker-compose traefik 2021-10-19 09:09:11 -07:00
d84b02ef82 🚦 Added traefik 2021-10-19 07:36:35 -07:00
84e2a10d1b 🔧 Tweak 2021-10-18 15:46:33 -07:00
2d1ccf7264 🔒 CORS configuration in docker-compose and Express 2021-10-18 12:11:30 -07:00
8b3743fe6b 🔧 Added a flag to docker-compose 2021-10-18 08:38:47 -07:00
a050a83724 🔧 Added CORS headers to nginx config 2021-10-18 08:18:50 -07:00
9df5adb69c 🔧 Added an entry for a socket passthrough 2021-10-17 22:14:51 -07:00
e9d744f4f5 🔧 Added network mode host to docker-compose services 2021-10-17 10:44:55 -07:00
f5116e1278 ✏️ Updates to deps 2021-10-15 23:15:29 -07:00
303a2cd54b ✏️ Edited name of the db service in docker-compose 2021-10-15 23:09:32 -07:00
492fd572e2 🔧 Wiring up Settings page 2021-10-15 23:02:49 -07:00
ed120fb230 📑 Added a AirDCPP Settings form WIP 2021-10-15 15:25:19 -07:00
049c4266f3 🔧 Fixed connection string interpolation for rabbitMQ in docker-compose config 2021-10-14 22:31:29 -07:00
972ba48e6f 🎮 Added a rabbitMQ connection string URL 2021-10-14 22:21:34 -07:00
86b9ec9d95 🔑 Added credentials for rabbitMQ connection 2021-10-14 18:22:36 -07:00
e878daa114 🧦 Fixed hostnames for docker-compose 2021-10-14 17:00:52 -07:00
cfd29cb4cd 🐇 Added rabbitMQ creds in docker-compose 2021-10-14 16:58:52 -07:00
c8eb2882bd 🍬 Hard-coded env file path 2021-10-14 09:30:12 -07:00
6c1a068d5e 🔧 Changed the rabbitMQ host to a env var 2021-10-14 09:18:03 -07:00
86b4223c3a 💼 Added a port mapping for rabbitMQ 2021-10-14 08:03:01 -07:00
490587735b 🔧 Switched to using service names 2021-10-13 21:10:06 -07:00
83b9a01d86 🪑 Added deps to threetwo-ui 2021-10-13 21:07:22 -07:00
c96ab4ee4a 🧹 Removed ${PWD} 2021-10-13 17:53:41 -07:00
4a5551a8f2 🔧 Fixed typo in docker-compose.yml 2021-10-13 17:46:45 -07:00
b2bcfd5422 🐳 Volume mapping debugging 2021-10-13 17:45:47 -07:00
809b753b41 🚀 Added a network to rabbitmq container 2021-10-13 08:26:40 -07:00
9121e94e91 ️ Added docker build status badge 2021-10-12 21:06:41 -07:00
d0e3332c2c 🤬 Adding docker-compose.env back 2021-10-12 12:20:43 -07:00
d622111a73 🏝 Set PWD for volume mappings in the correct .env files for docker-compose 2021-10-12 12:14:56 -07:00
5c39de3280 🏝 Added env file for docker-compose 2021-10-12 10:36:41 -07:00
7349eb05a3 🔧 Fixed paths in docker-compose 2021-10-12 10:13:07 -07:00
bce37906e9 🧹 Cleaning up dashboard typography 2021-10-12 00:12:43 -07:00
9ff79320da 👀 Auto-adder WIP 2021-10-11 14:53:49 -07:00
53b7e94135 🔧 Fixes to search listing 2021-10-11 08:52:39 -07:00
63fecd4592 🔧 Fixed an undefined check 2021-10-08 07:57:44 -07:00
478e105e48 🕵🏼‍♀️ TPB, Mini-series, GN detection WIP 2021-10-07 12:43:00 -07:00
2a81bfcc2e 🦄 Added Discord channel details in README 2021-10-06 22:52:31 -07:00
3051680a85 💄 Beautifying Archive ops tab 2021-10-06 22:35:39 -07:00
fce8551731 💄 Card font-size tweak 2021-10-06 09:38:40 -07:00
339af88a5b 🔧 Beautifying the DnD grid 2021-10-05 16:40:10 -07:00
eba51803f6 🔧 DnD for comic book page reorganization first draft 2021-10-04 21:22:29 -07:00
eb85ede5d8 🧹 Cleaning up 2021-10-04 11:22:58 -07:00
9025085447 🧮 Functional scaffold for DnD grid 2021-10-02 12:58:21 -07:00
0a4c8fab81 🔧 Refactored the DnD grid scaffold a bit 2021-10-02 09:34:54 -07:00
75d5dc4281 🧹 Cleaning up the DnD scaffold 2021-10-01 23:41:00 -07:00
ebd5a8b95d 🐉 Drag and Drop scaffold 2021-09-30 11:53:04 -07:00
c134d2fc49 📦 Beautified carousel displaying extracted comic book images 2021-09-29 01:45:38 -07:00
b87a13d006 🪰 Making the "fly" out sliding panel reusable 2021-09-28 21:13:49 -07:00
8db52e0407 Added an object diff check to search_results_updated reducer 2021-09-25 08:52:29 -07:00
ca082b8220 👺 Fixed usage of context with sockets 2021-09-24 15:09:00 -07:00
7615e1fe52 Update README.md 2021-09-24 12:06:31 -07:00
9090ef2663 Added OPDS test instructions to README 2021-09-24 12:03:47 -07:00
51e9397055 🐞 Fixed a bug with search_results_updated listener for AirDC++ 2021-09-23 22:40:45 -07:00
2ba6e23efa 📖 Updated README 2021-09-23 22:12:50 -07:00
bbde67bb11 🍼 Cleaned up the OPDS feed 2021-09-23 18:13:11 -07:00
99e25b6cbb Merge pull request #4 from rishighan/dependabot/npm_and_yarn/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5
2021-09-23 10:09:58 -07:00
6838e3c767 Update docker-image.yml 2021-09-23 09:59:29 -07:00
5ffa29a718 Update docker-image.yml 2021-09-23 08:39:30 -07:00
49d0533a5d 🎛 Switched the layout on dashboard to masonry 2021-09-22 14:54:14 -07:00
dependabot[bot]
88f20becf3 Bump tmpl from 1.0.4 to 1.0.5
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-22 07:33:12 +00:00
384f1ce81e 📚 Added volume card stacks to the dashboard 2021-09-21 22:04:05 -07:00
048c7ba6d5 📚 Volume groups component 2021-09-21 17:15:58 -07:00
a7afc358d8 🍼 OPDS feed debugging for Panels on iOS 2021-09-21 10:06:17 -07:00
3aa5ea3427 Update docker-image.yml 2021-09-20 17:38:33 -07:00
873fa28391 Update docker-image.yml 2021-09-20 14:47:54 -07:00
7d56be71f9 📚 Added a library with a masonry grid 2021-09-20 08:24:03 -07:00
8e518c93a8 🙅🏽‍♂️ Ignored yarn's error log 2021-09-18 09:27:16 -07:00
be43c163dc 🔌 Socket + RabbitMQ setup for download-client touched folders/files 2021-09-18 09:25:59 -07:00
476a55614e 🚨 Implemented a notification system for background import 2021-09-16 09:24:14 -07:00
b40f63289a Update docker-image.yml 2021-09-15 09:36:19 -07:00
ab8a3740b5 Update docker-image.yml 2021-09-15 09:30:25 -07:00
99e1ce8b4f Rename docker-image1.yml to docker-image.yml 2021-09-15 09:21:04 -07:00
25150fac3a Delete docker-image.yml 2021-09-15 09:20:48 -07:00
3add60276b 🐳 Adding a Docker hub push step 2021-09-15 09:20:34 -07:00
c3587df058 ✏️ Added a word 2021-09-14 23:30:37 -07:00
757b03e3a6 Create docker-image.yml 2021-09-14 23:25:02 -07:00
0e039fb0a8 🐰 RabbitMQ for enqueuing comic import jobs 2021-09-14 23:13:04 -07:00
e95eeb5f27 🔧 Refactoring 2021-09-13 01:26:09 -07:00
0e085f6f06 🔧 Tweaks to TPB utils 2021-09-10 00:15:12 -07:00
ed0d7cd254 📔 Trade paperback detection 1st draft 2021-09-09 13:58:43 -07:00
67c3ab807c 🗾 Trying out some informational icons 2021-09-09 01:24:44 -07:00
f04641986d 🔧 Various fixes 2021-09-08 22:52:00 -07:00
f3d94f2a75 🃏 Added a vertical settings menu scaffold 2021-09-06 23:01:44 -07:00
3c58795286 🔧 Fixed Dockerfile path 2021-09-06 18:15:31 -07:00
efc8742699 ⚙️ Added Settings page scaffold 2021-09-06 17:50:25 -07:00
060a9143e8 Proptypes validation for components in ComicDetail 2021-09-06 15:01:21 -07:00
a173b4f971 🔄 Fixed a redux store issue with download counts 2021-09-06 01:53:27 -07:00
c8061e0d5c 🧽 Cleanup of various UX aspects 2021-09-04 22:11:59 -07:00
664f359ef7 🔧 Fixed loading state for DC++ search and type errors in bluelovers fast-glob module 2021-09-04 17:30:56 -07:00
bacde33567 🌄 Fixes to card component, and adding a key to DC++ search result rows 2021-09-04 15:30:58 -07:00
cdd27a1272 🔧 Added a placeholder for the actions menu 2021-09-04 12:31:06 -07:00
c098912d6e 🔧 Fixed the styling of the actions menu dropdown 2021-09-03 14:23:56 -07:00
e202ae45a4 💧 Added react-select to implement dropdown actions menu 2021-09-03 13:33:13 -07:00
1f8e0d6ff6 🔧 Fixes to AirDCPP searching WIP
1. Fixed the iterable for search results
2. Updated README
3. Listeners still a ?
2021-09-02 14:45:44 -07:00
3fc22c74ef 🔧 Adding corrected AirDCPP integration
1. Search event listeners
2. Reducer refactoring
2021-09-02 08:18:58 -07:00
c02b512e1c 🔧 Added port information to README 2021-09-02 00:21:17 -07:00
ae7a78de4d 🔧 Replaced the sliding panel component 2021-09-02 00:16:35 -07:00
4a4e04ff2b ✏️ Added instructions to create folders 2021-09-01 15:01:41 -07:00
96e8e1fb17 🔧 More configuration changes to docker-compose 2021-09-01 14:57:47 -07:00
7b29897c9f 📝 Added newlines in README 2021-09-01 12:27:01 -07:00
66aac9e35c 🐳 docker-compose volume mappings 2021-09-01 12:25:39 -07:00
762b5cb037 🐳 Added mongo container and fixed an ENV var 2021-09-01 09:06:26 -07:00
cc8d3febdb Merge pull request #2 from rishighan/dependabot/npm_and_yarn/tar-6.1.11
Bump tar from 6.1.5 to 6.1.11
2021-08-31 16:53:39 -07:00
dependabot[bot]
b18418f9ae Bump tar from 6.1.5 to 6.1.11
Bumps [tar](https://github.com/npm/node-tar) from 6.1.5 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.5...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 23:50:13 +00:00
ec914167e7 💽 Added mongo image 2021-08-31 13:22:07 -07:00
8f3fda5551 📝Edits to header in README 2021-08-31 13:03:30 -07:00
86d029ffba 📝 Edits to README 2021-08-31 12:51:53 -07:00
b3799edcc6 🐳 Added import service config 2021-08-31 12:48:37 -07:00
632b2dc267 🐳 Docker configuration change 2021-08-31 09:32:59 -07:00
5141d387b7 🐳 Added some alpine deps to the Dockerfile 2021-08-29 09:22:50 -07:00
d1fb000cf0 📝 Updated README 2021-08-28 15:25:38 -07:00
ee30a11123 🐳 Added dockerfile and README 2021-08-27 09:53:50 -07:00
482e890d95 🔧 Refactoring 2021-08-27 09:30:35 -07:00
4ebe543f6a 🔧 Refactoring the airdcpp reducer 2021-08-26 21:47:05 -07:00
2960c2dca4 ⬇️ Added bundles to downloads tab for corresponding comic book object 2021-08-25 23:07:12 -07:00
6994da072d 🔧 Fixed bugs with tab changes and state 2021-08-24 21:59:59 -07:00
140f690c77 ⬇️ Download panel first draft 2021-08-24 12:45:22 -07:00
8a1e6fb98a 🔧 AirDCPP metadata and download first draft 2021-08-23 14:34:55 -07:00
e5870fa1a8 🪛 WIP on the DCPP search results 2021-08-23 07:36:24 -07:00
0a269b33fb 🔧 Beautifying AirDC++ search results 2021-08-22 10:03:32 -07:00
9440cafe86 🔎 Displaying AirDCPP results on the UI 2021-08-20 23:53:36 -07:00
38fc53c8c3 🔧 AirDCPP UI integration 2021-08-20 15:19:29 -07:00
f6650fc18d 💄 Beautified metadata tabs 2021-08-20 09:17:27 -07:00
7aa3db125b 🌁 Generalizing the card component Part 1 2021-08-19 15:38:18 -07:00
2184b20887 🔧 Ability to import CV search result into mongo 2021-08-18 22:16:15 -07:00
d7f0bdcdfe 💽 Wiring up simple mongo import from CV result 2021-08-18 13:06:13 -07:00
d9003bf2c7 🔧 WIP AirDCPP sockets 2021-08-17 20:58:14 -07:00
9b880a2749 🔧 Updated ThreeTwo types 2021-08-17 09:55:39 -07:00
afdff65b6b 🔧 Fixed table pagination controls and counts 2021-08-16 22:11:44 -07:00
d92b2246cb 🏓 Library table styling WIP 2021-08-16 13:44:03 -07:00
7c8ee36505 🔌 Working UI integration airdcpp-socket 2021-08-14 23:40:03 -07:00
a20da523b2 🔌 AirDC++ socket scaffold 2021-08-13 08:47:11 -07:00
9aee8176b0 🪧 Logo and positioning 2021-08-12 16:44:52 -07:00
d25720caba 🪢 Wired up CV basic search 2021-08-11 23:05:37 -07:00
0baf3e436f WIP 2021-08-11 16:23:37 -07:00
a2768d18e9 🔎 Search UI build 2021-08-11 16:23:15 -07:00
2226f5cc03 🪛 Building out the search page 2021-08-10 15:11:52 -07:00
4080572c44 🔎 Search page scaffold 2021-08-10 08:22:18 -07:00
3c8a330336 🔧 Fixed a bad state and got metadata tab groups working 2021-08-09 16:02:34 -07:00
2a2d996d54 🪛 Adding details to the ComicDetail tab groups 2021-08-09 14:05:18 -07:00
49759929af 📆 Added timestamp for last CV scrape application 2021-08-07 01:14:10 -07:00
b2fb21146d 🤼‍♀️ Ability to apply a select ComicVine match first draft 2021-08-06 13:43:42 -07:00
8df48ea6ae 🏗 Scaffold for CV metadata import 2021-08-05 09:17:56 -07:00
05fb0fad2b 📝 Refactoring 2021-08-04 15:46:49 -07:00
0de430f826 Merge pull request #1 from rishighan/dependabot/npm_and_yarn/tar-6.1.5 2021-08-04 09:16:50 -07:00
7eed36134e 🔧 WIP Library 2021-08-03 22:11:16 -07:00
dependabot[bot]
f22e7e52be Bump tar from 6.1.0 to 6.1.5
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.5.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.5)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-04 04:20:06 +00:00
24eaeff9bc 🏓 Library page scaffold 2021-08-03 13:16:45 -07:00
7c3035fcd5 ☝🏼👇🏼 Sorting the ComicVine search matches 2021-08-03 08:48:10 -07:00
802dee8857 🧪 AirDCPP Socket scaffold 2021-07-31 13:24:54 -07:00
ae5f0b2c0f 🔍 CV match scorer WIP 2021-07-31 12:01:29 -07:00
5a7f30f007 🪛 Refactoring to use the updated rendition path 2021-07-27 22:02:12 -07:00
57f7621c0e 🤼‍♀️ Comic Vine Match algorithm, 2nd draft 2021-07-22 16:07:52 -07:00
233 changed files with 49886 additions and 29344 deletions

View File

@@ -0,0 +1,163 @@
<p align="center">
<img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" />
</p>
<h1 align="center">caveman-compress</h1>
<p align="center">
<strong>shrink memory file. save token every session.</strong>
</p>
---
A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically.
Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever.
## What It Do
```
/caveman:compress CLAUDE.md
```
```
CLAUDE.md ← compressed (Claude reads this — fewer tokens every session)
CLAUDE.original.md ← human-readable backup (you edit this)
```
Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits.
## Benchmarks
Real results on real project files:
| File | Original | Compressed | Saved |
|------|----------:|----------:|------:|
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
| `project-notes.md` | 1145 | 535 | **53.3%** |
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
| **Average** | **898** | **481** | **46%** |
All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly.
## Before / After
<table>
<tr>
<td width="50%">
### 📄 Original (706 tokens)
> "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime."
</td>
<td width="50%">
### 🪨 Caveman (285 tokens)
> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early."
</td>
</tr>
</table>
**Same instructions. 60% fewer tokens. Every. Single. Session.**
## Security
`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do.
## Install
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman:compress`.
If you need local files, the compress skill lives at:
```bash
caveman-compress/
```
**Requires:** Python 3.10+
## Usage
```
/caveman:compress <filepath>
```
Examples:
```
/caveman:compress CLAUDE.md
/caveman:compress docs/preferences.md
/caveman:compress todos.md
```
### What files work
| Type | Compress? |
|------|-----------|
| `.md`, `.txt`, `.rst` | ✅ Yes |
| Extensionless natural language | ✅ Yes |
| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |
| `*.original.md` | ❌ Skip (backup files) |
## How It Work
```
/caveman:compress CLAUDE.md
detect file type (no tokens)
Claude compresses (tokens — one call)
validate output (no tokens)
checks: headings, code blocks, URLs, file paths, bullets
if errors: Claude fixes cherry-picked issues only (tokens — targeted fix)
does NOT recompress — only patches broken parts
retry up to 2 times
write compressed → CLAUDE.md
write original → CLAUDE.original.md
```
Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.
## What Is Preserved
Caveman compress natural language. It never touch:
- Code blocks (` ``` ` fenced or indented)
- Inline code (`` `backtick content` ``)
- URLs and links
- File paths (`/src/components/...`)
- Commands (`npm install`, `git commit`)
- Technical terms, library names, API names
- Headings (exact text preserved)
- Tables (structure preserved, cell text compressed)
- Dates, version numbers, numeric values
## Why This Matter
`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote.
Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste.
```
┌────────────────────────────────────────────┐
│ TOKEN SAVINGS PER FILE █████ 46% │
│ SESSIONS THAT BENEFIT ██████████ 100% │
│ INFORMATION PRESERVED ██████████ 100% │
│ SETUP TIME █ 1x │
└────────────────────────────────────────────┘
```
## Part of Caveman
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy.
- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%)
- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)

View File

@@ -0,0 +1,31 @@
# Security
## Snyk High Risk Rating
`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.
### What triggers the rating
1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument.
2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written.
### What the skill does NOT do
- Does not execute user file content as code
- Does not make network requests except to Anthropic's API (via SDK or CLI)
- Does not access files outside the path the user provides
- Does not use shell=True or string interpolation in subprocess calls
- Does not collect or transmit any data beyond the file being compressed
### Auth behavior
If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.
### File size limit
Files larger than 500KB are rejected before any API call is made.
### Reporting a vulnerability
If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.

View File

@@ -0,0 +1,111 @@
---
name: caveman-compress
description: >
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
to save input tokens. Preserves all technical substance, code, URLs, and structure.
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
Trigger: /caveman:compress <filepath> or "compress memory file"
---
# Caveman Compress
## Purpose
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
## Trigger
`/caveman:compress <filepath>` or when user asks to compress a memory file.
## Process
1. The compression scripts live in `caveman-compress/scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `caveman-compress/scripts/__main__.py`.
2. Run:
cd caveman-compress && python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- if still failing after 2 retries: report error to user, leave original file untouched
4. Return result to user
## Compression Rules
### Remove
- Articles: a, an, the
- Filler: just, really, basically, actually, simply, essentially, generally
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
- Hedging: "it might be worth", "you could consider", "it would be good to"
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
- Connective fluff: "however", "furthermore", "additionally", "in addition"
### Preserve EXACTLY (never modify)
- Code blocks (fenced ``` and indented)
- Inline code (`backtick content`)
- URLs and links (full URLs, markdown links)
- File paths (`/src/components/...`, `./config.yaml`)
- Commands (`npm install`, `git commit`, `docker build`)
- Technical terms (library names, API names, protocols, algorithms)
- Proper nouns (project names, people, companies)
- Dates, version numbers, numeric values
- Environment variables (`$HOME`, `NODE_ENV`)
### Preserve Structure
- All markdown headings (keep exact heading text, compress body below)
- Bullet point hierarchy (keep nesting level)
- Numbered lists (keep numbering)
- Tables (compress cell text, keep structure)
- Frontmatter/YAML headers in markdown files
### Compress
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
- Drop "you should", "make sure to", "remember to" — just state the action
- Merge redundant bullets that say the same thing differently
- Keep one example where multiple examples show the same pattern
CRITICAL RULE:
Anything inside ``` ... ``` must be copied EXACTLY.
Do not:
- remove comments
- remove spacing
- reorder lines
- shorten commands
- simplify anything
Inline code (`...`) must be preserved EXACTLY.
Do not modify anything inside backticks.
If file contains code blocks:
- Treat code blocks as read-only regions
- Only compress text outside them
- Do not merge sections around code
## Pattern
Original:
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
Compressed:
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
Original:
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
Compressed:
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
## Boundaries
- ONLY compress natural language files (.md, .txt, extensionless)
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- If file has mixed content (prose + code), compress ONLY the prose sections
- If unsure whether something is code or prose, leave it unchanged
- Original file is backed up as FILE.original.md before overwriting
- Never compress FILE.original.md (skip it)

View File

@@ -0,0 +1,9 @@
"""Caveman compress scripts.
This package provides tools to compress natural language markdown files
into caveman format to save input tokens.
"""
__all__ = ["cli", "compress", "detect", "validate"]
__version__ = "1.0.0"

View File

@@ -0,0 +1,3 @@
from .cli import main
main()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
# Support both direct execution and module import
try:
from .validate import validate
except ImportError:
sys.path.insert(0, str(Path(__file__).parent))
from validate import validate
try:
import tiktoken
_enc = tiktoken.get_encoding("o200k_base")
except ImportError:
_enc = None
def count_tokens(text):
if _enc is None:
return len(text.split()) # fallback: word count
return len(_enc.encode(text))
def benchmark_pair(orig_path: Path, comp_path: Path):
orig_text = orig_path.read_text()
comp_text = comp_path.read_text()
orig_tokens = count_tokens(orig_text)
comp_tokens = count_tokens(comp_text)
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
result = validate(orig_path, comp_path)
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
def print_table(rows):
print("\n| File | Original | Compressed | Saved % | Valid |")
print("|------|----------|------------|---------|-------|")
for r in rows:
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'' if r[4] else ''} |")
def main():
# Direct file pair: python3 benchmark.py original.md compressed.md
if len(sys.argv) == 3:
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
if not orig.exists():
print(f"❌ Not found: {orig}")
sys.exit(1)
if not comp.exists():
print(f"❌ Not found: {comp}")
sys.exit(1)
print_table([benchmark_pair(orig, comp)])
return
# Glob mode: repo_root/tests/caveman-compress/
tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress"
if not tests_dir.exists():
print(f"❌ Tests dir not found: {tests_dir}")
sys.exit(1)
rows = []
for orig in sorted(tests_dir.glob("*.original.md")):
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
if comp.exists():
rows.append(benchmark_pair(orig, comp))
if not rows:
print("No compressed file pairs found.")
return
print_table(rows)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Caveman Compress CLI
Usage:
caveman <filepath>
"""
import sys
from pathlib import Path
from .compress import compress_file
from .detect import detect_file_type, should_compress
def print_usage():
print("Usage: caveman <filepath>")
def main():
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
filepath = Path(sys.argv[1])
# Check file exists
if not filepath.exists():
print(f"❌ File not found: {filepath}")
sys.exit(1)
if not filepath.is_file():
print(f"❌ Not a file: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
# Detect file type
file_type = detect_file_type(filepath)
print(f"Detected: {file_type}")
# Check if compressible
if not should_compress(filepath):
print("Skipping: file is not natural language (code/config)")
sys.exit(0)
print("Starting caveman compression...\n")
try:
success = compress_file(filepath)
if success:
print("\nCompression completed successfully")
backup_path = filepath.with_name(filepath.stem + ".original.md")
print(f"Compressed: {filepath}")
print(f"Original: {backup_path}")
sys.exit(0)
else:
print("\n❌ Compression failed after retries")
sys.exit(2)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import re
import subprocess
from pathlib import Path
from typing import List
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# Filenames and paths that almost certainly hold secrets or PII. Compressing
# them ships raw bytes to the Anthropic API — a third-party data boundary that
# developers on sensitive codebases cannot cross. detect.py already skips .env
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
# slip through the natural-language filter. This is a hard refuse before read.
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def is_sensitive_path(filepath: Path) -> bool:
"""Heuristic denylist for files that must never be shipped to a third-party API."""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# Normalize separators so "api-key" and "api_key" both match "apikey".
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth)
try:
result = subprocess.run(
["claude", "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
# Resolve and validate path
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
# Refuse files that look like they contain secrets or PII. Compressing ships
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
# loudly rather than silently exfiltrate credentials or keys. Override is
# intentional: the user must rename the file if the heuristic is wrong.
if is_sensitive_path(filepath):
raise ValueError(
f"Refusing to compress {filepath}: filename looks sensitive "
"(credentials, keys, secrets, or known private paths). "
"Compression sends file contents to the Anthropic API. "
"Rename the file if this is a false positive."
)
print(f"Processing: {filepath}")
if not should_compress(filepath):
print("Skipping (not natural language)")
return False
original_text = filepath.read_text(errors="ignore")
backup_path = filepath.with_name(filepath.stem + ".original.md")
# Check if backup already exists to prevent accidental overwriting
if backup_path.exists():
print(f"⚠️ Backup file already exists: {backup_path}")
print("The original backup may contain important content.")
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
return False
# Step 1: Compress
print("Compressing with Claude...")
compressed = call_claude(build_compress_prompt(original_text))
# Save original as backup, write compressed to original path
backup_path.write_text(original_text)
filepath.write_text(compressed)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\nValidation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
filepath.write_text(original_text)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
filepath.write_text(compressed)
return True

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
import json
import re
from pathlib import Path
# Extensions that are natural language and compressible
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst"}
# Extensions that are code/config and should be skipped
SKIP_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
# Patterns that indicate a line is code
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
re.compile(r"^\s*(def |class |function |async function |export )"),
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
re.compile(r"^\s*@\w+"), # decorators/annotations
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
]
def _is_code_line(line: str) -> bool:
"""Check if a line looks like code."""
return any(p.match(line) for p in CODE_PATTERNS)
def _is_json_content(text: str) -> bool:
"""Check if content is valid JSON."""
try:
json.loads(text)
return True
except (json.JSONDecodeError, ValueError):
return False
def _is_yaml_content(lines: list[str]) -> bool:
"""Heuristic: check if content looks like YAML."""
yaml_indicators = 0
for line in lines[:30]:
stripped = line.strip()
if stripped.startswith("---"):
yaml_indicators += 1
elif re.match(r"^\w[\w\s]*:\s", stripped):
yaml_indicators += 1
elif stripped.startswith("- ") and ":" in stripped:
yaml_indicators += 1
# If most non-empty lines look like YAML
non_empty = sum(1 for l in lines[:30] if l.strip())
return non_empty > 0 and yaml_indicators / non_empty > 0.6
def detect_file_type(filepath: Path) -> str:
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
Returns:
One of: 'natural_language', 'code', 'config', 'unknown'
"""
ext = filepath.suffix.lower()
# Extension-based classification
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
if ext in SKIP_EXTENSIONS:
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
# Extensionless files (like CLAUDE.md, TODO) — check content
if not ext:
try:
text = filepath.read_text(errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
return "config"
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
non_empty = sum(1 for l in lines if l.strip())
if non_empty > 0 and code_lines / non_empty > 0.4:
return "code"
return "natural_language"
return "unknown"
def should_compress(filepath: Path) -> bool:
"""Return True if the file is natural language and should be compressed."""
if not filepath.is_file():
return False
# Skip backup files
if filepath.name.endswith(".original.md"):
return False
return detect_file_type(filepath) == "natural_language"
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python detect.py <file1> [file2] ...")
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
import re
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
# crude but effective path detection
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
def __init__(self):
self.is_valid = True
self.errors = []
self.warnings = []
def add_error(self, msg):
self.is_valid = False
self.errors.append(msg)
def add_warning(self, msg):
self.warnings.append(msg)
def read_file(path: Path) -> str:
return path.read_text(errors="ignore")
# ---------- Extractors ----------
def extract_headings(text):
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
def extract_code_blocks(text):
"""Line-based fenced code block extractor.
Handles ``` and ~~~ fences with variable length (CommonMark: closing
fence must use same char and be at least as long as opening). Supports
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
content).
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
# Unclosed fences are silently skipped — they indicate malformed markdown
# and including them would cause false-positive validation failures.
return blocks
def extract_urls(text):
return set(URL_REGEX.findall(text))
def extract_paths(text):
return set(PATH_REGEX.findall(text))
def count_bullets(text):
return len(BULLET_REGEX.findall(text))
# ---------- Validators ----------
def validate_headings(orig, comp, result):
h1 = extract_headings(orig)
h2 = extract_headings(comp)
if len(h1) != len(h2):
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
if h1 != h2:
result.add_warning("Heading text/order changed")
def validate_code_blocks(orig, comp, result):
c1 = extract_code_blocks(orig)
c2 = extract_code_blocks(comp)
if c1 != c2:
result.add_error("Code blocks not preserved exactly")
def validate_urls(orig, comp, result):
u1 = extract_urls(orig)
u2 = extract_urls(comp)
if u1 != u2:
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
def validate_paths(orig, comp, result):
p1 = extract_paths(orig)
p2 = extract_paths(comp)
if p1 != p2:
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
def validate_bullets(orig, comp, result):
b1 = count_bullets(orig)
b2 = count_bullets(comp)
if b1 == 0:
return
diff = abs(b1 - b2) / b1
if diff > 0.15:
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
# ---------- Main ----------
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
result = ValidationResult()
orig = read_file(original_path)
comp = read_file(compressed_path)
validate_headings(orig, comp, result)
validate_code_blocks(orig, comp, result)
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
return result
# ---------- CLI ----------
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
print(f"\nValid: {res.is_valid}")
if res.errors:
print("\nErrors:")
for e in res.errors:
print(f" - {e}")
if res.warnings:
print("\nWarnings:")
for w in res.warnings:
print(f" - {w}")

View File

@@ -0,0 +1,59 @@
---
name: caveman-help
description: >
Quick-reference card for all caveman modes, skills, and commands.
One-shot display, not a persistent mode. Trigger: /caveman-help,
"caveman help", "what caveman commands", "how do I use caveman".
---
# Caveman Help
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
## Modes
| Mode | Trigger | What change |
|------|---------|-------------|
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
Mode stick until changed or session end.
## Skills
| Skill | Trigger | What it do |
|-------|---------|-----------|
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
| **caveman-compress** | `/caveman:compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
| **caveman-help** | `/caveman-help` | This card. |
## Deactivate
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
## Configure Default Mode
Default mode = `full`. Change it:
**Environment variable** (highest priority):
```bash
export CAVEMAN_DEFAULT_MODE=ultra
```
**Config file** (`~/.config/caveman/config.json`):
```json
{ "defaultMode": "lite" }
```
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
Resolution: env var > config file > `full`.
## More
Full docs: https://github.com/JuliusBrussee/caveman

View File

@@ -0,0 +1,67 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照致重繪。useMemo .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.

View File

@@ -0,0 +1,111 @@
---
name: compress
description: >
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
to save input tokens. Preserves all technical substance, code, URLs, and structure.
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
Trigger: /caveman:compress <filepath> or "compress memory file"
---
# Caveman Compress
## Purpose
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
## Trigger
`/caveman:compress <filepath>` or when user asks to compress a memory file.
## Process
1. This SKILL.md lives alongside `scripts/` in the same directory. Find that directory.
2. Run:
cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- if still failing after 2 retries: report error to user, leave original file untouched
4. Return result to user
## Compression Rules
### Remove
- Articles: a, an, the
- Filler: just, really, basically, actually, simply, essentially, generally
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
- Hedging: "it might be worth", "you could consider", "it would be good to"
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
- Connective fluff: "however", "furthermore", "additionally", "in addition"
### Preserve EXACTLY (never modify)
- Code blocks (fenced ``` and indented)
- Inline code (`backtick content`)
- URLs and links (full URLs, markdown links)
- File paths (`/src/components/...`, `./config.yaml`)
- Commands (`npm install`, `git commit`, `docker build`)
- Technical terms (library names, API names, protocols, algorithms)
- Proper nouns (project names, people, companies)
- Dates, version numbers, numeric values
- Environment variables (`$HOME`, `NODE_ENV`)
### Preserve Structure
- All markdown headings (keep exact heading text, compress body below)
- Bullet point hierarchy (keep nesting level)
- Numbered lists (keep numbering)
- Tables (compress cell text, keep structure)
- Frontmatter/YAML headers in markdown files
### Compress
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
- Drop "you should", "make sure to", "remember to" — just state the action
- Merge redundant bullets that say the same thing differently
- Keep one example where multiple examples show the same pattern
CRITICAL RULE:
Anything inside ``` ... ``` must be copied EXACTLY.
Do not:
- remove comments
- remove spacing
- reorder lines
- shorten commands
- simplify anything
Inline code (`...`) must be preserved EXACTLY.
Do not modify anything inside backticks.
If file contains code blocks:
- Treat code blocks as read-only regions
- Only compress text outside them
- Do not merge sections around code
## Pattern
Original:
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
Compressed:
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
Original:
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
Compressed:
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
## Boundaries
- ONLY compress natural language files (.md, .txt, extensionless)
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- If file has mixed content (prose + code), compress ONLY the prose sections
- If unsure whether something is code or prose, leave it unchanged
- Original file is backed up as FILE.original.md before overwriting
- Never compress FILE.original.md (skip it)

View File

@@ -0,0 +1,9 @@
"""Caveman compress scripts.
This package provides tools to compress natural language markdown files
into caveman format to save input tokens.
"""
__all__ = ["cli", "compress", "detect", "validate"]
__version__ = "1.0.0"

View File

@@ -0,0 +1,3 @@
from .cli import main
main()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
# Support both direct execution and module import
try:
from .validate import validate
except ImportError:
sys.path.insert(0, str(Path(__file__).parent))
from validate import validate
try:
import tiktoken
_enc = tiktoken.get_encoding("o200k_base")
except ImportError:
_enc = None
def count_tokens(text):
if _enc is None:
return len(text.split()) # fallback: word count
return len(_enc.encode(text))
def benchmark_pair(orig_path: Path, comp_path: Path):
orig_text = orig_path.read_text()
comp_text = comp_path.read_text()
orig_tokens = count_tokens(orig_text)
comp_tokens = count_tokens(comp_text)
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
result = validate(orig_path, comp_path)
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
def print_table(rows):
print("\n| File | Original | Compressed | Saved % | Valid |")
print("|------|----------|------------|---------|-------|")
for r in rows:
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'' if r[4] else ''} |")
def main():
# Direct file pair: python3 benchmark.py original.md compressed.md
if len(sys.argv) == 3:
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
if not orig.exists():
print(f"❌ Not found: {orig}")
sys.exit(1)
if not comp.exists():
print(f"❌ Not found: {comp}")
sys.exit(1)
print_table([benchmark_pair(orig, comp)])
return
# Glob mode: repo_root/tests/caveman-compress/
tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress"
if not tests_dir.exists():
print(f"❌ Tests dir not found: {tests_dir}")
sys.exit(1)
rows = []
for orig in sorted(tests_dir.glob("*.original.md")):
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
if comp.exists():
rows.append(benchmark_pair(orig, comp))
if not rows:
print("No compressed file pairs found.")
return
print_table(rows)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Caveman Compress CLI
Usage:
caveman <filepath>
"""
import sys
from pathlib import Path
from .compress import compress_file
from .detect import detect_file_type, should_compress
def print_usage():
print("Usage: caveman <filepath>")
def main():
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
filepath = Path(sys.argv[1])
# Check file exists
if not filepath.exists():
print(f"❌ File not found: {filepath}")
sys.exit(1)
if not filepath.is_file():
print(f"❌ Not a file: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
# Detect file type
file_type = detect_file_type(filepath)
print(f"Detected: {file_type}")
# Check if compressible
if not should_compress(filepath):
print("Skipping: file is not natural language (code/config)")
sys.exit(0)
print("Starting caveman compression...\n")
try:
success = compress_file(filepath)
if success:
print("\nCompression completed successfully")
backup_path = filepath.with_name(filepath.stem + ".original.md")
print(f"Compressed: {filepath}")
print(f"Original: {backup_path}")
sys.exit(0)
else:
print("\n❌ Compression failed after retries")
sys.exit(2)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import re
import subprocess
from pathlib import Path
from typing import List
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# Filenames and paths that almost certainly hold secrets or PII. Compressing
# them ships raw bytes to the Anthropic API — a third-party data boundary that
# developers on sensitive codebases cannot cross. detect.py already skips .env
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
# slip through the natural-language filter. This is a hard refuse before read.
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def is_sensitive_path(filepath: Path) -> bool:
"""Heuristic denylist for files that must never be shipped to a third-party API."""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# Normalize separators so "api-key" and "api_key" both match "apikey".
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth)
try:
result = subprocess.run(
["claude", "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
# Resolve and validate path
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
# Refuse files that look like they contain secrets or PII. Compressing ships
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
# loudly rather than silently exfiltrate credentials or keys. Override is
# intentional: the user must rename the file if the heuristic is wrong.
if is_sensitive_path(filepath):
raise ValueError(
f"Refusing to compress {filepath}: filename looks sensitive "
"(credentials, keys, secrets, or known private paths). "
"Compression sends file contents to the Anthropic API. "
"Rename the file if this is a false positive."
)
print(f"Processing: {filepath}")
if not should_compress(filepath):
print("Skipping (not natural language)")
return False
original_text = filepath.read_text(errors="ignore")
backup_path = filepath.with_name(filepath.stem + ".original.md")
# Check if backup already exists to prevent accidental overwriting
if backup_path.exists():
print(f"⚠️ Backup file already exists: {backup_path}")
print("The original backup may contain important content.")
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
return False
# Step 1: Compress
print("Compressing with Claude...")
compressed = call_claude(build_compress_prompt(original_text))
# Save original as backup, write compressed to original path
backup_path.write_text(original_text)
filepath.write_text(compressed)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\nValidation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
filepath.write_text(original_text)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
filepath.write_text(compressed)
return True

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
import json
import re
from pathlib import Path
# Extensions that are natural language and compressible
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst"}
# Extensions that are code/config and should be skipped
SKIP_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
# Patterns that indicate a line is code
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
re.compile(r"^\s*(def |class |function |async function |export )"),
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
re.compile(r"^\s*@\w+"), # decorators/annotations
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
]
def _is_code_line(line: str) -> bool:
"""Check if a line looks like code."""
return any(p.match(line) for p in CODE_PATTERNS)
def _is_json_content(text: str) -> bool:
"""Check if content is valid JSON."""
try:
json.loads(text)
return True
except (json.JSONDecodeError, ValueError):
return False
def _is_yaml_content(lines: list[str]) -> bool:
"""Heuristic: check if content looks like YAML."""
yaml_indicators = 0
for line in lines[:30]:
stripped = line.strip()
if stripped.startswith("---"):
yaml_indicators += 1
elif re.match(r"^\w[\w\s]*:\s", stripped):
yaml_indicators += 1
elif stripped.startswith("- ") and ":" in stripped:
yaml_indicators += 1
# If most non-empty lines look like YAML
non_empty = sum(1 for l in lines[:30] if l.strip())
return non_empty > 0 and yaml_indicators / non_empty > 0.6
def detect_file_type(filepath: Path) -> str:
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
Returns:
One of: 'natural_language', 'code', 'config', 'unknown'
"""
ext = filepath.suffix.lower()
# Extension-based classification
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
if ext in SKIP_EXTENSIONS:
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
# Extensionless files (like CLAUDE.md, TODO) — check content
if not ext:
try:
text = filepath.read_text(errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
return "config"
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
non_empty = sum(1 for l in lines if l.strip())
if non_empty > 0 and code_lines / non_empty > 0.4:
return "code"
return "natural_language"
return "unknown"
def should_compress(filepath: Path) -> bool:
"""Return True if the file is natural language and should be compressed."""
if not filepath.is_file():
return False
# Skip backup files
if filepath.name.endswith(".original.md"):
return False
return detect_file_type(filepath) == "natural_language"
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python detect.py <file1> [file2] ...")
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
import re
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
# crude but effective path detection
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
def __init__(self):
self.is_valid = True
self.errors = []
self.warnings = []
def add_error(self, msg):
self.is_valid = False
self.errors.append(msg)
def add_warning(self, msg):
self.warnings.append(msg)
def read_file(path: Path) -> str:
return path.read_text(errors="ignore")
# ---------- Extractors ----------
def extract_headings(text):
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
def extract_code_blocks(text):
"""Line-based fenced code block extractor.
Handles ``` and ~~~ fences with variable length (CommonMark: closing
fence must use same char and be at least as long as opening). Supports
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
content).
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
# Unclosed fences are silently skipped — they indicate malformed markdown
# and including them would cause false-positive validation failures.
return blocks
def extract_urls(text):
return set(URL_REGEX.findall(text))
def extract_paths(text):
return set(PATH_REGEX.findall(text))
def count_bullets(text):
return len(BULLET_REGEX.findall(text))
# ---------- Validators ----------
def validate_headings(orig, comp, result):
h1 = extract_headings(orig)
h2 = extract_headings(comp)
if len(h1) != len(h2):
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
if h1 != h2:
result.add_warning("Heading text/order changed")
def validate_code_blocks(orig, comp, result):
c1 = extract_code_blocks(orig)
c2 = extract_code_blocks(comp)
if c1 != c2:
result.add_error("Code blocks not preserved exactly")
def validate_urls(orig, comp, result):
u1 = extract_urls(orig)
u2 = extract_urls(comp)
if u1 != u2:
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
def validate_paths(orig, comp, result):
p1 = extract_paths(orig)
p2 = extract_paths(comp)
if p1 != p2:
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
def validate_bullets(orig, comp, result):
b1 = count_bullets(orig)
b2 = count_bullets(comp)
if b1 == 0:
return
diff = abs(b1 - b2) / b1
if diff > 0.15:
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
# ---------- Main ----------
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
result = ValidationResult()
orig = read_file(original_path)
comp = read_file(compressed_path)
validate_headings(orig, comp, result)
validate_code_blocks(orig, comp, result)
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
return result
# ---------- CLI ----------
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
print(f"\nValid: {res.is_valid}")
if res.errors:
print("\nErrors:")
for e in res.errors:
print(f" - {e}")
if res.warnings:
print("\nWarnings:")
for w in res.warnings:
print(f" - {w}")

View File

@@ -1,17 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/transform-runtime",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": ["minify"]
}
}
}

1
.claude/skills/caveman Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/caveman

View File

@@ -0,0 +1 @@
../../.agents/skills/caveman-compress

1
.claude/skills/caveman-help Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/caveman-help

1
.claude/skills/compress Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/compress

View File

@@ -0,0 +1,379 @@
---
name: jsdoc
description: Commenting and documentation guidelines. Auto-activate when the user discusses comments, documentation, docstrings, code clarity, API docs, JSDoc, or asks about commenting strategies.
---
Auto-activate when: User discusses comments, documentation, docstrings, code clarity, code quality, API docs, JSDoc, Python docstrings, or asks about commenting strategies.
Core Principle
Write code that speaks for itself. Comment only when necessary to explain WHY, not WHAT.
Most code does not need comments. Well-written code with clear naming and structure is self-documenting.
The best comment is the one you don't need to write because the code is already obvious.
The Commenting Philosophy
When to Comment
✅ DO comment when explaining:
WHY something is done (business logic, design decisions)
Complex algorithms and their reasoning
Non-obvious trade-offs or constraints
Workarounds for bugs or limitations
API contracts and public interfaces
Regex patterns and what they match
Performance considerations or optimizations
Constants and magic numbers
Gotchas or surprising behaviors
❌ DON'T comment when:
The code is obvious and self-explanatory
The comment repeats the code (redundant)
Better naming would eliminate the need
The comment would become outdated quickly
It's decorative or organizational noise
It states what a standard language construct does
Comment Anti-Patterns
❌ 1. Obvious Comments
BAD:
counter = 0 # Initialize counter to zero
counter += 1 # Increment counter by one
user_name = input("Enter name: ") # Get user name from input
Better: No comment needed - the code is self-explanatory.
❌ 2. Redundant Comments
BAD:
def get_user_name(user):
return user.name # Return the user's name
def calculate_total(items):
# Loop through items and sum the prices
total = 0
for item in items:
total += item.price
return total
Better:
def get_user_name(user):
return user.name
def calculate_total(items):
return sum(item.price for item in items)
❌ 3. Outdated Comments
BAD:
# Calculate tax at 5% rate
tax = price * 0.08 # Actually 8%, comment is wrong
# DEPRECATED: Use new_api_function() instead
def old_function(): # Still being used, comment is misleading
pass
Better: Keep comments in sync with code, or remove them entirely.
❌ 4. Noise Comments
BAD:
# Start of function
def calculate():
# Declare variable
result = 0
# Return result
return result
# End of function
Better: Remove all of these comments.
❌ 5. Dead Code & Changelog Comments
BAD:
# Don't comment out code - use version control
# def old_function():
# return "deprecated"
# Don't maintain history in comments
# Modified by John on 2023-01-15
# Fixed bug reported by Sarah on 2023-02-03
Better: Delete the code. Git has the history.
Good Comment Examples
✅ Complex Business Logic
# Apply progressive tax brackets: 10% up to $10k, 20% above
# This matches IRS publication 501 for 2024
def calculate_progressive_tax(income):
if income <= 10000:
return income * 0.10
else:
return 1000 + (income - 10000) * 0.20
✅ Non-obvious Algorithms
# Using Floyd-Warshall for all-pairs shortest paths
# because we need distances between all nodes.
# Time: O(n³), Space: O(n²)
for k in range(vertices):
for i in range(vertices):
for j in range(vertices):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
✅ Regex Patterns
# Match email format: username@domain.extension
# Allows letters, numbers, dots, hyphens in username
# Requires valid domain and 2+ char extension
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
✅ API Constraints or Gotchas
# GitHub API rate limit: 5000 requests/hour for authenticated users
# We implement exponential backoff to handle rate limiting
await rate_limiter.wait()
response = await fetch(github_api_url)
✅ Workarounds for Bugs
# HACK: Workaround for bug in library v2.1.0
# Remove after upgrading to v2.2.0
# See: https://github.com/library/issues/123
if library_version == "2.1.0":
apply_workaround()
Decision Framework
Before writing a comment, ask yourself:
Step 1: Is the code self-explanatory?
If YES → No comment needed
If NO → Continue to step 2
Step 2: Would a better variable/function name eliminate the need?
If YES → Refactor the code instead
If NO → Continue to step 3
Step 3: Does this explain WHY, not WHAT?
If explaining WHAT → Refactor code to be clearer
If explaining WHY → Good comment candidate
Step 4: Will this help future maintainers?
If YES → Write the comment
If NO → Skip it
Special Cases for Comments
Public APIs and Docstrings
Python Docstrings
def calculate_compound_interest(
principal: float,
rate: float,
time: int,
compound_frequency: int = 1
) -> float:
"""
Calculate compound interest using the standard formula.
Args:
principal: Initial amount invested
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Time period in years
compound_frequency: Times per year interest compounds (default: 1)
Returns:
Final amount after compound interest
Raises:
ValueError: If any parameter is negative
Example:
>>> calculate_compound_interest(1000, 0.05, 10)
1628.89
"""
if principal < 0 or rate < 0 or time < 0:
raise ValueError("Parameters must be non-negative")
# Compound interest formula: A = P(1 + r/n)^(nt)
return principal * (1 + rate / compound_frequency) ** (compound_frequency * time)
JavaScript/TypeScript JSDoc
/**
* Fetch user data from the API.
*
* @param {string} userId - The unique user identifier
* @param {Object} options - Configuration options
* @param {boolean} options.includeProfile - Include profile data (default: true)
* @param {number} options.timeout - Request timeout in ms (default: 5000)
*
* @returns {Promise<User>} User object with requested fields
*
* @throws {Error} If userId is invalid or request fails
*
* @example
* const user = await fetchUser('123', { includeProfile: true });
*/
async function fetchUser(userId, options = {}) {
// Implementation
}
Constants and Configuration
# Based on network reliability studies (95th percentile)
MAX_RETRIES = 3
# AWS Lambda timeout is 15s, leaving 5s buffer for cleanup
API_TIMEOUT = 10000 # milliseconds
# Cache duration optimized for balance between freshness and load
# See: docs/performance-tuning.md
CACHE_TTL = 300 # 5 minutes
Annotations for TODOs and Warnings
# TODO: Replace with proper authentication after security review
# Issue: #456
def temporary_auth(user):
return True
# WARNING: This function modifies the original array instead of creating a copy
def sort_in_place(arr):
arr.sort()
return arr
# FIXME: Memory leak in production - investigate connection pooling
# Ticket: JIRA-789
def get_connection():
return create_connection()
# PERF: Consider caching this result if called frequently in hot path
def expensive_calculation(data):
return complex_algorithm(data)
# SECURITY: Validate input to prevent SQL injection before using in query
def build_query(user_input):
sanitized = escape_sql(user_input)
return f"SELECT * FROM users WHERE name = '{sanitized}'"
Common Annotation Keywords
TODO: - Work that needs to be done
FIXME: - Known bugs that need fixing
HACK: - Temporary workarounds
NOTE: - Important information or context
WARNING: - Critical information about usage
PERF: - Performance considerations
SECURITY: - Security-related notes
BUG: - Known bug documentation
REFACTOR: - Code that needs refactoring
DEPRECATED: - Soon-to-be-removed code
Refactoring Over Commenting
Instead of Commenting Complex Code...
BAD: Complex code with comment
# Check if user is admin or has special permissions
if user.role == "admin" or (user.permissions and "special" in user.permissions):
grant_access()
...Extract to Named Function
GOOD: Self-explanatory through naming
def user_has_admin_access(user):
return user.role == "admin" or has_special_permission(user)
def has_special_permission(user):
return user.permissions and "special" in user.permissions
if user_has_admin_access(user):
grant_access()
Language-Specific Examples
JavaScript
// Good: Explains WHY we debounce
// Debounce search to reduce API calls (500ms wait after last keystroke)
const debouncedSearch = debounce(searchAPI, 500);
// Bad: Obvious
let count = 0; // Initialize count to zero
count++; // Increment count
// Good: Explains algorithm choice
// Using Set for O(1) lookup instead of Array.includes() which is O(n)
const seen = new Set(ids);
Python
# Good: Explains the algorithm choice
# Using binary search because data is sorted and we need O(log n) performance
index = bisect.bisect_left(sorted_list, target)
# Bad: Redundant
def get_total(items):
return sum(items) # Return the sum of items
# Good: Explains why we're doing this
# Extract to separate function for type checking in mypy
def validate_user(user):
if not user or not user.id:
raise ValueError("Invalid user")
return user
TypeScript
// Good: Explains the type assertion
// TypeScript can't infer this is never null after the check
const element = document.getElementById('app') as HTMLElement;
// Bad: Obvious
const sum = a + b; // Add a and b
// Good: Explains non-obvious behavior
// spread operator creates shallow copy; use JSON for deep copy
const newConfig = { ...config };
Comment Quality Checklist
Before committing, ensure your comments:
Explain WHY, not WHAT
Are grammatically correct and clear
Will remain accurate as code evolves
Add genuine value to code understanding
Are placed appropriately (above the code they describe)
Use proper spelling and professional language
Follow team conventions for annotation keywords
Could not be replaced by better naming or structure
Are not obvious statements about language features
Reference tickets/issues when applicable
Summary
Priority order:
Clear code - Self-explanatory through naming and structure
Good comments - Explain WHY when necessary
Documentation - API docs, docstrings for public interfaces
No comments - Better than bad comments that lie or clutter
Remember: Comments are a failure to make the code self-explanatory. Use them sparingly and wisely.
Key Takeaways
Goal Approach
Reduce comments Improve naming, extract functions, simplify logic
Improve clarity Use self-explanatory code structure, clear variable names
Document APIs Use docstrings/JSDoc for public interfaces
Explain WHY Comment only business logic, algorithms, workarounds
Maintain accuracy Update comments when code changes, or remove them

View File

@@ -0,0 +1,360 @@
---
name: typescript
description: TypeScript engineering guidelines based on Google's style guide. Use when writing, reviewing, or refactoring TypeScript code in this project.
---
Comprehensive guidelines for writing production-quality TypeScript based on Google's TypeScript Style Guide.
Naming Conventions
Type Convention Example
Classes, Interfaces, Types, Enums UpperCamelCase UserService, HttpClient
Variables, Parameters, Functions lowerCamelCase userName, processData
Global Constants, Enum Values CONSTANT_CASE MAX_RETRIES, Status.ACTIVE
Type Parameters Single letter or UpperCamelCase T, ResponseType
Naming Principles
Descriptive names, avoid ambiguous abbreviations
Treat acronyms as words: loadHttpUrl not loadHTTPURL
No prefixes like opt_ for optional parameters
No trailing underscores for private properties
Single-letter variables only when scope is <10 lines
Variable Declarations
// Always use const by default
const users = getUsers();
// Use let only when reassignment is needed
let count = 0;
count++;
// Never use var
// var x = 1; // WRONG
// One variable per declaration
const a = 1;
const b = 2;
// const a = 1, b = 2; // WRONG
Types and Interfaces
Prefer Type Aliases Over Interfaces
// Good: type alias for object shapes
type User = {
id: string;
name: string;
email?: string;
};
// Avoid: interface for object shapes
// interface User {
// id: string;
// name: string;
// }
// Type aliases work for everything: objects, unions, intersections, mapped types
type Status = 'active' | 'inactive';
type Combined = TypeA & TypeB;
type Handler = (event: Event) => void;
// Benefits of types over interfaces:
// 1. Consistent syntax for all type definitions
// 2. Cannot be merged/extended unexpectedly (no declaration merging)
// 3. Better for union types and computed properties
// 4. Works with utility types more naturally
Type Inference
Leverage inference for trivially inferred types:
// Good: inference is clear
const name = 'Alice';
const items = [1, 2, 3];
// Good: explicit for complex expressions
const result: ProcessedData = complexTransformation(input);
Array Types
// Simple types: use T[]
const numbers: number[];
const names: readonly string[];
// Multi-dimensional: use T[][]
const matrix: number[][];
// Complex types: use Array<T>
const handlers: Array<(event: Event) => void>;
Null and Undefined
// Prefer optional fields over union with undefined
interface Config {
timeout?: number; // Good
// timeout: number | undefined; // Avoid
}
// Type aliases must NOT include |null or |undefined
type UserId = string; // Good
// type UserId = string | null; // WRONG
// May use == for null comparison (catches both null and undefined)
if (value == null) {
// handles both null and undefined
}
Types to Avoid
// Avoid any - use unknown instead
function parse(input: unknown): Data { }
// Avoid {} - use unknown, Record<string, T>, or object
function process(obj: Record<string, unknown>): void { }
// Use lowercase primitives
let name: string; // Good
// let name: String; // WRONG
// Never use wrapper objects
// new String('hello') // WRONG
Classes
Structure
class UserService {
// Fields first, initialized where declared
private readonly cache = new Map<string, User>();
private lastAccess: Date | null = null;
// Constructor with parameter properties
constructor(
private readonly api: ApiClient,
private readonly logger: Logger,
) {}
// Methods separated by blank lines
async getUser(id: string): Promise<User> {
// ...
}
private validateId(id: string): boolean {
// ...
}
}
Visibility
class Example {
// private by default, only use public when needed externally
private internalState = 0;
// readonly for properties never reassigned after construction
readonly id: string;
// Never use #private syntax - use TypeScript visibility
// #field = 1; // WRONG
private field = 1; // Good
}
Avoid Arrow Functions as Properties
class Handler {
// Avoid: arrow function as property
// handleClick = () => { ... };
// Good: instance method
handleClick(): void {
// ...
}
}
// Bind at call site if needed
element.addEventListener('click', () => handler.handleClick());
Static Methods
Never use this in static methods
Call on defining class, not subclasses
Functions
Prefer Function Declarations
// Good: function declaration for named functions
function processData(input: Data): Result {
return transform(input);
}
// Arrow functions when type annotation needed
const handler: EventHandler = (event) => {
// ...
};
Arrow Function Bodies
// Concise body only when return value is used
const double = (x: number) => x * 2;
// Block body when return should be void
const log = (msg: string) => {
console.log(msg);
};
Parameters
// Use rest parameters, not arguments
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Destructuring for multiple optional params
interface Options {
timeout?: number;
retries?: number;
}
function fetch(url: string, { timeout = 5000, retries = 3 }: Options = {}) {
// ...
}
// Never name a parameter 'arguments'
Imports and Exports
Always Use Named Exports
// Good: named exports
export function processData() { }
export class UserService { }
export interface Config { }
// Never use default exports
// export default class UserService { } // WRONG
Import Styles
// Module import for large APIs
import * as fs from 'fs';
// Named imports for frequently used symbols
import { readFile, writeFile } from 'fs/promises';
// Type-only imports when only used as types
import type { User, Config } from './types';
Module Organization
Use modules, never namespace Foo { }
Never use require() - use ES6 imports
Use relative imports within same project
Avoid excessive ../../../
Control Structures
Always Use Braces
// Good
if (condition) {
doSomething();
}
// Exception: single-line if
if (condition) return early;
Loops
// Prefer for...of for arrays
for (const item of items) {
process(item);
}
// Use Object methods with for...of for objects
for (const [key, value] of Object.entries(obj)) {
// ...
}
// Never use unfiltered for...in on arrays
Equality
// Always use === and !==
if (a === b) { }
// Exception: == null catches both null and undefined
if (value == null) { }
Switch Statements
switch (status) {
case Status.Active:
handleActive();
break;
case Status.Inactive:
handleInactive();
break;
default:
// Always include default, even if empty
break;
}
Exception Handling
// Always throw Error instances
throw new Error('Something went wrong');
// throw 'error'; // WRONG
// Catch with unknown type
try {
riskyOperation();
} catch (e: unknown) {
if (e instanceof Error) {
logger.error(e.message);
}
throw e;
}
// Empty catch needs justification comment
try {
optional();
} catch {
// Intentionally ignored: fallback behavior handles this
}
Type Assertions
// Use 'as' syntax, not angle brackets
const input = value as string;
// const input = <string>value; // WRONG in TSX, avoid everywhere
// Double assertion through unknown when needed
const config = (rawData as unknown) as Config;
// Add comment explaining why assertion is safe
const element = document.getElementById('app') as HTMLElement;
// Safe: element exists in index.html
Strings
// Use single quotes for string literals
const name = 'Alice';
// Template literals for interpolation or multiline
const message = `Hello, ${name}!`;
const query = `
SELECT *
FROM users
WHERE id = ?
`;
// Never use backslash line continuations
Disallowed Features
Feature Alternative
var const or let
Array() constructor [] literal
Object() constructor {} literal
any type unknown
namespace modules
require() import
Default exports Named exports
#private fields private modifier
eval() Never use
const enum Regular enum
debugger Remove before commit
with Never use
Prototype modification Never modify

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
comics
userdata

View File

@@ -1,30 +0,0 @@
module.exports = {
extends: [
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors.
],
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
plugins: ["@typescript-eslint"],
settings: {
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
},
react: {
version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
// Fine tune rules
rules: {
"@typescript-eslint/no-var-requires": 0,
},
};

20
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Docker Image CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@v5
with:
name: frishi/threetwo
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

11
.gitignore vendored
View File

@@ -4,10 +4,19 @@ comics/
docs/
userdata/
dist/
storybook-static/*
src/client/assets/scss/App.css
server/
/server/
node_modules/
src/**/*.jsx
tests/__coverage__/
tests/**/*.jsx
src/client/assets/scss/App.css.map
yarn-error.log
.nova
environment.list
.env
src/client/assets/img/missing-file.pxd
*.pxd
.parcel-cache
src/stories

View File

@@ -1,4 +1,4 @@
module.exports = {
semi: true,
trailingComma: "all",
export default {
semi: true,
trailingComma: "all",
};

19
.storybook/main.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

View File

@@ -0,0 +1,3 @@
<script>
window.global = window;
</script>

18
.storybook/preview.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
backgrounds: {
default: "light",
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@@ -1,45 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
appveyor.yml
circle.yml
codeship-services.yml
codeship-steps.yml
wercker.yml
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.travis.yml
# misc
*.md

36
Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# Use Node.js 22 as the base image
FROM node:22-alpine
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
# Set the working directory inside the container
WORKDIR /threetwo
# Copy package.json and yarn.lock to leverage Docker cache
COPY package.json yarn.lock ./
# Install build dependencies necessary for native modules (for node-sass)
RUN apk --no-cache add \
g++ \
make \
python3 \
autoconf \
automake \
libtool \
nasm \
git
# Install node modules
RUN yarn install --ignore-engines
# Explicitly install sass
RUN yarn add -D sass
# Copy the rest of the application files into the container
COPY . .
# Expose the application port (default for Vite)
EXPOSE 5173
# Start the application with yarn
ENTRYPOINT ["yarn", "start"]

View File

@@ -0,0 +1,72 @@
# ThreeTwo!
ThreeTwo! _aims to be_ a comic book curation app.
[![Docker Image CI](https://github.com/rishighan/threetwo/actions/workflows/docker-image.yml/badge.svg)](https://github.com/rishighan/threetwo/actions/workflows/docker-image.yml)
### Screenshots
#### Dashboard
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/Dashboard.jpg)
#### Issue View
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/ComicDetail.jpg)
#### DC++ Search
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/DC%2B%2BSearching.jpg)
#### Import
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/Import.jpg)
#### Comic Vine Matching, Metadata Scraping
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/CVMatching.jpg)
### 🦄 Early Development Support Channel
Please help me test the early builds of `ThreeTwo!` on its official [Discord](https://discord.gg/n4HZ4j33uT)
Discuss ideas and implementations with me, and get status, progress updates!
## Dependencies
ThreeTwo! currently is set up as:
1. The UI, this repo.
2. [threetwo-core-service](https://github.com/rishighan/threetwo-core-service)
3. [threetwo-metadata-service](https://github.com/rishighan/threetwo-metadata-service)
4. [threetwo-acquisition-service](https://github.com/rishighan/threetwo-acquisition-service)
5. [threetwo-ui-typings](https://github.com/rishighan/threetwo-frontend-types) which are the types used across the UI, installable as an `npm` dependency.
## Docker Instructions
See [threetwo-docker-compose](https://github.com/rishighan/threetwo-docker-compose) for instructions on building the entire stack.
## Local Development
For debugging and troubleshooting, you can run this app locally using these steps:
1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git`
2. `yarn run dev` (you can ignore the warnings)
3. This will open `http://localhost:5173` in your default browser
4. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
## Troubleshooting
### Docker
1. `docker-compose up` is taking a long time
This is primarily because `threetwo-import-service` pulls `calibre` from the CDN and it has been known to be extremely slow. I can't find a more reliable alternative, so give it some time to finish downloading.
2. What folder do my comics go in?
Your comics go in the `comics` directory at the root of this project.
## Contribution Guidelines
See [contribution guidelines](https://github.com/rishighan/threetwo/blob/master/contributing.md)

1
__mocks__/fileMock.cjs Normal file
View File

@@ -0,0 +1 @@
module.exports = 'test-file-stub';

16
codegen.yml Normal file
View File

@@ -0,0 +1,16 @@
schema: http://localhost:3000/graphql
documents: 'src/client/graphql/**/*.graphql'
generates:
src/client/graphql/generated.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
fetcher:
func: './fetcher#fetcher'
isReactHook: false
exposeFetcher: true
exposeQueryKeys: true
addInfiniteQuery: true
reactQueryVersion: 5

59
eslint.config.js Normal file
View File

@@ -0,0 +1,59 @@
import js from "@eslint/js";
import typescript from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
import react from "eslint-plugin-react";
import prettier from "eslint-plugin-prettier";
import cssModules from "eslint-plugin-css-modules";
import storybook from "eslint-plugin-storybook";
export default [
js.configs.recommended,
{
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
"@typescript-eslint": typescript,
react,
prettier,
"css-modules": cssModules,
storybook,
},
settings: {
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
},
react: {
version: "detect",
},
},
rules: {
...typescript.configs.recommended.rules,
...react.configs.recommended.rules,
...prettier.configs.recommended.rules,
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/react-in-jsx-scope": "off",
"no-undef": "off",
},
},
{
files: ["**/*.stories.{js,jsx,ts,tsx}"],
rules: {
...storybook.configs.recommended.rules,
},
},
{
ignores: ["dist/**", "node_modules/**", "build/**"],
},
];

1
funding.yml Normal file
View File

@@ -0,0 +1 @@
github: [rishighan]

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Three Two!</title>
</head>
<body class="dark:bg-slate-600">
<div id="root"></div>
<script type="module" src="/src/client/index.tsx"></script>
</body>
</html>

28
jest.config.cjs Normal file
View File

@@ -0,0 +1,28 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.cjs',
},
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
],
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
tsconfig: {
jsx: 'react',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx',
],
};

25
jest.setup.cjs Normal file
View File

@@ -0,0 +1,25 @@
require('@testing-library/jest-dom');
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;

View File

@@ -1,22 +1,25 @@
{
"tags": { "allowUnknownTags": true },
"source": {
"include": ["./src/"],
"includePattern": "\\.(jsx|js|ts|tsx)$"
},
"plugins": [
"better-docs/component",
"better-docs/category",
"plugins/markdown",
"node_modules/better-docs/typescript"
],
"templates": { "better-docs": { "name": "My React components" } },
"opts": {
"destination": "docs/",
"readme": "README.md",
"recurse": true,
"encoding": "utf8",
"verbose": true,
"template": "node_modules/better-docs"
}
}
"tags": {
"allowUnknownTags": false
},
"source": {
"include": [
"./src/client"
],
"includePattern": "\\.(jsx|js|ts|tsx)$"
},
"plugins": [
"plugins/markdown"
],
"opts": {
"template": "node_modules/tui-jsdoc-template",
"encoding": "utf8",
"destination": "docs/",
"recurse": true,
"verbose": true
},
"templates": {
"cleverLinks": false,
"monospaceLinks": false
}
}

View File

@@ -1,13 +0,0 @@
{
"ignore": [
"**/*.test.ts",
"**/*.spec.ts",
"node_modules",
"src/client"
],
"watch": [
"src/server"
],
"exec": "tsc -p tsconfig.server.json && node server/",
"ext": "ts"
}

20050
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,150 +1,153 @@
{
"name": "threetwo",
"version": "0.0.2",
"description": "ThreeTwo! A comic book curator.",
"main": "server/index.js",
"typings": "server/index.js",
"version": "0.1.0",
"type": "module",
"description": "ThreeTwo! A good comic book curator.",
"scripts": {
"build": "webpack --mode production",
"start": "npm run build && npm run server",
"client": "webpack serve --mode development --devtool inline-source-map --hot",
"server": "tsc -p tsconfig.server.json && node server/",
"dev": "concurrently \"nodemon\" \"npm run client\"",
"server-dev": "nodemon",
"docs": "jsdoc -c jsdoc.json"
"build": "vite build",
"dev": "rimraf dist && yarn build && vite",
"start": "yarn build && vite",
"docs": "jsdoc -c jsdoc.json",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"codegen": "wait-on http-get://localhost:3000/graphql/health && graphql-codegen",
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
"knip": "knip"
},
"author": "Rishi Ghan",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.17",
"@bluelovers/fast-glob": "^3.0.4",
"@types/event-stream": "^3.3.34",
"@types/mime-types": "^2.1.0",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"@types/sharp": "^0.28.0",
"@types/socket.io": "^3.0.2",
"@types/socket.io-client": "^3.0.0",
"@types/through2": "^2.0.36",
"airdcpp-apisocket": "^2.4.1",
"antd": "^4.16.5",
"babel-polyfill": "^6.26.0",
"better-docs": "^2.3.2",
"calibre-opds": "^1.0.7",
"comlink-loader": "^2.0.0",
"ellipsize": "^0.1.0",
"event-stream": "^4.0.1",
"express": "^4.17.1",
"fastest-validator": "^1.11.0",
"final-form": "^4.20.2",
"fs-extra": "^9.1.0",
"http-response-stream": "^1.0.7",
"imghash": "^0.0.8",
"jsdoc": "^3.6.7",
"opds-extra": "^3.0.9",
"pretty-bytes": "^5.6.0",
"react": "^17.0.1",
"react-collapsible": "^2.8.3",
"react-dom": "^17.0.1",
"react-final-form": "^6.5.3",
"react-spinners": "^0.11.0",
"react-window-dynamic-list": "^2.3.5",
"sharp": "^0.28.1",
"socket.io-client": "^4.1.2",
"threetwo-ui-typings": "^1.0.1",
"voca": "^1.4.0",
"websocket": "^1.0.34",
"ws": "^7.5.3",
"ws-calibre": "bluelovers/ws-calibre",
"xregexp": "^5.0.2"
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@floating-ui/react": "^0.27.18",
"@floating-ui/react-dom": "^2.1.7",
"@popperjs/core": "^2.11.8",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.3",
"@types/mime-types": "^3.0.1",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^5.1.4",
"airdcpp-apisocket": "^3.0.0-beta.14",
"axios": "^1.13.5",
"axios-cache-interceptor": "^1.11.4",
"axios-rate-limit": "^1.6.2",
"babel-plugin-styled-components": "^2.1.4",
"date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"ellipsize": "^0.7.0",
"embla-carousel-react": "^8.6.0",
"filename-parser": "^1.0.4",
"final-form": "^5.0.0",
"final-form-arrays": "^4.0.0",
"focus-trap-react": "^12.0.0",
"graphql": "^16.13.1",
"history": "^5.3.0",
"html-to-text": "^9.0.5",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2",
"immer": "^11.1.4",
"jsdoc": "^4.0.5",
"lodash": "^4.17.23",
"motion": "^12.38.0",
"pretty-bytes": "^7.1.0",
"prop-types": "^15.8.1",
"qs": "^6.15.0",
"react": "^19.2.4",
"react-collapsible": "^2.10.0",
"react-comic-viewer": "^0.5.1",
"react-day-picker": "^9.13.2",
"react-dom": "^19.2.4",
"react-fast-compare": "^3.2.2",
"react-final-form": "^7.0.0",
"react-final-form-arrays": "^4.0.0",
"react-i18next": "^16.5.4",
"react-loader-spinner": "^8.0.2",
"react-modal": "^3.16.3",
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1",
"react-select": "^5.10.2",
"react-select-async-paginate": "^0.7.11",
"react-sliding-pane": "^7.3.0",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^11.0.5",
"socket.io-client": "^4.8.3",
"styled-components": "^6.3.11",
"threetwo-ui-typings": "^1.0.14",
"vaul": "^1.1.2",
"vite": "^7.3.1",
"vite-plugin-html": "^3.2.2",
"websocket": "^1.0.35",
"zustand": "^5.0.11"
},
"devDependencies": {
"@babel/cli": "^7.13.10",
"@babel/core": "^7.13.10",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.13.0",
"@fortawesome/fontawesome-free": "^5.15.3",
"@root/walk": "^1.1.0",
"@tsconfig/node14": "^1.0.0",
"@types/express": "^4.17.8",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168",
"@types/mongoose": "^5.7.37",
"@types/node": "^14.14.34",
"@types/pino": "^6.3.7",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.16",
"@types/unzipper": "^0.10.3",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"awesome-typescript-loader": "^5.2.1",
"axios": "^0.21.1",
"axios-rate-limit": "^1.3.0",
"babel-eslint": "^10.0.0",
"babel-loader": "^8.2.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"body-parser": "^1.19.0",
"buffer": "^6.0.3",
"bulma": "^0.9.3",
"clean-webpack-plugin": "^1.0.0",
"comlink": "^4.3.0",
"compromise": "^13.10.5",
"compromise-dates": "^2.0.1",
"compromise-numbers": "^1.2.0",
"compromise-sentences": "^0.2.0",
"concurrently": "^4.0.0",
"connected-react-router": "^6.9.1",
"css-loader": "^5.1.2",
"eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"etl": "^0.6.12",
"express": "^4.17.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"image-webpack-loader": "^7.0.1",
"@eslint/js": "^10.0.0",
"@graphql-codegen/cli": "^6.1.2",
"@graphql-codegen/typescript": "^5.0.8",
"@graphql-codegen/typescript-operations": "^5.0.8",
"@graphql-codegen/typescript-react-query": "^6.1.2",
"@iconify-json/solar": "^1.2.5",
"@iconify/json": "^2.2.443",
"@iconify/tailwind": "^1.2.0",
"@iconify/tailwind4": "^1.2.1",
"@iconify/utils": "^3.1.0",
"@storybook/addon-essentials": "^8.6.17",
"@storybook/addon-interactions": "^8.6.17",
"@storybook/addon-links": "^8.6.17",
"@storybook/addon-onboarding": "^8.6.17",
"@storybook/blocks": "^8.6.17",
"@storybook/react": "^8.6.17",
"@storybook/react-vite": "^8.6.17",
"@storybook/testing-library": "^0.2.2",
"@tailwindcss/postcss": "^4.2.1",
"@tanstack/eslint-plugin-query": "^5.91.4",
"@tanstack/react-query-devtools": "^5.91.3",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/ellipsize": "^0.1.3",
"@types/html-to-text": "^9.0.4",
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.6.0",
"@types/prop-types": "^15.7.15",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-table": "^7.7.20",
"autoprefixer": "^10.4.27",
"docdash": "^2.0.2",
"eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^62.7.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-storybook": "^0.11.1",
"identity-obj-proxy": "^3.0.0",
"install": "^0.13.0",
"jest": "^26.6.3",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^1.4.1",
"mongoose": "^5.10.11",
"node-sass": "^5.0.0",
"node-unrar-js": "^1.0.1",
"nodemon": "^1.17.3",
"npm": "^7.9.0",
"pino": "^6.11.2",
"pino-pretty": "^4.7.1",
"prettier": "^2.2.1",
"qs": "^6.10.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hot-loader": "^4.13.0",
"react-redux": "^7.2.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"redux-thunk": "^2.3.0",
"rimraf": "^3.0.2",
"sass-loader": "^11.0.1",
"source-map-loader": "^0.2.4",
"string-similarity": "^4.0.4",
"style-loader": "^2.0.0",
"tslint": "^6.1.3",
"typescript": "^4.2.3",
"unzipper": "^0.10.11",
"url-loader": "^1.0.1",
"webpack": "^5.33.2",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"postcss": "^8.5.6",
"postcss-import": "^16.1.1",
"prettier": "^3.8.1",
"react-refresh": "^0.18.0",
"rimraf": "^6.1.3",
"sass": "^1.97.3",
"storybook": "^8.6.17",
"tailwindcss": "^4.2.2",
"ts-jest": "^29.4.6",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^6.0.2",
"wait-on": "^9.0.4"
},
"resolutions": {
"jackspeak": "2.1.1"
}
}

View File

@@ -0,0 +1,211 @@
# Implementation Plan: Directory Status Check for Import.tsx
## Overview
Add functionality to `Import.tsx` that checks if the required directories (`comics` and `userdata`) exist before allowing the import process to start. If either directory is missing, display a warning banner to the user and disable the import functionality.
## API Endpoint
- **Endpoint**: `GET /api/library/getDirectoryStatus`
- **Response Structure**:
```typescript
interface DirectoryStatus {
comics: { exists: boolean };
userdata: { exists: boolean };
}
```
## Implementation Details
### 1. Add Directory Status Type
In [`Import.tsx`](src/client/components/Import/Import.tsx:1), add a type definition for the directory status response:
```typescript
interface DirectoryStatus {
comics: { exists: boolean };
userdata: { exists: boolean };
}
```
### 2. Create useQuery Hook for Directory Status
Use `@tanstack/react-query` (already imported) to fetch directory status on component mount:
```typescript
const { data: directoryStatus, isLoading: isCheckingDirectories, error: directoryError } = useQuery({
queryKey: ['directoryStatus'],
queryFn: async (): Promise<DirectoryStatus> => {
const response = await axios.get('http://localhost:3000/api/library/getDirectoryStatus');
return response.data;
},
refetchOnWindowFocus: false,
staleTime: 30000, // Cache for 30 seconds
});
```
### 3. Derive Missing Directories State
Compute which directories are missing from the query result:
```typescript
const missingDirectories = useMemo(() => {
if (!directoryStatus) return [];
const missing: string[] = [];
if (!directoryStatus.comics?.exists) missing.push('comics');
if (!directoryStatus.userdata?.exists) missing.push('userdata');
return missing;
}, [directoryStatus]);
const hasAllDirectories = missingDirectories.length === 0;
```
### 4. Create Warning Banner Component
Add a warning banner that displays when directories are missing, positioned above the import button. This uses the same styling patterns as the existing error banner:
```tsx
{/* Directory Status Warning */}
{!isCheckingDirectories && missingDirectories.length > 0 && (
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-amber-500 bg-amber-50 dark:bg-amber-900/20 p-4">
<div className="flex items-start gap-3">
<span className="w-6 h-6 text-amber-600 dark:text-amber-400 mt-0.5">
<i className="h-6 w-6 icon-[solar--folder-error-bold]"></i>
</span>
<div className="flex-1">
<p className="font-semibold text-amber-800 dark:text-amber-300">
Required Directories Missing
</p>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-1">
The following directories do not exist and must be created before importing:
</p>
<ul className="list-disc list-inside text-sm text-amber-700 dark:text-amber-400 mt-2">
{missingDirectories.map((dir) => (
<li key={dir}>
<code className="bg-amber-100 dark:bg-amber-900/50 px-1 rounded">{dir}</code>
</li>
))}
</ul>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-2">
Please ensure these directories are mounted correctly in your Docker configuration.
</p>
</div>
</div>
</div>
)}
```
### 5. Disable Import Button When Directories Missing
Modify the button's `disabled` prop and click handler:
```tsx
<button
className="..."
onClick={handleForceReImport}
disabled={isForceReImporting || hasActiveSession || !hasAllDirectories}
title={!hasAllDirectories
? "Cannot import: Required directories are missing"
: "Re-import all files to fix Elasticsearch indexing issues"}
>
```
### 6. Update handleForceReImport Guard
Add early return in the handler for missing directories:
```typescript
const handleForceReImport = async () => {
setImportError(null);
// Check for missing directories
if (!hasAllDirectories) {
setImportError(
`Cannot start import: Required directories are missing (${missingDirectories.join(', ')}). Please check your Docker volume configuration.`
);
return;
}
// ... existing logic
};
```
## File Changes Summary
| File | Changes |
|------|---------|
| [`src/client/components/Import/Import.tsx`](src/client/components/Import/Import.tsx) | Add useQuery for directory status, warning banner UI, disable button logic |
| [`src/client/components/Import/Import.test.tsx`](src/client/components/Import/Import.test.tsx) | Add tests for directory status scenarios |
## Test Cases to Add
### Import.test.tsx Updates
1. **Should show warning banner when comics directory is missing**
2. **Should show warning banner when userdata directory is missing**
3. **Should show warning banner when both directories are missing**
4. **Should disable import button when directories are missing**
5. **Should enable import button when all directories exist**
6. **Should handle directory status API error gracefully**
Example test structure:
```typescript
describe('Import Component - Directory Status', () => {
beforeEach(() => {
jest.clearAllMocks();
// Mock successful directory status by default
(axios.get as jest.Mock) = jest.fn().mockResolvedValue({
data: { comics: { exists: true }, userdata: { exists: true } }
});
});
test('should show warning when comics directory is missing', async () => {
(axios.get as jest.Mock).mockResolvedValue({
data: { comics: { exists: false }, userdata: { exists: true } }
});
render(<Import />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText('Required Directories Missing')).toBeInTheDocument();
expect(screen.getByText('comics')).toBeInTheDocument();
});
});
test('should disable import button when directories are missing', async () => {
(axios.get as jest.Mock).mockResolvedValue({
data: { comics: { exists: false }, userdata: { exists: true } }
});
render(<Import />, { wrapper: createWrapper() });
await waitFor(() => {
const button = screen.getByRole('button', { name: /Force Re-Import/i });
expect(button).toBeDisabled();
});
});
});
```
## Architecture Diagram
```mermaid
flowchart TD
A[Import Component Mounts] --> B[Fetch Directory Status]
B --> C{API Success?}
C -->|Yes| D{All Directories Exist?}
C -->|No| E[Show Error Banner]
D -->|Yes| F[Enable Import Button]
D -->|No| G[Show Warning Banner]
G --> H[Disable Import Button]
F --> I[User Clicks Import]
I --> J[Proceed with Import]
```
## Notes
- The directory status is fetched once on mount with a 30-second stale time
- The warning uses amber/yellow colors to differentiate from error messages (red)
- The existing `importError` state and UI can remain unchanged
- No changes needed to the backend - the endpoint already exists

7
postcss.config.js Normal file
View File

@@ -0,0 +1,7 @@
export default {
plugins: {
"postcss-import": {},
"@tailwindcss/postcss": {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Three Two!</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

BIN
screenshots/CVMatching.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

BIN
screenshots/ComicDetail.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

BIN
screenshots/Dashboard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
screenshots/Import.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
screenshots/Library.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

25
skills-lock.json Normal file
View File

@@ -0,0 +1,25 @@
{
"version": 1,
"skills": {
"caveman": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"computedHash": "a818cdc41dcfaa50dd891c5cb5e5705968338de02e7e37949ca56e8c30ad4176"
},
"caveman-compress": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"computedHash": "300fb8578258161e1752a2a4142a7e9ff178c960bcb83b84422e2987421f33bf"
},
"caveman-help": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"computedHash": "3cd5f7d3f88c8ef7b16a6555dc61f5a11b14151386697609ab6887ab8b5f059d"
},
"compress": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"computedHash": "05c97bc3120108acd0b80bdef7fb4fa7c224ba83c8d384ccbc97f92e8a065918"
}
}
}

47
src/app.css Normal file
View File

@@ -0,0 +1,47 @@
@import "tailwindcss";
@config "../tailwind.config.ts";
html, body {
overflow-x: hidden;
}
/* Custom Project Fonts */
@font-face {
font-family: "PP Object Sans Regular";
src: url("/fonts/PPObjectSans-Regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "PP Object Sans Heavy";
src: url("/fonts/PPObjectSans-Heavy.otf") format("opentype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "PP Object Sans Slanted";
src: url("/fonts/PPObjectSans-Slanted.otf") format("opentype");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "PP Object Sans HeavySlanted";
src: url("/fonts/PPObjectSans-HeavySlanted.otf") format("opentype");
font-weight: 700;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Hasklig Regular";
src: url("/fonts/Hasklig-Regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
}

View File

@@ -1,159 +1,18 @@
# Client side boilerplate with ReactJS library and Typescript
## ThreeTwo UI
## Introduction
##### I have tried my best to document the project through folder organization, comments, and actual JSDocs where applicable. Unit tests and I have not agreed for a long time now, and I think it won't change anytime soon.
In the client side boilerplate, Typescript has been used to achieve a more structured and maintainable source code. ReactJS library which is one of the most important libraries for UI development alongside the other big names in the market, has been picked over to build the presentation layer of the application. Also for CSS, Less has been used to make CSS more functional.
### Less
This folder houses all the components, utils and libraries that make up ThreeTwo's UI
[Less](http://lesscss.org/) is a backwards-compatible language extension for CSS. Less helps to write CSS in a functional way and It's really easy to read and understand.
It is based on React 18, and uses:
### ESLint
1. _zustand_ for state management
2. _socket.io_ for transferring data in real-time
3. _React Router_ for routing
4. React DnD for drag-and-drop
5. @tanstack/react-table for all tables
6. @tanstack/react-query for API calls
[ESLint](https://eslint.org/) is a pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript and Typescript.
[.eslintrc.json file](<(https://eslint.org/docs/user-guide/configuring)>) (alternatively configurations can be written in Javascript or YAML as well) is used describe the configurations required for ESLint. Below is the .eslintrc.json file which has been used.
```javascript
{
"extends": ["airbnb"],
"env": {
"browser": true,
"node": true
},
"rules": {
"no-console": "off",
"comma-dangle": "off",
"react/jsx-filename-extension": "off"
}
}
```
[Airbnb's Javascript Style Guide](https://github.com/airbnb/javascript) which has been used by the majority of JavaScript and Typescript developers worldwide. Since the aim is support for both client (browser) and server side (Node.js) source code, the **env** has been set to browser and node.
Optionally, you can override the current settings by installing `eslint` globally and running `eslint --init` to change the configurations to suit your needs. [**no-console**](https://eslint.org/docs/rules/no-console), [**comma-dangle**](https://eslint.org/docs/rules/comma-dangle) and [**react/jsx-filename-extension**](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md) rules have been turned off.
### Webpack
[Webpack](https://webpack.js.org/) is a module bundler. Its main purpose is to capable Front-end developers to experience a modular programming style and bundle JavaScript and CSS files for usage in a browser.
[webpack.config.js](https://webpack.js.org/configuration/) file has been used to describe the configurations required for webpack. Below is the webpack.config.js file which has been used.
```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const outputDirectory = 'dist';
module.exports = {
entry: ['babel-polyfill', './src/client/index.tsx'],
output: {
path: path.join(__dirname, outputDirectory),
filename: './js/[name].bundle.js'
},
devtool: "source-map",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.tsx?$/,
use:[
{
loader: "awesome-typescript-loader"
},
],
exclude: /node_modules/
},
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
},
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: './Less',
hmr: process.env.NODE_ENV === 'development',
},
},
{ loader: 'css-loader' },
{
loader: 'less-loader',
options: {
strictMath: true,
noIeCompat: true,
}
},
]
},
{
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
},
]
},
resolve: {
extensions: ['*', '.ts', '.tsx', '.js', '.jsx', '.json', '.less']
},
devServer: {
port: 3000,
open: true,
proxy: {
'/api': 'http://localhost:8050'
}
},
plugins: [
new CleanWebpackPlugin([outputDirectory]),
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
title: "Book Manager",
}),
new MiniCssExtractPlugin({
filename: './css/[name].css',
chunkFilename: './css/[id].css',
}),
new CopyPlugin([
{ from: './src/client/Assets', to: 'assets' },
])
],
};
```
1. **entry:** entry: ./src/client/index.tsx is where the application starts executing and Webpack starts bundling.
Note: babel-polyfill is added to support async/await. Read more [here](https://babeljs.io/docs/en/babel-polyfill#usage-in-node-browserify-webpack).
2. **output path and filename:** the target directory and the filename for the bundled output.
3. **module loaders:** Module loaders are transformations that are applied on the source code of a module. We pass all the js file through [babel-loader](https://github.com/babel/babel-loader) to transform JSX to Javascript. CSS files are passed through [css-loaders](https://github.com/webpack-contrib/css-loader) and [style-loaders](https://github.com/webpack-contrib/style-loader) to load and bundle CSS files. Fonts and images are loaded through url-loader.
4. **Dev Server:** Configurations for the webpack-dev-server which will be described in coming section.
5. **plugins:** [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin) is a webpack plugin to remove the build directory before building. [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) simplifies creation of HTML files to serve your webpack bundles. It loads the template (public/index.html) and injects the output bundle.
### Webpack dev server
[Webpack dev server](https://webpack.js.org/configuration/dev-server/) is used along with webpack. It provides a development server that enables live reloading for the client side code changes.
The devServer section of webpack.config.js contains the configuration required to run webpack-dev-server which is given below.
```javascript
devServer: {
port: 3000,
open: true,
proxy: {
"/api": "http://localhost:8050"
}
}
```
[**Port**](https://webpack.js.org/configuration/dev-server/#devserver-port) specifies the Webpack dev server to listen on this particular port (3000 in this case). When [**open**](https://webpack.js.org/configuration/dev-server/#devserver-open) is set to true, it will automatically open the home page on start-up. [Proxying](https://webpack.js.org/configuration/dev-server/#devserver-proxy) URLs can be useful when you have a separate API backend development server, and you want to send API requests on the same domain.

View File

@@ -1,55 +0,0 @@
import axios from "axios";
import rateLimiter from "axios-rate-limit";
import qs from "qs";
import {
CV_SEARCH_SUCCESS,
CV_API_CALL_IN_PROGRESS,
CV_API_GENERIC_FAILURE,
} from "../constants/action-types";
import { COMICBOOKINFO_SERVICE_URI } from "../constants/endpoints";
const http = rateLimiter(axios.create(), {
maxRequests: 1,
perMilliseconds: 1000,
maxRPS: 1,
});
export const comicinfoAPICall = (options) => async (dispatch) => {
try {
dispatch({
type: CV_API_CALL_IN_PROGRESS,
inProgress: true,
});
const serviceURI = COMICBOOKINFO_SERVICE_URI + options.callURIAction;
const response = await http(serviceURI, {
method: options.callMethod,
params: options.callParams,
data: options.data ? options.data : null,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: "repeat" });
},
});
switch (options.callURIAction) {
case "search":
dispatch({
type: CV_SEARCH_SUCCESS,
result: response.data,
});
break;
default:
console.log("Could not complete request.");
}
} catch (error) {
console.log(error);
dispatch({
type: CV_API_GENERIC_FAILURE,
error,
});
}
};

View File

@@ -1,161 +0,0 @@
import axios from "axios";
import { IFolderData, IExtractedComicBookCoverFile } from "threetwo-ui-typings";
import { API_BASE_URI, SOCKET_BASE_URI } from "../constants/endpoints";
import { io } from "socket.io-client";
import {
IMS_COMICBOOK_METADATA_FETCHED,
IMS_SOCKET_CONNECTION_CONNECTED,
IMS_RECENT_COMICS_FETCHED,
CV_API_CALL_IN_PROGRESS,
CV_SEARCH_SUCCESS,
CV_CLEANUP,
} from "../constants/action-types";
import { refineQuery } from "../shared/utils/filenameparser.utils";
import { matchScorer } from "../shared/utils/searchmatchscorer.utils";
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
return axios
.request<Array<IFolderData>>({
url: "http://localhost:3000/api/import/walkFolders",
method: "POST",
data: {
basePathToWalk: path,
},
transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
return data;
})
.catch((error) => error);
}
/**
* Fetches comic book covers along with some metadata
*
* using {@link Renderer}.
*
* Used by external plugins
*
* @param {Object} options
* @return {Promise<string>} HTML of the page
*/
export const fetchComicBookMetadata = (options) => async (dispatch) => {
const extractionOptions = {
sourceFolder: options,
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
paginationOptions: {
pageLimit: 25,
page: 1,
},
};
const walkedFolders = await walkFolder("./comics");
const socket = io(SOCKET_BASE_URI, {
reconnectionDelayMax: 10000,
});
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
dispatch({
type: IMS_SOCKET_CONNECTION_CONNECTED,
socketConnected: true,
});
});
socket.on("disconnect", () => {
console.log(`disconnect`);
});
socket.emit("importComicsInDB", {
action: "getComicCovers",
params: {
extractionOptions,
walkedFolders,
},
});
socket.on("comicBookCoverMetadata", (data: IExtractedComicBookCoverFile) => {
dispatch({
type: IMS_COMICBOOK_METADATA_FETCHED,
data,
dataTransferred: true,
});
});
};
export const getRecentlyImportedComicBooks = (options) => async (dispatch) => {
const { paginationOptions } = options;
return axios
.request({
url: "http://localhost:3000/api/import/getRecentlyImportedComicBooks",
method: "POST",
data: {
paginationOptions,
},
})
.then((response) => {
dispatch({
type: IMS_RECENT_COMICS_FETCHED,
data: response.data,
});
});
};
export const fetchComicVineMatches = (searchPayload) => (dispatch) => {
try {
const issueString = searchPayload.rawFileDetails.path.split("/").pop();
let seriesSearchQuery = {};
const issueSearchQuery = refineQuery(issueString);
if (searchPayload.rawFileDetails.containedIn !== "comics") {
seriesSearchQuery = refineQuery(
searchPayload.rawFileDetails.containedIn.split("/").pop(),
);
}
dispatch({
type: CV_API_CALL_IN_PROGRESS,
});
axios
.request({
url: "http://localhost:3080/api/comicvine/fetchseries",
method: "POST",
data: {
format: "json",
sort: "name%3Aasc",
query: issueSearchQuery.searchParams.searchTerms.name,
fieldList: "id",
limit: "10",
offset: "0",
resources: "issue",
},
transformResponse: [
(r) => {
const searchMatches = JSON.parse(r);
return matchScorer(searchMatches.results, {
issue: issueSearchQuery,
series: seriesSearchQuery,
});
},
],
})
.then((response) => {
dispatch({
type: CV_SEARCH_SUCCESS,
searchResults: response.data,
searchQueryObject: {
issue: issueSearchQuery,
series: seriesSearchQuery,
},
});
});
/* return { issueSearchQuery, series: seriesSearchQuery.searchParams }; */
} catch (error) {
console.log(error);
}
dispatch({
type: CV_CLEANUP,
});
};

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="160px"
height="160px"
viewBox="0 0 16 16"
version="1.1"
id="SVGRoot">
<defs
id="defs222">
<linearGradient
id="linearGradient5642">
<stop
style="stop-color:#483e37;stop-opacity:1"
offset="0"
id="stop5638" />
<stop
style="stop-color:#339cc7;stop-opacity:0;"
offset="1"
id="stop5640" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient5642"
id="linearGradient5658"
x1="13.811552"
y1="7.9124665"
x2="8.5506983"
y2="2.2998061"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.47081933,0.18167275)" />
<linearGradient
xlink:href="#linearGradient5642"
id="linearGradient5658-6"
x1="13.811552"
y1="7.9124665"
x2="8.5506983"
y2="2.2998061"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-5.8345778,0.9966572)" />
<linearGradient
xlink:href="#linearGradient5642"
id="linearGradient5658-6-2"
x1="13.811552"
y1="7.9124665"
x2="8.5506983"
y2="2.2998061"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2.2875303,6.2704208)" />
</defs>
<metadata
id="metadata225">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1">
<circle
style="fill:#fd8635;fill-opacity:1;stroke-width:0.34364313;stroke:none;stroke-opacity:1"
id="path78-7"
cx="4.9123297"
cy="5.9923167"
r="4.75" />
<circle
style="opacity:0.61000001;fill:url(#linearGradient5658-6);fill-opacity:1;stroke-width:0.3436431"
id="path78-3-5-1"
cx="4.9046054"
cy="5.9923072"
r="4.75" />
</g>
<g
id="layer2">
<circle
style="fill:#fd8635;fill-opacity:1;stroke-width:0.34364313"
id="path78"
cx="8.4543009"
cy="11.261043"
r="4.75" />
<circle
style="opacity:0.61000001;fill:url(#linearGradient5658-6-2);fill-opacity:1;stroke-width:0.3436431"
id="path78-3-5-1-9"
cx="8.4516525"
cy="11.266071"
r="4.75" />
</g>
<g
id="layer3">
<circle
style="opacity:1;fill:#348fed;fill-opacity:1;stroke-width:0.3436431"
id="path78-3"
cx="11.211483"
cy="5.1752739"
r="4.75" />
<circle
style="opacity:0.61000001;fill:url(#linearGradient5658);fill-opacity:1;stroke-width:0.3436431"
id="path78-3-5"
cx="11.210003"
cy="5.1773238"
r="4.75" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,6 @@
<svg width="384" height="512" viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path id="Path" fill="#e1933c" fill-rule="evenodd" stroke="#393838" stroke-width="21" stroke-linecap="round" stroke-linejoin="round" d="M 215.786591 23.351563 C 215.786591 23.351563 98.801674 21.473022 81.803864 21.200073 C 21.811596 20.236725 23.297552 52.264709 22.462652 104.258026 C 21.073503 190.766876 22.587183 312.129272 22.115429 437.295349 C 21.974922 474.574432 57.495609 491.702881 94.239006 492.460449 C 172.222427 494.068359 287.278351 491.559753 287.278351 491.559753 C 287.278351 491.559753 361.530945 494.731964 361.601563 409.742523 C 361.689667 303.742554 361.407318 172.708862 361.407318 172.708862 L 215.786591 23.351563 L 215.786591 23.351563 Z"/>
<path id="Shape" fill="none" stroke="#ffffff" stroke-width="32" stroke-linecap="round" stroke-linejoin="round" d="M 110 270 L 264 270 M 162 330 L 220 330"/>
<path id="Line" fill="none" stroke="#ffffff" stroke-width="33" stroke-linecap="round" stroke-linejoin="round" d="M 110 395 L 264 394.999084"/>
<path id="path1" fill="none" stroke="#393838" stroke-width="21" stroke-linecap="round" stroke-linejoin="round" d="M 207 23 L 208 175 L 349 175"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 2.4.3 -->
<svg width="624" height="561" viewBox="0 0 624 561" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<text id="LCG" xml:space="preserve"><tspan x="57" y="282" font-family="DIN Alternate" font-size="288" font-weight="700" fill="#ff4300" letter-spacing="-2.88" xml:space="preserve">L</tspan><tspan font-family="DIN Alternate" font-size="288" font-weight="700" fill="#ff4300" letter-spacing="11.52" xml:space="preserve">CG</tspan></text>
<path id="Rounded-Rectangle" fill="#ff4300" fill-rule="evenodd" stroke="none" d="M 96 322 C 84.954399 322 76 330.954407 76 342 L 76 346 C 76 357.045593 84.954399 366 96 366 L 193 366 C 204.045593 366 213 357.045593 213 346 L 213 342 C 213 330.954407 204.045593 322 193 322 Z"/>
<path id="Rounded-Rectangle-copy-2" fill="#ff4300" fill-rule="evenodd" stroke="none" d="M 425 322 C 413.954407 322 405 330.954407 405 342 L 405 346 C 405 357.045593 413.954407 366 425 366 L 522 366 C 533.045593 366 542 357.045593 542 346 L 542 342 C 542 330.954407 533.045593 322 522 322 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<svg width="384" height="512" viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path id="Path" fill="none" stroke="#ed230d" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 212 23 L 78 23 C 78 23 20 33 20 107 C 20 193.52002 25 440 25 440 C 25 440 20 494 98 494 C 176 494 291 490 291 490 C 291 490 365.293945 491.979614 364 407 C 362.386169 301.012268 360 170 360 170 L 212 23 L 212 23 Z"/>
<path id="Ellipse" fill="none" stroke="#ed230d" stroke-width="37" stroke-linecap="round" stroke-linejoin="round" d="M 266 320 C 266 279.13092 232.86908 246 192 246 C 151.13092 246 118 279.13092 118 320 C 118 360.86908 151.13092 394 192 394 C 232.86908 394 266 360.86908 266 320 Z"/>
<path id="Ellipse-copy" fill="none" stroke="#ed230d" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="0.1 16" stroke-dashoffset="0" d="M 223 320.5 C 223 303.103027 208.896973 289 191.5 289 C 174.103027 289 160 303.103027 160 320.5 C 160 337.896973 174.103027 352 191.5 352 C 208.896973 352 223 337.896973 223 320.5 Z"/>
<path id="path1" fill="none" stroke="#ed230d" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 207 23 L 208 175 L 349 175"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="384" height="512" viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path id="Path" fill="none" stroke="#ed230d" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="56 56" stroke-dashoffset="56" d="M 221 23 L 78 23 C 78 23 20 33 20 107 C 20 193.52002 25 440 25 440 C 25 440 20 494 98 494 C 176 494 291 490 291 490 C 291 490 365.293945 491.979614 364 407 C 362.386169 301.012268 360 170 360 170 L 212 23 L 221 23 Z"/>
<path id="Ellipse" fill="none" stroke="#ed230d" stroke-width="25" stroke-linecap="round" stroke-linejoin="round" d="M 266 340 C 266 299.13092 232.86908 266 192 266 C 151.13092 266 118 299.13092 118 340 C 118 380.86908 151.13092 414 192 414 C 232.86908 414 266 380.86908 266 340 Z"/>
<path id="path1" fill="none" stroke="#ed230d" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="56 56" stroke-dashoffset="56" d="M 181 3 L 181 173 L 269 174 L 305 175"/>
</svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="104px" height="160px" viewBox="0 0 104 160" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<!-- Generated by Pixelmator Pro 2.1.2 -->
<defs>
<linearGradient id="linear-gradient" gradientUnits="userSpaceOnUse" x1="41.41" y1="30.667" x2="78.01" y2="133.767">
<stop offset="0" stop-color="#ffecd2" stop-opacity="1"/>
<stop offset="1" stop-color="#fcb69f" stop-opacity="1"/>
</linearGradient>
</defs>
<path id="Path" d="M12.093 0.081 C5.414 0.081 0 5.465 0 12.105 L0 147.976 C0 154.617 5.414 160 12.093 160 L91.907 160 C98.586 160 104 154.617 104 147.976 L104 12.105 C104 5.465 98.586 0.081 91.907 0.081 Z" fill-opacity="1" fill="url(#linear-gradient)" stroke="none"/>
<defs>
<text id="string" transform="matrix(1.0 0.0 0.0 1.0 20.0 54.0)">
<tspan fill="#252422" y="18.0" x="17.492" font-size="18" font-family="Avenir-Oblique, Avenir" text-decoration="none">NO </tspan>
<tspan fill="#252422" y="43.0" x="1.823000000000004" font-size="18" font-family="Avenir-Oblique, Avenir" text-decoration="none">COVER</tspan>
</text>
</defs>
<use id="NO-COVER" xlink:href="#string"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="112px" height="28px" viewBox="0 0 112 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<!-- Generated by Pixelmator Pro 2.2 -->
<defs>
<image id="image" width="45px" height="28px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAcCAYAAAD1PDaSAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAALaADAAQAAAABAAAAHAAAAADa6ZRmAAALuElEQVRYCZVXCZRUxRWt5f9ep3u6Z9+gGRxwkEVcgIAKQURWAQFBRYMLGghqDKInajwuxwVQlAAalyNLICGI0QCiBkUQhIOETRlZRGBgNmZfenr/v37um9AcJAzqO+d2Vb+qX3Xr1atXrzi7uMgzzWY73WzQa0AEsNrp056avqPvaQ4OKCAOJICLjkWd25M8NMwBaOCXgG+Ac+Uy/HkW8AJvAh8CFxMdjT6gH3ClEKzQqUnH8GJXdmVQtTa2GrGKYCISjKqtaF8FhIALSnukaYK/2YQeTBHO5gajpTv+3wo0nhnFgfJ94KTfpZc0hhPUNh6oB84XWvQQwcTYPEemx6NcCU9W9YkFE7P75HhEEWdc55y3wrgimrCsQQvLd1WHYjF88wgQPX8w+i8upITOqXGZWlL4YU5Jp/evcUsHuQeRSgqR1vbN6lSzd3ZgqschqX1ysvGcsgBkX52eNn7WtsCyjJ0dVnbcNHqqsWZa7m15qeJSELYsi9dwodxCWE6XnbOvZ3Xwee2C3GPMOeP8qNoeae6QTsvN7JcIJr193D1O4qvrgWR/4bJJ7kvRbsFkmeMv91agbRRAO5SUgToTL7ySPcv2dNqMjp31vN665CrabXV2XciMD3mjYkf3Ocd3jllSXnao0ggxxVuxAieXVu+nbswko/RMDnR+mSRxvp45hc4twXSLWbYpKaPpcGQCtNUkpuAK5uBtB3T6AE8DdOTb1IeklxTyvqUdnz90m2fYYJwzOmyxFU2fbOnx8vd1feeXfnmsNvpWOKFmllRG5o57t+xrw+KfoU8bn7/uagqiXglcUOgEX0hU2IwoSynwknygo3fyhKegM/kZLUJG40p3wyZZKbaeTl0akYR5E/TLgfu3Fy7xBnj+NPiqVWXWbxhT+YCrMlabgxDxBtr3AQaQLwV7qCBdfi6FVYQYYjVGrG3f1cZo8a8AF5SkpdPQSj6UcaZXPGzG4hVmTRn9T+HuK2xcI6Ijk+1Rg0UOnI62covVaILl+t0Sh4l1Aa6f4h+ZCcI9qO+S1nVvX3licmp5rHYTCD8E1X8AIlykS/HSxumdem76XafH4d5TLMWDAxaUJpRiX6G9XUsTabLe4rxc+8w0v7YC9SuAONxiz6iKB8u5YCGN8bRh3gE0yHCArG4YprVv6t9P12OCJljTlumSbugbdCZzHvPfTeGSHTFL9z55elEvVBcBqwGKChSxrgWe+fCe/JyiTJnKLGVHZObT19RsDscUxfylQLuxmkhTvE1ZuTBPLpmfW2yzsT/hvwdY0pBoTouo+GnFLflc2swAdOmAHSDZmCK5H3N1trgFItwJXZUmtOJUkeIJq5g+sfxRujAoxu8BiAS5492Y9IHFk3IP9syzXUIuga9Pzt3UtOXTQy1kwKcBWly7QoPUp3pt2CkV8PuFVZDvDB8/EZkK/VuwdmhtcEvTZM/Qwgzhz3ZKe03EjNEFEQaOJDAfLjKFX5AmTqwhroxvHqqdV7AjvJfVJZrnQneQGiAUER7ukePovequPLfXLqdwzkyci3d6zDvhjxuKDPJHADt3cSFLV4TCiVhrxJIC8Wve45n50A0AaBt3fGscLUKVa5w7M0QaWY5chCSGDogeoi2ChBKKypDJzI3rmjcLEH4b/5O3KBF+wmUXndben9fFa+eXMa7slU3G211fPFYAwtT+OJC8vFBtX4h01DCsT+csajyplOIZGSLg80hyAboF120N7TEQP1HlYrJvaDMqNwDk15ZpwcxYEOptNkdxEqgFaKe+BEjIbebpgqXsebhzpSa4j8wRiqiyAQtP0PmpBp4AGoCfJUSaZM2O3a3xeNzSpOTsrtt9ZLV7gfrSaFnU5GYLdbpc77YLBUUYIsIMkMYhgssLqyFixqFKTkw+SSslCz7jtWtG1SSb3yfiV+G2rjtVnzje57VTtVjxerSTC13wuob+gpIk3WCa1rdrN4YqyNo3DXFfAvJ5+MJugFWN2VBPXw9KufJ6FGRZOrw8GjdVXLG96GLSeYKO3CcpdDvOtgvhOzlR9LNr5kD7yYpTRS8eWzro9VP1obj5Z7SvBtrcK/nRzymTpGnCFStWN4UQCJSuc29qikaWGwyULWtZ13Y4dCW7FtiyqD4BkBKuYZdWocUF1WksGoeE6ve5pCiqmKzpGjdzDMW0sZv09ERCdYGFZ6N9O5Dsj+pPSpLr2VyCvjje3GqqkqPxDywh+MRxqaXQjQW2rGzagCjzv/HnZ82msNQTKPC5JVxUFKDODEVh76yMyLTzG46PlwGnsK6LGNw594C9alt1dBN6kP+Wn+358yoUWWih4HHmrqcKhMJY5fy/NCBGK3b7aMfVOGKos5agCnM4bg116uPoTiRp6wfhNjSYaPMIhRuSKrSybria7ywZpwe8dit3Y5X22a8/ZrvnHAjPQNsS4KIxGO0XEjpDU4C23OasyaGgSTfU1SW6wGxcaDKQk2U/Ct1tOXqmXSCwwI01h2Vz6hwXN2MjnHZRSukl6uToBgqyxMznervqXbrpCxvaBzd/EXEdbDGegZ7yjV/iDuh+VogsGXAoac4lTf83h0JKxhMiRi6y8Lks2vKOIBaGy+bgsEnBWforWbPqkGs4N07v0JZfcDoIvG3RTpsQqTOK47/iTGvq+M/EcXxPZyAZr1H9RULzE+EHXS5+BK+dvqij+LE0IijXbfk6fJrU2VlixK2j/M9+1GFhORGGmdJx+YlJef377PvDpalOnVGiBftxPD+QETLmLvZrSGq5N5zgi8MJ8zrolgI/FSEo7pPL0f3gAnzAtcDzwKKbR/kd65YFCvv29hbjv7vNsVEhoVV1Br7fviva6caBLs6F0GdM877qXd5Sa33jDyMrpi2KW9LIEabbb8lYMi4zPLtEMBZXHw3WoozHzWm7rB3oO4TGA84XIpgKXH0GBVIKIitMUymPW+c2OzPvmJSmjxysZzptMkCpwrHSCM3XI0maLD4dbjDCKey7du8Ph5Typ9M+0GGsGbfMHyh5KmQxQQdC8Raf4GG8LuxNcOg2C6OfJTJcrDXNnoDL8NhXFUYv9KWMjUJnUmyoDAJuQZB0pqSnm9Iyj465gR+dMNI1xoioLb40OQzxM4s+oLSCStwdylTW6aagSelxapJ0d7wJx34ReOe7gJbftduJ0YnmFqvan8aywNuKesu44YlFZdBJN6Mbea8hDt8YMvr9gCcgMy0okLjIbM0bg/vTUpkmrU7oWw+Qa9AuUlrwMIg4rhgzYfvw2Y+Mx+umID+ys1e3lldT8EqSzCvvIOtBcF2pMLg6WiMqNP3R2g3VNfEcxPhqtH2VJG1DHhwrkh2Gwtr5v0kdU/Le+s2lv52amoVtoa1k1XesCua/fo+NaUhBYVxZ3rXZ6IepQAgRmud6dNv2Ey2whO0A2q+5yq9nfxKJ0fVM308Ebknx+Tb//qNPx+hCPoIhaDEaZ4kq7JbHMkQdMoLUMF5Dy99r2fje2mbl9khnOGxF4TKn0PcNgM6amSRdGrHiWphFj6cwV/Zjaff07L/p4/rpd3oUogieg8qK5h/2Mc0yLBOZGMcDNKYXCGU7okTUCxfCfIgfyDXCnuKl7uChgVdnavmfVMZOSSkXCF3PvPfdFWVZhZ2GCsYLkbDgdZKo3bnqH1s/f3Ox7vPK8mDQNOCOStNYNBJRdA52QHcYJS2cwulZSZKm7K3u9orHw2sLFrQ4mC09YBZyUwXrwDkLAdywbI26kd1SpVWl9oKzSVhT8B+G72fF/xpIpJFbJ3SXa+LyCQt7TPz4aX7IWdvkzajtP+mFl/NzLrs0LKQ2jIIMrFa1fu7zu49s28ojTU0OzLurqUltR9loIq9NJNren3RntCtJ0rSSF3dHDz4ZZbE1dmUf0Dc3sEOKg/fRSUDiBy9RVvWtK/fkvTYzF9HNAY84LQ5ft10Vr+sHO9vjBtsvOY+0Ot1py8fPU93Gs/4dgs0hjzfdi9wZFxOuJ7wn/734tS3716/bhWFXAa3ARQmi/f/kv5XnqvnoFJszAAAAAElFTkSuQmCC"/>
</defs>
<use id="Background-Layer" xlink:href="#image" x="41px" y="0px" width="45px" height="28px"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,177 +0,0 @@
@import "../../../../node_modules/bulma/bulma.sass";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
$bg-color: yellow;
$border-color: red;
.app {
font-family: helvetica, arial, sans-serif;
padding: 2em;
border: 5px solid $border-color;
p {
background-color: $bg-color;
}
}
.navbar-item.is-mega {
position: static;
.is-mega-menu-title {
margin-bottom: 0;
padding: 0.375rem 1rem;
}
}
.min {
overflow: visible;
.tags {
display: inline;
margin-right: 5px;
margin-left: 5px;
&:first-child {
margin-left: 0;
}
}
pre {
border-radius: 0.4em;
margin: 10px 0 10px 0;
white-space: pre-wrap;
}
}
.generic-card {
max-width: 200px;
.truncate {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
img {
max-width: 200px;
}
}
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
column-gap: 0.5em;
row-gap: 1.2em;
.card {
max-width: 200px;
margin: 0 0 15px 0;
.is-horizontal {
flex-direction: row;
display: flex;
flex-basis: 50ex;
flex-grow: 0;
flex-shrink: 1;
box-shadow: none;
.card-image {
align-self: center;
.image {
max-width: 60px;
img {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0.25em;
border-bottom-left-radius: 0.25em;
}
}
}
.card-content {
align-self: center;
flex: 1;
padding-left: 1em;
padding-top: 0;
padding-bottom: 0;
font-size: 0.8em;
ul {
li.status {
margin-top: 10px;
}
}
.truncate {
width: 400px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.is-divider {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
}
}
}
.comic-vine-match-drawer {
// comic detail drawer
.search-criteria-card {
width: 100%;
.card-content {
padding: 10px;
.ant-divider-horizontal {
margin: 12px 0;
}
}
}
.field {
margin: 5px 0 0 0;
}
}
// comicvine search results
.search-results-container {
margin: 15px 0 0 0;
overflow: hidden;
table {
width: 100%;
tbody tr:nth-child(odd) {
background: #f6f6f6;
border-radius: 5px;
}
}
.search-result {
display: flex;
flex-direction: row;
padding: 10px;
margin: 0 0 10px 0;
.cover-image {
border-radius: 5px;
}
.search-result-details {
width: 100%;
.score {
float: right;
}
margin-left: 10px;
}
}
}
// progress
.progress-indicator-container {
height: 100%;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
.indicator {
padding: 5px;
width: 120px;
height: 120px;
}
}

View File

@@ -1,36 +1,49 @@
import * as React from "react";
import { hot } from "react-hot-loader";
import Dashboard from "./Dashboard";
/**
* @fileoverview Root application component.
* Provides the main layout structure with navigation, content outlet,
* and toast notifications. Initializes socket connection on mount.
* @module components/App
*/
import Import from "./Import";
import { ComicDetail } from "./ComicDetail";
import React, { ReactElement, useEffect } from "react";
import { Outlet } from "react-router-dom";
import { Navbar2 } from "./shared/Navbar2";
import { ToastContainer } from "react-toastify";
import "../../app.css";
import { useStore } from "../store";
import { Switch, Route } from "react-router";
import Navbar from "./Navbar";
import "../assets/scss/App.scss";
/**
* Root application component that provides the main layout structure.
*
* Features:
* - Initializes WebSocket connection to the server on mount
* - Renders the navigation bar across all routes
* - Provides React Router outlet for child routes
* - Includes toast notification container for app-wide notifications
*
* @returns {ReactElement} The root application layout
* @example
* // Used as the root element in React Router configuration
* const router = createBrowserRouter([
* {
* path: "/",
* element: <App />,
* children: [...]
* }
* ]);
*/
export const App = (): ReactElement => {
useEffect(() => {
useStore.getState().getSocket("/"); // Connect to the base namespace
}, []);
return (
<>
<Navbar2 />
<Outlet />
<ToastContainer stacked hideProgressBar />
</>
);
};
class App extends React.Component {
public render() {
return (
<div>
<Navbar />
<Switch>
<Route exact path="/">
<Dashboard />
</Route>
<Route path="/import">
<Import path={"./comics"} />
</Route>
<Route
path={"/comic/details/:comicObjectId"}
component={ComicDetail}
/>
</Switch>
</div>
);
}
}
declare let module: Record<string, unknown>;
export default hot(module)(App);
export default App;

View File

@@ -1,70 +0,0 @@
import * as React from "react";
import { IExtractedComicBookCoverFile } from "threetwo-ui-typings";
import {
removeLeadingPeriod,
escapePoundSymbol,
} from "../shared/utils/formatting.utils";
import { isUndefined, isEmpty } from "lodash";
import { Link } from "react-router-dom";
import ellipsize from "ellipsize";
interface IProps {
comicBookCoversMetadata: IExtractedComicBookCoverFile;
mongoObjId?: number;
}
interface IState {}
class Card extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
}
public drawCoverCard = (
metadata: IExtractedComicBookCoverFile,
): JSX.Element => {
const filePath = encodeURI(
"http://localhost:3000" +
removeLeadingPeriod(metadata.path) +
"/" +
metadata.name,
);
return (
<div>
<div className="card generic-card">
<div>
<div className="card-image">
<figure className="image">
<img
src={escapePoundSymbol(filePath)}
alt="Placeholder image"
/>
</figure>
</div>
<div className="card-content">
<ul>
<Link to={"/comic/details/" + this.props.mongoObjId}>
<li className="has-text-weight-semibold">
{ellipsize(metadata.name, 18)}
</li>
</Link>
</ul>
</div>
</div>
</div>
</div>
);
};
public render() {
return (
<>
{!isUndefined(this.props.comicBookCoversMetadata) &&
!isEmpty(this.props.comicBookCoversMetadata) &&
this.drawCoverCard(this.props.comicBookCoversMetadata)}
</>
);
}
}
export default Card;

View File

@@ -1,146 +0,0 @@
import React, { useState, useEffect, useCallback, ReactElement } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import Card from "./Card";
import MatchResult from "./MatchResult";
import ComicVineSearchForm from "./ComicVineSearchForm";
import { css } from "@emotion/react";
import PuffLoader from "react-spinners/PuffLoader";
import { isEmpty, isUndefined } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
import { fetchComicVineMatches } from "../actions/fileops.actions";
import { Drawer, Divider } from "antd";
const prettyBytes = require("pretty-bytes");
import "antd/dist/antd.css";
import { useDispatch, useSelector } from "react-redux";
type ComicDetailProps = {};
export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
const [page, setPage] = useState(1);
const [visible, setVisible] = useState(false);
const [comicDetail, setComicDetail] = useState<{
rawFileDetails: IExtractedComicBookCoverFile;
}>();
const comicVineSearchResults = useSelector(
(state: RootState) => state.comicInfo.searchResults,
);
const comicVineSearchQueryObject = useSelector(
(state: RootState) => state.comicInfo.searchQuery,
);
const comicVineAPICallProgress = useSelector(
(state: RootState) => state.comicInfo.inProgress,
);
const { comicObjectId } = useParams<{ comicObjectId: string }>();
useEffect(() => {
axios
.request({
url: `http://localhost:3000/api/import/getComicBookById`,
method: "POST",
data: {
id: comicObjectId,
},
})
.then((response) => {
setComicDetail(response.data);
})
.catch((error) => console.log(error));
}, [page]);
const dispatch = useDispatch();
const openDrawerWithCVMatches = useCallback(() => {
setVisible(true);
dispatch(fetchComicVineMatches(comicDetail));
}, [dispatch, comicDetail]);
const onClose = () => {
setVisible(false);
};
return (
<section className="container">
{!isEmpty(comicDetail) && !isUndefined(comicDetail) && (
<>
<h1 className="title">{comicDetail.rawFileDetails.name}</h1>
<div className="columns">
<div className="column is-narrow">
<Card comicBookCoversMetadata={comicDetail.rawFileDetails} />
</div>
<div className="column">
<p>{comicDetail.rawFileDetails.containedIn}</p>
<p>{prettyBytes(comicDetail.rawFileDetails.fileSize)}</p>
<button className="button" onClick={openDrawerWithCVMatches}>
<span className="icon">
<i className="fas fa-magic"></i>
</span>
<span>Match on Comic Vine</span>
</button>
</div>
</div>
<Drawer
title="ComicVine Search Results"
placement="right"
width={640}
closable={false}
onClose={onClose}
visible={visible}
className="comic-vine-match-drawer"
>
{!isEmpty(comicVineSearchQueryObject) &&
!isUndefined(comicVineSearchQueryObject) ? (
<div className="card search-criteria-card">
<div className="card-content">
<ComicVineSearchForm />
<Divider />
<p className="is-size-6">Searching against:</p>
<div className="field is-grouped is-grouped-multiline">
<div className="control">
<div className="tags has-addons">
<span className="tag">Title</span>
<span className="tag is-info">
{
comicVineSearchQueryObject.issue.searchParams
.searchTerms.name
}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Number</span>
<span className="tag is-info">
{
comicVineSearchQueryObject.issue.searchParams
.searchTerms.number
}
</span>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="progress-indicator-container">
<div className="indicator">
<PuffLoader loading={comicVineAPICallProgress} />
</div>
</div>
)}
<div className="search-results-container">
{!isEmpty(comicVineSearchResults) && (
<MatchResult matchData={comicVineSearchResults} />
)}
</div>
</Drawer>
</>
)}
</section>
);
};

View File

@@ -0,0 +1,448 @@
import React, {
ReactElement,
useEffect,
useRef,
useState,
} from "react";
import ellipsize from "ellipsize";
import { Form, Field } from "react-final-form";
import { isEmpty, isNil, map } from "lodash";
import { useStore } from "../../store";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { AIRDCPP_SERVICE_BASE_URI } from "../../constants/endpoints";
import type { Socket } from "socket.io-client";
import type { AcquisitionPanelProps } from "../../types";
interface HubData {
hub_url: string;
identity: { name: string };
value: string;
}
interface AirDCPPSearchResult {
id: string;
dupe?: unknown;
type: { id: string; str: string };
name: string;
slots: { total: number; free: number };
users: { user: { nicks: string; flags: string[] } };
size: number;
}
export const AcquisitionPanel = (
props: AcquisitionPanelProps,
): ReactElement => {
const socketRef = useRef<Socket | undefined>(undefined);
const [dcppQuery, setDcppQuery] = useState({});
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<AirDCPPSearchResult[]>([]);
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<{ id?: string; owner?: string; expires_in?: number }>({});
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<{ query?: { pattern: string; extensions: string[]; file_type: string } }>({});
const { comicObjectId } = props;
const issueName = props.query.issue.name || "";
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
useEffect(() => {
const socket = useStore.getState().getSocket("manual");
socketRef.current = socket;
// --- Handlers ---
const handleResultAdded = ({ result }: any) => {
setAirDCPPSearchResults((prev) =>
prev.some((r) => r.id === result.id) ? prev : [...prev, result],
);
};
const handleResultUpdated = ({ result }: any) => {
setAirDCPPSearchResults((prev) => {
const idx = prev.findIndex((r) => r.id === result.id);
if (idx === -1) return prev;
if (JSON.stringify(prev[idx]) === JSON.stringify(result)) return prev;
const next = [...prev];
next[idx] = result;
return next;
});
};
const handleSearchInitiated = (data: any) => {
setAirDCPPSearchInstance(data.instance);
};
const handleSearchesSent = (data: any) => {
setAirDCPPSearchInfo(data.searchInfo);
};
// --- Subscribe once ---
socket.on("searchResultAdded", handleResultAdded);
socket.on("searchResultUpdated", handleResultUpdated);
socket.on("searchInitiated", handleSearchInitiated);
socket.on("searchesSent", handleSearchesSent);
return () => {
socket.off("searchResultAdded", handleResultAdded);
socket.off("searchResultUpdated", handleResultUpdated);
socket.off("searchInitiated", handleSearchInitiated);
socket.off("searchesSent", handleSearchesSent);
// if you want to fully close the socket:
// useStore.getState().disconnectSocket("/manual");
};
}, []);
const {
data: settings,
isLoading,
isError,
} = useQuery({
queryKey: ["settings"],
queryFn: async () =>
await axios({
url: "http://localhost:3000/api/settings/getAllSettings",
method: "GET",
}),
});
const { data: hubs } = useQuery({
queryKey: ["hubs"],
queryFn: async () =>
await axios({
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
method: "POST",
data: {
host: settings?.data.directConnect?.client?.host,
},
}),
enabled: !isEmpty(settings?.data.directConnect?.client?.host),
});
useEffect(() => {
const dcppSearchQuery = {
query: {
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
extensions: ["cbz", "cbr", "cb7"],
},
hub_urls: map(hubs?.data, (item) => item.value),
priority: 5,
};
setDcppQuery(dcppSearchQuery);
}, [hubs, sanitizedIssueName]);
const search = async (searchData: any) => {
setAirDCPPSearchResults([]);
socketRef.current?.emit("call", "socket.search", {
query: searchData,
namespace: "/manual",
config: {
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
});
};
const download = async (
searchInstanceId: string | number,
resultId: string,
comicObjectId: string,
name: string,
size: number,
type: unknown,
config: Record<string, unknown>,
): Promise<void> => {
socketRef.current?.emit(
"call",
"socket.download",
{
searchInstanceId,
resultId,
comicObjectId,
name,
size,
type,
config,
},
(data: any) => {
// Download initiated
},
);
};
const getDCPPSearchResults = async (searchQuery: { issueName: string }) => {
const manualQuery = {
query: {
pattern: `${searchQuery.issueName}`,
extensions: ["cbz", "cbr", "cb7"],
},
hub_urls: [hubs?.data[0].hub_url],
priority: 5,
};
search(manualQuery);
};
return (
<>
<div className="mt-5 mb-3">
{!isEmpty(hubs?.data) ? (
<Form
onSubmit={getDCPPSearchResults}
initialValues={{
issueName,
}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<Field name="issueName">
{({ input, meta }) => {
return (
<div className="max-w-fit">
<div className="flex flex-row bg-slate-300 dark:bg-slate-400 rounded-l-lg">
<div className="w-10 pl-2 pt-1 text-gray-400 dark:text-gray-200">
<i className="icon-[solar--magnifer-bold-duotone] h-7 w-7" />
</div>
<input
{...input}
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
placeholder="Type an issue/volume name"
/>
<button
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
type="submit"
>
<div className="flex flex-row">
Search DC++
<div className="h-5 w-5 ml-2">
<img
src="/src/client/assets/img/airdcpp_logo.svg"
className="h-5 w-5"
/>
</div>
</div>
</button>
</div>
</div>
);
}}
</Field>
</form>
)}
/>
) : (
<article
role="alert"
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
>
No AirDC++ hub configured. Please configure it in{" "}
<code>Settings &gt; AirDC++ &gt; Hubs</code>.
</article>
)}
</div>
{/* configured hub */}
{!isEmpty(hubs?.data) && (
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
<span className="pr-1 pt-1">
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
</span>
{hubs && hubs?.data[0].hub_url}
</span>
)}
{/* AirDC++ search instance details */}
{!isNil(airDCPPSearchInstance) &&
!isEmpty(airDCPPSearchInfo) &&
!isNil(hubs) && (
<div className="flex flex-row gap-3 my-5 font-hasklig">
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
<dl>
<dt>
<div className="mb-1">
{hubs?.data.map((value: HubData, idx: number) => (
<span className="tag is-warning" key={idx}>
{value.identity.name}
</span>
))}
</div>
</dt>
<dt>
Query:
<span className="has-text-weight-semibold">
{airDCPPSearchInfo.query?.pattern}
</span>
</dt>
<dd>
Extensions:
<span className="has-text-weight-semibold">
{airDCPPSearchInfo.query?.extensions.join(", ")}
</span>
</dd>
<dd>
File type:
<span className="has-text-weight-semibold">
{airDCPPSearchInfo.query?.file_type}
</span>
</dd>
</dl>
</div>
<div className="block max-w-sm p-6 h-fit text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
<dl>
<dt>Search Instance: {airDCPPSearchInstance.id}</dt>
<dt>Owned by {airDCPPSearchInstance.owner}</dt>
<dd>Expires in: {airDCPPSearchInstance.expires_in}</dd>
</dl>
</div>
</div>
)}
{/* AirDC++ results */}
<div className="">
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
<div className="overflow-x-auto max-w-full mt-6">
<table className="w-full table-auto text-sm text-gray-900 dark:text-slate-100">
<thead>
<tr className="border-b border-gray-300 dark:border-slate-700">
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Name
</th>
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Type
</th>
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Slots
</th>
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Actions
</th>
</tr>
</thead>
<tbody>
{map(
airDCPPSearchResults,
({ dupe, type, name, id, slots, users, size }, idx) => (
<tr
key={idx}
className={
!isNil(dupe)
? "border-b border-gray-200 dark:border-slate-700 bg-gray-100 dark:bg-gray-700"
: "border-b border-gray-200 dark:border-slate-700 text-sm"
}
>
{/* NAME */}
<td className="whitespace-nowrap px-3 py-3 text-gray-700 dark:text-slate-300 max-w-xs">
<p className="mb-2">
{/* TODO: Switch to Solar icon */}
{type.id === "directory" && (
<i className="fas fa-folder mr-1"></i>
)}
{ellipsize(name, 45)}
</p>
<dl>
<dd>
<div className="inline-flex flex-wrap gap-1">
{!isNil(dupe) && (
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
<i className="icon-[solar--copy-bold-duotone] w-4 h-4"></i>
Dupe
</span>
)}
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
<i className="icon-[solar--user-rounded-bold-duotone] w-4 h-4"></i>
{users.user.nicks}
</span>
{users.user.flags.map((flag, idx) => (
<span
key={idx}
className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900"
>
<i className="icon-[solar--tag-horizontal-bold-duotone] w-4 h-4"></i>
{flag}
</span>
))}
</div>
</dd>
</dl>
</td>
{/* TYPE */}
<td className="px-2 py-3">
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
<i className="icon-[solar--zip-file-bold-duotone] w-4 h-4"></i>
{type.str}
</span>
</td>
{/* SLOTS */}
<td className="px-2 py-3">
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
<i className="icon-[solar--settings-minimalistic-bold-duotone] w-4 h-4"></i>
{slots.total} slots; {slots.free} free
</span>
</td>
{/* ACTIONS */}
<td className="px-2 py-3">
<button
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent"
onClick={() =>
download(
airDCPPSearchInstance.id ?? "",
id,
comicObjectId,
name,
size,
type,
{
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
)
}
>
Download
<i className="icon-[solar--download-bold-duotone] w-4 h-4"></i>
</button>
</td>
</tr>
),
)}
</tbody>
</table>
</div>
) : (
<div className="">
<article
role="alert"
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
>
<div>
The default search term is an auto-detected title; you may need
to change it to get better matches if the auto-detected one
doesn't work.
</div>
</article>
<article
role="alert"
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
>
<div>
Searching via <strong>AirDC++</strong> is still in{" "}
<strong>alpha</strong>. Some searches may take arbitrarily long,
or may not work at all. Searches from{" "}
<code className="font-hasklig">ADCS</code> hubs are more
reliable than <code className="font-hasklig">NMDCS</code> ones.
</div>
</article>
</div>
)}
</div>
</>
);
};
export default AcquisitionPanel;

View File

@@ -0,0 +1,46 @@
import React, { ReactElement } from "react";
import Select, { StylesConfig, SingleValue } from "react-select";
import { ActionOption } from "../actionMenuConfig";
interface MenuConfiguration {
filteredActionOptions: ActionOption[];
customStyles: StylesConfig<ActionOption, false>;
handleActionSelection: (action: SingleValue<ActionOption>) => void;
}
interface MenuProps {
data?: unknown;
handlers?: {
setSlidingPanelContentId: (id: string) => void;
setVisible: (visible: boolean) => void;
};
configuration: MenuConfiguration;
}
export const Menu = (props: MenuProps): ReactElement => {
const {
filteredActionOptions,
customStyles,
handleActionSelection,
} = props.configuration;
return (
<Select<ActionOption, false>
placeholder={
<span className="inline-flex flex-row items-center gap-2 pt-1">
<div className="w-6 h-6">
<i className="icon-[solar--cursor-bold-duotone] w-6 h-6"></i>
</div>
<div>Select An Action</div>
</span>
}
styles={customStyles}
name="actions"
isSearchable={false}
options={filteredActionOptions}
onChange={handleActionSelection}
/>
);
};
export default Menu;

View File

@@ -0,0 +1,74 @@
import React from "react";
import prettyBytes from "pretty-bytes";
import dayjs from "dayjs";
import ellipsize from "ellipsize";
import { map } from "lodash";
import { DownloadProgressTick } from "./DownloadProgressTick";
interface BundleData {
id: string;
name: string;
target: string;
size: number;
}
interface AirDCPPBundlesProps {
data: BundleData[];
}
export const AirDCPPBundles = (props: AirDCPPBundlesProps) => {
return (
<div className="overflow-x-auto w-fit mt-6">
<table className="min-w-full text-sm text-gray-900 dark:text-slate-100">
<thead>
<tr className="border-b border-gray-300 dark:border-slate-700">
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Filename
</th>
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Size
</th>
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Download Status
</th>
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
Bundle ID
</th>
</tr>
</thead>
<tbody>
{map(props.data, (bundle, index) => (
<tr
key={bundle.id}
className={
Number(index) !== props.data.length - 1
? "border-b border-gray-200 dark:border-slate-700"
: ""
}
>
<td className="px-3 py-2 align-top">
<h5 className="font-medium text-gray-800 dark:text-slate-200">
{ellipsize(bundle.name, 58)}
</h5>
<p className="text-xs text-gray-500 dark:text-slate-400">
{ellipsize(bundle.target, 88)}
</p>
</td>
<td className="px-3 py-2 align-top">
{prettyBytes(bundle.size)}
</td>
<td className="px-3 py-2 align-top">
<DownloadProgressTick bundleId={bundle.id} />
</td>
<td className="px-3 py-2 align-top">
<span className="text-xs text-yellow-800 dark:text-yellow-300 font-medium">
{bundle.id}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,75 @@
import React, { ReactElement, useCallback, useState } from "react";
import axios from "axios";
import { isNil } from "lodash";
import Creatable from "react-select/creatable";
import { withAsyncPaginate } from "react-select-async-paginate";
import { METRON_SERVICE_URI } from "../../../constants/endpoints";
const CreatableAsyncPaginate = withAsyncPaginate(Creatable);
export interface AsyncSelectPaginateProps {
metronResource?: string;
placeholder?: string | React.ReactNode;
value?: object;
onChange?(...args: unknown[]): unknown;
meta?: Record<string, unknown>;
input?: Record<string, unknown>;
name?: string;
type?: string;
}
interface AdditionalType {
page: number | null;
}
interface MetronResultItem {
name?: string;
__str__?: string;
id: number;
}
export const AsyncSelectPaginate = (props: AsyncSelectPaginateProps): ReactElement => {
const [isAddingInProgress, setIsAddingInProgress] = useState(false);
const loadData = useCallback(async (
query: string,
_loadedOptions: unknown,
additional?: AdditionalType
) => {
const page = additional?.page ?? 1;
const options = {
method: "GET",
resource: props.metronResource || "",
query: { name: query, page },
};
const response = await axios.post(`${METRON_SERVICE_URI}/fetchResource`, options);
const results = response.data.results.map((result: MetronResultItem) => ({
label: result.name || result.__str__,
value: result.id,
}));
return {
options: results,
hasMore: !isNil(response.data.next),
additional: {
page: !isNil(response.data.next) ? page + 1 : null,
},
};
}, [props.metronResource]);
return (
<CreatableAsyncPaginate
debounceTimeout={200}
isDisabled={isAddingInProgress}
value={props.value}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loadOptions={loadData as any}
placeholder={props.placeholder}
onChange={props.onChange}
additional={{
page: 1,
}}
/>
);
};
export default AsyncSelectPaginate;

View File

@@ -0,0 +1,224 @@
import React, { useState, ReactElement, useCallback, useMemo } from "react";
import { useParams } from "react-router-dom";
import Card from "../shared/Carda";
import { RawFileDetails } from "./RawFileDetails";
import TabControls from "./TabControls";
import { Menu } from "./ActionMenu/Menu";
import { isEmpty, isUndefined, isNil, filter } from "lodash";
import { components } from "react-select";
import "react-sliding-pane/dist/react-sliding-pane.css";
import SlidingPane from "react-sliding-pane";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { styled } from "styled-components";
import type { ComicDetailProps } from "../../types";
// Extracted modules
import { useComicVineMatching } from "./useComicVineMatching";
import { createTabConfig } from "./tabConfig";
import { actionOptions, customStyles, ActionOption } from "./actionMenuConfig";
import { CVMatchesPanel, EditMetadataPanelWrapper } from "./SlidingPanelContent";
// Styled component - moved outside to prevent recreation
const StyledSlidingPanel = styled(SlidingPane)`
background: #ccc;
`;
/**
* Displays full comic detail: cover, file info, action menu, and tabbed panels
* for metadata, archive operations, and acquisition.
*
* @param data.queryClient - react-query client passed through to the CV match
* panel so it can invalidate queries after a match is applied.
* @param data.comicObjectId - optional override for the comic ID; used when the
* component is rendered outside a route that provides the ID via `useParams`.
*/
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
acquisition,
createdAt,
},
userSettings,
queryClient,
comicObjectId: comicObjectIdProp,
} = data;
const [activeTab, setActiveTab] = useState<number | undefined>(undefined);
const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const { comicVineMatches, prepareAndFetchMatches } = useComicVineMatching();
// Action event handlers
const openDrawerWithCVMatches = () => {
prepareAndFetchMatches(rawFileDetails, comicvine);
setSlidingPanelContentId("CVMatches");
setVisible(true);
};
const openEditMetadataPanel = useCallback(() => {
setSlidingPanelContentId("editComicBookMetadata");
setVisible(true);
}, []);
// Hide "match on Comic Vine" when there are no raw file details — matching
// requires file metadata to seed the search query.
const filteredActionOptions: ActionOption[] = actionOptions.filter((item) => {
if (isUndefined(rawFileDetails)) {
return item.value !== "match-on-comic-vine";
}
return true;
});
const handleActionSelection = (action: ActionOption | null) => {
if (!action) return;
switch (action.value) {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-metdata":
openEditMetadataPanel();
break;
default:
break;
}
};
// Check for metadata availability
const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
const hasAnyMetadata =
isComicBookMetadataAvailable ||
!isEmpty(comicInfo) ||
!isNil(locg);
const areRawFileDetailsAvailable =
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
const { issueName, url } = determineCoverFile({
rawFileDetails,
comicvine,
locg,
});
// Query for airdc++
const airDCPPQuery = useMemo(() => ({
issue: { name: issueName },
}), [issueName]);
// Create tab configuration
const openReconcilePanel = useCallback(() => {
setSlidingPanelContentId("metadataReconciliation");
setVisible(true);
}, []);
const tabGroup = useMemo(() => createTabConfig({
data: data.data,
hasAnyMetadata,
areRawFileDetailsAvailable,
airDCPPQuery,
comicObjectId: _id,
userSettings,
issueName,
acquisition,
onReconcileMetadata: openReconcilePanel,
}), [data.data, hasAnyMetadata, areRawFileDetailsAvailable, airDCPPQuery, _id, userSettings, issueName, acquisition, openReconcilePanel]);
const filteredTabs = useMemo(() => tabGroup.filter((tab) => tab.shouldShow), [tabGroup]);
// Sliding panel content mapping
const renderSlidingPanelContent = () => {
switch (slidingPanelContentId) {
case "CVMatches":
return (
<CVMatchesPanel
rawFileDetails={rawFileDetails}
inferredMetadata={inferredMetadata}
comicVineMatches={comicVineMatches}
// Prefer the route param; fall back to the data ID when rendered outside a route.
comicObjectId={comicObjectId || _id}
queryClient={queryClient}
onMatchApplied={() => {
setVisible(false);
setActiveTab(1);
}}
/>
);
case "editComicBookMetadata":
return <EditMetadataPanelWrapper rawFileDetails={rawFileDetails} />;
default:
return null;
}
};
return (
<section className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
<div className="section">
{!isNil(data) && !isEmpty(data) && (
<>
<div>
<div className="flex flex-row mt-5">
<Card
imageUrl={url}
orientation={"cover-only"}
hasDetails={false}
/>
{/* raw file details */}
{!isUndefined(rawFileDetails) &&
!isEmpty(rawFileDetails?.cover) && (
<div className="grid">
<RawFileDetails
data={{
rawFileDetails,
inferredMetadata,
createdAt,
}}
>
{/* action dropdown */}
<div className="mt-1 flex flex-row gap-2 w-full">
<Menu
data={data.data}
handlers={{ setSlidingPanelContentId, setVisible }}
configuration={{
filteredActionOptions,
customStyles,
handleActionSelection,
}}
/>
</div>
</RawFileDetails>
</div>
)}
</div>
</div>
<TabControls
filteredTabs={filteredTabs}
downloadCount={acquisition?.directconnect?.downloads?.length || 0}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
<StyledSlidingPanel
isOpen={visible}
onRequestClose={() => setVisible(false)}
title={"Comic Vine Search Matches"}
width={"600px"}
>
{renderSlidingPanelContent()}
</StyledSlidingPanel>
</>
)}
</div>
</section>
);
};
export default ComicDetail;

View File

@@ -0,0 +1,40 @@
import React, { ReactElement } from "react";
import { useParams } from "react-router-dom";
import { ComicDetail } from "../ComicDetail/ComicDetail";
import { useQueryClient } from "@tanstack/react-query";
import { useGetComicByIdQuery } from "../../graphql/generated";
import { adaptGraphQLComicToLegacy } from "../../graphql/adapters/comicAdapter";
export const ComicDetailContainer = (): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const queryClient = useQueryClient();
const {
data: comicBookDetailData,
isLoading,
isError,
} = useGetComicByIdQuery(
{ id: comicObjectId! },
{ enabled: !!comicObjectId }
);
if (isError) {
return <div>Error loading comic details</div>;
}
if (isLoading) {
return <div>Loading...</div>;
}
const adaptedData = comicBookDetailData?.comic
? adaptGraphQLComicToLegacy(comicBookDetailData.comic)
: null;
return adaptedData ? (
<ComicDetail
data={adaptedData}
queryClient={queryClient}
comicObjectId={comicObjectId}
/>
) : null;
};

View File

@@ -0,0 +1,113 @@
import React, { ReactElement } from "react";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import dayjs from "dayjs";
import { isEmpty, isUndefined } from "lodash";
import Card from "../shared/Carda";
import { convert } from "html-to-text";
import type { ComicVineDetailsProps } from "../../types";
export const ComicVineDetails = (props: ComicVineDetailsProps): ReactElement => {
const { data, updatedAt } = props;
if (!data || !data.volumeInformation) {
return <div className="text-slate-500 dark:text-gray-400">No ComicVine data available</div>;
}
const detectedIssueType = data.volumeInformation.description
? detectIssueTypes(data.volumeInformation.description)
: undefined;
return (
<div className="text-slate-500 dark:text-gray-400">
<div className="">
<div>
<div className="flex flex-row gap-4">
<div className="min-w-fit">
<Card
imageUrl={data.volumeInformation.image?.thumb_url}
orientation={"cover-only"}
hasDetails={false}
/>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-row">
<div>
{/* Title */}
<div>
<div className="text-lg">{data.name}</div>
<div className="text-sm">
Is a part of{" "}
<span className="has-text-info">
{data.volumeInformation.name}
</span>
</div>
</div>
{/* Comicvine metadata */}
<div className="mt-2">
<div className="text-md">ComicVine Metadata</div>
<div className="text-sm">
Last scraped on{" "}
{updatedAt ? dayjs(updatedAt).format("MMM D YYYY [at] h:mm a") : "Unknown"}
</div>
<div className="text-sm">
ComicVine Issue ID
<span>{data.id}</span>
</div>
</div>
</div>
{/* Publisher details */}
<div className="ml-8">
Published by{" "}
<span>{data.volumeInformation.publisher?.name}</span>
<div>
Total issues in this volume{" "}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="text-md text-slate-900 dark:text-slate-900">
{data.volumeInformation.count_of_issues}
</span>
</span>
</div>
<div>
{data.issue_number && (
<div className="">
<span>Issue Number</span>
<span>{data.issue_number}</span>
</div>
)}
{!isUndefined(detectedIssueType) ? (
<div>
<span>Detected Type</span>
<span>
{detectedIssueType.displayName}
</span>
</div>
) : data.resource_type ? (
<div>
<span>Type</span>
<span>{data.resource_type}</span>
</div>
) : null}
</div>
</div>
</div>
{/* Description */}
<div className="mt-3 w-3/4">
{!isEmpty(data.description) &&
data.description &&
convert(data.description, {
baseElements: {
selectors: ["p"],
},
})}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ComicVineDetails;

View File

@@ -0,0 +1,49 @@
import React, { ReactElement } from "react";
import MatchResult from "./MatchResult";
import { isEmpty } from "lodash";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import type { ComicVineMatchPanelProps } from "../../types";
/** Displays ComicVine search results or a status message while searching. */
export const ComicVineMatchPanel = ({ props: comicVineData }: ComicVineMatchPanelProps): ReactElement => {
const { comicObjectId, comicVineMatches, queryClient, onMatchApplied } = comicVineData;
const { comicvine } = useStore(
useShallow((state) => ({
comicvine: state.comicvine,
})),
);
return (
<>
<div>
{!isEmpty(comicVineMatches) ? (
<MatchResult
matchData={comicVineMatches}
comicObjectId={comicObjectId}
queryClient={queryClient}
onMatchApplied={onMatchApplied}
/>
) : (
<>
<article
role="alert"
className="mt-4 rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600 text-sm"
>
<div>
<p>ComicVine match results are an approximation.</p>
<p>
Auto-matching is not available yet. If you see no results or
poor quality ones, you can override the search query
parameters to get better ones.
</p>
</div>
</article>
<div className="text-md my-5">{comicvine.scrapingStatus}</div>
</>
)}
</div>
</>
);
};
export default ComicVineMatchPanel;

View File

@@ -0,0 +1,99 @@
import React, { useCallback } from "react";
import { Form, Field } from "react-final-form";
import { ValidationErrors } from "final-form";
interface ComicVineSearchFormProps {
rawFileDetails?: Record<string, unknown>;
}
interface SearchFormValues {
issueName?: string;
issueNumber?: string;
issueYear?: string;
}
/**
* Component for performing search against ComicVine
*
* @component
* @example
* return (
* <ComicVineSearchForm data={rawFileDetails} />
* )
*/
export const ComicVineSearchForm = (props: ComicVineSearchFormProps) => {
const onSubmit = useCallback((value: SearchFormValues) => {
const userInititatedQuery = {
inferredIssueDetails: {
name: value.issueName,
number: value.issueNumber,
subtitle: "",
year: value.issueYear,
},
};
// dispatch(fetchComicVineMatches(data, userInititatedQuery));
}, []);
const validate = (_values: SearchFormValues): ValidationErrors | undefined => {
return undefined;
};
const MyForm = () => (
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<label className="block py-1 text-slate-700 dark:text-slate-200">Issue Name</label>
<Field name="issueName">
{(props) => (
<input
{...props.input}
className="appearance-none bg-slate-100 dark:bg-slate-700 h-10 w-full rounded-md border border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 py-1 pr-7 pl-3 sm:text-md sm:leading-5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-300"
placeholder="Type the issue name"
/>
)}
</Field>
<div className="flex flex-row gap-4 mt-2">
<div>
<label className="block py-1 text-slate-700 dark:text-slate-200">Number</label>
<Field name="issueNumber">
{(props) => (
<input
{...props.input}
className="appearance-none bg-slate-100 dark:bg-slate-700 h-10 w-14 rounded-md border border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 py-1 pr-2 pl-3 sm:text-md sm:leading-5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-300"
placeholder="#"
/>
)}
</Field>
</div>
<div>
<label className="block py-1 text-slate-700 dark:text-slate-200">Year</label>
<Field name="issueYear">
{(props) => (
<input
{...props.input}
className="appearance-none bg-slate-100 dark:bg-slate-700 h-10 w-20 rounded-md border border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 py-1 pr-2 pl-3 sm:text-md sm:leading-5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-300"
placeholder="1984"
/>
)}
</Field>
</div>
<div className="flex items-end">
<button
type="submit"
className="flex h-10 items-center rounded-lg border border-green-500 dark:border-green-400 bg-green-500 dark:bg-green-600 px-4 py-2 text-white font-medium hover:bg-green-600 dark:hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 active:bg-green-700"
>
Search
</button>
</div>
</div>
</form>
)}
/>
);
return <MyForm />;
};
export default ComicVineSearchForm;

View File

@@ -0,0 +1,109 @@
import prettyBytes from "pretty-bytes";
import React, { ReactElement, useEffect, useRef, useState } from "react";
import { useStore } from "../../store";
import type { Socket } from "socket.io-client";
import type { DownloadProgressTickProps } from "../../types";
/**
* Shape of the download tick data received over the socket.
*/
type DownloadTickData = {
id: number;
name: string;
downloaded_bytes: number;
size: number;
speed: number;
seconds_left: number;
status: {
id: string;
str: string;
completed: boolean;
downloaded: boolean;
failed: boolean;
hook_error: any;
};
sources: {
online: number;
total: number;
str: string;
};
target: string;
};
export const DownloadProgressTick: React.FC<DownloadProgressTickProps> = ({
bundleId,
}): ReactElement | null => {
const socketRef = useRef<Socket | undefined>(undefined);
const [tick, setTick] = useState<DownloadTickData | null>(null);
useEffect(() => {
const socket = useStore.getState().getSocket("manual");
socketRef.current = socket;
socket.emit("call", "socket.listenFileProgress", {
namespace: "/manual",
config: {
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
});
/**
* Handler for each "downloadTick" event.
* Only update state if event.id matches bundleId.
*
* @param {DownloadTickData} data - Payload from the server
*/
const onDownloadTick = (data: DownloadTickData) => {
// Compare numeric data.id to string bundleId
if (data.id === parseInt(bundleId, 10)) {
setTick(data);
}
};
socket.on("downloadTick", onDownloadTick);
return () => {
socket.off("downloadTick", onDownloadTick);
};
}, [socketRef, bundleId]);
if (!tick) {
return <>Nothing detected.</>;
}
// Compute human-readable values and percentages
const downloaded = prettyBytes(tick.downloaded_bytes);
const total = prettyBytes(tick.size);
const percent = tick.size > 0
? Math.round((tick.downloaded_bytes / tick.size) * 100)
: 0;
const speed = prettyBytes(tick.speed) + "/s";
const minutesLeft = Math.round(tick.seconds_left / 60);
return (
<div className="mt-2 p-2 border rounded-md bg-white shadow-sm">
{/* Downloaded vs Total */}
<div className="mt-1 flex items-center space-x-2">
<span className="text-sm text-gray-700">{downloaded} of {total}</span>
</div>
{/* Progress bar */}
<div className="relative mt-2 h-2 bg-gray-200 rounded overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-green-500"
style={{ width: `${percent}%` }}
/>
</div>
<div className="mt-1 text-xs text-gray-600">{percent}% complete</div>
{/* Speed and Time Left */}
<div className="mt-2 flex space-x-4 text-xs text-gray-600">
<span>Speed: {speed}</span>
<span>Time left: {minutesLeft} min</span>
</div>
</div>
);
};
export default DownloadProgressTick;

View File

@@ -0,0 +1,154 @@
import React, { useEffect, ReactElement, useState, useMemo } from "react";
import { isEmpty, isNil, isUndefined, map } from "lodash";
import { AirDCPPBundles } from "./AirDCPPBundles";
import { TorrentDownloads, TorrentData } from "./TorrentDownloads";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import {
LIBRARY_SERVICE_BASE_URI,
QBITTORRENT_SERVICE_BASE_URI,
TORRENT_JOB_SERVICE_BASE_URI,
} from "../../constants/endpoints";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import { useParams } from "react-router-dom";
export interface TorrentDetails {
infoHash: string;
progress: number;
downloadSpeed?: number;
uploadSpeed?: number;
}
/**
* DownloadsPanel displays two tabs of download information for a specific comic:
* - DC++ (AirDCPP) bundles
* - Torrent downloads
* It also listens for real-time torrent updates via a WebSocket.
*
* @component
* @returns {ReactElement | null} The rendered DownloadsPanel or null if no socket is available.
*/
export const DownloadsPanel = (): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const [infoHashes, setInfoHashes] = useState<string[]>([]);
const [torrentDetails, setTorrentDetails] = useState<TorrentData[]>([]);
const [activeTab, setActiveTab] = useState<"directconnect" | "torrents">(
"directconnect",
);
const { socketIOInstance } = useStore(
useShallow((state: any) => ({ socketIOInstance: state.socketIOInstance })),
);
/**
* Registers socket listeners on mount and cleans up on unmount.
*/
useEffect(() => {
if (!socketIOInstance) return;
/**
* Handler for incoming torrent data events.
* Merges new entries or updates existing ones by infoHash.
*
* @param {TorrentDetails} data - Payload from the socket event.
*/
const handleTorrentData = (data: TorrentDetails) => {
setTorrentDetails((prev) => {
const idx = prev.findIndex((t) => t.infoHash === data.infoHash);
if (idx === -1) {
return [...prev, data];
}
const next = [...prev];
next[idx] = { ...next[idx], ...data };
return next;
});
};
socketIOInstance.on("AS_TORRENT_DATA", handleTorrentData);
return () => {
socketIOInstance.off("AS_TORRENT_DATA", handleTorrentData);
};
}, [socketIOInstance]);
// ————— DC++ Bundles (via REST) —————
const { data: bundles } = useQuery({
queryKey: ["bundles", comicObjectId],
queryFn: async () =>
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getBundles`,
method: "POST",
data: {
comicObjectId,
config: {
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
},
}),
});
// ————— Torrent Jobs (via REST) —————
const { data: rawJobs = [] } = useQuery<any[]>({
queryKey: ["torrents", comicObjectId],
queryFn: async () => {
const { data } = await axios.get(
`${TORRENT_JOB_SERVICE_BASE_URI}/getTorrentData`,
{ params: { trigger: activeTab } },
);
return Array.isArray(data) ? data : [];
},
initialData: [],
enabled: activeTab === "torrents",
});
// Only when rawJobs changes *and* activeTab === "torrents" should we update infoHashes:
useEffect(() => {
if (activeTab !== "torrents") return;
setInfoHashes(rawJobs.map((j: any) => j.infoHash));
}, [activeTab]);
return (
<>
<div className="mt-5 mb-3">
<nav className="flex space-x-2">
<button
onClick={() => setActiveTab("directconnect")}
className={`px-4 py-1 rounded-full text-sm font-medium transition-colors ${
activeTab === "directconnect"
? "bg-green-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
DC++
</button>
<button
onClick={() => setActiveTab("torrents")}
className={`px-4 py-1 rounded-full text-sm font-medium transition-colors ${
activeTab === "torrents"
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
Torrents
</button>
</nav>
<div className="mt-4">
{activeTab === "torrents" ? (
<TorrentDownloads data={torrentDetails} />
) : !isNil(bundles?.data) && bundles.data.length > 0 ? (
<AirDCPPBundles data={bundles.data} />
) : (
<p>No DC++ bundles found.</p>
)}
</div>
</div>
</>
);
};
export default DownloadsPanel;

View File

@@ -0,0 +1,303 @@
import React, { ReactElement } from "react";
import { Form, Field, FieldRenderProps } from "react-final-form";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import AsyncSelectPaginate from "./AsyncSelectPaginate/AsyncSelectPaginate";
import TextareaAutosize from "react-textarea-autosize";
interface EditMetadataPanelProps {
data: {
name?: string | null;
[key: string]: any;
};
}
/** Adapts react-final-form's Field render prop to AsyncSelectPaginate. */
const AsyncSelectPaginateAdapter = ({ input, ...rest }: FieldRenderProps<any>) => (
<AsyncSelectPaginate {...input} {...rest} onChange={(value) => input.onChange(value)} />
);
/** Adapts react-final-form's Field render prop to TextareaAutosize. */
const TextareaAutosizeAdapter = ({ input, ...rest }: FieldRenderProps<any>) => (
<TextareaAutosize {...input} {...rest} onChange={(value) => input.onChange(value)} />
);
/** Sliding panel form for manually editing comic metadata fields. */
export const EditMetadataPanel = ({ data }: EditMetadataPanelProps): ReactElement => {
const onSubmit = async () => {};
return (
<>
<Form
onSubmit={onSubmit}
mutators={{ ...arrayMutators }}
render={({
handleSubmit,
form: {
mutators: { push, pop },
},
}) => (
<form onSubmit={handleSubmit}>
{/* Issue Name */}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Issue Details</label>
</div>
<div className="field-body">
<Field
name="issue_name"
component="input"
className="appearance-none w-full dark:bg-slate-400 bg-slate-100 h-10 rounded-md border-none text-gray-700 dark:text-slate-200 py-1 pr-7 pl-3 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
initialValue={data.name}
placeholder={"Issue Name"}
/>
</div>
</div>
{/* Issue Number and year */}
<div className="mt-4 flex flex-row gap-2">
<div>
<div className="text-sm">Issue Number</div>
<Field
name="issue_number"
component="input"
className="dark:bg-slate-400 w-20 bg-slate-100 py-2 px-2 rounded-md border-gray-300 h-10 dark:text-slate-200 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
placeholder="Issue Number"
/>
<p className="text-xs">Do not enter the first zero</p>
</div>
<div>
<div className="text-sm">Issue Year</div>
<Field
name="issue_year"
component="input"
className="dark:bg-slate-400 w-20 bg-slate-100 py-2 px-2 rounded-md border-gray-300 h-10 dark:text-slate-200 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
/>
</div>
<div>
<div className="text-sm">Page Count</div>
<Field
name="page_count"
component="input"
className="dark:bg-slate-400 w-20 bg-slate-100 py-2 px-2 rounded-md border-gray-300 h-10 dark:text-slate-200 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
placeholder="Page Count"
/>
</div>
</div>
{/* Description */}
<div className="mt-2">
<label className="text-sm">Description</label>
<Field
name={"description"}
className="dark:bg-slate-400 w-full min-h-24 bg-slate-100 py-2 px-2 rounded-md border-gray-300 h-10 dark:text-slate-200 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
component={TextareaAutosizeAdapter}
placeholder={"Description"}
/>
</div>
<hr />
<div className="field is-horizontal">
<div className="field-label">
<label className="label">Distributor Info</label>
</div>
<div className="field-body">
<div className="field is-expanded">
<div className="field">
<p className="control has-icons-left">
<Field
name="distributor_sku"
component="input"
className="input"
placeholder="SKU"
/>
{/* TODO: Switch to Solar icon */}
<span className="icon is-small is-left">
<i className="fa-solid fa-barcode"></i>
</span>
</p>
</div>
</div>
{/* UPC code */}
<div className="field">
<p className="control has-icons-left">
<Field
name="upc_code"
component="input"
className="input"
placeholder="UPC Code"
/>
{/* TODO: Switch to Solar icon */}
<span className="icon is-small is-left">
<i className="fa-solid fa-box"></i>
</span>
</p>
</div>
</div>
</div>
<hr />
{/* Publisher */}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Publisher</label>
</div>
<div className="field-body">
<div className="field">
<p className="control is-expanded has-icons-left">
<Field
name={"publisher"}
component={AsyncSelectPaginateAdapter}
placeholder={
/* TODO: Switch to Solar icon */
<div>
<i className="fas fa-print mr-2"></i> Publisher
</div>
}
metronResource={"publisher"}
/>
</p>
</div>
</div>
</div>
{/* Arc */}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Story Arc</label>
</div>
<div className="field-body">
<div className="field">
<p className="control is-expanded has-icons-left">
<Field
name={"story_arc"}
component={AsyncSelectPaginateAdapter}
placeholder={
/* TODO: Switch to Solar icon */
<div>
<i className="fas fa-book-open mr-2"></i> Story Arc
</div>
}
metronResource={"arc"}
/>
</p>
</div>
</div>
</div>
{/* series */}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Series</label>
</div>
<div className="field-body">
<div className="field">
<p className="control is-expanded has-icons-left">
<Field
name={"series"}
component={AsyncSelectPaginateAdapter}
placeholder={
/* TODO: Switch to Solar icon */
<div>
<i className="fas fa-layer-group mr-2"></i> Series
</div>
}
metronResource={"series"}
/>
</p>
</div>
</div>
</div>
<hr />
{/* team credits */}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Team Credits</label>
</div>
<div className="field-body mt-4">
<div className="field">
<div className="buttons">
<button
type="button"
className="button is-small"
onClick={() => push("credits", undefined)}
>
Add credit
</button>
<button
type="button"
className="button is-small"
onClick={() => pop("credits")}
>
Remove credit
</button>
</div>
</div>
</div>
</div>
<FieldArray name="credits">
{({ fields }) =>
fields.map((name, index) => (
<div className="field is-horizontal" key={name}>
<div className="field-label is-normal">
<label></label>
</div>
<div className="field-body">
<div className="field">
<p className="control">
<Field
name={`${name}.creator`}
component={AsyncSelectPaginateAdapter}
placeholder={
/* TODO: Switch to Solar icon */
<div>
<i className="fa-solid fa-ghost"></i> Creator
</div>
}
metronResource={"creator"}
/>
</p>
</div>
<div className="field">
<p className="control">
<Field
name={`${name}.role`}
metronResource={"role"}
placeholder={
/* TODO: Switch to Solar icon */
<div>
<i className="fa-solid fa-key"></i> Role
</div>
}
component={AsyncSelectPaginateAdapter}
/>
</p>
</div>
{/* TODO: Switch to Solar icon */}
<span
className="icon is-danger mt-2"
onClick={() => fields.remove(index)}
style={{ cursor: "pointer" }}
>
<i className="fas fa-times"></i>
</span>
</div>
</div>
))
}
</FieldArray>
</form>
)}
/>
</>
);
};
export default EditMetadataPanel;

View File

@@ -0,0 +1,178 @@
import React from "react";
import { isNil, map } from "lodash";
import { convert } from "html-to-text";
import ellipsize from "ellipsize";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import axios from "axios";
import { useGetComicByIdQuery } from "../../graphql/generated";
import type { MatchResultProps } from "../../types";
const handleBrokenImage = (e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.src = "http://localhost:3050/dist/img/noimage.svg";
};
interface ComicVineMatch {
description?: string;
name?: string;
score: string | number;
issue_number: string | number;
cover_date: string;
image: {
thumb_url: string;
};
volume: {
name: string;
};
volumeInformation: {
results: {
image: {
icon_url: string;
};
count_of_issues: number;
publisher: {
name: string;
};
};
};
}
export const MatchResult = (props: MatchResultProps) => {
const applyCVMatch = async (match: ComicVineMatch, comicObjectId: string) => {
try {
const response = await axios.request({
url: `${LIBRARY_SERVICE_BASE_URI}/applyComicVineMetadata`,
method: "POST",
data: {
match,
comicObjectId,
},
});
// Invalidate and refetch the comic book metadata
if (props.queryClient) {
await props.queryClient.invalidateQueries({
queryKey: useGetComicByIdQuery.getKey({ id: comicObjectId }),
});
}
// Call the callback to close panel and switch tabs
if (props.onMatchApplied) {
props.onMatchApplied();
}
return response;
} catch (error) {
console.error("Error applying ComicVine match:", error);
throw error;
}
};
return (
<>
<span className="flex items-center mt-6">
<span className="text-md text-slate-500 dark:text-slate-500 pr-5">
ComicVine Matches
</span>
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
</span>
{map(props.matchData, (match, idx) => {
let issueDescription = "";
if (!isNil(match.description)) {
issueDescription = convert(match.description, {
baseElements: {
selectors: ["p"],
},
});
}
const bestMatchCSSClass = idx === 0 ? "bg-green-100" : "bg-slate-300";
return (
<div className={`${bestMatchCSSClass} my-5 p-4 rounded-lg`} key={idx}>
<div className="flex flex-row gap-4">
<div className="min-w-fit">
<img
className="rounded-md"
src={match.image.thumb_url}
onError={handleBrokenImage}
/>
</div>
<div>
<div className="flex flex-row mb-1 justify-end">
{match.name ? (
<p className="text-md w-full">{match.name}</p>
) : null}
{/* score */}
<span className="inline-flex h-fit w-fit items-center bg-green-50 text-sm text-slate-800 font-medium px-2 rounded-md dark:text-slate-900 dark:bg-green-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--course-up-line-duotone] w-4 h-4"></i>
</span>
<span className="text-slate-900 dark:text-slate-900">
{parseInt(match.score, 10)}
</span>
</span>
</div>
<span className="flex flex-row gap-2 mb-2">
<span className="inline-flex items-center bg-slate-50 text-sm text-slate-800 font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--hashtag-outline] w-4 h-4"></i>
</span>
<span className="text-slate-900 dark:text-slate-900">
{parseInt(match.issue_number, 10)}
</span>
</span>
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-sm font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--calendar-mark-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-slate-900 dark:text-slate-900">
Cover Date: {match.cover_date}
</span>
</span>
</span>
<div className="text-sm">
{ellipsize(issueDescription, 300)}
</div>
</div>
</div>
<div className="flex flex-row gap-2 my-4 ml-10">
<div className="">
<img
src={match.volumeInformation.results.image.icon_url}
className="rounded-md w-full"
onError={handleBrokenImage}
/>
</div>
<div className="">
<span>{match.volume.name}</span>
<div className="text-sm">
<p>
Total Issues:
{match.volumeInformation.results.count_of_issues}
</p>
<p>
Published by{" "}
{match.volumeInformation.results.publisher.name}
</p>
</div>
</div>
</div>
<div className="flex justify-end">
<button
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
onClick={() => applyCVMatch(match, props.comicObjectId)}
>
<span className="text-md">Apply Match</span>
<span className="w-5 h-5">
<i className="h-5 w-5 icon-[solar--magic-stick-3-bold-duotone]"></i>
</span>
</button>
</div>
</div>
);
})}
</>
);
};
export default MatchResult;

View File

@@ -0,0 +1,116 @@
import React, { ReactElement, ReactNode } from "react";
import prettyBytes from "pretty-bytes";
import { isEmpty } from "lodash";
import { format, parseISO, isValid } from "date-fns";
import {
RawFileDetails as RawFileDetailsType,
InferredMetadata,
} from "../../graphql/generated";
type RawFileDetailsProps = {
data?: {
rawFileDetails?: RawFileDetailsType;
inferredMetadata?: InferredMetadata;
createdAt?: string;
};
children?: ReactNode;
};
/** Renders raw file info, inferred metadata, and import timestamp for a comic. */
export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
const { rawFileDetails, inferredMetadata, createdAt } = props.data || {};
return (
<>
<div className="max-w-2xl ml-5">
<div className="px-4 sm:px-6">
<p className="text-gray-500 dark:text-gray-400">
<span className="text-xl">{rawFileDetails?.name}</span>
</p>
</div>
<div className="px-4 py-5 sm:px-6">
<dl className="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div className="sm:col-span-1">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
Raw File Details
</dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
{rawFileDetails?.containedIn}
{"/"}
{rawFileDetails?.name}
{rawFileDetails?.extension}
</dd>
</div>
<div className="sm:col-span-1">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
Inferred Issue Metadata
</dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
Series Name: {inferredMetadata?.issue?.name}
{!isEmpty(inferredMetadata?.issue?.number) ? (
<span className="tag is-primary is-light">
{inferredMetadata?.issue?.number}
</span>
) : null}
</dd>
</div>
<div className="sm:col-span-1">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
MIMEType
</dt>
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
{/* File extension */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pt-1">
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{rawFileDetails?.mimeType}
</span>
</span>
</dd>
</div>
<div className="sm:col-span-1">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
File Size
</dt>
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
{/* size */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{rawFileDetails?.fileSize ? prettyBytes(rawFileDetails.fileSize) : "N/A"}
</span>
</span>
</dd>
</div>
<div className="sm:col-span-2">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
Import Details
</dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
{createdAt && isValid(parseISO(createdAt)) ? (
<>
{format(parseISO(createdAt), "dd MMMM, yyyy")},{" "}
{format(parseISO(createdAt), "h aaaa")}
</>
) : "N/A"}
</dd>
</div>
<div className="sm:col-span-2">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
Actions
</dt>
<dd className="mt-1 text-sm text-gray-900">{props.children}</dd>
</div>
</dl>
</div>
</div>
</>
);
};
export default RawFileDetails;

View File

@@ -0,0 +1,105 @@
import React, { useState } from "react";
import { ComicVineSearchForm } from "./ComicVineSearchForm";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { EditMetadataPanel } from "./EditMetadataPanel";
import type { RawFileDetails, InferredMetadata } from "../../graphql/generated";
interface CVMatchesPanelProps {
rawFileDetails?: RawFileDetails;
inferredMetadata: InferredMetadata;
comicVineMatches: any[];
comicObjectId: string;
queryClient: any;
onMatchApplied: () => void;
};
/**
* Collapsible container for manual ComicVine search form.
* Allows users to manually search when auto-match doesn't yield results.
*/
const CollapsibleSearchForm: React.FC<{ rawFileDetails?: RawFileDetails }> = ({
rawFileDetails,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="border border-slate-300 dark:border-slate-600 rounded-lg overflow-hidden">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-4 py-3 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors text-left"
aria-expanded={isExpanded}
>
<span className="flex items-center gap-2 text-slate-700 dark:text-slate-200 font-medium">
<svg
className={`w-4 h-4 transition-transform ${isExpanded ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
Manual Search
</span>
<span className="text-sm text-slate-500 dark:text-slate-400">
{isExpanded ? "Click to collapse" : "No results? Search manually"}
</span>
</button>
{isExpanded && (
<div className="p-4 bg-white dark:bg-slate-800">
<ComicVineSearchForm rawFileDetails={rawFileDetails} />
</div>
)}
</div>
);
};
/**
* Sliding panel content for ComicVine match search.
*
* Renders a search form pre-populated from `rawFileDetails`, a preview of the
* inferred issue being searched for, and a list of ComicVine match candidates
* the user can apply to the comic.
*
* @param props.onMatchApplied - Called after the user selects and applies a match,
* allowing the parent to close the panel and refresh state.
*/
export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
rawFileDetails,
inferredMetadata,
comicVineMatches,
comicObjectId,
queryClient,
onMatchApplied,
}) => (
<>
<div className="border-slate-500 border rounded-lg p-2 mb-3">
<p className="text-slate-600 dark:text-slate-300">Searching for:</p>
{inferredMetadata.issue ? (
<>
<span className="text-slate-800 dark:text-slate-100 font-medium">{inferredMetadata.issue?.name} </span>
<span className="text-slate-600 dark:text-slate-300"> # {inferredMetadata.issue?.number} </span>
</>
) : null}
</div>
<CollapsibleSearchForm rawFileDetails={rawFileDetails} />
<ComicVineMatchPanel
props={{
comicVineMatches,
comicObjectId,
queryClient,
onMatchApplied,
}}
/>
</>
);
type EditMetadataPanelWrapperProps = {
rawFileDetails?: RawFileDetails;
};
export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({
rawFileDetails,
}) => <EditMetadataPanel data={rawFileDetails ?? {}} />;

View File

@@ -0,0 +1,82 @@
import React, { ReactElement, Suspense, useState } from "react";
import { isNil } from "lodash";
interface TabItem {
id: number;
name: string;
icon: React.ReactNode;
content: React.ReactNode;
shouldShow?: boolean;
}
interface TabControlsProps {
filteredTabs: TabItem[];
downloadCount: number;
activeTab?: number;
setActiveTab?: (id: number) => void;
}
export const TabControls = (props: TabControlsProps): ReactElement => {
const { filteredTabs, downloadCount, activeTab, setActiveTab } = props;
const [active, setActive] = useState(filteredTabs[0].id);
// Use controlled state if provided, otherwise use internal state
const currentActive = activeTab !== undefined ? activeTab : active;
const handleSetActive = (id: number) => {
if (setActiveTab) {
setActiveTab(id);
} else {
setActive(id);
}
};
return (
<>
<div className="hidden sm:block mt-7 mb-3 w-fit">
<div className="border-b border-gray-200">
<nav className="flex gap-6" aria-label="Tabs">
{filteredTabs.map(({ id, name, icon }: TabItem) => (
<a
key={id}
className={`inline-flex shrink-0 items-center gap-2 px-1 py-1 text-md font-medium text-gray-500 dark:text-gray-400 hover:border-gray-300 hover:border-b hover:dark:text-slate-200 ${
currentActive === id
? "border-b border-cyan-50 dark:text-slate-200"
: "border-b border-transparent"
}`}
aria-current="page"
onClick={() => handleSetActive(id)}
>
{/* Downloads tab and count badge */}
<>
{id === 6 && !isNil(downloadCount) ? (
<span className="inline-flex flex-row">
{/* download count */}
<span className="inline-flex mx-2 items-center bg-slate-200 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-orange-400">
<span className="text-md text-slate-500 dark:text-slate-900">
{icon}
</span>
</span>
<i className="h-5 w-5 icon-[solar--download-bold-duotone] text-slate-500 dark:text-slate-300" />
</span>
) : (
<span className="w-5 h-5">{icon}</span>
)}
{name}
</>
</a>
))}
</nav>
</div>
</div>
<Suspense fallback={null}>
{filteredTabs.map(({ id, content }: TabItem) => (
<React.Fragment key={id}>
{currentActive === id ? content : null}
</React.Fragment>
))}
</Suspense>
</>
);
};
export default TabControls;

View File

@@ -0,0 +1,254 @@
import React, { ReactElement, useCallback, useEffect, useState } from "react";
import { DnD } from "../../shared/Draggable/DnD";
import { isEmpty } from "lodash";
import SlidingPane from "react-sliding-pane";
import { Canvas } from "../../shared/Canvas";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import {
IMAGETRANSFORMATION_SERVICE_BASE_URI,
LIBRARY_SERVICE_BASE_URI,
LIBRARY_SERVICE_HOST,
} from "../../../constants/endpoints";
import { useStore } from "../../../store";
import { useShallow } from "zustand/react/shallow";
import { escapePoundSymbol } from "../../../shared/utils/formatting.utils";
export const ArchiveOperations = (props: { data: any }): ReactElement => {
const { data } = props;
const getSocket = useStore((state) => state.getSocket);
const queryClient = useQueryClient();
// sliding panel config
const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
// current image
const [currentImage, setCurrentImage] = useState<string>("");
const [uncompressedArchive, setUncompressedArchive] = useState<string[]>([]);
const [imageAnalysisResult, setImageAnalysisResult] = useState<any>({});
const [shouldRefetchComicBookData, setShouldRefetchComicBookData] =
useState(false);
const constructImagePaths = (data: string[]): Array<string> => {
return data?.map((path: string) =>
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
);
};
// Listen to the uncompression complete event and orchestrate the final payload
useEffect(() => {
const socket = getSocket("/");
if (!socket) return;
const handleUncompressionComplete = (data: any) => {
setUncompressedArchive(constructImagePaths(data?.uncompressedArchive));
};
socket.on("LS_UNCOMPRESSION_JOB_COMPLETE", handleUncompressionComplete);
return () => {
socket.off("LS_UNCOMPRESSION_JOB_COMPLETE", handleUncompressionComplete);
};
}, [getSocket]);
useEffect(() => {
let isMounted = true;
if (data.rawFileDetails?.archive?.uncompressed) {
const fetchUncompressedArchive = async () => {
try {
const response = await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/walkFolders`,
method: "POST",
data: {
basePathToWalk: data?.rawFileDetails?.archive?.expandedPath,
extensions: [".jpg", ".jpeg", ".png", ".bmp", "gif"],
},
transformResponse: async (responseData) => {
const parsedData = JSON.parse(responseData);
const paths = parsedData.map((pathObject: any) => {
return `${pathObject.containedIn}/${pathObject.name}${pathObject.extension}`;
});
const uncompressedArchive = constructImagePaths(paths);
if (isMounted) {
setUncompressedArchive(uncompressedArchive);
setShouldRefetchComicBookData(true);
}
},
});
} catch (error) {
// Error handling could be added here if needed
}
};
fetchUncompressedArchive();
}
// Cleanup function
return () => {
isMounted = false;
setUncompressedArchive([]);
};
}, [data]);
const analyzeImage = async (imageFilePath: string) => {
const response = await axios({
url: `${IMAGETRANSFORMATION_SERVICE_BASE_URI}/analyze`,
method: "POST",
data: {
imageFilePath,
},
});
setImageAnalysisResult(response?.data);
queryClient.invalidateQueries({ queryKey: ["uncompressedArchive"] });
};
const {
data: uncompressionResult,
refetch,
isLoading,
isSuccess,
} = useQuery({
queryFn: async () =>
await axios({
method: "POST",
url: `http://localhost:3000/api/library/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: data.rawFileDetails.filePath,
comicObjectId: data._id,
options: {
type: "full",
purpose: "analysis",
imageResizeOptions: {
baseWidth: 275,
},
},
},
}),
queryKey: ["uncompressedArchive"],
enabled: false,
});
useEffect(() => {
if (isSuccess && shouldRefetchComicBookData) {
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
setShouldRefetchComicBookData(false);
}
}, [isSuccess, shouldRefetchComicBookData, queryClient]);
// sliding panel init
const contentForSlidingPanel: Record<string, { content: () => React.ReactElement }> = {
imageAnalysis: {
content: () => {
return (
<div>
<pre className="text-sm">{currentImage}</pre>
{!isEmpty(imageAnalysisResult) ? (
<pre className="p-2 mt-3">
<Canvas data={imageAnalysisResult} />
</pre>
) : null}
<pre className="font-hasklig mt-3 text-sm">
{JSON.stringify(imageAnalysisResult?.analyzedData, null, 2)}
</pre>
</div>
);
},
},
};
// sliding panel handlers
const openImageAnalysisPanel = useCallback((imageFilePath: string) => {
setSlidingPanelContentId("imageAnalysis");
analyzeImage(imageFilePath);
setCurrentImage(imageFilePath);
setVisible(true);
}, []);
return (
<div key={2}>
<article
role="alert"
className="mt-4 text-md rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
>
<div>
<p>You can perform several operations on your comic book archive.</p>
<p>
Uncompressing, re-organizing the individual pages, then
re-compressing to a different format, for example.
</p>
<p>You can also analyze color histograms of pages.</p>
</div>
</article>
<div className="mt-5">
{data.rawFileDetails.archive?.uncompressed &&
!isEmpty(uncompressedArchive) ? (
<article
role="alert"
className="mt-4 text-md rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
>
This issue is already uncompressed at:
<p>
<code className="font-hasklig text-sm">
{data.rawFileDetails.archive.expandedPath}
</code>
<div className="">It has {uncompressedArchive?.length} pages</div>
</p>
</article>
) : null}
<div className="flex flex-row gap-2 mt-4">
{isEmpty(uncompressedArchive) ? (
<button
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
onClick={() => refetch()}
>
<span className="text-md">Unpack Comic Archive</span>
<span className="w-6 h-6">
<i className="h-6 w-6 icon-[solar--box-bold-duotone]"></i>
</span>
</button>
) : null}
{!isEmpty(uncompressedArchive) ? (
<div>
<button
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
onClick={() => refetch()}
>
<span className="text-md">Convert to .cbz</span>
<span className="w-6 h-6">
<i className="h-6 w-6 icon-[solar--zip-file-bold-duotone]"></i>
</span>
</button>
</div>
) : null}
</div>
</div>
<div>
<div className="mt-10">
{!isEmpty(uncompressedArchive) ? (
<DnD
data={uncompressedArchive}
onClickHandler={openImageAnalysisPanel}
/>
) : null}
</div>
</div>
<SlidingPane
isOpen={visible}
onRequestClose={() => setVisible(false)}
title={"Image Analysis"}
width={"600px"}
>
{slidingPanelContentId !== "" &&
contentForSlidingPanel[slidingPanelContentId].content()}
</SlidingPane>
</div>
);
};
export default ArchiveOperations;

Some files were not shown because too many files have changed in this diff Show More