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
This commit was merged in pull request #98.
This commit is contained in:
16
index.html
16
index.html
@@ -1,16 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Three Two!</title>
|
<title>Three Two!</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="dark:bg-slate-600">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/client/index.tsx"></script>
|
<script type="module" src="/src/client/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
10
package.json
10
package.json
@@ -25,7 +25,7 @@
|
|||||||
"@tanstack/react-table": "^8.9.3",
|
"@tanstack/react-table": "^8.9.3",
|
||||||
"@types/mime-types": "^2.1.0",
|
"@types/mime-types": "^2.1.0",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"airdcpp-apisocket": "^2.5.0-beta.2",
|
"airdcpp-apisocket": "^2.5.0-beta.2",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"axios-cache-interceptor": "^1.0.1",
|
"axios-cache-interceptor": "^1.0.1",
|
||||||
@@ -61,7 +61,6 @@
|
|||||||
"react-router-dom": "^6.9.0",
|
"react-router-dom": "^6.9.0",
|
||||||
"react-select": "^5.8.0",
|
"react-select": "^5.8.0",
|
||||||
"react-select-async-paginate": "^0.7.2",
|
"react-select-async-paginate": "^0.7.2",
|
||||||
"react-slick": "^0.29.0",
|
|
||||||
"react-sliding-pane": "^7.1.0",
|
"react-sliding-pane": "^7.1.0",
|
||||||
"react-stickynode": "^4.1.0",
|
"react-stickynode": "^4.1.0",
|
||||||
"react-textarea-autosize": "^8.3.4",
|
"react-textarea-autosize": "^8.3.4",
|
||||||
@@ -76,6 +75,8 @@
|
|||||||
"zustand": "^4.4.6"
|
"zustand": "^4.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/solar": "^1.1.8",
|
||||||
|
"@iconify/tailwind": "^0.1.4",
|
||||||
"@storybook/addon-essentials": "^7.4.1",
|
"@storybook/addon-essentials": "^7.4.1",
|
||||||
"@storybook/addon-interactions": "^7.4.1",
|
"@storybook/addon-interactions": "^7.4.1",
|
||||||
"@storybook/addon-links": "^7.4.1",
|
"@storybook/addon-links": "^7.4.1",
|
||||||
@@ -95,8 +96,8 @@
|
|||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-redux": "^7.1.25",
|
"@types/react-redux": "^7.1.25",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bulma": "^0.9.4",
|
|
||||||
"docdash": "^2.0.2",
|
"docdash": "^2.0.2",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
@@ -111,11 +112,14 @@
|
|||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^29.6.3",
|
"jest": "^29.6.3",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"postcss-import": "^15.1.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"rimraf": "^4.1.3",
|
"rimraf": "^4.1.3",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
"storybook": "^7.3.2",
|
"storybook": "^7.3.2",
|
||||||
|
"tailwindcss": "^3.3.5",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
|
|||||||
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-import": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/fonts/Hasklig-Regular.otf
Normal file
BIN
public/fonts/Hasklig-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Heavy.otf
Normal file
BIN
public/fonts/PPObjectSans-Heavy.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-HeavySlanted.otf
Normal file
BIN
public/fonts/PPObjectSans-HeavySlanted.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Regular.otf
Normal file
BIN
public/fonts/PPObjectSans-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Slanted.otf
Normal file
BIN
public/fonts/PPObjectSans-Slanted.otf
Normal file
Binary file not shown.
@@ -1,637 +1,15 @@
|
|||||||
@import "/node_modules/bulma/bulma.sass";
|
@tailwind base;
|
||||||
$fa-font-path: "/node_modules/@fortawesome/fontawesome-free/webfonts";
|
@tailwind components;
|
||||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
@tailwind utilities;
|
||||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
|
||||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
|
||||||
$bg-color: yellow;
|
|
||||||
$border-color: red;
|
|
||||||
|
|
||||||
$volume-color: #fdecd1;
|
@layer base {
|
||||||
$issue-color: #f2f1f9;
|
@font-face {
|
||||||
$size-8: 0.9rem;
|
font-family: "PP Object Sans Regular";
|
||||||
$size-9: 0.7rem;
|
src: url("/fonts/PPObjectSans-Regular.otf") format("opentype");
|
||||||
$flexSize: 4em;
|
}
|
||||||
$boxSpacing: 1em;
|
|
||||||
$colorText: #404646;
|
|
||||||
body {
|
|
||||||
background: #fffcc;
|
|
||||||
}
|
|
||||||
.is-size-8 {
|
|
||||||
font-size: $size-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-size-9 {
|
@font-face {
|
||||||
font-size: $size-9;
|
font-family: "Hasklig Regular";
|
||||||
}
|
src: url("/fonts/Hasklig-Regular.otf") format("opentype");
|
||||||
|
|
||||||
.small-tag {
|
|
||||||
align-items: center;
|
|
||||||
background-color: #fff6de;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #4a4a4a;
|
|
||||||
display: inline-flex;
|
|
||||||
font-size: $size-9;
|
|
||||||
height: 1.5em;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1.5;
|
|
||||||
padding-left: 0.55em;
|
|
||||||
padding-right: 0.55em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// global style overrides
|
|
||||||
|
|
||||||
pre {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
.app {
|
|
||||||
font-family: helvetica, arial, sans-serif;
|
|
||||||
padding: 2em;
|
|
||||||
border: 5px solid $border-color;
|
|
||||||
|
|
||||||
p {
|
|
||||||
background-color: $bg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Navbar
|
|
||||||
.navbar {
|
|
||||||
border-bottom: 1px solid #f2f1f9;
|
|
||||||
.download-progress-meter {
|
|
||||||
margin-left: -300px;
|
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
.airdcpp-status {
|
|
||||||
min-width: 300px;
|
|
||||||
line-height: 1.7rem;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background: #454a59;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #454a59;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulsating-circle {
|
|
||||||
position: relative;
|
|
||||||
left: -120%;
|
|
||||||
top: 20%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 300%;
|
|
||||||
height: 300%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-left: -100%;
|
|
||||||
margin-top: -100%;
|
|
||||||
border-radius: 45px;
|
|
||||||
background-color: #01a4e9;
|
|
||||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: green;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
|
||||||
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-ring {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.33);
|
|
||||||
}
|
|
||||||
80%,
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-item.is-mega {
|
|
||||||
position: static;
|
|
||||||
|
|
||||||
.is-mega-menu-title {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding: 0.375rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
|
|
||||||
// slick slider overrides
|
|
||||||
.slick-slider {
|
|
||||||
margin-left: -10px;
|
|
||||||
.slick-list {
|
|
||||||
padding: 0 0px 15px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.recent-comics-container {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -22px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
.recent-comics-column {
|
|
||||||
padding-left: 22px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.volumes-container {
|
|
||||||
.stack {
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
/* The top layer shadow */ 0 -1px 1px rgba(0, 0, 0, 0.15),
|
|
||||||
/* The second layer */ 0 -10px 0 -5px #eee,
|
|
||||||
/* The second layer shadow */ 0 -10px 1px -4px rgba(0, 0, 0, 0.15),
|
|
||||||
/* The third layer */ 0 -20px 0 -10px #eee,
|
|
||||||
/* The third layer shadow */ 0 -20px 1px -9px rgba(0, 0, 0, 0.15);
|
|
||||||
img {
|
|
||||||
height: auto;
|
|
||||||
border-top-left-radius: 0.5rem;
|
|
||||||
border-top-right-radius: 0.5rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
.stack-title {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
margin: -5px 0 0 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
box-shadow: 1px 8px 23px 7px rgba(0, 0, 0, 0.12);
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.volumes-grid {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -30px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.volumes-grid-column {
|
|
||||||
padding-left: 22px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.min {
|
|
||||||
overflow: visible;
|
|
||||||
margin: auto;
|
|
||||||
.tag__custom {
|
|
||||||
height: auto !important;
|
|
||||||
padding: 0.3rem;
|
|
||||||
white-space: unset !important;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #effaf5;
|
|
||||||
color: #257953;
|
|
||||||
}
|
|
||||||
.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 {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #fff;
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0.4rem;
|
|
||||||
border-bottom-right-radius: 0.4rem;
|
|
||||||
box-shadow: 1px 8px 23px 7px rgba(0, 0, 0, 0.12);
|
|
||||||
|
|
||||||
.green-border {
|
|
||||||
border: 1px dotted #168b64;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.truncate {
|
|
||||||
width: 100px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.partial-rounded-card-image {
|
|
||||||
figure {
|
|
||||||
display: flex;
|
|
||||||
img {
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rounded-card-image {
|
|
||||||
figure {
|
|
||||||
display: flex;
|
|
||||||
img {
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
.card-title {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
.custom-icon,
|
|
||||||
i {
|
|
||||||
margin: 4px 4px 4px 0;
|
|
||||||
}
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-container {
|
|
||||||
// display: grid;
|
|
||||||
// grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
// column-gap: 0.5em;
|
|
||||||
// row-gap: 1.2em;
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
|
|
||||||
.partial-rounded-card-image {
|
|
||||||
img {
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rounded-card-image {
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-horizontal {
|
|
||||||
// margin: $boxSpacing / 2;
|
|
||||||
border-radius: 1.5em;
|
|
||||||
height: $flexSize;
|
|
||||||
max-width: $flexSize * 3;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
.card-image {
|
|
||||||
// leaving this here... for posterity
|
|
||||||
img.image {
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
height: 100%;
|
|
||||||
max-width: $flexSize * 1.3;
|
|
||||||
object-fit: cover;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
img.cropped-image {
|
|
||||||
width: 70px;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
height: 64px;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: 100% 0;
|
|
||||||
// flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
align-self: top;
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 0.7em;
|
|
||||||
padding-top: 0.4em;
|
|
||||||
padding-bottom: 0em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// raw file details
|
|
||||||
.raw-file-details {
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: beige;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comic-viewer {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
// comicvine metadata
|
|
||||||
.comicvine-metadata {
|
|
||||||
background-color: #f2f1f9;
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-metadata {
|
|
||||||
background-color: #fbffee;
|
|
||||||
padding: 0.8em;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
.name {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #4a4f50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comicInfo-metadata {
|
|
||||||
background-color: #f7ebdd;
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comic Detail
|
|
||||||
.comic-detail {
|
|
||||||
dl {
|
|
||||||
dd {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
.airdcpp-text {
|
|
||||||
margin: 0 0 0 0.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AirDC++ search results
|
|
||||||
.dupe-search-result {
|
|
||||||
background: lavender;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
.search {
|
|
||||||
.main-search-bar {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #999;
|
|
||||||
border-radius: 0;
|
|
||||||
outline: 0;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Library
|
|
||||||
.header-area {
|
|
||||||
width: 100%;
|
|
||||||
padding: 25px 0 15px 0;
|
|
||||||
position: sticky;
|
|
||||||
z-index: 9999;
|
|
||||||
background: #fffffc;
|
|
||||||
top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.library {
|
|
||||||
.table-controls {
|
|
||||||
background: #fffffc;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: sticky;
|
|
||||||
top: 126px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
margin: 0;
|
|
||||||
background: #fffffc;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: separate;
|
|
||||||
width: 100%;
|
|
||||||
thead {
|
|
||||||
position: sticky;
|
|
||||||
top: 250px;
|
|
||||||
z-index: 1;
|
|
||||||
background: #fffffc;
|
|
||||||
min-height: 130px;
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
td {
|
|
||||||
border: 0 none;
|
|
||||||
.card {
|
|
||||||
margin: 8px 0 7px 0;
|
|
||||||
.name {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comic Detail
|
|
||||||
.control-palette {
|
|
||||||
background-color: #fff6de;
|
|
||||||
display: inline-block;
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
// padding: 1.5rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// airdcpp downloads tab
|
|
||||||
.tabs {
|
|
||||||
.download-icon-labels {
|
|
||||||
.downloads-count {
|
|
||||||
margin: 0 1em -1px 0.4em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.download-tab-name {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawer content padding override
|
|
||||||
.slide-pane__content {
|
|
||||||
padding: 24px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-pane__header {
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume detail
|
|
||||||
.volume-details {
|
|
||||||
.is-volume-related {
|
|
||||||
$tag-background-color: $volume-color;
|
|
||||||
}
|
|
||||||
.issues-container {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -10px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
.issues-column {
|
|
||||||
max-width: 102px;
|
|
||||||
margin: 10px;
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Potential issue matches in library slideout panel
|
|
||||||
.potential-matches-container {
|
|
||||||
.potential-issue-match {
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
background-color: beige;
|
|
||||||
padding: 10px;
|
|
||||||
pre {
|
|
||||||
padding: 5px;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
white-space: pre-wrap; /* Since CSS 2.1 */
|
|
||||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
|
||||||
white-space: -pre-wrap; /* Opera 4-6 */
|
|
||||||
white-space: -o-pre-wrap; /* Opera 7 */
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.generic-card {
|
|
||||||
max-width: 90px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// comicvine search results
|
|
||||||
.search-results-container {
|
|
||||||
margin: 15px 0 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.search-result {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f2f1f9;
|
|
||||||
.cover-image {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.search-result-details {
|
|
||||||
.score {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.volume-information {
|
|
||||||
margin-top: -2.5em;
|
|
||||||
width: 80%;
|
|
||||||
background: #fdecd1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.vertical-line {
|
|
||||||
position: relative;
|
|
||||||
top: -25px;
|
|
||||||
left: 1.5rem;
|
|
||||||
border: 2px dotted #ccc;
|
|
||||||
width: 20px;
|
|
||||||
min-height: 35px;
|
|
||||||
|
|
||||||
border-color: transparent transparent #f3a22d #f3a22d;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Library grid
|
|
||||||
.my-masonry-grid {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -30px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.my-masonry-grid_column {
|
|
||||||
padding-left: 30px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-masonry-grid_column > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import Navbar from "./shared/Navbar";
|
import { Navbar2 } from "./shared/Navbar2";
|
||||||
import "../assets/scss/App.scss";
|
import "../assets/scss/App.scss";
|
||||||
|
|
||||||
export const App = (): ReactElement => {
|
export const App = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar2 />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import React, { useCallback, ReactElement, useEffect, useState } from "react";
|
import React, { useCallback, ReactElement, useEffect, useState } from "react";
|
||||||
import {
|
import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions";
|
||||||
downloadAirDCPPItem,
|
|
||||||
getBundlesForComic,
|
|
||||||
sleep,
|
|
||||||
} from "../../actions/airdcpp.actions";
|
|
||||||
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
|
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
|
||||||
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
@@ -13,6 +9,7 @@ import { isEmpty, isNil, map } from "lodash";
|
|||||||
import { useStore } from "../../store";
|
import { useStore } from "../../store";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
interface IAcquisitionPanelProps {
|
interface IAcquisitionPanelProps {
|
||||||
query: any;
|
query: any;
|
||||||
@@ -28,14 +25,22 @@ export const AcquisitionPanel = (
|
|||||||
airDCPPSocketInstance,
|
airDCPPSocketInstance,
|
||||||
airDCPPClientConfiguration,
|
airDCPPClientConfiguration,
|
||||||
airDCPPSessionInformation,
|
airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||||
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
|
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
|
||||||
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick: state.airDCPPDownloadTick,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface SearchData {
|
||||||
|
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
||||||
|
hub_urls: string[] | undefined | null;
|
||||||
|
priority: PriorityEnum;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the hubs list from an AirDCPP Socket
|
* Get the hubs list from an AirDCPP Socket
|
||||||
*/
|
*/
|
||||||
@@ -43,34 +48,15 @@ export const AcquisitionPanel = (
|
|||||||
queryKey: ["hubs"],
|
queryKey: ["hubs"],
|
||||||
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
||||||
});
|
});
|
||||||
|
const { comicObjectId } = props;
|
||||||
const issueName = props.query.issue.name || "";
|
const issueName = props.query.issue.name || "";
|
||||||
// const { settings } = props;
|
|
||||||
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
||||||
|
|
||||||
// Selectors for picking state
|
|
||||||
// const airDCPPSearchResults = useSelector((state: RootState) => {
|
|
||||||
// return state.airdcpp.searchResults;
|
|
||||||
// });
|
|
||||||
// const isAirDCPPSearchInProgress = useSelector(
|
|
||||||
// (state: RootState) => state.airdcpp.isAirDCPPSearchInProgress,
|
|
||||||
// );
|
|
||||||
// const searchInfo = useSelector(
|
|
||||||
// (state: RootState) => state.airdcpp.searchInfo,
|
|
||||||
// );
|
|
||||||
// const searchInstance: SearchInstance = useSelector(
|
|
||||||
// (state: RootState) => state.airdcpp.searchInstance,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const settings = useSelector((state: RootState) => state.settings.data);
|
|
||||||
// const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
|
||||||
interface SearchData {
|
|
||||||
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
|
||||||
hub_urls: string[] | undefined | null;
|
|
||||||
priority: PriorityEnum;
|
|
||||||
}
|
|
||||||
const [dcppQuery, setDcppQuery] = useState({});
|
const [dcppQuery, setDcppQuery] = useState({});
|
||||||
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState([]);
|
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState([]);
|
||||||
|
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
||||||
|
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState({});
|
||||||
|
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState({});
|
||||||
|
|
||||||
// Construct a AirDC++ query based on metadata inferred, upon component mount
|
// Construct a AirDC++ query based on metadata inferred, upon component mount
|
||||||
// Pre-populate the search input with the search string, so that
|
// Pre-populate the search input with the search string, so that
|
||||||
@@ -88,15 +74,18 @@ export const AcquisitionPanel = (
|
|||||||
setDcppQuery(dcppSearchQuery);
|
setDcppQuery(dcppSearchQuery);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to perform a search via an AirDC++ websocket
|
||||||
|
* @param {SearchData} data - a SearchData query
|
||||||
|
* @param {any} ADCPPSocket - an intialized AirDC++ socket instance
|
||||||
|
*/
|
||||||
const search = async (data: SearchData, ADCPPSocket: any) => {
|
const search = async (data: SearchData, ADCPPSocket: any) => {
|
||||||
try {
|
try {
|
||||||
if (!ADCPPSocket.isConnected()) {
|
if (!ADCPPSocket.isConnected()) {
|
||||||
await ADCPPSocket();
|
await ADCPPSocket();
|
||||||
}
|
}
|
||||||
const instance: SearchInstance = await ADCPPSocket.post("search");
|
const instance: SearchInstance = await ADCPPSocket.post("search");
|
||||||
// dispatch({
|
setAirDCPPSearchStatus(true);
|
||||||
// type: AIRDCPP_SEARCH_IN_PROGRESS,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// We want to get notified about every new result in order to make the user experience better
|
// We want to get notified about every new result in order to make the user experience better
|
||||||
await ADCPPSocket.addListener(
|
await ADCPPSocket.addListener(
|
||||||
@@ -142,6 +131,9 @@ export const AcquisitionPanel = (
|
|||||||
const currentInstance = await ADCPPSocket.get(
|
const currentInstance = await ADCPPSocket.get(
|
||||||
`search/${instance.id}`,
|
`search/${instance.id}`,
|
||||||
);
|
);
|
||||||
|
setAirDCPPSearchInstance(currentInstance);
|
||||||
|
setAirDCPPSearchInfo(searchInfo);
|
||||||
|
console.log("Asdas", airDCPPSearchInfo);
|
||||||
if (currentInstance.result_count === 0) {
|
if (currentInstance.result_count === 0) {
|
||||||
// ...nothing was received, show an informative message to the user
|
// ...nothing was received, show an informative message to the user
|
||||||
console.log("No more search results.");
|
console.log("No more search results.");
|
||||||
@@ -149,11 +141,8 @@ export const AcquisitionPanel = (
|
|||||||
|
|
||||||
// The search can now be considered to be "complete"
|
// The search can now be considered to be "complete"
|
||||||
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
||||||
// dispatch({
|
setAirDCPPSearchInstance(instance);
|
||||||
// type: AIRDCPP_HUB_SEARCHES_SENT,
|
setAirDCPPSearchStatus(false);
|
||||||
// searchInfo,
|
|
||||||
// instance,
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
instance.id,
|
instance.id,
|
||||||
);
|
);
|
||||||
@@ -164,6 +153,68 @@ export const AcquisitionPanel = (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to download a bundle associated with a search result from AirDC++
|
||||||
|
* @param {Number} searchInstanceId - description
|
||||||
|
* @param {String} resultId - description
|
||||||
|
* @param {String} comicObjectId - description
|
||||||
|
* @param {String} name - description
|
||||||
|
* @param {Number} size - description
|
||||||
|
* @param {any} type - description
|
||||||
|
* @param {any} ADCPPSocket - description
|
||||||
|
* @returns {void} - description
|
||||||
|
*/
|
||||||
|
const download = async (
|
||||||
|
searchInstanceId: Number,
|
||||||
|
resultId: String,
|
||||||
|
comicObjectId: String,
|
||||||
|
name: String,
|
||||||
|
size: Number,
|
||||||
|
type: any,
|
||||||
|
ADCPPSocket: any,
|
||||||
|
): void => {
|
||||||
|
try {
|
||||||
|
if (!ADCPPSocket.isConnected()) {
|
||||||
|
await ADCPPSocket.connect();
|
||||||
|
}
|
||||||
|
let bundleDBImportResult = {};
|
||||||
|
const downloadResult = await ADCPPSocket.post(
|
||||||
|
`search/${searchInstanceId}/results/${resultId}/download`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNil(downloadResult)) {
|
||||||
|
bundleDBImportResult = await axios({
|
||||||
|
method: "POST",
|
||||||
|
url: `http://localhost:3000/api/library/applyAirDCPPDownloadMetadata`,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
bundleId: downloadResult.bundle_info.id,
|
||||||
|
comicObjectId,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// dispatch({
|
||||||
|
// type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
|
||||||
|
// downloadResult,
|
||||||
|
// bundleDBImportResult,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// dispatch({
|
||||||
|
// type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||||
|
// comicBookDetail: bundleDBImportResult.data,
|
||||||
|
// IMS_inProgress: false,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
const getDCPPSearchResults = async (searchQuery) => {
|
const getDCPPSearchResults = async (searchQuery) => {
|
||||||
const manualQuery = {
|
const manualQuery = {
|
||||||
query: {
|
query: {
|
||||||
@@ -209,7 +260,6 @@ export const AcquisitionPanel = (
|
|||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
console.log("yaman", airDCPPSearchResults);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="comic-detail columns">
|
<div className="comic-detail columns">
|
||||||
@@ -245,7 +295,7 @@ export const AcquisitionPanel = (
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={
|
className={
|
||||||
false
|
airDCPPSearchStatus
|
||||||
? "button is-loading is-warning"
|
? "button is-loading is-warning"
|
||||||
: "button is-success is-light"
|
: "button is-success is-light"
|
||||||
}
|
}
|
||||||
@@ -273,6 +323,60 @@ export const AcquisitionPanel = (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AirDC++ search instance details */}
|
||||||
|
{!isNil(airDCPPSearchInstance) &&
|
||||||
|
!isEmpty(airDCPPSearchInfo) &&
|
||||||
|
!isNil(hubs) && (
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-one-quarter is-size-7">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-content">
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<div className="tags mb-1">
|
||||||
|
{hubs.map((value, idx) => (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div className="column is-one-quarter is-size-7">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-content">
|
||||||
|
<dl>
|
||||||
|
<dt>Search Instance: {airDCPPSearchInstance.id}</dt>
|
||||||
|
<dt>Owned by {airDCPPSearchInstance.owner}</dt>
|
||||||
|
<dd>Expires in: {airDCPPSearchInstance.expires_in}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* AirDC++ results */}
|
{/* AirDC++ results */}
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
|
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
|
||||||
@@ -342,12 +446,14 @@ export const AcquisitionPanel = (
|
|||||||
<button
|
<button
|
||||||
className="button is-small is-light is-success"
|
className="button is-small is-light is-success"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadDCPPResult(
|
download(
|
||||||
searchInstance.id,
|
airDCPPSearchInstance.id,
|
||||||
result.id,
|
result.id,
|
||||||
|
comicObjectId,
|
||||||
result.name,
|
result.name,
|
||||||
result.size,
|
result.size,
|
||||||
result.type,
|
result.type,
|
||||||
|
airDCPPSocketInstance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -69,17 +69,29 @@ export const Menu = (props): ReactElement => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const customStyles = {
|
||||||
|
option: (base, { data, isDisabled, isFocused, isSelected }) => {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
backgroundColor: isFocused ? "gray" : "black",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
control: (base) => ({
|
||||||
|
...base,
|
||||||
|
backgroundColor: "black",
|
||||||
|
border: "1px solid #CCC",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className="basic-single"
|
|
||||||
classNamePrefix="select"
|
|
||||||
components={{ Placeholder }}
|
components={{ Placeholder }}
|
||||||
placeholder={
|
placeholder={
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-solid fa-list"></i> Actions
|
<i className="fa-solid fa-list"></i> Actions
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
styles={customStyles}
|
||||||
name="actions"
|
name="actions"
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
options={filteredActionOptions}
|
options={filteredActionOptions}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
inferredMetadata,
|
inferredMetadata,
|
||||||
sourcedMetadata: { comicvine, locg, comicInfo },
|
sourcedMetadata: { comicvine, locg, comicInfo },
|
||||||
acquisition,
|
acquisition,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
},
|
},
|
||||||
userSettings,
|
userSettings,
|
||||||
} = data;
|
} = data;
|
||||||
@@ -246,46 +248,30 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
// 2. from the CV-scraped version
|
// 2. from the CV-scraped version
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<section className="container mx-auto">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{!isNil(data) && !isEmpty(data) && (
|
{!isNil(data) && !isEmpty(data) && (
|
||||||
<>
|
<>
|
||||||
<h1 className="title">{issueName}</h1>
|
<div>
|
||||||
<div className="columns is-multiline">
|
<div className="flex flex-row mt-5">
|
||||||
<div className="column is-narrow">
|
|
||||||
<Card
|
<Card
|
||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
orientation={"vertical"}
|
orientation={"cover-only"}
|
||||||
hasDetails={false}
|
hasDetails={false}
|
||||||
cardContainerStyle={{ maxWidth: 275 }}
|
|
||||||
/>
|
/>
|
||||||
{/* action dropdown */}
|
|
||||||
<div className="mt-4 is-size-7">
|
|
||||||
<Menu
|
|
||||||
data={data.data}
|
|
||||||
handlers={{ setSlidingPanelContentId, setVisible }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* raw file details */}
|
{/* raw file details */}
|
||||||
<div className="column">
|
|
||||||
{!isUndefined(rawFileDetails) &&
|
{!isUndefined(rawFileDetails) &&
|
||||||
!isEmpty(rawFileDetails.cover) && (
|
!isEmpty(rawFileDetails.cover) && (
|
||||||
<>
|
<div className="grid">
|
||||||
<RawFileDetails
|
<RawFileDetails
|
||||||
data={{
|
data={{
|
||||||
rawFileDetails: rawFileDetails,
|
rawFileDetails: rawFileDetails,
|
||||||
inferredMetadata: inferredMetadata,
|
inferredMetadata: inferredMetadata,
|
||||||
|
created_at: createdAt,
|
||||||
|
updated_at: updatedAt,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Read comic button */}
|
|
||||||
<button
|
|
||||||
className="button is-success is-light"
|
|
||||||
onClick={() => openModal(rawFileDetails.filePath)}
|
|
||||||
>
|
|
||||||
<i className="fa-solid fa-book-open mr-2"></i>
|
|
||||||
Read
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* <Modal
|
{/* <Modal
|
||||||
style={{ content: { marginTop: "2rem" } }}
|
style={{ content: { marginTop: "2rem" } }}
|
||||||
@@ -305,8 +291,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Modal> */}
|
</Modal> */}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* action dropdown */}
|
||||||
|
{/* <div className="mt-4 is-size-7">
|
||||||
|
<Menu
|
||||||
|
data={data.data}
|
||||||
|
handlers={{ setSlidingPanelContentId, setVisible }}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import React, { ReactElement } from "react";
|
|||||||
|
|
||||||
export const DownloadProgressTick = (props): ReactElement => {
|
export const DownloadProgressTick = (props): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div >
|
|
||||||
<h4 className="is-size-6">{props.data.name}</h4>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="is-size-3 has-text-weight-semibold">
|
<h4 className="is-size-5">{props.data.name}</h4>
|
||||||
|
<div>
|
||||||
|
<span className="is-size-4 has-text-weight-semibold">
|
||||||
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
||||||
{prettyBytes(props.data.size)}{" "}
|
{prettyBytes(props.data.size)}{" "}
|
||||||
</span>
|
</span>
|
||||||
@@ -20,13 +20,12 @@ export const DownloadProgressTick = (props): ReactElement => {
|
|||||||
%
|
%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div className="is-size-5">
|
<div className="is-size-6 mt-1 mb-2">
|
||||||
{prettyBytes(props.data.speed)} per second.
|
<p>{prettyBytes(props.data.speed)} per second.</p>
|
||||||
</div>
|
|
||||||
<div className="is-size-5">
|
|
||||||
Time left:
|
Time left:
|
||||||
{Math.round(parseInt(props.data.seconds_left) / 60)}
|
{Math.round(parseInt(props.data.seconds_left) / 60)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>{props.data.target}</div>
|
<div>{props.data.target}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,79 +2,138 @@ import React, { ReactElement } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
|
import { format, parseISO } from "date-fns";
|
||||||
|
|
||||||
export const RawFileDetails = (props): ReactElement => {
|
export const RawFileDetails = (props): ReactElement => {
|
||||||
const { rawFileDetails, inferredMetadata } = props.data;
|
const { rawFileDetails, inferredMetadata, created_at, updated_at } =
|
||||||
|
props.data;
|
||||||
|
const PaperClipIcon = () => <></>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="comic-detail raw-file-details column is-three-fifths">
|
<div className="max-w-2xl ml-5">
|
||||||
<dl>
|
<div className="px-4 sm:px-6">
|
||||||
<dt>Raw File Details</dt>
|
<p className="text-gray-500">
|
||||||
<dd className="is-size-7">
|
<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">
|
||||||
|
Raw File Details
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
{rawFileDetails.containedIn +
|
{rawFileDetails.containedIn +
|
||||||
"/" +
|
"/" +
|
||||||
rawFileDetails.name +
|
rawFileDetails.name +
|
||||||
rawFileDetails.extension}
|
rawFileDetails.extension}
|
||||||
</dd>
|
</dd>
|
||||||
<dd>
|
|
||||||
<div className="field is-grouped mt-2">
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Size</span>
|
|
||||||
<span className="tag is-info is-light">
|
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Extension</span>
|
|
||||||
<span className="tag is-primary is-light">
|
|
||||||
{rawFileDetails.extension}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">MIME type</span>
|
|
||||||
<span className="tag is-primary is-light">
|
|
||||||
{rawFileDetails.mimeType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="content comic-detail raw-file-details mt-3 column is-three-fifths">
|
|
||||||
<dl>
|
|
||||||
{/* inferred metadata */}
|
|
||||||
<dt>Inferred Issue Metadata</dt>
|
|
||||||
<dd>
|
|
||||||
<div className="field is-grouped mt-2">
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Name</span>
|
|
||||||
<span className="tag is-info is-light">
|
|
||||||
{inferredMetadata.issue.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Inferred Issue Metadata
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
Series Name: {inferredMetadata.issue.name}
|
||||||
{!isEmpty(inferredMetadata.issue.number) ? (
|
{!isEmpty(inferredMetadata.issue.number) ? (
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Number</span>
|
|
||||||
<span className="tag is-primary is-light">
|
<span className="tag is-primary is-light">
|
||||||
{inferredMetadata.issue.number}
|
{inferredMetadata.issue.number}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
</dd>
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">MIMEType</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-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="pr-1 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>
|
||||||
|
<dd className="text-sm text-gray-900"></dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">File Size</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-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">
|
||||||
|
{prettyBytes(rawFileDetails.fileSize)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Import Details
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{format(parseISO(created_at), "dd MMMM, yyyy")},{" "}
|
||||||
|
{format(parseISO(created_at), "h aaaa")}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">Attachments</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-200 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
<li className="flex items-center justify-between py-3 pl-3 pr-4 text-sm">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
<PaperClipIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 w-0 flex-1 truncate">
|
||||||
|
resume_back_end_developer.pdf
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between py-3 pl-3 pr-4 text-sm">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
{/* Read comic button */}
|
||||||
|
<button
|
||||||
|
className="button is-success is-light"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-book-open mr-2"></i>
|
||||||
|
Read
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -102,5 +161,7 @@ RawFileDetails.propTypes = {
|
|||||||
subtitle: PropTypes.string,
|
subtitle: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
created_at: PropTypes.string,
|
||||||
|
updated_at: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -11,19 +11,20 @@ export const TabControls = (props): ReactElement => {
|
|||||||
setActive(filteredTabs[0].id);
|
setActive(filteredTabs[0].id);
|
||||||
}, [acquisition]);
|
}, [acquisition]);
|
||||||
|
|
||||||
console.log(filteredTabs);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tabs">
|
<div className="hidden sm:block mt-7">
|
||||||
<ul>
|
<div className="border-b border-gray-200">
|
||||||
|
<nav className="-mb-px flex gap-6" aria-label="Tabs">
|
||||||
{filteredTabs.map(({ id, name, icon }) => (
|
{filteredTabs.map(({ id, name, icon }) => (
|
||||||
<li
|
<a
|
||||||
key={id}
|
key={id}
|
||||||
className={id === active ? "is-active" : ""}
|
className="inline-flex shrink-0 items-center gap-2 border-b-2 border-transparent px-1 pb-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
aria-current="page"
|
||||||
onClick={() => setActive(id)}
|
onClick={() => setActive(id)}
|
||||||
>
|
>
|
||||||
{/* Downloads tab and count badge */}
|
{/* Downloads tab and count badge */}
|
||||||
<a>
|
{/* <a>
|
||||||
{id === 6 && !isNil(acquisition.directconnect) ? (
|
{id === 6 && !isNil(acquisition.directconnect) ? (
|
||||||
<span className="download-icon-labels">
|
<span className="download-icon-labels">
|
||||||
<i className="fa-solid fa-download"></i>
|
<i className="fa-solid fa-download"></i>
|
||||||
@@ -35,10 +36,12 @@ export const TabControls = (props): ReactElement => {
|
|||||||
<span className="icon is-small">{icon}</span>
|
<span className="icon is-small">{icon}</span>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
|
</a> */}
|
||||||
|
{name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filteredTabs.map(({ id, content }) => {
|
{filteredTabs.map(({ id, content }) => {
|
||||||
return active === id ? content : null;
|
return active === id ? content : null;
|
||||||
|
|||||||
@@ -11,10 +11,40 @@ import {
|
|||||||
getComicBooks,
|
getComicBooks,
|
||||||
} from "../../actions/fileops.actions";
|
} from "../../actions/fileops.actions";
|
||||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||||
import { isEmpty, isNil } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Card } from "../shared/Carda";
|
||||||
|
import {
|
||||||
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
|
LIBRARY_SERVICE_HOST,
|
||||||
|
} from "../../constants/endpoints";
|
||||||
|
import {
|
||||||
|
determineCoverFile,
|
||||||
|
determineExternalMetadata,
|
||||||
|
} from "../../shared/utils/metadata.utils";
|
||||||
|
|
||||||
export const Dashboard = (): ReactElement => {
|
export const Dashboard = (): ReactElement => {
|
||||||
|
const { data: recentComics } = useQuery({
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBooks`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
paginationOptions: {
|
||||||
|
page: 0,
|
||||||
|
limit: 5,
|
||||||
|
sort: { updatedAt: "-1" },
|
||||||
|
},
|
||||||
|
predicate: { "acquisition.source.wanted": false },
|
||||||
|
comicStatus: "recent",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queryKey: ["recentComics"],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("hari om", recentComics);
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// dispatch(fetchVolumeGroups());
|
// dispatch(fetchVolumeGroups());
|
||||||
// dispatch(
|
// dispatch(
|
||||||
@@ -56,9 +86,102 @@ export const Dashboard = (): ReactElement => {
|
|||||||
// (state: RootState) => state.comicInfo.libraryStatistics,
|
// (state: RootState) => state.comicInfo.libraryStatistics,
|
||||||
// );
|
// );
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container mx-auto max-w-full">
|
||||||
<section className="section">
|
<section>
|
||||||
<h1 className="title">Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
|
<div className="grid grid-cols-5 gap-6">
|
||||||
|
{recentComics?.data.docs.map(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
_id,
|
||||||
|
rawFileDetails,
|
||||||
|
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||||
|
inferredMetadata,
|
||||||
|
acquisition: {
|
||||||
|
source: { name },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
idx,
|
||||||
|
) => {
|
||||||
|
const { issueName, url } = determineCoverFile({
|
||||||
|
rawFileDetails,
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
});
|
||||||
|
const { issue, coverURL, icon } = determineExternalMetadata(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const isComicVineMetadataAvailable =
|
||||||
|
!isUndefined(comicvine) &&
|
||||||
|
!isUndefined(comicvine.volumeInformation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
orientation="vertical-2"
|
||||||
|
key={idx}
|
||||||
|
imageUrl={`${LIBRARY_SERVICE_HOST}/${rawFileDetails.cover.filePath}`}
|
||||||
|
title={inferredMetadata.issue.name}
|
||||||
|
hasDetails
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<dt className="sr-only">Address</dt>
|
||||||
|
<dd className="text-sm my-1 flex flex-row gap-1">
|
||||||
|
{/* Issue number */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--hashtag-outline]"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900">
|
||||||
|
{inferredMetadata.issue.number}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/* File extension */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--file-bold-duotone] w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{rawFileDetails.extension}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center gap-1 my-2">
|
||||||
|
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
|
{/* ComicInfo.xml presence */}
|
||||||
|
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||||
|
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300"></i>
|
||||||
|
)}
|
||||||
|
{/* ComicVine metadata presence */}
|
||||||
|
{isComicVineMetadataAvailable && (
|
||||||
|
<span className="w-7 h-7">
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
|
alt={"ComicVine metadata detected."}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Raw file presence */}
|
||||||
|
{isNil(rawFileDetails) && (
|
||||||
|
<span className="h-6 w-5 sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
|
<i className="icon-[solar--file-corrupted-outline] h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import React, {
|
import React, { ReactElement, useEffect, useState } from "react";
|
||||||
ReactElement,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { getTransfers } from "../../actions/airdcpp.actions";
|
import { getTransfers } from "../../actions/airdcpp.actions";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
|
||||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import MetadataPanel from "../shared/MetadataPanel";
|
import MetadataPanel from "../shared/MetadataPanel";
|
||||||
@@ -17,18 +9,18 @@ interface IDownloadsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Downloads = (props: IDownloadsProps): ReactElement => {
|
export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||||
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
// const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
||||||
const {
|
const {
|
||||||
airDCPPState: { settings, socket },
|
airDCPPState: { settings, socket },
|
||||||
} = airDCPPConfiguration;
|
} = airDCPPConfiguration;
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
const airDCPPTransfers = useSelector(
|
// const airDCPPTransfers = useSelector(
|
||||||
(state: RootState) => state.airdcpp.transfers,
|
// (state: RootState) => state.airdcpp.transfers,
|
||||||
);
|
// );
|
||||||
const issueBundles = useSelector(
|
// const issueBundles = useSelector(
|
||||||
(state: RootState) => state.airdcpp.issue_bundles,
|
// (state: RootState) => state.airdcpp.issue_bundles,
|
||||||
);
|
// );
|
||||||
const [bundles, setBundles] = useState([]);
|
const [bundles, setBundles] = useState([]);
|
||||||
// Make the call to get all transfers from AirDC++
|
// Make the call to get all transfers from AirDC++
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -53,25 +53,6 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1a. Act on each comic issue successfully imported/failed, as indicated
|
|
||||||
// by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events
|
|
||||||
socketIOInstance.on("LS_COVER_EXTRACTED", (data) => {
|
|
||||||
const { completedJobCount, importResult } = data;
|
|
||||||
importJobQueue.setJobCount("successful", completedJobCount);
|
|
||||||
importJobQueue.setMostRecentImport(importResult.rawFileDetails.name);
|
|
||||||
});
|
|
||||||
socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
|
|
||||||
const { failedJobCount } = data;
|
|
||||||
importJobQueue.setJobCount("failed", failedJobCount);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1b. Clear the localStorage sessionId upon receiving the
|
|
||||||
// LS_IMPORT_QUEUE_DRAINED event
|
|
||||||
socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
|
|
||||||
localStorage.removeItem("sessionId");
|
|
||||||
importJobQueue.setStatus("drained");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
|
||||||
});
|
|
||||||
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||||
socketIOInstance.emit(
|
socketIOInstance.emit(
|
||||||
"call",
|
"call",
|
||||||
@@ -94,29 +75,35 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "running":
|
case "running":
|
||||||
return (
|
return (
|
||||||
<div className="control">
|
<div>
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-light"
|
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={() => {
|
onClick={() => {
|
||||||
toggleQueue("pause", "paused");
|
toggleQueue("pause", "paused");
|
||||||
importJobQueue.setStatus("paused");
|
importJobQueue.setStatus("paused");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-pause mr-2"></i> Pause
|
<span className="text-md">Pause</span>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="h-5 w-5 icon-[solar--pause-bold]"></i>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "paused":
|
case "paused":
|
||||||
return (
|
return (
|
||||||
<div className="control">
|
<div>
|
||||||
<button
|
<button
|
||||||
className="button is-success is-light"
|
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={() => {
|
onClick={() => {
|
||||||
toggleQueue("resume", "running");
|
toggleQueue("resume", "running");
|
||||||
importJobQueue.setStatus("running");
|
importJobQueue.setStatus("running");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-play mr-2"></i> Resume
|
<span className="text-md">Resume</span>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="h-5 w-5 icon-[solar--play-bold]"></i>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -129,17 +116,33 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div>
|
||||||
<section className="section is-small">
|
<section>
|
||||||
<h1 className="title">Import Comics</h1>
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<article className="message is-dark">
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
<div className="message-body">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
<p className="mb-2">
|
<div className="text-center sm:text-left">
|
||||||
<span className="tag is-medium is-info is-light">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
Import Comics
|
Import
|
||||||
</span>
|
</h1>
|
||||||
will add comics identified from the mapped folder into ThreeTwo's
|
|
||||||
database.
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Import comics into the ThreeTwo library.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||||
|
<article
|
||||||
|
role="alert"
|
||||||
|
className="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>
|
||||||
|
Importing will add comics identified from the mapped folder into
|
||||||
|
ThreeTwo's database.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Metadata from ComicInfo.xml, if present, will also be extracted.
|
Metadata from ComicInfo.xml, if present, will also be extracted.
|
||||||
@@ -150,115 +153,132 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<p className="buttons">
|
|
||||||
|
<div className="my-4">
|
||||||
|
{importJobQueue.status === "drained" ||
|
||||||
|
(importJobQueue.status === undefined && (
|
||||||
<button
|
<button
|
||||||
className={
|
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-5 py-3 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
importJobQueue.status === "drained" ||
|
|
||||||
importJobQueue.status === undefined
|
|
||||||
? "button is-medium"
|
|
||||||
: "button is-loading is-medium"
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
initiateImport();
|
initiateImport();
|
||||||
importJobQueue.setStatus("running");
|
importJobQueue.setStatus("running");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="icon">
|
<span className="text-md">Start Import</span>
|
||||||
<i className="fas fa-file-import"></i>
|
<span className="w-6 h-6">
|
||||||
|
<i className="h-6 w-6 icon-[solar--file-left-bold-duotone]"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>Start Import</span>
|
|
||||||
</button>
|
</button>
|
||||||
</p>
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{importJobQueue.status !== "drained" &&
|
{/* Activity */}
|
||||||
!isUndefined(importJobQueue.status) && (
|
{(importJobQueue.status === "running" ||
|
||||||
|
importJobQueue.status === "paused") && (
|
||||||
<>
|
<>
|
||||||
<table className="table">
|
<span className="flex items-center my-5 max-w-screen-lg">
|
||||||
<thead>
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
<tr>
|
Import Activity
|
||||||
<th>Completed Jobs</th>
|
</span>
|
||||||
<th>Failed Jobs</th>
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
<th>Queue Controls</th>
|
</span>
|
||||||
<th>Queue Status</th>
|
<div className="mt-5 flex flex-col gap-4 sm:mt-0 sm:flex-row sm:items-center">
|
||||||
</tr>
|
<dl className="grid grid-cols-2 gap-4 sm:grid-cols-2">
|
||||||
</thead>
|
{/* Successful import counts */}
|
||||||
|
<div className="flex flex-col rounded-lg bg-green-100 dark:bg-green-200 px-4 py-6 text-center">
|
||||||
<tbody>
|
<dd className="text-3xl text-green-600 md:text-5xl">
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
{importJobQueue.successfulJobCount > 0 && (
|
|
||||||
<div className="box has-background-success-light has-text-centered">
|
|
||||||
<span className="is-size-2 has-text-weight-bold">
|
|
||||||
{importJobQueue.successfulJobCount}
|
{importJobQueue.successfulJobCount}
|
||||||
</span>
|
</dd>
|
||||||
|
<dt className="text-lg font-medium text-gray-500">
|
||||||
|
imported
|
||||||
|
</dt>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{/* Failed job counts */}
|
||||||
</th>
|
<div className="flex flex-col rounded-lg bg-red-100 dark:bg-red-200 px-4 py-6 text-center">
|
||||||
<td>
|
<dd className="text-3xl text-red-600 md:text-5xl">
|
||||||
{importJobQueue.failedJobCount > 0 && (
|
|
||||||
<div className="box has-background-danger has-text-centered">
|
|
||||||
<span className="is-size-2 has-text-weight-bold">
|
|
||||||
{importJobQueue.failedJobCount}
|
{importJobQueue.failedJobCount}
|
||||||
|
</dd>
|
||||||
|
<dt className="text-lg font-medium text-gray-500">
|
||||||
|
failed
|
||||||
|
</dt>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col dark:text-slate-200 text-slate-400">
|
||||||
|
<dd>{renderQueueControls(importJobQueue.status)}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="mt-2 dark:text-slate-200 text-slate-400">
|
||||||
|
Imported: <span>{importJobQueue.mostRecentImport}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>{renderQueueControls(importJobQueue.status)}</td>
|
|
||||||
<td>
|
|
||||||
{importJobQueue.status !== undefined ? (
|
|
||||||
<span className="tag is-warning">
|
|
||||||
{importJobQueue.status}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
Imported{" "}
|
|
||||||
<span className="has-text-weight-bold">
|
|
||||||
{importJobQueue.mostRecentImport}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Past imports */}
|
{/* Past imports */}
|
||||||
{!isLoading && !isEmpty(data?.data) && (
|
{!isLoading && !isEmpty(data?.data) && (
|
||||||
<>
|
<div className="max-w-screen-lg">
|
||||||
<h3 className="subtitle is-4 mt-5">Past Imports</h3>
|
<span className="flex items-center mt-6">
|
||||||
<table className="table">
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
<thead>
|
Past Imports
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto mt-4 rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
||||||
|
<thead className="ltr:text-left rtl:text-right">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Time Started</th>
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
<th>Session Id</th>
|
Time Started
|
||||||
<th>Imported</th>
|
</th>
|
||||||
<th>Failed</th>
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Session Id
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Imported
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Failed
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody className="divide-y divide-gray-200">
|
||||||
{data?.data.map((jobResult, id) => {
|
{data?.data.map((jobResult, id) => {
|
||||||
return (
|
return (
|
||||||
<tr key={id}>
|
<tr key={id}>
|
||||||
<td>
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
{format(
|
{format(
|
||||||
new Date(jobResult.earliestTimestamp),
|
new Date(jobResult.earliestTimestamp),
|
||||||
"EEEE, hh:mma, do LLLL Y",
|
"EEEE, hh:mma, do LLLL Y",
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
<span className="tag is-warning">
|
<span className="tag is-warning">
|
||||||
{jobResult.sessionId}
|
{jobResult.sessionId}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
||||||
<span className="tag is-success">
|
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--check-circle-line-duotone] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">
|
||||||
{jobResult.completedJobs}
|
{jobResult.completedJobs}
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
||||||
<span className="tag is-danger">
|
<span className="inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-red-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--close-circle-line-duotone] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p className="whitespace-nowrap text-sm">
|
||||||
{jobResult.failedJobs}
|
{jobResult.failedJobs}
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -266,8 +286,10 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import T2Table from "../shared/T2Table";
|
|||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { format, fromUnixTime, parseISO } from "date-fns";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that tabulates the contents of the user's ThreeTwo Library.
|
* Component that tabulates the contents of the user's ThreeTwo Library.
|
||||||
@@ -53,45 +54,42 @@ export const Library = (): ReactElement => {
|
|||||||
|
|
||||||
const ComicInfoXML = (value) => {
|
const ComicInfoXML = (value) => {
|
||||||
return value.data ? (
|
return value.data ? (
|
||||||
<div className="comicvine-metadata mt-3">
|
<dl className="flex flex-col text-md p-3 ml-4 my-3 rounded-lg dark:bg-yellow-500 bg-yellow-300 w-max">
|
||||||
<dl>
|
{/* Series Name */}
|
||||||
<span className="tags has-addons is-size-7">
|
<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="tag">Series</span>
|
<span className="pr-1 pt-1">
|
||||||
<span className="tag is-warning is-light">
|
<i className="icon-[solar--bookmark-square-minimalistic-bold-duotone] w-5 h-5"></i>
|
||||||
{ellipsize(value.data.series[0], 25)}
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
{ellipsize(value.data.series[0], 45)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</dl>
|
<div className="flex flex-row mt-2 gap-2">
|
||||||
<dl>
|
{/* Pages */}
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<div className="control">
|
<span className="pr-1 pt-1">
|
||||||
<span className="tags has-addons is-size-7 mt-2">
|
<i className="icon-[solar--notebook-minimalistic-bold-duotone] w-5 h-5"></i>
|
||||||
<span className="tag">Pages</span>
|
</span>
|
||||||
<span className="tag is-info is-light has-text-weight-bold">
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
{value.data.pagecount[0]}
|
Pages: {value.data.pagecount[0]}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
{/* Issue number */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<div className="control">
|
<span className="pr-1 pt-1">
|
||||||
<span className="tags has-addons is-size-7 mt-2">
|
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i>
|
||||||
<span className="tag">Issue</span>
|
</span>
|
||||||
|
<span className="text-slate-900 dark:text-slate-900">
|
||||||
{!isNil(value.data.number) && (
|
{!isNil(value.data.number) && (
|
||||||
<span className="tag has-text-weight-bold is-success is-light">
|
<span>{parseInt(value.data.number[0], 10)}</span>
|
||||||
{parseInt(value.data.number[0], 10)}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WantedStatus = ({ value }) => {
|
|
||||||
return !value ? <span className="tag is-info is-light">Wanted</span> : null;
|
|
||||||
};
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -110,14 +108,10 @@ export const Library = (): ReactElement => {
|
|||||||
{
|
{
|
||||||
header: "ComicInfo.xml",
|
header: "ComicInfo.xml",
|
||||||
accessorKey: "_source.sourcedMetadata.comicInfo",
|
accessorKey: "_source.sourcedMetadata.comicInfo",
|
||||||
align: "center",
|
|
||||||
minWidth: 250,
|
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
!isEmpty(info.getValue()) ? (
|
!isEmpty(info.getValue()) ? (
|
||||||
<ComicInfoXML data={info.getValue()} />
|
<ComicInfoXML data={info.getValue()} />
|
||||||
) : (
|
) : null,
|
||||||
<span className="tag mt-5">No ComicInfo.xml</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -125,28 +119,41 @@ export const Library = (): ReactElement => {
|
|||||||
header: "Additional Metadata",
|
header: "Additional Metadata",
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: "Publisher",
|
header: "Date of Import",
|
||||||
accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
|
accessorKey: "_source.createdAt",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
return (
|
return !isNil(info.getValue()) ? (
|
||||||
!isNil(info.getValue()) && (
|
<div className="text-xs w-max ml-3 my-3 text-slate-600">
|
||||||
<h6 className="is-size-7 has-text-weight-bold">
|
<p>{format(parseISO(info.getValue()), "dd MMMM, yyyy")} </p>
|
||||||
{info.getValue().publisher.name}
|
{format(parseISO(info.getValue()), "h aaaa")}
|
||||||
</h6>
|
</div>
|
||||||
)
|
) : null;
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Something",
|
header: "Downloads",
|
||||||
accessorKey: "_source.acquisition.source.wanted",
|
accessorKey: "_source.acquisition",
|
||||||
cell: (info) => {
|
cell: (info) => (
|
||||||
!isUndefined(info.getValue()) ? (
|
<div className="flex flex-col gap-2 ml-3 my-3">
|
||||||
<WantedStatus value={info.getValue().toString()} />
|
<span className="inline-flex items-center w-fit bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
) : (
|
<span className="pr-1 pt-1">
|
||||||
"Nothing"
|
<i className="icon-[solar--folder-path-connect-bold-duotone] w-5 h-5"></i>
|
||||||
);
|
</span>
|
||||||
},
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
DC++: {info.getValue().directconnect.downloads.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="inline-flex w-fit items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
Torrent: {info.getValue().torrent.downloads.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -189,11 +196,23 @@ export const Library = (): ReactElement => {
|
|||||||
// value: PropTypes.bool.isRequired,
|
// value: PropTypes.bool.isRequired,
|
||||||
// };
|
// };
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<div>
|
||||||
<div className="section">
|
<section>
|
||||||
<div className="header-area">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<h1 className="title">Library</h1>
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Library
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Browse your comic book collection.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
{!isUndefined(searchResults?.hits) ? (
|
{!isUndefined(searchResults?.hits) ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="library">
|
<div className="library">
|
||||||
@@ -210,29 +229,36 @@ export const Library = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="columns">
|
<>
|
||||||
<div className="column is-two-thirds">
|
<article
|
||||||
<article className="message is-link">
|
role="alert"
|
||||||
<div className="message-body">
|
className="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"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
No comics were found in the library, Elasticsearch reports no
|
No comics were found in the library, Elasticsearch reports no
|
||||||
indices. Try importing a few comics into the library and come
|
indices. Try importing a few comics into the library and come
|
||||||
back.
|
back.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
<div className="block max-w-md p-6 bg-white border border-gray-200 my-3 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
|
<pre className="text-sm font-hasklig text-slate-700 dark:text-slate-700">
|
||||||
{!isUndefined(searchResults?.data?.meta?.body) ? (
|
{!isUndefined(searchResults?.data?.meta?.body) ? (
|
||||||
<pre>
|
<p>
|
||||||
{JSON.stringify(
|
{JSON.stringify(
|
||||||
searchResults.data.meta.body.error.root_cause,
|
searchResults.data.meta.body.error.root_cause,
|
||||||
null,
|
null,
|
||||||
4,
|
4,
|
||||||
)}
|
)}
|
||||||
</pre>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,36 +25,42 @@ export const SearchBar = (): ReactElement => {
|
|||||||
// );
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="box">
|
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
initialValues={{}}
|
initialValues={{}}
|
||||||
render={({ handleSubmit, form, submitting, pristine, values }) => (
|
render={({ handleSubmit, form, submitting, pristine, values }) => (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="field is-grouped">
|
|
||||||
<div className="control search is-expanded">
|
|
||||||
<Field name="search">
|
<Field name="search">
|
||||||
{({ input, meta }) => {
|
{({ input, meta }) => {
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-row w-full">
|
||||||
|
<div className="flex flex-row bg-slate-300 dark:bg-slate-500 rounded-l-lg p-2 min-w-full">
|
||||||
|
<div className="w-10 text-gray-400">
|
||||||
|
<i className="icon-[solar--magnifer-bold-duotone] h-7 w-7" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
{...input}
|
{...input}
|
||||||
className="input main-search-bar is-medium"
|
className="bg-slate-300 dark:bg-slate-500 outline-none text-lg text-gray-700 w-full"
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
placeholder="Type an issue/volume name"
|
placeholder="Type an issue/volume name"
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="control">
|
|
||||||
<button className="button is-medium" type="submit">
|
<button
|
||||||
|
className="sm:mt-0 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"
|
||||||
|
>
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,19 +35,16 @@ export const AirDCPPHubsForm = (): ReactElement => {
|
|||||||
* Get the hubs list from an AirDCPP Socket
|
* Get the hubs list from an AirDCPP Socket
|
||||||
*/
|
*/
|
||||||
const { data: hubs } = useQuery({
|
const { data: hubs } = useQuery({
|
||||||
queryKey: [],
|
queryKey: ["hubs"],
|
||||||
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
||||||
enabled: !!settings,
|
|
||||||
});
|
});
|
||||||
let hubList = {};
|
let hubList = {};
|
||||||
if (!isEmpty(hubs)) {
|
if (!isNil(hubs)) {
|
||||||
console.log("hs", hubs);
|
|
||||||
hubList = hubs.map(({ hub_url, identity }) => ({
|
hubList = hubs.map(({ hub_url, identity }) => ({
|
||||||
value: hub_url,
|
value: hub_url,
|
||||||
label: identity.name,
|
label: identity.name,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
console.log(hubList);
|
|
||||||
const { mutate } = useMutation({
|
const { mutate } = useMutation({
|
||||||
mutationFn: async (values) =>
|
mutationFn: async (values) =>
|
||||||
await axios({
|
await axios({
|
||||||
|
|||||||
@@ -3,11 +3,21 @@ import React, { ReactElement } from "react";
|
|||||||
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
|
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
|
||||||
const { settings } = settingsObject;
|
const { settings } = settingsObject;
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 is-clearfix">
|
<div>
|
||||||
<div className="card">
|
<span className="flex items-center mt-10 mb-4">
|
||||||
<div className="card-content">
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
<span className="tag is-pulled-right is-primary">Connected</span>
|
AirDC++ Client Information
|
||||||
<div className="content is-size-7">
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
<div className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
|
<span className="inline-flex justify-center rounded-full bg-emerald-100 mb-4 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--plug-circle-bold] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">Connected</p>
|
||||||
|
</span>
|
||||||
|
<p className="font-hasklig text-sm text-slate-700 dark:text-slate-700">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>{settings._id}</dt>
|
<dt>{settings._id}</dt>
|
||||||
<dt>Client version: {settings.system_info.client_version}</dt>
|
<dt>Client version: {settings.system_info.client_version}</dt>
|
||||||
@@ -19,13 +29,10 @@ export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
|
|||||||
<dt>Active Sessions: {settings.user.active_sessions}</dt>
|
<dt>Active Sessions: {settings.user.active_sessions}</dt>
|
||||||
<dt>
|
<dt>
|
||||||
Permissions:{" "}
|
Permissions:{" "}
|
||||||
<pre>
|
|
||||||
{JSON.stringify(settings.user.permissions, undefined, 2)}
|
{JSON.stringify(settings.user.permissions, undefined, 2)}
|
||||||
</pre>
|
|
||||||
</dt>
|
</dt>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
|||||||
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
||||||
? airDCPPClientConfiguration
|
? airDCPPClientConfiguration
|
||||||
: {};
|
: {};
|
||||||
console.log(airDCPPClientConfiguration);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConnectionForm
|
<ConnectionForm
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation, QueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const QbittorrentConnectionForm = (): ReactElement => {
|
export const QbittorrentConnectionForm = (): ReactElement => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
// fetch settings
|
// fetch settings
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["settings"],
|
queryKey: ["settings"],
|
||||||
@@ -13,7 +14,8 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const hostDetails = data?.data.bittorrent.client.host;
|
console.log(data);
|
||||||
|
const hostDetails = data?.data?.bittorrent?.client?.host;
|
||||||
// connect to qbittorrent client
|
// connect to qbittorrent client
|
||||||
const { data: connectionDetails } = useQuery({
|
const { data: connectionDetails } = useQuery({
|
||||||
queryKey: [],
|
queryKey: [],
|
||||||
@@ -35,7 +37,6 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
|||||||
}),
|
}),
|
||||||
enabled: !!connectionDetails,
|
enabled: !!connectionDetails,
|
||||||
});
|
});
|
||||||
console.log(qbittorrentClientInfo?.data);
|
|
||||||
// Update action using a mutation
|
// Update action using a mutation
|
||||||
const { mutate } = useMutation({
|
const { mutate } = useMutation({
|
||||||
mutationFn: async (values) =>
|
mutationFn: async (values) =>
|
||||||
@@ -44,6 +45,11 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
data: { settingsPayload: values, settingsKey: "bittorrent" },
|
data: { settingsPayload: values, settingsKey: "bittorrent" },
|
||||||
}),
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["settings", "qbittorrentClientInfo"],
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isError)
|
if (isError)
|
||||||
@@ -61,9 +67,24 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
|||||||
submitHandler={mutate}
|
submitHandler={mutate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<pre className="mt-5">
|
<span className="flex items-center mt-10 mb-4">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
qBittorrent Client Information
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
|
<span className="inline-flex justify-center rounded-full bg-emerald-100 mb-4 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--plug-circle-bold] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">Connected</p>
|
||||||
|
</span>
|
||||||
|
<pre className="font-hasklig text-sm text-slate-700 dark:text-slate-700">
|
||||||
{JSON.stringify(qbittorrentClientInfo?.data, null, 4)}
|
{JSON.stringify(qbittorrentClientInfo?.data, null, 4)}
|
||||||
</pre>
|
</pre>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,24 +50,45 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<div>
|
||||||
<div className="columns">
|
<section>
|
||||||
<div className="section column is-one-quarter">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<h1 className="title">Settings</h1>
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
<aside className="menu">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Import comics into the ThreeTwo library.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-cols max-w-screen-xl mx-auto">
|
||||||
|
<aside className="px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||||
{map(settingsObject, (settingObject, idx) => {
|
{map(settingsObject, (settingObject, idx) => {
|
||||||
return (
|
return (
|
||||||
<div key={idx}>
|
<div
|
||||||
<p className="menu-label">{settingObject.category}</p>
|
className="w-64 py-2 text-slate-700 dark:text-slate-400"
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
<h3 className="text-l pb-2">
|
||||||
|
{settingObject.category.toUpperCase()}
|
||||||
|
</h3>
|
||||||
{/* First level children */}
|
{/* First level children */}
|
||||||
{!isUndefined(settingObject.children) ? (
|
{!isUndefined(settingObject.children) ? (
|
||||||
<ul className="menu-list" key={settingObject.id}>
|
<ul key={settingObject.id}>
|
||||||
{map(settingObject.children, (item, idx) => {
|
{map(settingObject.children, (item, idx) => {
|
||||||
return (
|
return (
|
||||||
<li key={idx}>
|
<li key={idx} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active ? "is-active" : ""
|
item.id.toString() === active
|
||||||
|
? "is-active flex items-center"
|
||||||
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() => setActive(item.id.toString())}
|
onClick={() => setActive(item.id.toString())}
|
||||||
>
|
>
|
||||||
@@ -75,14 +96,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
</a>
|
</a>
|
||||||
{/* Second level children */}
|
{/* Second level children */}
|
||||||
{!isUndefined(item.children) ? (
|
{!isUndefined(item.children) ? (
|
||||||
<ul>
|
<ul className="pl-4 mt-2">
|
||||||
{map(item.children, (item, idx) => (
|
{map(item.children, (item, idx) => (
|
||||||
<li key={item.id}>
|
<li key={item.id} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active
|
item.id.toString() === active
|
||||||
? "is-active"
|
? "is-active flex items-center"
|
||||||
: ""
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setActive(item.id.toString())
|
setActive(item.id.toString())
|
||||||
@@ -103,10 +124,9 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* content for settings */}
|
{/* content for settings */}
|
||||||
<div className="section column is-half mt-6">
|
<div className="max-w-screen-xl">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{map(settingsContent, ({ id, content }) =>
|
{map(settingsContent, ({ id, content }) =>
|
||||||
active === id ? content : null,
|
active === id ? content : null,
|
||||||
@@ -115,6 +135,7 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { isEmpty, isNil } from "lodash";
|
|||||||
|
|
||||||
interface ICardProps {
|
interface ICardProps {
|
||||||
orientation: string;
|
orientation: string;
|
||||||
imageUrl: string;
|
imageUrl?: string;
|
||||||
hasDetails: boolean;
|
hasDetails?: boolean;
|
||||||
title?: PropTypes.ReactElementLike | null;
|
title?: PropTypes.ReactElementLike | null;
|
||||||
children?: PropTypes.ReactNodeLike;
|
children?: PropTypes.ReactNodeLike;
|
||||||
borderColorClass?: string;
|
borderColorClass?: string;
|
||||||
@@ -80,6 +80,85 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "vertical-2":
|
||||||
|
return (
|
||||||
|
<div className="block rounded-md w-fit h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500">
|
||||||
|
<img
|
||||||
|
alt="Home"
|
||||||
|
src={props.imageUrl}
|
||||||
|
className="rounded-t-md object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2 px-2">
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dd className="text-md text-slate-500 dark:text-black">
|
||||||
|
{props.title}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{props.hasDetails && <>{props.children}</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "horizontal-small":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row justify-start align-top gap-3 bg-slate-200 h-fit rounded-md shadow-md shadow-white-400">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden">
|
||||||
|
<img src={props.imageUrl} className="object-cover h-20 w-20" />
|
||||||
|
</div>
|
||||||
|
{/* details */}
|
||||||
|
<div className="w-fit h-fit pl-1 pr-2 py-1">
|
||||||
|
<p className="text-sm">{props.title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "horizontal-medium":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row items-center align-top gap-3 bg-slate-200 h-fit p-2 rounded-md shadow-md shadow-white-400">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
{/* details */}
|
||||||
|
<div className="pl-1 pr-2 py-1">
|
||||||
|
<p className="text-sm">{props.title}</p>
|
||||||
|
{props.hasDetails && <>{props.children}</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "cover-only":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-lg shadow-lg overflow-hidden w-fit h-fit">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "card-with-info-panel":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden w-fit h-fit">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
{/* myata-dyata */}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,91 +14,141 @@ export const ConnectionForm = ({
|
|||||||
onSubmit={submitHandler}
|
onSubmit={submitHandler}
|
||||||
initialValues={initialData}
|
initialValues={initialData}
|
||||||
render={({ handleSubmit }) => (
|
render={({ handleSubmit }) => (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} className="mt-10">
|
||||||
<h2>{formHeading}</h2>
|
<h2 className="text-xl">{formHeading}</h2>
|
||||||
<label className="label">Hostname</label>
|
<article
|
||||||
<div className="field has-addons">
|
role="alert"
|
||||||
<p className="control">
|
className="mt-4 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"
|
||||||
<span className="select">
|
>
|
||||||
<Field name="protocol" component="select">
|
<div>
|
||||||
|
<p>Configure your AirDC++ client connection here.</p>
|
||||||
|
<p>
|
||||||
|
Note that you need an instance of AirDC++ already running to
|
||||||
|
use this form to connect to it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
See{" "}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="http://airdcpp.net/docs/installation/installation.html"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>{" "}
|
||||||
|
for AirDC++ installation instructions for various platforms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<span className="flex items-center mt-6">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
Configure Connection
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex flex-row mt-4">
|
||||||
|
<div className="relative">
|
||||||
|
{/* protocol */}
|
||||||
|
<label className="block py-1">Protocol</label>
|
||||||
|
<Field
|
||||||
|
name="protocol"
|
||||||
|
component="select"
|
||||||
|
className="appearance-none 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"
|
||||||
|
>
|
||||||
<option>Protocol</option>
|
<option>Protocol</option>
|
||||||
<option value="http">http://</option>
|
<option value="http">http://</option>
|
||||||
<option value="https">https://</option>
|
<option value="https">https://</option>
|
||||||
</Field>
|
</Field>
|
||||||
</span>
|
<div className="absolute h-7 w-7 right-0 px-1 top-11 pointer-events-none">
|
||||||
</p>
|
<i className="icon-[solar--alt-arrow-down-bold]" />
|
||||||
<div className="control is-expanded">
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* hostname */}
|
||||||
<Field name="hostname" validate={hostNameValidator}>
|
<Field name="hostname" validate={hostNameValidator}>
|
||||||
{({ input, meta }) => (
|
{({ input, meta }) => (
|
||||||
<div>
|
<div className="flex flex-col">
|
||||||
|
<label className="block px-2 py-1">Hostname</label>
|
||||||
<input
|
<input
|
||||||
{...input}
|
{...input}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="hostname"
|
placeholder="Hostname"
|
||||||
className="input"
|
className="ml-2 dark:bg-slate-400 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>
|
||||||
{meta.error && meta.touched && (
|
{meta.error && meta.touched && (
|
||||||
<span className="is-size-7 has-text-danger">
|
<span className="text-sm text-red-400 px-2">
|
||||||
{meta.error}
|
{meta.error}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
|
||||||
<p className="control">
|
{/* port */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="block px-2 py-1">Port</label>
|
||||||
<Field
|
<Field
|
||||||
name="port"
|
name="port"
|
||||||
component="input"
|
component="input"
|
||||||
className="input"
|
className="ml-2 dark:bg-slate-400 bg-slate-100 px-2 block h-10 rounded-md sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
placeholder="port"
|
placeholder="Port"
|
||||||
/>
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
</div>
|
||||||
<label className="label">Credentials</label>
|
|
||||||
<div className="field-body">
|
<div className="flex flex-row mt-5">
|
||||||
<div className="field">
|
<div>
|
||||||
<p className="control is-expanded has-icons-left">
|
<label className="block py-1">Username</label>
|
||||||
|
<div className="relative">
|
||||||
<Field
|
<Field
|
||||||
name="username"
|
name="username"
|
||||||
component="input"
|
component="input"
|
||||||
className="input"
|
className="h-10 dark:bg-slate-500 bg-slate-200 rounded-md text-gray-700 dark:text-slate-200 py-1 px-10 mr-5 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
/>
|
/>
|
||||||
<span className="icon is-small is-left">
|
<span className="absolute h-6 w-6 left-2 top-2 inset-y-0 flex items-center px-0 pointer-events-none">
|
||||||
<i className="fa-solid fa-user-ninja"></i>
|
<i className="icon-[solar--user-bold-duotone] h-6 w-6 dark:text-slate-200" />
|
||||||
</span>
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
</div>
|
||||||
<p className="control is-expanded has-icons-left has-icons-right">
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block py-1">Password</label>
|
||||||
|
<div className="relative">
|
||||||
<Field
|
<Field
|
||||||
name="password"
|
name="password"
|
||||||
component="input"
|
component="input"
|
||||||
type="password"
|
type="password"
|
||||||
className="input"
|
className="h-10 dark:bg-slate-500 bg-slate-200 rounded-md text-gray-700 dark:text-slate-200 py-1 px-10 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
<span className="icon is-small is-left">
|
<span className="absolute left-2 top-2 inset-y-0 flex items-center px-0 pointer-events-none h-6 w-6">
|
||||||
<i className="fa-solid fa-lock"></i>
|
<i className="icon-[solar--lock-password-bold-duotone] h-6 w-6 dark:text-slate-200" />
|
||||||
</span>
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field is-grouped">
|
</div>
|
||||||
<p className="control">
|
<div className="flex flex-row gap-2">
|
||||||
<button type="submit" className="button is-primary">
|
<button
|
||||||
|
className="flex space-x-1 sm:mt-5 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-4 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span className="text-md">
|
||||||
{!isEmpty(initialData) ? "Update" : "Save"}
|
{!isEmpty(initialData) ? "Update" : "Save"}
|
||||||
|
</span>
|
||||||
|
<span className="w-6 h-6">
|
||||||
|
<i className="h-6 w-6 icon-[solar--diskette-bold-duotone]"></i>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="control">
|
<button
|
||||||
<button type="submit" className="button is-danger">
|
type="submit"
|
||||||
|
className="flex space-x-1 sm:mt-5 sm:flex-row sm:items-center rounded-lg border border-red-400 dark:border-red-200 bg-red-200 px-4 py-2 text-gray-500 hover:bg-transparent hover:text-red-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
>
|
||||||
{!isEmpty(initialData) && "Delete"}
|
{!isEmpty(initialData) && "Delete"}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -31,61 +31,53 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
{
|
{
|
||||||
name: "rawFileDetails",
|
name: "rawFileDetails",
|
||||||
content: () => (
|
content: () => (
|
||||||
<dl>
|
<dl className="dark:bg-[#647587] bg-slate-200 p-3 rounded-lg">
|
||||||
<dt>
|
<dt>
|
||||||
<h6
|
<p className="text-lg">{issueName}</p>
|
||||||
className="name has-text-weight-medium mb-1"
|
|
||||||
style={props.titleStyle}
|
|
||||||
>
|
|
||||||
{issueName}
|
|
||||||
</h6>
|
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="is-size-7">
|
<dd className="text-sm">
|
||||||
Is a part of{" "}
|
is a part of{" "}
|
||||||
<span className="has-text-weight-semibold">
|
<span className="underline">
|
||||||
{inferredMetadata.issue.name}
|
{inferredMetadata.issue.name}
|
||||||
|
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dd className="is-size-7 mt-2">
|
{/* Issue number */}
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
{inferredMetadata.issue.number && (
|
||||||
<div className="control">
|
<dd className="my-2">
|
||||||
<span className="tags">
|
<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
|
<span className="pr-1 pt-1">
|
||||||
className="tag is-success is-light has-text-weight-semibold"
|
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i>
|
||||||
style={props.tagsStyle}
|
|
||||||
>
|
|
||||||
{rawFileDetails.extension}
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
className="tag is-success is-light has-text-weight-semibold"
|
{inferredMetadata.issue.number}
|
||||||
style={props.tagsStyle}
|
</span>
|
||||||
>
|
</span>
|
||||||
|
</dd>
|
||||||
|
)}
|
||||||
|
<dd className="flex flex-row gap-2 w-max">
|
||||||
|
{/* 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="pr-1 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}
|
{rawFileDetails.mimeType}
|
||||||
</span>
|
</span>
|
||||||
<span
|
</span>
|
||||||
className="tag is-success is-light"
|
|
||||||
style={props.tagsStyle}
|
{/* 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">
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
{prettyBytes(rawFileDetails.fileSize)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div className="control">
|
|
||||||
{inferredMetadata.issue.number && (
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag is-light" style={props.tagsStyle}>
|
|
||||||
Issue #
|
|
||||||
</span>
|
|
||||||
<span className="tag is-warning" style={props.tagsStyle}>
|
|
||||||
{inferredMetadata.issue.number}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
),
|
),
|
||||||
@@ -208,25 +200,14 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="column" style={props.containerStyle}>
|
<div className="flex gap-5 my-3">
|
||||||
<div className="comic-detail issue-metadata">
|
|
||||||
<dl>
|
|
||||||
<dd>
|
|
||||||
<div className="columns mt-2">
|
|
||||||
<div className="column is-3">
|
|
||||||
<Card
|
<Card
|
||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
orientation={"vertical"}
|
orientation={"cover-only"}
|
||||||
hasDetails={false}
|
hasDetails={false}
|
||||||
imageStyle={props.imageStyle}
|
imageStyle={props.imageStyle}
|
||||||
// cardContainerStyle={{ maxWidth: 200 }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div>{metadataPanel.content()}</div>
|
||||||
<div className="column">{metadataPanel.content()}</div>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
121
src/client/components/shared/Navbar2.tsx
Normal file
121
src/client/components/shared/Navbar2.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { ReactElement, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useDarkMode } from "../../hooks/useDarkMode";
|
||||||
|
|
||||||
|
export const Navbar2 = (): ReactElement => {
|
||||||
|
const [colorTheme, setTheme] = useDarkMode();
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
|
||||||
|
const toggleDarkMode = (checked) => {
|
||||||
|
setTheme(colorTheme);
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="bg-white dark:bg-gray-900 gap-8 px-5 py-5 h-18 border-b-2 border-gray-300 dark:border-slate-200">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="mx-auto flex">
|
||||||
|
<img src="/src/client/assets/img/threetwo.png" alt="ThreeTwo!" />
|
||||||
|
|
||||||
|
{/* Main Navigation */}
|
||||||
|
<div className="flex flex-1 items-center justify-end md:justify-between">
|
||||||
|
<nav
|
||||||
|
aria-label="ThreeTwo Main Navigation"
|
||||||
|
className="hidden md:block"
|
||||||
|
>
|
||||||
|
<ul className="flex items-center gap-6 text-md">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/import"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/library"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Library
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Volumes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Downloads
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right-most Nav */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ul className="flex items-center gap-6 text-md">
|
||||||
|
{/* Settings Icon and text */}
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
className="flex items-center space-x-1 text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="icon-[solar--settings-outline] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<span>Settings</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{/* Light/Dark Mode toggle */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-gray-600 dark:text-white">Light</span>
|
||||||
|
<label
|
||||||
|
htmlFor="toggle"
|
||||||
|
className="relative inline-flex items-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleDarkMode}
|
||||||
|
/>
|
||||||
|
<span className="bg-gray-300 w-10 h-6 rounded-full"></span>
|
||||||
|
<span
|
||||||
|
className={`bg-white w-4 h-4 rounded-full absolute left-1 top-1 transition-transform duration-300 ease-in-out ${
|
||||||
|
darkMode ? "translate-x-4" : ""
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</label>
|
||||||
|
<span className="text-gray-600 dark:text-white">Dark</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -70,67 +70,46 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="container max-w-fit mx-14">
|
||||||
<div className="columns table-controls">
|
<div>
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
<div className="column is-half">
|
<div className="flex flex-row gap-2 justify-between mt-5">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
|
||||||
{/* pagination controls */}
|
{/* pagination controls */}
|
||||||
<nav className="pagination columns">
|
<div>
|
||||||
<div className="mr-4 has-text-weight-semibold has-text-left">
|
|
||||||
<p className="is-size-5">
|
|
||||||
Page {pageIndex} of {Math.ceil(totalPages / pageSize)}
|
Page {pageIndex} of {Math.ceil(totalPages / pageSize)}
|
||||||
</p>
|
|
||||||
<p>{totalPages} comics in all</p>
|
<p>{totalPages} comics in all</p>
|
||||||
</div>
|
{/* Prev/Next buttons */}
|
||||||
<div className="field has-addons">
|
<div className="inline-flex flex-row mt-4 mb-4">
|
||||||
<div className="control">
|
|
||||||
<button
|
<button
|
||||||
className="button"
|
|
||||||
onClick={() => goToPreviousPage()}
|
onClick={() => goToPreviousPage()}
|
||||||
disabled={pageIndex === 1}
|
disabled={pageIndex === 1}
|
||||||
|
className="dark:bg-slate-500 bg-slate-400 rounded-l border-slate-600 border-r pt-2 px-2"
|
||||||
>
|
>
|
||||||
<i className="fas fa-chevron-left"></i>
|
<i className="icon-[solar--arrow-left-linear] h-6 w-6"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div className="control">
|
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="dark:bg-slate-500 bg-slate-400 rounded-r pt-2 px-2"
|
||||||
onClick={() => goToNextPage()}
|
onClick={() => goToNextPage()}
|
||||||
disabled={pageIndex > Math.floor(totalPages / pageSize)}
|
disabled={pageIndex > Math.floor(totalPages / pageSize)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-chevron-right"></i>
|
<i className="icon-[solar--arrow-right-linear] h-6 w-6"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field has-addons ml-5">
|
|
||||||
<p className="control">
|
|
||||||
<button className="button">
|
|
||||||
<span className="icon is-small">
|
|
||||||
<i className="fa-solid fa-list"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<p className="control">
|
|
||||||
<button className="button">
|
|
||||||
<Link to="/library-grid">
|
|
||||||
<span className="icon is-small">
|
|
||||||
<i className="fa-solid fa-image"></i>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
<table className="table is-hoverable">
|
<table className="table-auto overflow-auto">
|
||||||
<thead>
|
<thead className="sticky top-0 bg-slate-200 dark:bg-slate-500">
|
||||||
{table.getHeaderGroups().map((headerGroup, idx) => (
|
{table.getHeaderGroups().map((headerGroup, idx) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header, idx) => (
|
{headerGroup.headers.map((header, idx) => (
|
||||||
<th key={header.id} colSpan={header.colSpan}>
|
<th
|
||||||
|
key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
className="px-3 py-3"
|
||||||
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
@@ -147,17 +126,22 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
{table.getRowModel().rows.map((row, idx) => {
|
{table.getRowModel().rows.map((row, idx) => {
|
||||||
return (
|
return (
|
||||||
<tr key={row.id} onClick={() => rowClickHandler(row)}>
|
<tr key={row.id} onClick={() => rowClickHandler(row)}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => {
|
||||||
<td key={cell.id}>
|
return (
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
<td key={cell.id} className="align-top">
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
17
src/client/hooks/useDarkMode.tsx
Normal file
17
src/client/hooks/useDarkMode.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const useDarkMode = () => {
|
||||||
|
const [theme, setTheme] = useState(localStorage.theme);
|
||||||
|
const colorTheme = theme === "dark" ? "light" : "dark";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
root.classList.remove(colorTheme);
|
||||||
|
root.classList.add(theme);
|
||||||
|
|
||||||
|
// save theme to local storage
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}, [theme, colorTheme]);
|
||||||
|
|
||||||
|
return [colorTheme, setTheme];
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { SOCKET_BASE_URI } from "../constants/endpoints";
|
|||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import AirDCPPSocket from "../services/DcppSearchService";
|
import AirDCPPSocket from "../services/DcppSearchService";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
/* Broadly, this file sets up:
|
/* Broadly, this file sets up:
|
||||||
* 1. The zustand-based global client state
|
* 1. The zustand-based global client state
|
||||||
@@ -23,6 +24,7 @@ export const useStore = create((set, get) => ({
|
|||||||
airDCPPSocketConnected: value,
|
airDCPPSocketConnected: value,
|
||||||
})),
|
})),
|
||||||
airDCPPDownloadTick: {},
|
airDCPPDownloadTick: {},
|
||||||
|
airDCPPTransfers: {},
|
||||||
// Socket.io state
|
// Socket.io state
|
||||||
socketIOInstance: {},
|
socketIOInstance: {},
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ export const useStore = create((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { getState, setState } = useStore;
|
const { getState, setState } = useStore;
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
/** Socket.IO initialization **/
|
/** Socket.IO initialization **/
|
||||||
// 1. Fetch sessionId from localStorage
|
// 1. Fetch sessionId from localStorage
|
||||||
@@ -114,6 +117,42 @@ socketIOInstance.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 1a. Act on each comic issue successfully imported/failed, as indicated
|
||||||
|
// by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events
|
||||||
|
socketIOInstance.on("LS_COVER_EXTRACTED", (data) => {
|
||||||
|
const { completedJobCount, importResult } = data;
|
||||||
|
setState((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
successfulJobCount: completedJobCount,
|
||||||
|
mostRecentImport: importResult.rawFileDetails.name,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
|
||||||
|
const { failedJobCount } = data;
|
||||||
|
setState((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
failedJobCount,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1b. Clear the localStorage sessionId upon receiving the
|
||||||
|
// LS_IMPORT_QUEUE_DRAINED event
|
||||||
|
socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
|
||||||
|
localStorage.removeItem("sessionId");
|
||||||
|
setState((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
status: "drained",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
console.log("a", queryClient);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to init AirDC++ Socket with supplied settings
|
* Method to init AirDC++ Socket with supplied settings
|
||||||
* @param configuration - credentials, and hostname details to init AirDC++ connection
|
* @param configuration - credentials, and hostname details to init AirDC++ connection
|
||||||
|
|||||||
25
tailwind.config.ts
Normal file
25
tailwind.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { addDynamicIconSelectors } from "@iconify/tailwind";
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["PP Object Sans Regular", "sans-serif"],
|
||||||
|
hasklig: ["Hasklig Regular", "monospace"],
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: {
|
||||||
|
DEFAULT: "1rem",
|
||||||
|
sm: "2rem",
|
||||||
|
lg: "4rem",
|
||||||
|
xl: "5rem",
|
||||||
|
"2xl": "6rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [addDynamicIconSelectors()],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user