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:
2023-12-21 16:23:29 -05:00
committed by GitHub
parent 68442894d0
commit 4f49e538a8
35 changed files with 1405 additions and 12089 deletions

View File

@@ -1,9 +1,5 @@
import React, { useCallback, ReactElement, useEffect, useState } from "react";
import {
downloadAirDCPPItem,
getBundlesForComic,
sleep,
} from "../../actions/airdcpp.actions";
import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions";
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
import { RootState, SearchInstance } from "threetwo-ui-typings";
import ellipsize from "ellipsize";
@@ -13,6 +9,7 @@ import { isEmpty, isNil, map } from "lodash";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
interface IAcquisitionPanelProps {
query: any;
@@ -28,14 +25,22 @@ export const AcquisitionPanel = (
airDCPPSocketInstance,
airDCPPClientConfiguration,
airDCPPSessionInformation,
airDCPPDownloadTick,
} = useStore(
useShallow((state) => ({
airDCPPSocketInstance: state.airDCPPSocketInstance,
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
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
*/
@@ -43,34 +48,15 @@ export const AcquisitionPanel = (
queryKey: ["hubs"],
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
});
const { comicObjectId } = props;
const issueName = props.query.issue.name || "";
// const { settings } = props;
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 [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
// Pre-populate the search input with the search string, so that
@@ -88,15 +74,18 @@ export const AcquisitionPanel = (
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) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket();
}
const instance: SearchInstance = await ADCPPSocket.post("search");
// dispatch({
// type: AIRDCPP_SEARCH_IN_PROGRESS,
// });
setAirDCPPSearchStatus(true);
// We want to get notified about every new result in order to make the user experience better
await ADCPPSocket.addListener(
@@ -142,6 +131,9 @@ export const AcquisitionPanel = (
const currentInstance = await ADCPPSocket.get(
`search/${instance.id}`,
);
setAirDCPPSearchInstance(currentInstance);
setAirDCPPSearchInfo(searchInfo);
console.log("Asdas", airDCPPSearchInfo);
if (currentInstance.result_count === 0) {
// ...nothing was received, show an informative message to the user
console.log("No more search results.");
@@ -149,11 +141,8 @@ export const AcquisitionPanel = (
// 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
// dispatch({
// type: AIRDCPP_HUB_SEARCHES_SENT,
// searchInfo,
// instance,
// });
setAirDCPPSearchInstance(instance);
setAirDCPPSearchStatus(false);
},
instance.id,
);
@@ -164,6 +153,68 @@ export const AcquisitionPanel = (
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 manualQuery = {
query: {
@@ -209,7 +260,6 @@ export const AcquisitionPanel = (
},
[],
);
console.log("yaman", airDCPPSearchResults);
return (
<>
<div className="comic-detail columns">
@@ -245,7 +295,7 @@ export const AcquisitionPanel = (
<button
type="submit"
className={
false
airDCPPSearchStatus
? "button is-loading is-warning"
: "button is-success is-light"
}
@@ -273,6 +323,60 @@ export const AcquisitionPanel = (
)}
</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 */}
<div className="columns">
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
@@ -342,12 +446,14 @@ export const AcquisitionPanel = (
<button
className="button is-small is-light is-success"
onClick={() =>
downloadDCPPResult(
searchInstance.id,
download(
airDCPPSearchInstance.id,
result.id,
comicObjectId,
result.name,
result.size,
result.type,
airDCPPSocketInstance,
)
}
>

View File

@@ -69,17 +69,29 @@ export const Menu = (props): ReactElement => {
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 (
<Select
className="basic-single"
classNamePrefix="select"
components={{ Placeholder }}
placeholder={
<span>
<i className="fa-solid fa-list"></i> Actions
</span>
}
styles={customStyles}
name="actions"
isSearchable={false}
options={filteredActionOptions}

View File

@@ -47,6 +47,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
acquisition,
createdAt,
updatedAt,
},
userSettings,
} = data;
@@ -246,67 +248,58 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
// 2. from the CV-scraped version
return (
<section className="container">
<section className="container mx-auto">
<div className="section">
{!isNil(data) && !isEmpty(data) && (
<>
<h1 className="title">{issueName}</h1>
<div className="columns is-multiline">
<div className="column is-narrow">
<div>
<div className="flex flex-row mt-5">
<Card
imageUrl={url}
orientation={"vertical"}
orientation={"cover-only"}
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 */}
<div className="column">
{/* raw file details */}
{!isUndefined(rawFileDetails) &&
!isEmpty(rawFileDetails.cover) && (
<>
<div className="grid">
<RawFileDetails
data={{
rawFileDetails: rawFileDetails,
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
style={{ content: { marginTop: "2rem" } }}
isOpen={modalIsOpen}
onAfterOpen={afterOpenModal}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<button onClick={closeModal}>close</button>
{extractedComicBook && (
<ComicViewer
pages={extractedComicBook}
direction="ltr"
className={{
closeButton: "border: 1px solid red;",
}}
/>
)}
</Modal> */}
</>
style={{ content: { marginTop: "2rem" } }}
isOpen={modalIsOpen}
onAfterOpen={afterOpenModal}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<button onClick={closeModal}>close</button>
{extractedComicBook && (
<ComicViewer
pages={extractedComicBook}
direction="ltr"
className={{
closeButton: "border: 1px solid red;",
}}
/>
)}
</Modal> */}
</div>
)}
{/* action dropdown */}
{/* <div className="mt-4 is-size-7">
<Menu
data={data.data}
handlers={{ setSlidingPanelContentId, setVisible }}
/>
</div> */}
</div>
</div>

View File

@@ -3,10 +3,10 @@ import React, { ReactElement } from "react";
export const DownloadProgressTick = (props): ReactElement => {
return (
<div >
<h4 className="is-size-6">{props.data.name}</h4>
<div>
<h4 className="is-size-5">{props.data.name}</h4>
<div>
<span className="is-size-3 has-text-weight-semibold">
<span className="is-size-4 has-text-weight-semibold">
{prettyBytes(props.data.downloaded_bytes)} of{" "}
{prettyBytes(props.data.size)}{" "}
</span>
@@ -20,13 +20,12 @@ export const DownloadProgressTick = (props): ReactElement => {
%
</progress>
</div>
<div className="is-size-5">
{prettyBytes(props.data.speed)} per second.
</div>
<div className="is-size-5">
<div className="is-size-6 mt-1 mb-2">
<p>{prettyBytes(props.data.speed)} per second.</p>
Time left:
{Math.round(parseInt(props.data.seconds_left) / 60)}
</div>
<div>{props.data.target}</div>
</div>
);

View File

@@ -2,78 +2,137 @@ import React, { ReactElement } from "react";
import PropTypes from "prop-types";
import prettyBytes from "pretty-bytes";
import { isEmpty } from "lodash";
import { format, parseISO } from "date-fns";
export const RawFileDetails = (props): ReactElement => {
const { rawFileDetails, inferredMetadata } = props.data;
const { rawFileDetails, inferredMetadata, created_at, updated_at } =
props.data;
const PaperClipIcon = () => <></>;
return (
<>
<div className="comic-detail raw-file-details column is-three-fifths">
<dl>
<dt>Raw File Details</dt>
<dd className="is-size-7">
{rawFileDetails.containedIn +
"/" +
rawFileDetails.name +
rawFileDetails.extension}
</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>
<div className="max-w-2xl ml-5">
<div className="px-4 sm:px-6">
<p className="text-gray-500">
<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.name +
rawFileDetails.extension}
</dd>
</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) ? (
<span className="tag is-primary is-light">
{rawFileDetails.extension}
{inferredMetadata.issue.number}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">MIME type</span>
<span className="tag is-primary is-light">
) : null}
</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>
</div>
</div>
</span>
</dd>
<dd className="text-sm text-gray-900"></dd>
</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}
<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>
</div>
</div>
{!isEmpty(inferredMetadata.issue.number) ? (
<div className="control">
<div className="tags has-addons">
<span className="tag">Number</span>
<span className="tag is-primary is-light">
{inferredMetadata.issue.number}
</span>
</div>
</div>
) : null}
<span className="text-md text-slate-500 dark:text-slate-900">
{prettyBytes(rawFileDetails.fileSize)}
</span>
</span>
</dd>
</div>
</dd>
</dl>
<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>
</div>
</div>
</>
);
@@ -102,5 +161,7 @@ RawFileDetails.propTypes = {
subtitle: PropTypes.string,
}),
}),
created_at: PropTypes.string,
updated_at: PropTypes.string,
}),
};
};

View File

@@ -11,19 +11,20 @@ export const TabControls = (props): ReactElement => {
setActive(filteredTabs[0].id);
}, [acquisition]);
console.log(filteredTabs);
return (
<>
<div className="tabs">
<ul>
{filteredTabs.map(({ id, name, icon }) => (
<li
key={id}
className={id === active ? "is-active" : ""}
onClick={() => setActive(id)}
>
{/* Downloads tab and count badge */}
<a>
<div className="hidden sm:block mt-7">
<div className="border-b border-gray-200">
<nav className="-mb-px flex gap-6" aria-label="Tabs">
{filteredTabs.map(({ id, name, icon }) => (
<a
key={id}
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)}
>
{/* Downloads tab and count badge */}
{/* <a>
{id === 6 && !isNil(acquisition.directconnect) ? (
<span className="download-icon-labels">
<i className="fa-solid fa-download"></i>
@@ -35,10 +36,12 @@ export const TabControls = (props): ReactElement => {
<span className="icon is-small">{icon}</span>
)}
{name}
</a> */}
{name}
</a>
</li>
))}
</ul>
))}
</nav>
</div>
</div>
{filteredTabs.map(({ id, content }) => {
return active === id ? content : null;