Skip to content

Commit 2b95585

Browse files
committed
Release v0.1.3
1 parent 20e481a commit 2b95585

20 files changed

Lines changed: 674 additions & 152 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ Thumbs.db
2222

2323
# Temp
2424
package-lock.json
25+
26+
# Local private notes
27+
LOCAL_WORKLOG.md

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@
3232
|:-:|:-:|
3333
| ![Settings](docs/screenshots/setting.png) | ![macOS](docs/screenshots/macos.png) |
3434

35-
## What's New In v0.1.2
35+
## What's New In v0.1.3
3636

37-
- **Audio Playback Support** — Play local audio files with artwork detection and the same Handy/funscript workflow
38-
- **Default Timeline Visibility Settings** — Choose whether the timeline and heatmap start enabled when opening scripted media
39-
- **Cleaner First Launch** — New installs start with both timeline and heatmap turned off by default
40-
- **Custom Windows App Icon** — Restored custom executable icon and updated Windows metadata/version info
41-
- **Refined Player UI** — Improved title bar version badge and updated settings to match the new playback options
37+
- **Edge-To-Edge Fullscreen Video** — Fullscreen playback now fills the screen instead of leaving unused letterboxed space
38+
- **Auto-Hiding Fullscreen Controls** — The playback bar and scripted overlays fade away while playing so the video can use the full area
39+
- **Expanded Subtitle Detection** — Matching `.vtt`, `.srt`, or timestamped `.txt` files can now be detected from the media folder and common subtitle/script subfolders
40+
- **Version Sync Cleanup** — UI version badges now read directly from the app package version to keep releases aligned
4241

4342
## Features
4443

@@ -66,7 +65,7 @@
6665

6766
### Windows
6867

69-
1. Download the latest `ScriptPlayerPlus-0.1.2-Windows-x64.zip` from [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)
68+
1. Download the latest `ScriptPlayerPlus-0.1.3-Windows-x64.zip` from [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)
7069
2. Extract and run `ScriptPlayerPlus.exe` — no installation required
7170

7271
### macOS

docs/README_JA.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@
3232
|:-:|:-:|
3333
| ![設定](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3434

35-
## v0.1.2 の追加内容
35+
## v0.1.3 の追加内容
3636

37-
- **オーディオ再生対応** — アートワーク検出付きでローカル音声ファイルを再生し、既存の Handy / funscript ワークフローをそのまま使えます
38-
- **タイムライン / ヒートマップの初期表示設定** — スクリプト付きメディアを開いたときに、タイムラインとヒートマップを初期表示するか設定できます
39-
- **初回状態の整理** — 新規インストール時はタイムラインとヒートマップがどちらもオフで始まります
40-
- **Windows アイコンの復元** — カスタム実行ファイルアイコンと Windows メタデータを再適用しました
41-
- **プレーヤー UI の調整** — バージョンバッジと設定画面を新機能に合わせて改善しました
37+
- **全画面表示の改善** — 全画面再生時に映像がより画面いっぱいに表示されるようになりました
38+
- **全画面コントロールの自動非表示** — 再生中は再生バーとスクリプトオーバーレイが徐々に消え、映像を全面で表示できます
39+
- **字幕検出の拡張** — メディアフォルダや字幕/スクリプト用サブフォルダ内の `.vtt``.srt`、タイムスタンプ付き `.txt` を自動検出して表示します
40+
- **バージョン表示の同期** — タイトルバーと設定画面のバージョン表記をアプリのパッケージ版数と同期しました
4241

4342
## 主な機能
4443

@@ -66,7 +65,7 @@
6665

6766
### Windows
6867

69-
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)から最新の `ScriptPlayerPlus-0.1.2-Windows-x64.zip` をダウンロード
68+
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)から最新の `ScriptPlayerPlus-0.1.3-Windows-x64.zip` をダウンロード
7069
2. 解凍して`ScriptPlayerPlus.exe`を実行 — インストール不要
7170

7271
### macOS

docs/README_KO.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@
3232
|:-:|:-:|
3333
| ![설정](screenshots/setting_kor.png) | ![macOS](screenshots/macos.png) |
3434

35-
## v0.1.2에서 추가된 내용
35+
## v0.1.3에서 추가된 내용
3636

