feat(search): 自定义引擎支持图标地址(png / svg)
- settings.search.engines[] 新增 icon:空=品牌图标/通用地球,'/' 开头走托管目录,或 http(s) 链接 - 后端校验图标地址(空 / 托管路径 / http(s),≤500 字符) - 后台每张引擎卡加「图标地址」输入;前后台都用组件库 EngineIcon 的 src(<img> 渲染,SVG 静态不执行脚本) - @stash/ui 0.3.4
This commit is contained in:
+3
-1
@@ -117,7 +117,7 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等
|
||||
"search": {
|
||||
"default": "internal",
|
||||
"engines": [
|
||||
{ "id": "google", "name": "Google", "url": "https://www.google.com/search?q={query}", "enabled": true },
|
||||
{ "id": "google", "name": "Google", "url": "https://www.google.com/search?q={query}", "icon": "", "enabled": true },
|
||||
{ "id": "bing", "name": "Bing", "url": "https://www.bing.com/search?q={query}", "enabled": true },
|
||||
{ "id": "baidu", "name": "百度", "url": "https://www.baidu.com/s?wd={query}", "enabled": true },
|
||||
{ "id": "sogou", "name": "搜狗", "url": "https://www.sogou.com/web?query={query}", "enabled": true },
|
||||
@@ -134,6 +134,8 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等
|
||||
- 外部引擎 URL 必须以 `http(s)://` 开头并包含 `{query}`,否则后端 400。
|
||||
- 引擎 id 只能用小写字母 / 数字 / `-` / `_`(1–30 位,`internal` 保留);内置五个用品牌图标,
|
||||
后台新加的引擎 id 自动生成为 `custom-xxx`、图标用通用地球(组件库 EngineIcon 自动回落)。
|
||||
- 每个引擎可带 `icon`(png / svg 地址,`/` 开头的托管路径或 http(s) 链接):给了就用 `<img>` 渲染自己
|
||||
的图标,留空回落到品牌图标 / 地球;SVG 走 img 是静态的,不执行脚本。
|
||||
- `background.light/dark` 为空时用内置 `stash-web/src/assets/site-bg.jpg` / `site-bg-dark.jpg`;否则填托管目录 `public/` 路径或完整 URL。
|
||||
- 旧 settings JSON 缺失字段会自动回落默认值;多余旧字段在下次保存时被丢弃。
|
||||
|
||||
|
||||
@@ -162,6 +162,8 @@ pub struct SearchEngineSettings {
|
||||
pub name: String,
|
||||
/// 搜索地址模板,用 `{query}` 表示关键词
|
||||
pub url: String,
|
||||
/// 自定义图标地址(png / svg);空表示用内置品牌图标或通用地球
|
||||
pub icon: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
@@ -182,30 +184,35 @@ impl Default for SearchSettings {
|
||||
id: "google".into(),
|
||||
name: "Google".into(),
|
||||
url: "https://www.google.com/search?q={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
},
|
||||
SearchEngineSettings {
|
||||
id: "bing".into(),
|
||||
name: "Bing".into(),
|
||||
url: "https://www.bing.com/search?q={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
},
|
||||
SearchEngineSettings {
|
||||
id: "baidu".into(),
|
||||
name: "百度".into(),
|
||||
url: "https://www.baidu.com/s?wd={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
},
|
||||
SearchEngineSettings {
|
||||
id: "sogou".into(),
|
||||
name: "搜狗".into(),
|
||||
url: "https://www.sogou.com/web?query={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
},
|
||||
SearchEngineSettings {
|
||||
id: "360".into(),
|
||||
name: "360".into(),
|
||||
url: "https://www.so.com/s?q={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -469,6 +476,19 @@ pub fn validate(settings: &Settings) -> Result<(), StashError> {
|
||||
"搜索地址需以 http(s):// 开头并包含 {query}".into(),
|
||||
));
|
||||
}
|
||||
let icon = engine.icon.trim();
|
||||
if icon.chars().count() > 500 {
|
||||
return Err(StashError::InvalidDirectory("引擎图标地址最多 500 个字符".into()));
|
||||
}
|
||||
if !icon.is_empty()
|
||||
&& !(icon.starts_with("http://")
|
||||
|| icon.starts_with("https://")
|
||||
|| icon.starts_with('/'))
|
||||
{
|
||||
return Err(StashError::InvalidDirectory(
|
||||
"引擎图标需为空、/ 开头的托管路径或 http(s):// 链接".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let default = settings.search.default.trim();
|
||||
if default != "internal" {
|
||||
@@ -637,6 +657,7 @@ mod tests {
|
||||
id: "my-engine_1".into(),
|
||||
name: "自定义".into(),
|
||||
url: "https://example.com/search?q={query}".into(),
|
||||
icon: "/favicon.svg".into(),
|
||||
enabled: true,
|
||||
});
|
||||
assert!(save(&mut conn, settings, 0).is_ok(), "合法自定义 id 应能保存");
|
||||
@@ -647,10 +668,21 @@ mod tests {
|
||||
id: bad_id.into(),
|
||||
name: "坏".into(),
|
||||
url: "https://example.com/?q={query}".into(),
|
||||
icon: String::new(),
|
||||
enabled: true,
|
||||
});
|
||||
assert!(save(&mut conn, bad, 0).is_err(), "{bad_id} 应被拒绝");
|
||||
}
|
||||
|
||||
let mut bad_icon = Settings::default();
|
||||
bad_icon.search.engines.push(SearchEngineSettings {
|
||||
id: "custom-x".into(),
|
||||
name: "坏图标".into(),
|
||||
url: "https://example.com/?q={query}".into(),
|
||||
icon: "javascript:alert(1)".into(),
|
||||
enabled: true,
|
||||
});
|
||||
assert!(save(&mut conn, bad_icon, 0).is_err(), "非法图标地址应被拒绝");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -69,7 +69,7 @@ type Route =
|
||||
| { kind: 'item'; key: string; itemId: string }
|
||||
| { kind: 'api' }
|
||||
|
||||
const INTERNAL_SEARCH = { id: 'internal', name: '站内搜索', url: '' } as const
|
||||
const INTERNAL_SEARCH = { id: 'internal', name: '站内搜索', url: '', icon: '' } as const
|
||||
|
||||
const SEARCH_ENGINE_KEY = 'stash-search-engine'
|
||||
|
||||
@@ -362,7 +362,12 @@ export default function AppNext() {
|
||||
INTERNAL_SEARCH,
|
||||
...site.search.engines
|
||||
.filter((engine) => engine.enabled)
|
||||
.map((engine) => ({ id: engine.id, name: engine.name, url: engine.url })),
|
||||
.map((engine) => ({
|
||||
id: engine.id,
|
||||
name: engine.name,
|
||||
url: engine.url,
|
||||
icon: engine.icon ?? '',
|
||||
})),
|
||||
]
|
||||
const searchEngine =
|
||||
searchEngines.find((engine) => engine.id === searchEngineId) ??
|
||||
@@ -930,7 +935,7 @@ export default function AppNext() {
|
||||
}
|
||||
onClick={() => setEngineOpen((open) => !open)}
|
||||
>
|
||||
<EngineIcon name={searchEngine.id} size={14} />
|
||||
<EngineIcon name={searchEngine.id} src={searchEngine.icon} size={14} />
|
||||
<span>{searchEngine.name}</span>
|
||||
</button>
|
||||
|
||||
@@ -977,7 +982,7 @@ export default function AppNext() {
|
||||
className={`engine-option${engine.id === searchEngine.id ? ' is-active' : ''}`}
|
||||
onClick={() => chooseEngine(engine.id)}
|
||||
>
|
||||
<EngineIcon name={engine.id} size={18} />
|
||||
<EngineIcon name={engine.id} src={engine.icon} size={18} />
|
||||
<span>{engine.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -2102,6 +2102,7 @@ function SearchPanel({
|
||||
id: `custom-${Date.now().toString(36)}`,
|
||||
name: '新搜索引擎',
|
||||
url: '',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -2147,7 +2148,7 @@ function SearchPanel({
|
||||
key={engine.id}
|
||||
>
|
||||
<div className="search-engine-head">
|
||||
<EngineIcon name={engine.id} size={18} />
|
||||
<EngineIcon name={engine.id} src={engine.icon} size={18} />
|
||||
<Text strong>{engine.name || engine.id}</Text>
|
||||
<div className="search-engine-actions">
|
||||
<Switch
|
||||
@@ -2178,6 +2179,18 @@ function SearchPanel({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField
|
||||
label="图标地址"
|
||||
htmlFor={`search-icon-${engine.id}`}
|
||||
hint="png / svg;留空用默认图标(内置品牌或通用地球),站内图片放 public/ 后填 /xxx.svg"
|
||||
>
|
||||
<Input
|
||||
id={`search-icon-${engine.id}`}
|
||||
value={engine.icon ?? ''}
|
||||
placeholder="/icons/xxx.svg 或 https://…"
|
||||
onChange={(event) => updateEngine(engine.id, { icon: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -84,6 +84,8 @@ export type SearchEngineSettings = {
|
||||
name: string
|
||||
/** 搜索地址模板,用 {query} 表示关键词 */
|
||||
url: string
|
||||
/** 自定义图标地址(png / svg);空表示用品牌图标或通用地球 */
|
||||
icon: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -99,30 +101,35 @@ export const DEFAULT_SEARCH_ENGINES: SearchEngineSettings[] = [
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
url: 'https://www.google.com/search?q={query}',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'bing',
|
||||
name: 'Bing',
|
||||
url: 'https://www.bing.com/search?q={query}',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'baidu',
|
||||
name: '百度',
|
||||
url: 'https://www.baidu.com/s?wd={query}',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'sogou',
|
||||
name: '搜狗',
|
||||
url: 'https://www.sogou.com/web?query={query}',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '360',
|
||||
name: '360',
|
||||
url: 'https://www.so.com/s?q={query}',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user