Fix: AA mirror behavior (#589)

- Refreshed available AA URLs
- Fixed potential redirect from AA itself causing mirror cache errors
- Added fully customizable mirror list in UI
- Segmented rotation behavior to Auto mode only

Fixes #588
This commit is contained in:
Alex
2026-02-06 10:04:31 +00:00
committed by GitHub
parent b10458a48b
commit f84fb082ad
15 changed files with 741 additions and 34 deletions
@@ -13,6 +13,7 @@ import {
CheckboxFieldConfig,
SelectFieldConfig,
MultiSelectFieldConfig,
TagListFieldConfig,
HeadingFieldConfig,
ActionButtonConfig,
ActionResult,
@@ -25,6 +26,7 @@ import {
CheckboxField,
SelectField,
MultiSelectField,
TagListField,
HeadingField,
ActionButton,
} from './settings/fields';
@@ -137,6 +139,15 @@ const renderField = (
disabled={isDisabled}
/>
);
case 'TagListField':
return (
<TagListField
field={field as TagListFieldConfig}
value={(value as string[]) ?? []}
onChange={(v) => onChange(v)}
disabled={isDisabled}
/>
);
case 'ActionButton':
return (
<ActionButton
@@ -9,6 +9,7 @@ import {
CheckboxFieldConfig,
SelectFieldConfig,
MultiSelectFieldConfig,
TagListFieldConfig,
OrderableListFieldConfig,
OrderableListItem,
ActionButtonConfig,
@@ -24,6 +25,7 @@ import {
CheckboxField,
SelectField,
MultiSelectField,
TagListField,
OrderableListField,
ActionButton,
HeadingField,
@@ -198,6 +200,15 @@ const renderField = (
disabled={isDisabled}
/>
);
case 'TagListField':
return (
<TagListField
field={field as TagListFieldConfig}
value={(value as string[]) ?? []}
onChange={(v) => onChange(v)}
disabled={isDisabled}
/>
);
case 'OrderableListField':
return (
<OrderableListField
@@ -0,0 +1,149 @@
import { useMemo, useRef, useState } from 'react';
import { TagListFieldConfig } from '../../../types/settings';
interface TagListFieldProps {
field: TagListFieldConfig;
value: string[];
onChange: (value: string[]) => void;
disabled?: boolean;
}
function normalizeTag(raw: string): string {
let s = raw.trim();
if (!s) return '';
// Strip wrapping quotes from pasted env/JSON style strings.
if (
(s.startsWith('"') && s.endsWith('"')) ||
(s.startsWith("'") && s.endsWith("'"))
) {
s = s.slice(1, -1).trim();
}
// Avoid special sentinel values.
if (s.toLowerCase() === 'auto') return '';
// Basic URL normalization to keep UX friendly; backend also normalizes on save.
if (!s.includes('://') && !s.startsWith('/')) {
s = `https://${s}`;
}
s = s.replace(/\/+$/, '');
return s.trim();
}
export const TagListField = ({ field, value, onChange, disabled }: TagListFieldProps) => {
const isDisabled = disabled ?? false;
const inputRef = useRef<HTMLInputElement>(null);
const [draft, setDraft] = useState('');
const tags = useMemo(() => (value ?? []).map(String).filter((t) => t.trim() !== ''), [value]);
const addFromRaw = (raw: string) => {
if (isDisabled) return;
const parts = raw
.split(/[\n,]+/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length === 0) return;
const next = [...tags];
for (const part of parts) {
const normalized = normalizeTag(part);
if (!normalized) continue;
if (next.includes(normalized)) continue;
next.push(normalized);
}
if (next.length !== tags.length) {
onChange(next);
}
};
const removeAt = (idx: number) => {
if (isDisabled) return;
onChange(tags.filter((_, i) => i !== idx));
};
const commitDraft = () => {
const raw = draft;
if (!raw.trim()) return;
addFromRaw(raw);
setDraft('');
};
return (
<div
className={`w-full px-3 py-2 rounded-lg border border-[var(--border-muted)]
bg-[var(--bg-soft)] text-sm
${isDisabled ? 'opacity-60 cursor-not-allowed' : 'cursor-text'}`}
onClick={() => {
if (isDisabled) return;
inputRef.current?.focus();
}}
>
<div className="flex flex-wrap gap-2 items-center">
{tags.map((tag, idx) => (
<span
key={`${tag}-${idx}`}
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full
border border-[var(--border-muted)] bg-[var(--bg)]
max-w-full"
title={tag}
>
<span className="truncate max-w-[22rem]">{tag}</span>
{!isDisabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeAt(idx);
}}
className="p-0.5 rounded-full hover:bg-[var(--hover-surface)]"
aria-label={`Remove ${tag}`}
>
<svg
className="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</span>
))}
{!isDisabled && (
<input
ref={inputRef}
type="text"
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
commitDraft();
return;
}
if (e.key === 'Backspace' && !draft && tags.length > 0) {
removeAt(tags.length - 1);
}
}}
onBlur={() => commitDraft()}
placeholder={tags.length === 0 ? field.placeholder : ''}
className="flex-1 min-w-[12rem] bg-transparent outline-none py-1"
/>
)}
{isDisabled && tags.length === 0 && (
<span className="opacity-60">{field.placeholder || 'No values'}</span>
)}
</div>
</div>
);
};
@@ -4,6 +4,7 @@ export { NumberField } from './NumberField';
export { CheckboxField } from './CheckboxField';
export { SelectField } from './SelectField';
export { MultiSelectField } from './MultiSelectField';
export { TagListField } from './TagListField';
export { OrderableListField } from './OrderableListField';
export { ActionButton } from './ActionButton';
export { TableField } from './TableField';
+8
View File
@@ -7,6 +7,7 @@ export type FieldType =
| 'CheckboxField'
| 'SelectField'
| 'MultiSelectField'
| 'TagListField'
| 'OrderableListField'
| 'TableField'
| 'ActionButton'
@@ -93,6 +94,12 @@ export interface MultiSelectFieldConfig extends BaseField {
variant?: 'pills' | 'dropdown'; // 'pills' (default) or 'dropdown' for checkbox dropdown style
}
export interface TagListFieldConfig extends BaseField {
type: 'TagListField';
value: string[];
placeholder?: string;
}
// OrderableListField types - generic drag-and-drop reorderable list
export interface OrderableListItem {
id: string;
@@ -163,6 +170,7 @@ export type SettingsField =
| CheckboxFieldConfig
| SelectFieldConfig
| MultiSelectFieldConfig
| TagListFieldConfig
| OrderableListFieldConfig
| TableFieldConfig
| ActionButtonConfig