37-
- **오디오 재생 지원** — 앨범 아트 감지와 함께 로컬 오디오 파일을 재생하고 기존 Handy / funscript 흐름을 그대로 사용할 수 있습니다
38-
- **기본 타임라인 / 히트맵 표시 설정** — 스크립트가 있는 미디어를 열 때 타임라인과 히트맵을 기본으로 켤지 설정에서 선택할 수 있습니다
39-
- **더 깔끔한 초기 상태** — 새 설치 기준으로 타임라인과 히트맵은 둘 다 꺼진 상태에서 시작합니다
40-
- **커스텀 Windows 아이콘 복원** — 실행 파일 아이콘과 Windows 메타데이터를 다시 정리했습니다
41-
- **플레이어 UI 정리** — 버전 배지와 설정 화면이 새 기능에 맞게 다듬어졌습니다
37+
- **전체화면 영상 채우기 개선** — 전체화면 재생 시 영상이 화면을 더 꽉 채우도록 표시 방식을 조정했습니다
38+
- **전체화면 컨트롤 자동 숨김** — 재생 중에는 재생바와 스크립트 오버레이가 서서히 사라져 영상 영역을 온전히 사용할 수 있습니다
39+
- **확장된 자막 탐지** — 미디어 폴더와 자막/스크립트 하위 폴더의 `.vtt`, `.srt`, 타임스탬프형 `.txt` 자막을 자동으로 찾아 표시합니다
40+
- **버전 표시 동기화** — 타이틀바와 설정 화면의 버전 배지가 앱 패키지 버전과 항상 같도록 정리했습니다
4241

4342
## 주요 기능
4443

@@ -66,7 +65,7 @@
6665

6766
### Windows
6867

69-
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)에서 최신 `ScriptPlayerPlus-0.1.2-Windows-x64.zip` 다운로드
68+
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)에서 최신 `ScriptPlayerPlus-0.1.3-Windows-x64.zip` 다운로드
7069
2. 압축 해제 후 `ScriptPlayerPlus.exe` 실행 — 설치 불필요
7170

7271
### macOS

