Skip to content

Commit 362c7b8

Browse files
EstrellaXDclaude
andcommitted
feat(ui): redesign search panel with modal and filter system
- Replace dropdown search results with full-screen modal - Add 4-tier filter chips: subtitle group, resolution, subtitle type, season - Auto-extract filter options from search results - Add confirmation modal before subscribing - Support toggle close (click search input, Escape, or X button) - Responsive grid layout: 1-4 columns based on viewport - SSE streaming with fade-in card animations - Add i18n translations for search filters and confirmation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8e7e893 commit 362c7b8

10 files changed

Lines changed: 1868 additions & 92 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Search Panel Redesign
2+
3+
## Overview
4+
5+
Redesign the search bar component from a dropdown list to a full modal-based search experience with advanced filtering capabilities.
6+
7+
## Problems with Current Implementation
8+
9+
1. **Click-outside clears everything** - `v-on-click-outside="clearSearch"` causes accidental loss of results
10+
2. **Limited result display** - Absolute positioned list with no scroll container
11+
3. **No explicit close control** - User has no intentional way to dismiss except clicking outside
12+
4. **No filtering** - Results often contain same anime with different subtitle groups/seasons, hard to find the right one
13+
14+
## Design Goals
15+
16+
- Prevent accidental dismissal of search results
17+
- Support displaying many results with proper scrolling
18+
- Enable filtering by subtitle group, resolution, subtitle type, and season
19+
- Provide confirmation step before subscribing
20+
21+
---
22+
23+
## Search Panel Structure
24+
25+
### Trigger & Toggle Behavior
26+
27+
- Clicking the search input opens the search modal (if closed) or closes it (if open)
28+
- Pressing `Escape` closes the modal
29+
- A visible `×` close button in the modal header provides explicit dismissal
30+
- Clicking the backdrop does NOT close (prevents accidental loss of results)
31+
32+
### Modal Layout
33+
34+
```
35+
┌─────────────────────────────────────────────────────────┐
36+
│ 🔍 [Search input] [provider ▼] [×] │
37+
├─────────────────────────────────────────────────────────┤
38+
│ 字幕组: [喵萌奶茶屋] [ANi] [桜都] [LoliHouse] [+3] │
39+
│ 分辨率: [1080p] [720p] [4K] │
40+
│ 字幕语言: [简中 CHS] [繁中 CHT] [双语] [内嵌] [外挂] │
41+
│ 季度: [S1] [S2] [剧场版] │
42+
│ [清除筛选] 8/24 结果 │
43+
├─────────────────────────────────────────────────────────┤
44+
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
45+
│ │ Result │ │ Result │ │ Result │ │
46+
│ │ Card │ │ Card │ │ Card │ │
47+
│ └─────────┘ └─────────┘ └─────────┘ │
48+
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
49+
│ │ Result │ │ Result │ │ Result │ │
50+
│ │ Card │ │ Card │ │ Card │ │
51+
│ └─────────┘ └─────────┘ └─────────┘ │
52+
│ (scrollable grid) │
53+
└─────────────────────────────────────────────────────────┘
54+
```
55+
56+
- Modal centered on screen with backdrop overlay
57+
- Fixed header with search input, provider selector, close button
58+
- Filter chips section below header (sticky when scrolling)
59+
- Scrollable grid of result cards (3 columns on desktop, responsive)
60+
61+
---
62+
63+
## Filter Chip System
64+
65+
### Four Filter Dimensions
66+
67+
1. **字幕组 (Subtitle Group)** - Fansub team name
68+
2. **分辨率 (Resolution)** - 720p, 1080p, 4K/2160p
69+
3. **字幕语言 (Subtitle Type)** - CHS (简中), CHT (繁中), 双语 (Dual), 内嵌 (Hardcoded), 外挂 (External ASS/SRT)
70+
4. **季度 (Season)** - S1, S2, Movie/剧场版, OVA
71+
72+
### Auto-generated Filters
73+
74+
- As results stream in, extract unique values for each dimension
75+
- Chips appear dynamically as new filter values are discovered
76+
- Parsing logic extracts metadata from torrent titles
77+
78+
### Filter Behavior
79+
80+
- Chips are toggleable - click to activate (highlighted), click again to deactivate
81+
- Multiple filters can be active simultaneously
82+
- Logic: AND within same category, OR across categories
83+
- Active filters show with filled background, inactive with outline style
84+
- "清除筛选" (Clear all) button appears when any filter is active
85+
- Result count updates dynamically: "显示 8 / 24 个结果"
86+
87+
### Overflow Handling
88+
89+
- If a category has >5 options, show first 4 + `[+N more]` chip that expands
90+
- Each category row can collapse/expand independently
91+
- Collapsed state shows badge count for active filters
92+
93+
---
94+
95+
## Result Cards
96+
97+
### Card Design (Compact Grid Item)
98+
99+
```
100+
┌──────────────────────────┐
101+
│ ┌──────┐ 葬送的芙莉莲 │
102+
│ │poster│ Frieren │
103+
│ │ │ ───────────── │
104+
│ └──────┘ 喵萌奶茶屋 │
105+
│ 1080p · 简中 │
106+
│ S1 · 全28集 │
107+
└──────────────────────────┘
108+
```
109+
110+
### Card Content
111+
112+
- Thumbnail/poster (if available from API, placeholder if not)
113+
- Title (Chinese + Romaji/English)
114+
- Subtitle group badge
115+
- Resolution + Subtitle type tags
116+
- Season + Episode count
117+
118+
### Streaming Animation
119+
120+
- Cards fade in with slight upward slide (`opacity: 0 → 1`, `translateY: 8px → 0`)
121+
- Staggered delay: each card delays 50ms after previous
122+
- Grid re-flows smoothly as new cards appear
123+
- Filter changes use same fade animation for showing/hiding
124+
125+
### Empty & Loading States
126+
127+
| State | Display |
128+
|-------|---------|
129+
| Initial | "输入关键词开始搜索" |
130+
| Searching | Spinner in search input, cards stream in |
131+
| No results | "未找到相关结果,试试其他关键词" |
132+
| Filtered to zero | "没有符合筛选条件的结果" + "清除筛选" button |
133+
134+
---
135+
136+
## Confirmation Modal
137+
138+
When user clicks a result card, a nested modal appears for review before subscribing.
139+
140+
### Layout
141+
142+
```
143+
┌─────────────────────────────────────────────────────┐
144+
│ 添加订阅 [×] │
145+
├─────────────────────────────────────────────────────┤
146+
│ ┌────────┐ │
147+
│ │ │ 葬送的芙莉莲 │
148+
│ │ poster │ Sousou no Frieren │
149+
│ │ │ ★ 9.2 · 2023年秋 · 全28集 │
150+
│ └────────┘ │
151+
├─────────────────────────────────────────────────────┤
152+
│ RSS 源: [当前选择的RSS链接] [复制] │
153+
│ 字幕组: 喵萌奶茶屋 │
154+
│ 分辨率: 1080p │
155+
│ 字幕类型: 简体中文 (内嵌) │
156+
├─────────────────────────────────────────────────────┤
157+
│ 高级设置 ▼ │
158+
│ ┌─────────────────────────────────────────────┐ │
159+
│ │ 过滤规则: [自动生成的filter] │ │
160+
│ │ 保存路径: /media/anime/葬送的芙莉莲/ │ │
161+
│ │ 重命名: ☑ 启用自动重命名 │ │
162+
│ └─────────────────────────────────────────────┘ │
163+
├─────────────────────────────────────────────────────┤
164+
│ [取消] [确认订阅 ✓] │
165+
└─────────────────────────────────────────────────────┘
166+
```
167+
168+
### Behavior
169+
170+
- Shows parsed metadata for user review
171+
- Advanced settings collapsed by default
172+
- "确认订阅" adds to subscriptions and closes both modals
173+
- "取消" returns to search results (results preserved)
174+
- Escape key returns to search results (not full close)
175+
176+
---
177+
178+
## Keyboard Navigation
179+
180+
| Key | Action |
181+
|-----|--------|
182+
| `Enter` (in search input) | Trigger search |
183+
| `Escape` | Close confirmation modal (if open) → close search modal |
184+
| `Tab` | Navigate through filter chips, then result cards |
185+
| `Enter` (on focused card) | Open confirmation modal |
186+
| Arrow keys | Navigate grid (optional enhancement) |
187+
188+
---
189+
190+
## Responsive Design
191+
192+
### Breakpoints
193+
194+
| Viewport | Grid | Modal Width | Behavior |
195+
|----------|------|-------------|----------|
196+
| Desktop (>1024px) | 3 columns | 800px centered | Full experience |
197+
| Tablet (768-1024px) | 2 columns | 90% width | Filters collapse to single row |
198+
| Mobile (<768px) | 1 column | Full screen | Bottom sheet style |
199+
200+
### Mobile Adaptations
201+
202+
- Modal becomes full-screen bottom sheet
203+
- Filter chips horizontally scrollable (single row)
204+
- Cards stack vertically (single column)
205+
- Swipe down to close (in addition to × button)
206+
207+
---
208+
209+
## Component Structure
210+
211+
```
212+
ab-search-modal.vue (new - main modal container)
213+
├── ab-search-header.vue (search input + provider + close)
214+
├── ab-search-filters.vue (new - filter chips)
215+
├── ab-search-results.vue (new - scrollable grid)
216+
│ └── ab-search-card.vue (new - individual result card)
217+
└── ab-search-confirm.vue (new - confirmation modal)
218+
```
219+
220+
### State Management
221+
222+
Extend `useSearchStore` with:
223+
- `filters` - active filter state per dimension
224+
- `filteredResults` - computed results after filter application
225+
- `showModal` - modal open/close state
226+
- `selectedResult` - currently selected result for confirmation
227+
228+
---
229+
230+
## Implementation Notes
231+
232+
### Filter Parsing
233+
234+
Need to extract metadata from torrent titles. Common patterns:
235+
- Subtitle group: `[喵萌奶茶屋]`, `【ANi】`
236+
- Resolution: `1080p`, `720p`, `2160p`, `4K`
237+
- Subtitle type: `简体`, `繁體`, `CHS`, `CHT`, `简日双语`, `内嵌`, `外挂`
238+
- Season: `S01`, `Season 1`, `第一季`, `剧场版`, `Movie`, `OVA`
239+
240+
### SSE Streaming
241+
242+
Keep existing SSE implementation but:
243+
- Parse metadata from each result as it arrives
244+
- Update filter options incrementally
245+
- Apply active filters to new results immediately
246+
247+
### Animation
248+
249+
Use Vue's `<TransitionGroup>` with staggered delays:
250+
```css
251+
.card-enter-active {
252+
transition: all 0.3s ease;
253+
transition-delay: calc(var(--index) * 50ms);
254+
}
255+
.card-enter-from {
256+
opacity: 0;
257+
transform: translateY(8px);
258+
}
259+
```
Lines changed: 24 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,46 @@
11
<script lang="ts" setup>
2-
import { vOnClickOutside } from '@vueuse/components';
2+
import AbSearchModal from './search/ab-search-modal.vue';
33
import type { BangumiRule } from '#/bangumi';
44
55
defineEmits<{
66
(e: 'add-bangumi', bangumiRule: BangumiRule): void;
77
}>();
88
9-
const showProvider = ref(false);
10-
const { providers, provider, loading, inputValue, bangumiList } = storeToRefs(
11-
useSearchStore()
12-
);
13-
const { getProviders, onSearch, clearSearch } = useSearchStore();
9+
const { showModal, provider, loading, inputValue } = storeToRefs(useSearchStore());
10+
const { toggleModal, onSearch, getProviders } = useSearchStore();
1411
1512
onMounted(() => {
1613
getProviders();
1714
});
1815
19-
function onSelect(site: string) {
20-
provider.value = site;
21-
showProvider.value = false;
16+
// Handle click on search input - toggle modal
17+
function handleSearchClick() {
18+
toggleModal();
19+
}
20+
21+
// Handle search trigger from input
22+
function handleSearch() {
23+
if (!showModal.value) {
24+
toggleModal();
25+
}
26+
onSearch();
2227
}
2328
</script>
2429

2530
<template>
31+
<!-- Compact search trigger -->
2632
<ab-search
27-
v-model:inputValue="inputValue"
33+
v-model:input-value="inputValue"
2834
:provider="provider"
2935
:loading="loading"
30-
@search="onSearch"
31-
@select="() => (showProvider = !showProvider)"
36+
@search="handleSearch"
37+
@select="handleSearchClick"
38+
@click="handleSearchClick"
3239
/>
3340

34-
<transition name="dropdown">
35-
<div
36-
v-show="showProvider"
37-
v-on-click-outside="() => (showProvider = false)"
38-
class="provider-dropdown"
39-
>
40-
<div
41-
v-for="site in providers"
42-
:key="site"
43-
class="provider-item"
44-
@click="() => onSelect(site)"
45-
>
46-
{{ site }}
47-
</div>
48-
</div>
49-
</transition>
50-
51-
<div v-on-click-outside="clearSearch" class="search-results">
52-
<transition-group name="fade-list" tag="ul" class="search-results-list">
53-
<li v-for="bangumi in bangumiList" :key="bangumi.order">
54-
<ab-bangumi-card
55-
:bangumi="bangumi.value"
56-
type="search"
57-
@click="() => $emit('add-bangumi', bangumi.value)"
58-
/>
59-
</li>
60-
</transition-group>
61-
</div>
41+
<!-- Search Modal -->
42+
<AbSearchModal
43+
@close="toggleModal"
44+
@add-bangumi="(bangumi) => $emit('add-bangumi', bangumi)"
45+
/>
6246
</template>
63-
64-
<style lang="scss" scoped>
65-
.provider-dropdown {
66-
position: absolute;
67-
top: 84px;
68-
left: 540px;
69-
width: 120px;
70-
border-radius: var(--radius-md);
71-
background: var(--color-surface);
72-
border: 1px solid var(--color-border);
73-
box-shadow: var(--shadow-lg);
74-
z-index: 50;
75-
overflow: hidden;
76-
transition: background-color var(--transition-normal),
77-
border-color var(--transition-normal);
78-
}
79-
80-
.provider-item {
81-
padding: 10px 12px;
82-
font-size: 14px;
83-
color: var(--color-primary);
84-
cursor: pointer;
85-
user-select: none;
86-
overflow: hidden;
87-
text-overflow: ellipsis;
88-
white-space: nowrap;
89-
transition: background-color var(--transition-fast), color var(--transition-fast);
90-
91-
&:hover {
92-
background: var(--color-primary);
93-
color: #fff;
94-
}
95-
}
96-
97-
.search-results {
98-
position: absolute;
99-
top: 84px;
100-
left: 192px;
101-
z-index: 30;
102-
}
103-
104-
.search-results-list {
105-
display: flex;
106-
flex-direction: column;
107-
gap: 12px;
108-
list-style: none;
109-
padding: 0;
110-
margin: 0;
111-
}
112-
</style>

0 commit comments

Comments
 (0)