🔧 Genericized AirDC++ settings

1. You can now enter your AirDC++ client settings in the settings menu and the UI will read from them
2. Hubs can also be selected for search
This commit is contained in:
2021-11-23 17:26:48 -08:00
parent 610038247f
commit 84f242184e
10 changed files with 180 additions and 113 deletions

View File

@@ -59,7 +59,7 @@
"react-notification-system": "^0.4.0", "react-notification-system": "^0.4.0",
"react-notification-system-redux": "^2.0.1", "react-notification-system-redux": "^2.0.1",
"react-responsive-carousel": "^3.2.21", "react-responsive-carousel": "^3.2.21",
"react-select": "^4.3.1", "react-select": "^5.2.1",
"react-sliding-pane": "^7.0.0", "react-sliding-pane": "^7.0.0",
"react-stickynode": "^4.0.0", "react-stickynode": "^4.0.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",

View File

@@ -6,11 +6,11 @@ import {
import { SETTINGS_SERVICE_BASE_URI } from "../constants/endpoints"; import { SETTINGS_SERVICE_BASE_URI } from "../constants/endpoints";
export const saveSettings = export const saveSettings =
(settingsObject, airdcppUserSettings) => async (dispatch) => { (settingsPayload, settingsObjectId?) => async (dispatch) => {
const result = await axios({ const result = await axios({
url: `${SETTINGS_SERVICE_BASE_URI}/saveSettings`, url: `${SETTINGS_SERVICE_BASE_URI}/saveSettings`,
method: "POST", method: "POST",
data: { settingsObject, airdcppUserSettings }, data: { settingsPayload, settingsObjectId },
}); });
console.log(result.data); console.log(result.data);
dispatch({ dispatch({

View File

@@ -7,7 +7,7 @@ import {
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState, SearchInstance } from "threetwo-ui-typings"; import { RootState, SearchInstance } from "threetwo-ui-typings";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map, pick } from "lodash";
import { AirDCPPSocketContext } from "../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../context/AirDCPPSocket";
interface IAcquisitionPanelProps { interface IAcquisitionPanelProps {
comicBookMetadata: any; comicBookMetadata: any;
@@ -43,8 +43,8 @@ export const AcquisitionPanel = (
async (searchQuery) => { async (searchQuery) => {
dispatch( dispatch(
search(searchQuery, ADCPPSocket, { search(searchQuery, ADCPPSocket, {
username: `${userSettings.directConnect.client.username}`, username: `${userSettings.directConnect.client.host.username}`,
password: `${userSettings.directConnect.client.password}`, password: `${userSettings.directConnect.client.host.password}`,
}), }),
); );
}, },
@@ -59,7 +59,7 @@ export const AcquisitionPanel = (
extensions: ["cbz", "cbr"], extensions: ["cbz", "cbr"],
}, },
// "comic-scans.no-ip.biz:24674", // "comic-scans.no-ip.biz:24674",
hub_urls: ["perfection.comichub.org:777"], hub_urls: map(userSettings.directConnect.client.hubs, (item) => item.value),
priority: 5, priority: 5,
}; };
@@ -73,16 +73,16 @@ export const AcquisitionPanel = (
comicBookObjectId, comicBookObjectId,
ADCPPSocket, ADCPPSocket,
{ {
username: `${userSettings.directConnect.client.username}`, username: `${userSettings.directConnect.client.host.username}`,
password: `${userSettings.directConnect.client.password}`, password: `${userSettings.directConnect.client.host.password}`,
}, },
), ),
); );
// this is to update the download count badge on the downloads tab // this is to update the download count badge on the downloads tab
dispatch( dispatch(
getBundlesForComic(comicBookObjectId, ADCPPSocket, { getBundlesForComic(comicBookObjectId, ADCPPSocket, {
username: `${userSettings.directConnect.client.username}`, username: `${userSettings.directConnect.client.host.username}`,
password: `${userSettings.directConnect.client.password}`, password: `${userSettings.directConnect.client.host.password}`,
}), }),
); );
}, },
@@ -126,6 +126,17 @@ export const AcquisitionPanel = (
<div className="card-content"> <div className="card-content">
<div className="content"> <div className="content">
<dl> <dl>
<dt>
<div className="tags">
{userSettings.directConnect.client.hubs.map(
({ value }) => (
<span className="tag" key={value}>
{value}
</span>
),
)}
</div>
</dt>
<dt>Query: {searchInfo.query.pattern}</dt> <dt>Query: {searchInfo.query.pattern}</dt>
<dd> <dd>
Extensions: {searchInfo.query.extensions.join(", ")} Extensions: {searchInfo.query.extensions.join(", ")}

View File

@@ -1,42 +0,0 @@
import React, { ReactElement } from "react";
import { Form, Field } from "react-final-form";
export const AirDCPPConnectionForm = (): ReactElement => {
const onSubmit = () => {};
const validate = async () => {};
return (
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div>
<h3 className="title">AirDC++ Connection Settings</h3>
<h6 className="subtitle has-text-grey-light">
Configure AirDC++ connection settings such as hostname and
credentials
</h6>
</div>
<div className="field">
<label className="label">AirDC++ Host</label>
<div className="control">
<Field
name="airdcpp_hostname"
component="input"
className="input"
placeholder="adc://hub.url"
/>
</div>
</div>
<button type="submit" className="button is-primary">
Submit
</button>
</form>
)}
/>
);
};
export default AirDCPPConnectionForm;

