Skip to content

Commit 8457125

Browse files
Nattuhanclaude
andcommitted
v1.0.1: Add multi-select and bulk actions
- Add multi-select for videos (Ctrl+Click, checkbox on hover) - Add bulk action bar (tag edit, move to folder, delete) - Add Ctrl+A to select all, Escape to clear selection - Fix menubar icon not showing - Translate README to Japanese - Fix release workflow duplicate binaries (publish: never) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b08541d commit 8457125

File tree

13 files changed

+532
-47
lines changed

13 files changed

+532
-47
lines changed

.github/workflows/release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ jobs:
4040

4141
- name: Build and Package
4242
run: pnpm dist:${{ matrix.platform }}
43-
env:
44-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4543

4644
- name: Upload artifacts
4745
uses: actions/upload-artifact@v4

README.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,71 @@
11
# Reference Viewer
22

3-
A desktop application for organizing and viewing video references. Import videos from YouTube or local files, organize them with folders and tags, and quickly browse with GIF thumbnail previews.
3+
動画リファレンスを整理・閲覧するためのデスクトップアプリ。YouTubeやローカルファイルから動画をインポートし、フォルダやタグで整理して、GIFサムネイルでプレビューできます。
44

55
![License](https://img.shields.io/badge/license-MIT-blue.svg)
66

7-
## Features
7+
## 機能
88

9-
- **YouTube Import**: Download clips from YouTube with custom start/end times
10-
- **Local Import**: Import video files from your computer
11-
- **GIF Thumbnails**: Hover to preview videos as animated GIFs
12-
- **Folder Organization**: Organize videos into folders with drag-and-drop
13-
- **Tag System**: Add colored tags to categorize videos
14-
- **Video Player**: Built-in player with speed control, loop, and volume settings
15-
- **Memo/Notes**: Add notes to each video
16-
- **Open Original**: Jump to the original YouTube video at the clip's start time
9+
- **YouTubeインポート**: 開始・終了時間を指定してクリップをダウンロード
10+
- **ローカルインポート**: PCから動画ファイルをインポート
11+
- **GIFサムネイル**: ホバーでGIFアニメーションプレビュー
12+
- **フォルダ整理**: ドラッグ&ドロップでフォルダに整理
13+
- **タグシステム**: カラータグで動画を分類
14+
- **動画プレイヤー**: 速度調整、ループ、音量設定付き
15+
- **メモ機能**: 各動画にメモを追加
16+
- **元動画を開く**: YouTubeの元動画をクリップの開始位置で開く
1717

18-
## Download
18+
## ダウンロード
1919

20-
Download the latest version from [Releases](https://github.com/Nattuhan/movie-reference-viewer/releases):
20+
[Releases](https://github.com/Nattuhan/movie-reference-viewer/releases) から最新版をダウンロード:
2121

2222
- **Windows**: `Reference-Viewer-Setup-x.x.x.exe`
2323
- **Mac (Intel)**: `Reference-Viewer-x.x.x.dmg`
2424
- **Mac (Apple Silicon)**: `Reference-Viewer-x.x.x-arm64.dmg`
2525

26-
## Development
26+
## 開発
2727

28-
### Prerequisites
28+
### 必要なもの
2929

3030
- Node.js 20+
3131
- pnpm
3232

33-
### Setup
33+
### セットアップ
3434

3535
```bash
36-
# Install dependencies
36+
# 依存関係をインストール
3737
pnpm install
3838

39-
# Download ffmpeg and yt-dlp binaries
39+
# ffmpegとyt-dlpバイナリをダウンロード
4040
pnpm download-binaries
4141

42-
# Start development server
42+
# 開発サーバーを起動
4343
pnpm dev
4444
```
4545

46-
### Build
46+
### ビルド
4747

4848
```bash
49-
# Build for current platform
49+
# 現在のプラットフォーム向けにビルド
5050
pnpm dist
5151

52-
# Build for specific platform
52+
# 特定のプラットフォーム向けにビルド
5353
pnpm dist:win
5454
pnpm dist:mac
5555
```
5656

57-
## Tech Stack
57+
## 技術スタック
5858

59-
- **Frontend**: React, TypeScript, Zustand
60-
- **Backend**: Electron, better-sqlite3
61-
- **Video Processing**: ffmpeg (LGPL), yt-dlp
62-
- **Build**: Vite, electron-builder
59+
- **フロントエンド**: React, TypeScript, Zustand
60+
- **バックエンド**: Electron, better-sqlite3
61+
- **動画処理**: ffmpeg (LGPL), yt-dlp
62+
- **ビルド**: Vite, electron-builder
6363

64-
## License
64+
## ライセンス
6565

6666
MIT
6767

68-
### Third-party Licenses
68+
### サードパーティライセンス
6969

7070
- [ffmpeg](https://ffmpeg.org/) - LGPL v2.1+
7171
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - Unlicense

assets/icons/icon.ico

39.3 KB
Binary file not shown.

electron-builder.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extraResources:
2020
icon: assets/icons/icon.png
2121

2222
mac:
23+
icon: assets/icons/icon.png
2324
category: public.app-category.video
2425
target:
2526
- target: dmg
@@ -30,6 +31,7 @@ mac:
3031
gatekeeperAssess: false
3132

3233
win:
34+
icon: assets/icons/icon.ico
3335
target:
3436
- target: nsis
3537
arch:

electron/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ async function createWindow() {
1515
height: 900,
1616
minWidth: 900,
1717
minHeight: 600,
18+
icon: path.join(__dirname, '../assets/icons/icon.png'),
1819
webPreferences: {
1920
preload: path.join(__dirname, 'preload.js'),
2021
contextIsolation: true,

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "movie-reference-viewer",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Video reference viewer for creative professionals",
55
"main": "electron-dist/main.js",
66
"scripts": {
@@ -12,8 +12,8 @@
1212
"build:electron": "tsc -p tsconfig.electron.json",
1313
"pack": "npm run build && electron-builder --dir",
1414
"dist": "npm run build && electron-builder",
15-
"dist:mac": "npm run build && electron-builder --mac",
16-
"dist:win": "npm run build && electron-builder --win",
15+
"dist:mac": "npm run build && electron-builder --mac --publish never",
16+
"dist:win": "npm run build && electron-builder --win --publish never",
1717
"postinstall": "electron-builder install-app-deps",
1818
"download-binaries": "tsx scripts/download-binaries.ts",
1919
"lint": "eslint . --ext .ts,.tsx",
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
import { useTagStore } from '../../stores/tagStore';
3+
import './Dialog.css';
4+
5+
const TAG_COLOR = '#6b7280';
6+
7+
interface BulkTagEditDialogProps {
8+
isOpen: boolean;
9+
videoIds: number[];
10+
onClose: () => void;
11+
onSave: () => void;
12+
}
13+
14+
export function BulkTagEditDialog({ isOpen, videoIds, onClose, onSave }: BulkTagEditDialogProps) {
15+
const { tags, fetchTags, createTag } = useTagStore();
16+
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
17+
const [newTagName, setNewTagName] = useState('');
18+
const [isCreating, setIsCreating] = useState(false);
19+
const [isSaving, setIsSaving] = useState(false);
20+
const inputRef = useRef<HTMLInputElement>(null);
21+
22+
useEffect(() => {
23+
if (isOpen) {
24+
fetchTags();
25+
setSelectedTagIds([]);
26+
setNewTagName('');
27+
setIsCreating(false);
28+
}
29+
}, [isOpen, fetchTags]);
30+
31+
const toggleTag = (tagId: number) => {
32+
setSelectedTagIds((prev) =>
33+
prev.includes(tagId)
34+
? prev.filter((id) => id !== tagId)
35+
: [...prev, tagId]
36+
);
37+
};
38+
39+
const handleCreateTag = async () => {
40+
if (!newTagName.trim()) return;
41+
42+
const newTag = await createTag(newTagName.trim(), TAG_COLOR);
43+
setSelectedTagIds((prev) => [...prev, newTag.id]);
44+
setNewTagName('');
45+
setIsCreating(false);
46+
};
47+
48+
const handleSave = async () => {
49+
if (selectedTagIds.length === 0) {
50+
onClose();
51+
return;
52+
}
53+
54+
setIsSaving(true);
55+
try {
56+
// Add selected tags to each video
57+
for (const videoId of videoIds) {
58+
const currentTags = await window.electronAPI.tag.getByVideo(videoId);
59+
const currentTagIds = currentTags.map((t: { id: number }) => t.id);
60+
const newTagIds = [...new Set([...currentTagIds, ...selectedTagIds])];
61+
await window.electronAPI.tag.setForVideo(videoId, newTagIds);
62+
}
63+
onSave();
64+
onClose();
65+
} finally {
66+
setIsSaving(false);
67+
}
68+
};
69+
70+
const handleKeyDown = (e: React.KeyboardEvent) => {
71+
if (e.key === 'Escape') {
72+
if (isCreating) {
73+
setIsCreating(false);
74+
setNewTagName('');
75+
} else {
76+
onClose();
77+
}
78+
}
79+
};
80+
81+
const handleInputKeyDown = (e: React.KeyboardEvent) => {
82+
if (e.key === 'Enter') {
83+
e.preventDefault();
84+
handleCreateTag();
85+
} else if (e.key === 'Escape') {
86+
setIsCreating(false);
87+
setNewTagName('');
88+
}
89+
};
90+
91+
useEffect(() => {
92+
if (isCreating && inputRef.current) {
93+
inputRef.current.focus();
94+
}
95+
}, [isCreating]);
96+
97+
if (!isOpen) return null;
98+
99+
return (
100+
<div className="dialog-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
101+
<div className="dialog" onKeyDown={handleKeyDown}>
102+
<div className="dialog-header">
103+
<h3>{videoIds.length} 件の動画にタグを追加</h3>
104+
</div>
105+
<div className="dialog-body">
106+
<p className="dialog-hint">選択したタグが追加されます(既存のタグは保持)</p>
107+
<div className="tag-list-edit">
108+
{tags.map((tag) => (
109+
<button
110+
key={tag.id}
111+
className={`tag-checkbox ${selectedTagIds.includes(tag.id) ? 'selected' : ''}`}
112+
onClick={() => toggleTag(tag.id)}
113+
>
114+
{tag.name}
115+
</button>
116+
))}
117+
118+
{isCreating ? (
119+
<div className="new-tag-input">
120+
<input
121+
ref={inputRef}
122+
type="text"
123+
value={newTagName}
124+
onChange={(e) => setNewTagName(e.target.value)}
125+
onKeyDown={handleInputKeyDown}
126+
placeholder="Tag name..."
127+
className="tag-input"
128+
/>
129+
<button className="tag-input-btn" onClick={handleCreateTag}>+</button>
130+
</div>
131+
) : (
132+
<button className="tag-checkbox add-tag" onClick={() => setIsCreating(true)}>
133+
+ New Tag
134+
</button>
135+
)}
136+
</div>
137+
</div>
138+
<div className="dialog-footer">
139+
<button type="button" className="btn-secondary" onClick={onClose} disabled={isSaving}>
140+
Cancel
141+
</button>
142+
<button type="button" className="btn-primary" onClick={handleSave} disabled={isSaving}>
143+
{isSaving ? 'Saving...' : 'Add Tags'}
144+
</button>
145+
</div>
146+
</div>
147+
</div>
148+
);
149+
}

src/components/common/Dialog.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@
169169
}
170170

171171
.dialog-hint {
172-
margin-top: 8px;
173-
font-size: 11px;
172+
margin: 0 0 12px 0;
173+
font-size: 12px;
174174
color: var(--text-muted);
175175
}
176176

src/components/video/VideoCard.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,58 @@
148148
font-size: 10px;
149149
color: var(--text-muted);
150150
}
151+
152+
/* Selection styles */
153+
.video-card {
154+
position: relative;
155+
}
156+
157+
.video-card.selected {
158+
border-color: var(--accent);
159+
box-shadow: 0 0 0 2px var(--accent);
160+
}
161+
162+
.selection-checkbox {
163+
position: absolute;
164+
top: 8px;
165+
left: 8px;
166+
z-index: 10;
167+
cursor: pointer;
168+
}
169+
170+
.selection-checkbox .checkbox {
171+
width: 20px;
172+
height: 20px;
173+
border-radius: 4px;
174+
background-color: rgba(0, 0, 0, 0.6);
175+
border: 2px solid rgba(255, 255, 255, 0.7);
176+
display: flex;
177+
align-items: center;
178+
justify-content: center;
179+
transition: all 0.15s ease;
180+
}
181+
182+
.selection-checkbox .checkbox:hover {
183+
border-color: var(--accent);
184+
background-color: rgba(0, 0, 0, 0.8);
185+
}
186+
187+
.selection-checkbox .checkbox.checked {
188+
background-color: var(--accent);
189+
border-color: var(--accent);
190+
}
191+
192+
.selection-checkbox .checkbox span {
193+
color: white;
194+
font-size: 12px;
195+
font-weight: bold;
196+
line-height: 1;
197+
}
198+
199+
.video-card.list .selection-checkbox {
200+
position: relative;
201+
top: auto;
202+
left: auto;
203+
align-self: center;
204+
margin-right: 4px;
205+
}

0 commit comments

Comments
 (0)