feat(frontend): make services sortable

This commit is contained in:
Viren070
2025-01-09 14:57:16 +00:00
parent 6051402367
commit 71d570e14e
4 changed files with 154 additions and 19 deletions
+20 -3
View File
@@ -11,6 +11,8 @@ import {
VisualTag,
AudioTag,
Encode,
ServiceDetail,
ServiceCredential,
} from '@aiostreams/types';
import SortableCardList from '../../components/SortableCardList';
import ServiceInput from '../../components/ServiceInput';
@@ -686,7 +688,7 @@ export default function Configure() {
Enable the services you have accounts with and enter your
credentials.
</p>
{services.map((service) => (
{services.map((service, index) => (
<ServiceInput
key={service.id}
serviceName={service.name}
@@ -701,8 +703,8 @@ export default function Configure() {
}}
fields={
serviceDetails
.find((detail) => detail.id === service.id)
?.credentials.map((credential) => ({
.find((detail: ServiceDetail) => detail.id === service.id)
?.credentials.map((credential: ServiceCredential) => ({
label: credential.label,
link: credential.link,
value: service.credentials[credential.id] || '',
@@ -722,6 +724,21 @@ export default function Configure() {
},
})) || []
}
moveService={(direction) => {
const newServices = [...services];
const serviceIndex = newServices.findIndex(
(s) => s.id === service.id
);
const [movedService] = newServices.splice(serviceIndex, 1);
if (direction === 'up' && serviceIndex > 0) {
newServices.splice(serviceIndex - 1, 0, movedService);
} else if (direction === 'down' && serviceIndex < newServices.length) {
newServices.splice(serviceIndex + 1, 0, movedService);
}
setServices(newServices);
}}
canMoveUp={index > 0}
canMoveDown={index < services.length - 1}
/>
))}
@@ -51,3 +51,29 @@
flex-direction: column;
gap: 5px;
}
.actions {
display: flex;
align-items: center;
}
.actionButton {
background-color: transparent;
border-radius: var(--borderRadius);
display: flex;
margin: 5px;
font-size: 1.2em;
align-items: center;
border-width: 0;
transition: transform 0.2s;
-webkit-tap-highlight-color: transparent;
}
.actionButton:hover {
transform: scale(1.1);
cursor: pointer;
}
.actionButton:active {
transform: scale(0.9);
}
@@ -3,7 +3,7 @@ import styles from './ServiceInput.module.css';
interface Field {
label: string;
link: string;
link?: string;
value: string;
setValue: (value: string) => void;
}
@@ -13,6 +13,9 @@ interface ServiceInputProps {
enabled: boolean;
setEnabled: (enabled: boolean) => void;
fields: Field[];
moveService: (direction: 'up' | 'down') => void;
canMoveUp: boolean;
canMoveDown: boolean;
}
const ServiceInput: React.FC<ServiceInputProps> = ({
@@ -20,6 +23,9 @@ const ServiceInput: React.FC<ServiceInputProps> = ({
enabled,
setEnabled,
fields,
moveService,
canMoveUp,
canMoveDown,
}) => {
return (
<div
@@ -27,12 +33,79 @@ const ServiceInput: React.FC<ServiceInputProps> = ({
>
<div className={styles.header}>
<span className={styles.serviceName}>{serviceName}</span>
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
className={styles.checkbox}
/>
<div className={styles.actions}>
{canMoveUp && (
<button
className={styles.actionButton}
onClick={() => moveService('up')}
>
<svg
fill="#ffffff"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="24px"
height="24px"
viewBox="0 0 96.154 96.154"
xmlSpace="preserve"
stroke="#ffffff"
transform="matrix(1, 0, 0, -1, 0, 0)"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
{' '}
<g>
{' '}
<path d="M0.561,20.971l45.951,57.605c0.76,0.951,2.367,0.951,3.127,0l45.956-57.609c0.547-0.689,0.709-1.716,0.414-2.61 c-0.061-0.187-0.129-0.33-0.186-0.437c-0.351-0.65-1.025-1.056-1.765-1.056H2.093c-0.736,0-1.414,0.405-1.762,1.056 c-0.059,0.109-0.127,0.253-0.184,0.426C-0.15,19.251,0.011,20.28,0.561,20.971z"></path>{' '}
</g>{' '}
</g>
</svg>
</button>
)}
{canMoveDown && (
<button
className={styles.actionButton}
onClick={() => moveService('down')}
>
<svg
fill="#ffffff"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 96.154 96.154"
stroke="#ffffff"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
{' '}
<g>
{' '}
<path d="M0.561,20.971l45.951,57.605c0.76,0.951,2.367,0.951,3.127,0l45.956-57.609c0.547-0.689,0.709-1.716,0.414-2.61 c-0.061-0.187-0.129-0.33-0.186-0.437c-0.351-0.65-1.025-1.056-1.765-1.056H2.093c-0.736,0-1.414,0.405-1.762,1.056 c-0.059,0.109-0.127,0.253-0.184,0.426C-0.15,19.251,0.011,20.28,0.561,20.971z"></path>{' '}
</g>{' '}
</g>
</svg>
</button>
)}
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
className={styles.checkbox}
/>
</div>
</div>
{enabled && (
@@ -40,15 +113,20 @@ const ServiceInput: React.FC<ServiceInputProps> = ({
{fields.map((field, index) => (
<div key={index} className={styles.field}>
<label>
{field.label}. Get it{' '}
<a
href={field.link}
target="_blank"
rel="noreferrer"
style={{ textDecoration: 'underline' }}
>
here
</a>
{field.label}
{field.link && (
<>
{'. Get it '}
<a
href={field.link}
target="_blank"
rel="noopener noreferrer"
className={styles.link}
>
here
</a>
</>
)}
</label>
<input
type="text"
+14
View File
@@ -193,3 +193,17 @@ export interface AddonDetail {
supportedServices: string[];
options?: AddonOptionDetail[];
}
export interface ServiceDetail {
name: string;
id: string;
shortName: string;
knownNames: string[];
credentials: ServiceCredential[];
}
export interface ServiceCredential {
id: string;
label: string;
link?: string;
}