View File

@@ -0,0 +1,98 @@
import React, { ReactElement, useEffect, useState } from "react";
import { Form, Field } from "react-final-form";
import axios from "axios";
import { useDispatch } from "react-redux";
import { isEmpty, isUndefined } from "lodash";
import Select from "react-select";
import { saveSettings } from "../../actions/settings.actions";
export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => {
const { settings } = airDCPPClientUserSettings;
const dispatch = useDispatch();
const [hubList, setHubList] = useState([]);
useEffect(() => {
if (!isEmpty(settings)) {
axios({
url: `${settings.directConnect.client.host.protocol}://${settings.directConnect.client.host.hostname}/api/v1/hubs`,
method: "GET",
headers: {
Authorization: `${settings.directConnect.client.airDCPPUserSettings.auth_token}`,
},
}).then((hubs) => {
const hubSelectionOptions = hubs.data.map(({ hub_url, identity }) => ({
value: hub_url,
label: identity.name,
}));
setHubList(hubSelectionOptions);
});
}
}, []);
const onSubmit = (values) => {
if (!isUndefined(values.hubs)) {
dispatch(saveSettings({ hubs: values.hubs }, settings._id));
}
};
const validate = async () => {};
const SelectAdapter = ({ input, ...rest }) => {
return <Select {...input} {...rest} isClearable isMulti />;
};
return (
<>
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div>
<h3 className="title">Hubs</h3>
<h6 className="subtitle has-text-grey-light">
Select the hubs you want to perform searches against.
</h6>
</div>
<div className="field">
<label className="label">AirDC++ Host</label>
<div className="control">
<Field
name="hubs"
component={SelectAdapter}
className="basic-multi-select"
placeholder="Select Hubs to Search Against"
options={hubList}
/>
</div>
</div>
<button type="submit" className="button is-primary">
Submit
</button>
</form>
)}
/>
<div className="mt-4">
<article className="message is-warning">
<div className="message-body is-size-6 is-family-secondary">
Your selection in the dropdown <strong>will replace</strong> the
existing selection.
</div>
</article>
</div>
<div className="box mt-3">
<h6>Selected hubs</h6>
{settings.directConnect.client.hubs.map(({ value, label }) => (
<div key={value}>
<div>{label}</div>
<span className="is-size-7">{value}</span>
</div>
))}
</div>
</>
);
};
export default AirDCPPHubsForm;

View File