docs/README_ZH.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@
3232
|:-:|:-:|
3333
| ![设置](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3434

35-
## v0.1.2 新增内容
35+
## v0.1.3 新增内容
3636

37-
- **音频播放支持** — 现在可以播放本地音频文件,并自动检测封面图,同时保留原有 Handy / funscript 工作流
38-
- **时间线 / 热力图默认显示设置** — 可以在设置中决定打开带脚本的媒体时是否默认显示时间线和热力图
39-
- **更干净的初始状态** — 新安装时,时间线和热力图默认都处于关闭状态
40-
- **恢复自定义 Windows 图标** — 重新应用了自定义可执行文件图标和 Windows 元数据
41-
- **播放器 UI 调整** — 版本徽标和设置界面已经根据新功能做了整理
37+
- **全屏铺满显示** — 全屏播放时,视频现在会更完整地铺满整个屏幕
38+
- **全屏控件自动隐藏** — 播放过程中,进度条和脚本叠层会逐渐淡出,让视频占满可用区域
39+
- **扩展字幕检测** — 现在会自动从媒体文件夹以及常见字幕/脚本子文件夹中查找 `.vtt``.srt` 和带时间戳的 `.txt` 字幕
40+
- **版本显示同步** — 标题栏和设置页中的版本徽标现在会直接跟随应用包版本
4241

4342
## 主要功能
4443

@@ -66,7 +65,7 @@
6665

6766
### Windows
6867

69-
1.[Releases](https://github.com/sioaeko/scriptplayer-plus/releases) 下载最新的 `ScriptPlayerPlus-0.1.2-Windows-x64.zip`
68+
1.[Releases](https://github.com/sioaeko/scriptplayer-plus/releases) 下载最新的 `ScriptPlayerPlus-0.1.3-Windows-x64.zip`
7069
2. 解压后运行 `ScriptPlayerPlus.exe` — 无需安装
7170

7271
### macOS

electron/main.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@ const VIDEO_EXTS = ['.mp4', '.mkv', '.avi', '.webm', '.mov', '.wmv']
1010
const AUDIO_EXTS = ['.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg', '.opus', '.wma']
1111
const MEDIA_EXTS = [...VIDEO_EXTS, ...AUDIO_EXTS]
1212
const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp']
13+
const SUBTITLE_EXTS = ['.vtt', '.srt', '.txt']
14+
const SUBTITLE_DIR_KEYWORDS = [
15+
'script',
16+
'scripts',
17+
'subtitle',
18+
'subtitles',
19+
'subs',
20+
'caption',
21+
'captions',
22+
'lyric',
23+
'lyrics',
24+
'transcript',
25+
'translation',
26+
'translated',
27+
'자막',
28+
'대본',
29+
'번역',
30+
'스크립트',
31+
'가사',
32+
'字幕',
33+
'翻译',
34+
'翻譯',
35+
'脚本',
36+
'歌詞',
37+
'歌词',
38+
]
39+
const MAX_SUBTITLE_SEARCH_DEPTH = 2
1340

1441
let mainWindow: BrowserWindow | null = null
1542

@@ -176,6 +203,25 @@ ipcMain.handle('fs:findArtwork', async (_event, mediaPath: string) => {
176203
}
177204
})
178205

206+
ipcMain.handle('fs:readSubtitles', async (_event, mediaPath: string) => {
207+
try {
208+
return findSubtitleFilesForMedia(mediaPath)
209+
.map((subtitlePath) => {
210+
try {
211+
return {
212+
path: subtitlePath,
213+
content: readSubtitleContent(subtitlePath),
214+
}
215+
} catch {
216+
return null
217+
}
218+
})
219+
.filter((entry): entry is { path: string; content: string } => entry !== null)
220+
} catch {
221+
return []
222+
}
223+
})
224+
179225
// ============================================================
180226
// NAS (WebDAV / FTP) Service
181227
// ============================================================
@@ -227,6 +273,110 @@ function findArtworkForMedia(mediaPath: string): string | null {
227273
return path.join(dir, images[0])
228274
}
229275

276+
function findSubtitleFilesForMedia(mediaPath: string): string[] {
277+
const mediaDir = path.dirname(mediaPath)
278+
const ext = path.extname(mediaPath)
279+
const baseName = path.basename(mediaPath, ext).toLowerCase()
280+
281+
return collectSubtitleCandidates(mediaDir)
282+
.map((filePath) => ({
283+
filePath,
284+
score: scoreSubtitleCandidate(filePath, mediaDir, baseName),
285+
}))
286+
.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
287+
.map(({ filePath }) => filePath)
288+
}
289+
290+
function collectSubtitleCandidates(rootDir: string): string[] {
291+
const results = new Set<string>()
292+
const visited = new Set<string>()
293+
294+
const walk = (currentDir: string, depth: number, matchedKeyword: boolean) => {
295+
if (visited.has(currentDir)) return
296+
visited.add(currentDir)
297+
298+
let entries: fs.Dirent[]
299+
try {
300+
entries = fs.readdirSync(currentDir, { withFileTypes: true })
301+
} catch {
302+
return
303+
}
304+
305+
for (const entry of entries) {
306+
if (!entry.isFile()) continue
307+
const ext = path.extname(entry.name).toLowerCase()
308+
if (SUBTITLE_EXTS.includes(ext)) {
309+
results.add(path.join(currentDir, entry.name))
310+
}
311+
}
312+
313+
if (depth >= MAX_SUBTITLE_SEARCH_DEPTH) return
314+
315+
for (const entry of entries) {
316+
if (!entry.isDirectory()) continue
317+
const nextMatchedKeyword = matchedKeyword || directoryLooksLikeSubtitle(entry.name)
318+
const shouldDescend = depth === 0 || nextMatchedKeyword
319+
if (!shouldDescend) continue
320+
walk(path.join(currentDir, entry.name), depth + 1, nextMatchedKeyword)
321+
}
322+
}
323+
324+
walk(rootDir, 0, false)
325+
return Array.from(results)
326+
}
327+
328+
function directoryLooksLikeSubtitle(name: string): boolean {
329+
const normalized = name.toLowerCase()
330+
return SUBTITLE_DIR_KEYWORDS.some((keyword) => normalized.includes(keyword))
331+
}
332+
333+
function scoreSubtitleCandidate(filePath: string, mediaDir: string, baseName: string): number {
334+
const ext = path.extname(filePath).toLowerCase()
335+
const stem = path.basename(filePath, ext).toLowerCase()
336+
const fileName = path.basename(filePath).toLowerCase()
337+
const relativeDir = path.relative(mediaDir, path.dirname(filePath)).toLowerCase()
338+
339+
let score = 0
340+
341+
if (ext === '.vtt') score += 400
342+
else if (ext === '.srt') score += 320
343+
else if (ext === '.txt') score += 240
344+
345+
if (path.dirname(filePath) === mediaDir) score += 120
346+
if (stem === baseName) score += 1200
347+
else if (stem.startsWith(`${baseName}.`)) score += 950
348+
else if (stem.includes(baseName)) score += 700
349+
350+
if (directoryLooksLikeSubtitle(relativeDir)) score += 180
351+
if (fileName.includes('subtitle') || fileName.includes('caption') || fileName.includes('lyrics')) score += 80
352+
if (fileName.includes('자막') || fileName.includes('대본') || fileName.includes('번역')) score += 80
353+
354+
return score
355+
}
356+
357+
function readSubtitleContent(filePath: string): string {
358+
const buffer = fs.readFileSync(filePath)
359+
const utf8 = buffer.toString('utf-8')
360+
const utf8ReplacementCount = countReplacementChars(utf8)
361+
362+
if (utf8ReplacementCount === 0) {
363+
return utf8
364+
}
365+
366+
try {
367+
const eucKr = new TextDecoder('euc-kr').decode(buffer)
368+
if (countReplacementChars(eucKr) < utf8ReplacementCount) {
369+
return eucKr
370+
}
371+
} catch {}
372+
373+
return utf8
374+
}
375+
376+
function countReplacementChars(value: string): number {
377+
return (value.match(/\uFFFD/g) ?? []).length
378+
}
379+
230380
// ---- WebDAV helpers (raw HTTP) ----
231381

232382
function webdavRequest(

electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
1818
saveFunscript: (videoPath: string, data: string) => ipcRenderer.invoke('fs:saveFunscript', videoPath, data),
1919
getVideoUrl: (filePath: string) => ipcRenderer.invoke('fs:getVideoUrl', filePath),
2020
findArtwork: (mediaPath: string) => ipcRenderer.invoke('fs:findArtwork', mediaPath),
21+
readSubtitles: (mediaPath: string) => ipcRenderer.invoke('fs:readSubtitles', mediaPath),
2122

2223
// NAS operations
2324
nasWebdavConnect: (url: string, username: string, password: string) =>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scriptplayer-plus",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "ScriptPlayer+ - Funscript video player with Handy integration",
55
"license": "MIT",
66
"main": "dist-electron/main.js",

src/App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import TitleBar from './components/TitleBar'
33
import Sidebar from './components/Sidebar'
44
import VideoPlayer from './components/VideoPlayer'
55
import Settings from './components/Settings'
6-
import { VideoFile, Funscript, FunscriptAction, MediaType } from './types'
6+
import { VideoFile, Funscript, FunscriptAction, MediaType, SubtitleCue, SubtitleFile } from './types'
77
import { parseFunscript } from './services/funscript'
88
import { handyService, HandyUploadStatus } from './services/handy'
99
import { AppSettings, loadSettings, saveSettings } from './services/settings'
10+
import { parseSubtitleFile } from './services/subtitles'
1011
import { useTranslation } from './i18n'
1112

1213
const VIDEO_EXTS = ['.mp4', '.mkv', '.avi', '.webm', '.mov', '.wmv']
@@ -27,6 +28,7 @@ export default function App() {
2728
const [currentFileType, setCurrentFileType] = useState<MediaType | null>(null)
2829
const [artworkUrl, setArtworkUrl] = useState<string | null>(null)
2930
const [funscript, setFunscript] = useState<Funscript | null>(null)
31+
const [subtitleCues, setSubtitleCues] = useState<SubtitleCue[]>([])
3032
const [handyConnected, setHandyConnected] = useState(false)
3133
const [scriptUploadUrl, setScriptUploadUrl] = useState<string | null>(null)
3234
const [handyUploadStatus, setHandyUploadStatus] = useState<HandyUploadStatus>('idle')
@@ -70,6 +72,9 @@ export default function App() {
7072

7173
setCurrentFile(filePath)
7274
setCurrentFileType(resolvedType)
75+
setFunscript(null)
76+
setSubtitleCues([])
77+
setScriptUploadUrl(null)
7378

7479
const url = await window.electronAPI.getVideoUrl(filePath)
7580
setVideoUrl(url)
@@ -83,10 +88,12 @@ export default function App() {
8388
}
8489
}
8590

91+
const subtitleFiles = await window.electronAPI.readSubtitles(filePath)
92+
setSubtitleCues(selectSubtitleCues(subtitleFiles))
93+
8694
const script = await window.electronAPI.readFunscript(filePath, settings.scriptFolder)
8795
const parsed = script ? parseFunscript(script) : null
8896
setFunscript(parsed)
89-
setScriptUploadUrl(null)
9097

9198
if (parsed && handyService.isConnected) {
9299
uploadToHandy(parsed.actions)
@@ -224,6 +231,7 @@ export default function App() {
224231
currentFileName={currentFile ? getFileName(currentFile) : null}
225232
artworkUrl={artworkUrl}
226233
actions={actions}
234+
subtitleCues={subtitleCues}
227235
onTimeUpdate={handleTimeUpdate}
228236
onPlay={handlePlay}
229237
onPause={handlePause}
@@ -235,6 +243,7 @@ export default function App() {
235243
timelineHeight={settings.timelineHeight}
236244
timelineWindow={settings.timelineWindow}
237245
speedColors={settings.speedColors}
246+
subtitleFontSize={settings.subtitleFontSize}
238247
/>
239248
</div>
240249

@@ -251,3 +260,14 @@ export default function App() {
251260
function getFileName(filePath: string): string {
252261
return filePath.split(/[\\/]/).pop() || ''
253262
}
263+
264+
function selectSubtitleCues(subtitleFiles: SubtitleFile[]): SubtitleCue[] {
265+
for (const subtitleFile of subtitleFiles) {
266+
const cues = parseSubtitleFile(subtitleFile.content, subtitleFile.path)
267+
if (cues.length > 0) {
268+
return cues
269+
}
270+
}
271+
272+
return []
273+
}

0 commit comments

Comments
 (0)