@@ -2,6 +2,7 @@ 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 className="mt-4 is-clearfix">
<div className="card"> <div className="card">
@@ -15,21 +16,21 @@ export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
<dt> <dt>
Client version:{" "} Client version:{" "}
{ {
settings.directConnect.client.airdcppUserSettings.system_info settings.directConnect.client.airDCPPUserSettings.system_info
.client_version .client_version
} }
</dt> </dt>
<dt> <dt>
Hostname:{" "} Hostname:{" "}
{ {
settings.directConnect.client.airdcppUserSettings.system_info settings.directConnect.client.airDCPPUserSettings.system_info
.hostname .hostname
} }
</dt> </dt>
<dt> <dt>
Platform:{" "} Platform:{" "}
{ {
settings.directConnect.client.airdcppUserSettings.system_info settings.directConnect.client.airDCPPUserSettings.system_info
.platform .platform
} }
</dt> </dt>
@@ -37,7 +38,7 @@ export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
<dt> <dt>
Username:{" "} Username:{" "}
{ {
settings.directConnect.client.airdcppUserSettings.user settings.directConnect.client.airDCPPUserSettings.user
.username .username
} }
</dt> </dt>
@@ -45,7 +46,7 @@ export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
<dt> <dt>
Active Sessions:{" "} Active Sessions:{" "}
{ {
settings.directConnect.client.airdcppUserSettings.user settings.directConnect.client.airDCPPUserSettings.user
.active_sessions .active_sessions
} }
</dt> </dt>
@@ -53,7 +54,7 @@ export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
Permissions:{" "} Permissions:{" "}
<pre> <pre>
{JSON.stringify( {JSON.stringify(
settings.directConnect.client.airdcppUserSettings.user settings.directConnect.client.airDCPPUserSettings.user
.permissions, .permissions,
undefined, undefined,
2, 2,

View File

@@ -1,27 +1,18 @@
import React, { ReactElement, useCallback, useContext, useEffect } from "react"; import React, { ReactElement, useCallback, useContext, useEffect } from "react";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { useSelector, useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { import { saveSettings, deleteSettings } from "../../actions/settings.actions";
getSettings, import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
saveSettings,
deleteSettings,
} from "../actions/settings.actions";
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettings/AirDCPPSettingsConfirmation";
import axios from "axios"; import axios from "axios";
import { AirDCPPSocketContext } from "../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import AirDCPPSocket from "../services/DcppSearchService"; import AirDCPPSocket from "../../services/DcppSearchService";
import { isUndefined, isEmpty } from "lodash"; import { isUndefined, isEmpty } from "lodash";
export const AirDCPPSettingsForm = (): ReactElement => { export const AirDCPPSettingsForm = (airDCPPClientSettings): ReactElement => {
const airDCPPClientSettings = useSelector( const { settings } = airDCPPClientSettings;
(state: RootState) => state.settings.data,
);
const { setADCPPSocket } = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { const { setADCPPSocket } = useContext(AirDCPPSocketContext);
dispatch(getSettings());
}, []);
const onSubmit = async (values) => { const onSubmit = async (values) => {
try { try {
const airDCPPResponse = await axios({ const airDCPPResponse = await axios({
@@ -33,20 +24,17 @@ export const AirDCPPSettingsForm = (): ReactElement => {
}, },
}); });
if (airDCPPResponse.status === 200) { if (airDCPPResponse.status === 200) {
dispatch(saveSettings(values, airDCPPResponse.data)); dispatch(
saveSettings({
host: values,
airDCPPUserSettings: airDCPPResponse.data,
}),
);
setADCPPSocket( setADCPPSocket(
new AirDCPPSocket({ new AirDCPPSocket({
hostname: `${values.hostname}`, hostname: `${values.hostname}`,
}), }),
); );
const hubList = await axios({
url: `${values.protocol}://${values.hostname}/api/v1/hubs`,
method: "GET",
params: {
username: values.username,
password: values.password,
},
});
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -60,9 +48,8 @@ export const AirDCPPSettingsForm = (): ReactElement => {
const validate = async () => {}; const validate = async () => {};
const initFormData = const initFormData =
!isEmpty(airDCPPClientSettings.directConnect) || !isEmpty(settings.directConnect) || !isUndefined(settings.directConnect)
!isUndefined(airDCPPClientSettings.directConnect) ? settings.directConnect.client.host
? airDCPPClientSettings.directConnect.client
: {}; : {};
return ( return (
<> <>
@@ -140,11 +127,11 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</form> </form>
)} )}
/> />
{!isEmpty(settings) ? (
{!isEmpty(airDCPPClientSettings) ? ( <AirDCPPSettingsConfirmation settings={settings} />
<AirDCPPSettingsConfirmation settings={airDCPPClientSettings} />
) : null} ) : null}
{!isEmpty(airDCPPClientSettings) ? (
{!isEmpty(settings) ? (
<p className="control mt-4"> <p className="control mt-4">
<button className="button is-danger" onClick={removeSettings}> <button className="button is-danger" onClick={removeSettings}>
Delete Delete

View File

@@ -87,11 +87,11 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
useEffect(() => { useEffect(() => {
if (isEmpty(ADCPPSocket) && !isEmpty(userSettings)) { if (isEmpty(ADCPPSocket) && !isEmpty(userSettings)) {
console.log(userSettings.directConnect.client.hostname); console.log(userSettings.directConnect.client.host.hostname);
setADCPPSocket( setADCPPSocket(
new AirDCPPSocket({ new AirDCPPSocket({
protocol: `${userSettings.directConnect.client.protocol}`, protocol: `${userSettings.directConnect.client.host.protocol}`,
hostname: `${userSettings.directConnect.client.hostname}`, hostname: `${userSettings.directConnect.client.host.hostname}`,
}), }),
); );
} }

View File

@@ -1,19 +1,31 @@
import React, { useState, useEffect, useCallback, ReactElement } from "react"; import React, { useState, useEffect, useCallback, ReactElement } from "react";
import { AirDCPPSettingsForm } from "./AirDCPPSettingsForm"; import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
import { AirDCPPConnectionForm } from "./AirDCPPConnectionForm"; import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
import settingsObject from "../constants/settings/settingsMenu.json"; import settingsObject from "../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash"; import { isEmpty, isUndefined, map } from "lodash";
import { useDispatch, useSelector } from "react-redux";
import { getSettings } from "../actions/settings.actions";
interface ISettingsProps {} interface ISettingsProps {}
export const Settings = (props: ISettingsProps): ReactElement => { export const Settings = (props: ISettingsProps): ReactElement => {
// fetch saved AirDC++ settings, if any
const airDCPPClientSettings = useSelector(
(state: RootState) => state.settings.data,
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getSettings());
}, []);
const [active, setActive] = useState("gen-db"); const [active, setActive] = useState("gen-db");
const settingsContent = [ const settingsContent = [
{ {
id: "adc-hubs", id: "adc-hubs",
content: ( content: (
<> <>
<AirDCPPConnectionForm /> {!isEmpty(airDCPPClientSettings) ? (
<AirDCPPHubsForm settings={airDCPPClientSettings} />
) : null}
</> </>
), ),
}, },
@@ -21,7 +33,7 @@ export const Settings = (props: ISettingsProps): ReactElement => {
id: "adc-connection", id: "adc-connection",
content: ( content: (
<> <>
<AirDCPPSettingsForm /> <AirDCPPSettingsForm settings={airDCPPClientSettings} />
</> </>
), ),
}, },

View File

@@ -1991,6 +1991,13 @@
"@types/history" "*" "@types/history" "*"
"@types/react" "*" "@types/react" "*"
"@types/react-transition-group@^4.4.0":
version "4.4.4"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
dependencies:
"@types/react" "*"
"@types/react-window@^1.8.2": "@types/react-window@^1.8.2":
version "1.8.5" version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
@@ -11057,13 +11064,6 @@ react-hot-loader@^4.13.0:
shallowequal "^1.1.0" shallowequal "^1.1.0"
source-map "^0.7.3" source-map "^0.7.3"
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==
dependencies:
prop-types "^15.5.8"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -11191,17 +11191,17 @@ react-router@5.2.1, react-router@^5.2.0:
tiny-invariant "^1.0.2" tiny-invariant "^1.0.2"
tiny-warning "^1.0.0" tiny-warning "^1.0.0"
react-select@^4.3.1: react-select@^5.2.1:
version "4.3.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.2.1.tgz#416c25c6b79b94687702374e019c4f2ed9d159d6"
integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q== integrity sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==
dependencies: dependencies:
"@babel/runtime" "^7.12.0" "@babel/runtime" "^7.12.0"
"@emotion/cache" "^11.4.0" "@emotion/cache" "^11.4.0"
"@emotion/react" "^11.1.1" "@emotion/react" "^11.1.1"
"@types/react-transition-group" "^4.4.0"
memoize-one "^5.0.0" memoize-one "^5.0.0"
prop-types "^15.6.0" prop-types "^15.6.0"
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0" react-transition-group "^4.3.0"
react-sliding-pane@^7.0.0: react-sliding-pane@^7.0.0: