Full Code of fordes123/subtitles-view for AI

main f7a97b834a66 cached
155 files
358.4 KB
93.3k tokens
466 symbols
1 requests
Download .txt
Showing preview only (414K chars total). Download the full file or copy to clipboard to get everything.
Repository: fordes123/subtitles-view
Branch: main
Commit: f7a97b834a66
Files: 155
Total size: 358.4 KB

Directory structure:
gitextract__0rcvlyu/

├── .github/
│   └── workflows/
│       └── subtitles-view.yml
├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src/
    └── main/
        ├── java/
        │   └── org/
        │       └── fordes/
        │           └── subtitles/
        │               └── view/
        │                   ├── SubtitlesViewApplication.java
        │                   ├── config/
        │                   │   ├── ApplicationConfig.java
        │                   │   └── ExecutorConfig.java
        │                   ├── constant/
        │                   │   ├── CommonConstant.java
        │                   │   └── StyleClassConstant.java
        │                   ├── controller/
        │                   │   ├── DelayInitController.java
        │                   │   ├── EditTool.java
        │                   │   ├── Export.java
        │                   │   ├── MainController.java
        │                   │   ├── MainEditor.java
        │                   │   ├── QuickStart.java
        │                   │   ├── Setting.java
        │                   │   ├── SidebarAfter.java
        │                   │   ├── SidebarBefore.java
        │                   │   ├── SidebarBottom.java
        │                   │   ├── SpeechConversion.java
        │                   │   ├── SubtitleSearch.java
        │                   │   ├── SyncEditor.java
        │                   │   ├── TitleBar.java
        │                   │   ├── Toast.java
        │                   │   ├── ToolBox.java
        │                   │   └── VoiceConvert.java
        │                   ├── enums/
        │                   │   ├── EditToolEventEnum.java
        │                   │   ├── FileEnum.java
        │                   │   ├── FontIcon.java
        │                   │   ├── ServiceProvider.java
        │                   │   ├── ServiceType.java
        │                   │   └── SevenZipEnum.java
        │                   ├── event/
        │                   │   ├── AbstractToastEvent.java
        │                   │   ├── EditToolEvent.java
        │                   │   ├── FileOpenEvent.java
        │                   │   ├── LoadingEvent.java
        │                   │   ├── ThemeChangeEvent.java
        │                   │   ├── ToastChooseEvent.java
        │                   │   ├── ToastConfirmEvent.java
        │                   │   └── TranslateEvent.java
        │                   ├── factory/
        │                   │   └── TranslateServiceFactory.java
        │                   ├── handler/
        │                   │   ├── CallBackHandler.java
        │                   │   ├── EditToolEventHandler.java
        │                   │   ├── FileOpenEventHandler.java
        │                   │   ├── ToastEventHandler.java
        │                   │   └── ToastHandler.java
        │                   ├── mapper/
        │                   │   ├── InterfaceMapper.java
        │                   │   ├── LanguageMapper.java
        │                   │   ├── SearchCasesMapper.java
        │                   │   └── VersionMapper.java
        │                   ├── model/
        │                   │   ├── DTO/
        │                   │   │   ├── AvailableServiceInfo.java
        │                   │   │   ├── Subtitle.java
        │                   │   │   ├── TranslateResult.java
        │                   │   │   └── Video.java
        │                   │   ├── PO/
        │                   │   │   ├── FileRecord.java
        │                   │   │   ├── Interface.java
        │                   │   │   ├── Language.java
        │                   │   │   ├── SearchCases.java
        │                   │   │   ├── ServiceInterface.java
        │                   │   │   └── Version.java
        │                   │   └── search/
        │                   │       ├── Cases.java
        │                   │       ├── Engine.java
        │                   │       ├── Result.java
        │                   │       └── Selector.java
        │                   ├── service/
        │                   │   ├── ConfigService.java
        │                   │   ├── Impl/
        │                   │   │   └── InterfaceServiceImpl.java
        │                   │   ├── InterfaceService.java
        │                   │   ├── SearchService.java
        │                   │   └── translate/
        │                   │       ├── AliTranslateService.java
        │                   │       ├── BaiduTranslateService.java
        │                   │       ├── HuoShanTranslateService.java
        │                   │       ├── TencentTranslateService.java
        │                   │       ├── TranslateService.java
        │                   │       └── thread/
        │                   │           ├── AliTranslateThread.java
        │                   │           ├── BaiduTranslateThread.java
        │                   │           ├── HuoShanTranslateThread.java
        │                   │           ├── TencentTranslateThread.java
        │                   │           └── TranslateThread.java
        │                   └── utils/
        │                       ├── ArchiveUtil.java
        │                       ├── CacheUtil.java
        │                       ├── FileUtils.java
        │                       ├── SubtitleUtil.java
        │                       ├── TranslateUtil.java
        │                       ├── search/
        │                       │   ├── HTMLParsing.java
        │                       │   ├── JSONParsing.java
        │                       │   ├── Parsing.java
        │                       │   └── ParsingFactory.java
        │                       └── submerge/
        │                           ├── SubmergeAPI.java
        │                           ├── TimedLinesAPI.java
        │                           ├── constant/
        │                           │   └── FontName.java
        │                           ├── parser/
        │                           │   ├── ASSParser.java
        │                           │   ├── BaseParser.java
        │                           │   ├── LRCParser.java
        │                           │   ├── ParserFactory.java
        │                           │   ├── SRTParser.java
        │                           │   ├── SubtitleParser.java
        │                           │   └── exception/
        │                           │       ├── InvalidAssSubException.java
        │                           │       ├── InvalidColorCode.java
        │                           │       ├── InvalidFileException.java
        │                           │       ├── InvalidSRTSubException.java
        │                           │       └── InvalidSubException.java
        │                           ├── subtitle/
        │                           │   ├── ass/
        │                           │   │   ├── ASSSub.java
        │                           │   │   ├── ASSTime.java
        │                           │   │   ├── Events.java
        │                           │   │   ├── ScriptInfo.java
        │                           │   │   └── V4Style.java
        │                           │   ├── common/
        │                           │   │   ├── SubtitleLine.java
        │                           │   │   ├── SubtitleTime.java
        │                           │   │   ├── TimedLine.java
        │                           │   │   ├── TimedObject.java
        │                           │   │   └── TimedTextFile.java
        │                           │   ├── config/
        │                           │   │   ├── Font.java
        │                           │   │   └── SimpleSubConfig.java
        │                           │   ├── lrc/
        │                           │   │   ├── LRCLine.java
        │                           │   │   ├── LRCSub.java
        │                           │   │   └── LRCTime.java
        │                           │   └── srt/
        │                           │       ├── SRTLine.java
        │                           │       ├── SRTSub.java
        │                           │       └── SRTTime.java
        │                           └── utils/
        │                               ├── ColorUtils.java
        │                               ├── ConvertUtils.java
        │                               └── EncodeUtils.java
        └── resources/
            ├── application.yml
            ├── banner.txt
            ├── css/
            │   ├── edit-tool.css
            │   ├── font.css
            │   ├── main-editor.css
            │   ├── quick-start.css
            │   ├── setting.css
            │   ├── speech-conversion.css
            │   ├── styles.css
            │   ├── subtitle-search.css
            │   ├── title-bar.css
            │   ├── toast.css
            │   └── tool-box.css
            ├── font/
            │   └── buttersans-Rounded.otf
            ├── fxml/
            │   ├── edit-tool.fxml
            │   ├── export.fxml
            │   ├── main-editor.fxml
            │   ├── main-view.fxml
            │   ├── quick-start.fxml
            │   ├── setting.fxml
            │   ├── sidebar-after.fxml
            │   ├── sidebar-before.fxml
            │   ├── sidebar-bottom.fxml
            │   ├── speech-conversion.fxml
            │   ├── subtitle-search.fxml
            │   ├── sync-editor.fxml
            │   ├── title-bar.fxml
            │   ├── toast.fxml
            │   ├── tool-box.fxml
            │   └── voice-convert.fxml
            ├── logback/
            │   └── logback-spring.xml
            └── mapper/
                └── InterfaceMapper.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/subtitles-view.yml
================================================
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: CI Build

on:
  push:
    paths-ignore:
      - 'README.md'
    branches: [ main ]
  pull_request:
    paths-ignore:
      - 'README.md'
    branches: [ main ]

  workflow_dispatch:
    inputs:
      generateInstaller:
        description: 'generateInstaller'
        required: true
        type: choice
        options:
          - 'true'
          - 'false'
        default: 'false'
      customizedJre:
        description: 'customizedJre'
        required: true
        type: choice
        options:
          - 'true'
          - 'false'
        default: 'false'

jobs:
  bundling-for-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven
      - name: Build with Maven
        run: mvn --file pom.xml -Dplatform=windows -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=true -DcreateTarball=false -B package
      - name: Archive production artifacts
        uses: actions/upload-artifact@v3
        with:
          name: windows
          path: |
            target/subtitles-view-*.*
            !target/subtitles-view-*.jar
  bundling-for-linux:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven
      - name: Build with Maven
        run: mvn --file pom.xml -Dplatform=linux -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package
      - name: Archive production artifacts
        uses: actions/upload-artifact@v3
        with:
          name: linux
          path: |
            target/subtitles-view-*.*
            !target/subtitles-view-*.jar
  bundling-for-mac:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven
      - name: Build with Maven
        run: mvn --file pom.xml -Dplatform=mac -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package
      - name: Archive production artifacts
        uses: actions/upload-artifact@v3
        with:
          name: mac
          path: |
            target/subtitles-view-*.*
            !target/subtitles-view-*.jar

================================================
FILE: .gitignore
================================================
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/

### VS Code ###
.vscode/
/src/test

### custom ###
/logs
/src/test/


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 fordes123

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Subtitles-View

[![stars](https://img.shields.io/github/stars/fordes123/Subtitles-View?color=%23e74c3c)]()
[![forks](https://img.shields.io/github/forks/fordes123/Subtitles-View?color=%232ecc71)]()
[![release](https://img.shields.io/github/v/release/fordes123/Subtitles-View.svg)](https://github.com/fordes123/Subtitles-View/releases)
[![license](https://img.shields.io/github/license/fordes123/Subtitles-View?color=%239b59b6)](https://opensource.org/licenses/MIT)
 

这是一个基于`JavaFX`的程序,致力于简单、优雅、高效处理和编辑字幕。适配SRT、ASS等字幕格式,并且支持视频语音转换与字幕翻译,欢迎体验.

> ⚠️ 很遗憾,此仓库已停止维护

## ✨ 特性

- 🎁 现代化的界面,简洁明快
- 🦄 在线语音转换,简单为视频生成字幕并翻译
- ☑️ 多种视频与字幕格式支持
- ✏ 便捷化字幕编辑功能,帮助快速修正机器翻译
- 🎯 在线的字幕搜索与下载
- 🎈 深色浅色模式一键切换
- ⛏ 更多特性待开发...

## 🎉 应用界面

![浅色模式](./screenshot/home.png "⚠️界面可能已经更新,请以具体程序为准")

## ☑️ TODO

- [x] 框架搭建以及迁移重构
- [x] UI调整,深浅色跟随系统等
- [x] 字幕搜索、下载支持:`字幕库`、`伪射手网`、`A4k字幕网`
- [x] 文字翻译服务适配:`百度翻译`、`阿里翻译`、`腾讯翻译`、`火山翻译`
- [ ] 语音转换服务适配
- [ ] 简单的视频处理支持,如字幕分离、水印、格式转换等

## 🧑🏻‍🔧技术栈

- `Maven`
- `JavaFX`
- `SpringBoot`
- `SQLite`
- `Mybatis-Plus`

## 📢 项目说明

- 兴趣之作,欢迎提出任何修改意见,但不保证任何更新以及功能的可靠性
- 设计支持跨平台,但未经测试,现阶段以`Windows`平台为主
- 程序无任何收费和用户信息收集行为。所有在线服务如:语音转写、在线翻译均为第三方提供,与本程序无关

## 🛠 快速开始

### 从源代码构建

```shell
# 请保证你的JDK版本不低于11,否则无法通过编译
git clone https://github.com/fordes123/subtitles-view.git
cd subtitles-view
mvn clean install
mvn run
```

或者

fork 本项目, 在 `WorkFlows` 中运行 `CI Build`, Github Action 将根据配置自动为你构建对应程序包
<details>
<summary>查看引导</summary>
<img src="./screenshot/action.png" alt="">
</details>

### 获取可执行文件

- 正式发行版 [🚀 Releases](https://github.com/fordes123/Subtitles-View/releases/)
- 自动构建的测试版 [🤖 CI](https://github.com/fordes123/subtitles-view/actions)

(由于正在积极开发中,暂时没有 Release 版本,预览以及体验可使用 CI 版本)

## 🤝 交流反馈

- 提交 [📌Issues](https://github.com/fordes123/Subtitles-View/issues)
- 博客评论区 [📌Blog Page](https://blog.fordes.top/archives/subtitles-view.html)

## 📜 开源许可

- 基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。


================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <name>subtitles-view</name>
    <groupId>org.fordes</groupId>
    <artifactId>subtitles-view</artifactId>
    <version>2.0.0-Alpha</version>
    <description>subtitles-view</description>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.2</spring-boot.version>
        <javafx-weaver.version>1.3.0</javafx-weaver.version>
        <lombok.version>1.18.22</lombok.version>
        <hutool.version>5.7.18</hutool.version>
        <javafx.version>11.0.2</javafx.version>
        <jfoenix.version>9.0.9</jfoenix.version>
        <jSystemThemeDetector.version>3.8</jSystemThemeDetector.version>
        <jsoup.version>1.14.3</jsoup.version>
        <sqlite.version>3.36.0.3</sqlite.version>
        <mybatis-plus.version>3.5.1</mybatis-plus.version>
        <sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
        <javapackager.version>1.6.7</javapackager.version>
        <juniversalchardet.version>2.4.0</juniversalchardet.version>
        <richtextfx.version>0.10.9</richtextfx.version>

        <!--javapackager configuration-->
        <platform>windows</platform>
        <bundleJre>true</bundleJre>
        <generateInstaller>false</generateInstaller>
        <customizedJre>false</customizedJre>
        <createZipball>true</createZipball>
        <createTarball>false</createTarball>
    </properties>

    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>

    <dependencies>

        <!--javafx springboot 整合-->
        <dependency>
            <groupId>com.github.fordes123</groupId>
            <artifactId>spring-boot-jfx</artifactId>
            <version>0.0.1</version>
        </dependency>

        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

        <!--javafx组件-->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-base</artifactId>
            <version>${javafx.version}</version>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-graphics</artifactId>
            <version>${javafx.version}</version>
        </dependency>

        <!--Material Design 控件-->
        <dependency>
            <groupId>com.jfoenix</groupId>
            <artifactId>jfoenix</artifactId>
            <version>${jfoenix.version}</version>
        </dependency>

        <!--jsoup,html解析-->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>${jsoup.version}</version>
        </dependency>

        <!--sqlite 数据库-->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>${sqlite.version}</version>
        </dependency>

        <!--mybatis-plus ORM-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!--7z 解压-->
        <dependency>
            <groupId>net.sf.sevenzipjbinding</groupId>
            <artifactId>sevenzipjbinding</artifactId>
            <version>${sevenzipjbinding.version}</version>
        </dependency>

        <dependency>
            <groupId>net.sf.sevenzipjbinding</groupId>
            <artifactId>sevenzipjbinding-all-platforms</artifactId>
            <version>${sevenzipjbinding.version}</version>
        </dependency>

        <!--文件编码检测-->
        <dependency>
            <groupId>com.github.albfernandez</groupId>
            <artifactId>juniversalchardet</artifactId>
            <version>${juniversalchardet.version}</version>
        </dependency>

        <!--jackson-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>

        <dependency>
            <groupId>org.fxmisc.richtext</groupId>
            <artifactId>richtextfx</artifactId>
            <version>${richtextfx.version}</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>io.github.fvarrui</groupId>
                <artifactId>javapackager</artifactId>
                <version>${javapackager.version}</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <phase>package</phase>
                        <goals>
                            <goal>package</goal>
                        </goals>
                        <configuration>
                            <platform>${platform}</platform>
                            <mainClass>org.fordes.subtitles.view.SubtitlesViewApplication</mainClass>
                            <bundleJre>${bundleJre}</bundleJre>
                            <customizedJre>${customizedJre}</customizedJre>
                            <generateInstaller>${generateInstaller}</generateInstaller>
                            <administratorRequired>false</administratorRequired>
                            <createZipball>${createZipball}</createZipball>
                            <createTarball>${createTarball}</createTarball>
                            <vmArgs>-XX:TieredStopAtLevel=1 -noverify</vmArgs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>


================================================
FILE: src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java
================================================
package org.fordes.subtitles.view;

import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import com.jthemedetecor.OsThemeDetector;
import javafx.scene.Parent;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.fordes.jfx.annotation.JFXApplication;
import org.fordes.jfx.annotation.Tray;
import org.fordes.jfx.core.ProxyApplication;
import org.fordes.jfx.core.ProxyLauncher;
import org.fordes.jfx.core.StageReadyEvent;
import org.fordes.subtitles.view.config.ApplicationConfig;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.event.ThemeChangeEvent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.awt.*;
import java.io.IOException;

/**
 * @author fordes
 */
@Slf4j
@AllArgsConstructor
@SpringBootApplication
@JFXApplication(value = "/fxml/main-view.fxml", title = "SubtitlesView Alpha", style = StageStyle.TRANSPARENT,
        css = {"/css/styles.css", "/css/font.css"}, osThemeDetector = true, darkStyleClass = "dark", icons = {"/icon/logo.ico"},
        systemTray = @Tray(value = true, image = "/icon/logo.png", toolTip = "SubtitlesView"))
public class SubtitlesViewApplication extends ProxyApplication {

    private final ApplicationConfig config;

    public static String applicationName;

    private static final long timeMillis = System.currentTimeMillis();

    @Value("${spring.application.name}")
    public void setApplicationName(String applicationName) {
        SubtitlesViewApplication.applicationName = applicationName;
    }

    public static void main(String[] args) {
        ProxyLauncher.run(SubtitlesViewApplication.class, args);
    }

    @Override
    public void handleEvent(StageReadyEvent event) throws IOException, AWTException {
        super.handleEvent(event);
        log.info("{} 启动成功! 耗时: {} ms", applicationName, System.currentTimeMillis() - timeMillis);
    }

    @Override
    public void loadFXMLBefore(Stage stage, JFXApplication property) {
        //stage存入单例池
        Singleton.put(stage);
        super.loadFXMLBefore(stage, property);
    }

    @Override
    public void initAfter(Stage stage) {
        stage.getScene().setFill(null);
        //监听全屏状态,切换样式
        stage.fullScreenProperty().addListener((observableValue, aBoolean, t1) -> {
            stage.getScene().getRoot().getStyleClass().remove(t1 ?
                    StyleClassConstant.NORMAL_SCREEN : StyleClassConstant.FULL_SCREEN);
            stage.getScene().getRoot().getStyleClass().add(t1 ?
                    StyleClassConstant.FULL_SCREEN : StyleClassConstant.NORMAL_SCREEN);
        });
        super.initAfter(stage);
    }

    @Override
    public void registerOsThemeDetector(OsThemeDetector detector, Stage stage, JFXApplication property) {
        Parent root = stage.getScene().getRoot();
        if (StrUtil.isNotEmpty(property.darkStyleClass())) {
            detector.registerListener(isDark -> {
                if (config.getTheme() == null) {
                    switchTheme(detector, root, property, isDark);
                }
            });
            //监听主题切换事件
            stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event ->
                    switchTheme(detector, root, property, event.isDark()));
            //初始主题
            switchTheme(detector, root, property, config.getTheme());
        }
    }

    private void switchTheme(OsThemeDetector detector, Parent root, JFXApplication property, Boolean isDark) {
        if (isDark != null) {
            if (isDark) {
                if (!root.getStyleClass().contains(property.darkStyleClass())) {
                    root.getStyleClass().add(property.darkStyleClass());
                }
            } else {
                root.getStyleClass().remove(property.darkStyleClass());
            }
            config.setCurrentTheme(isDark);
        } else {
            switchTheme(detector, root, property, detector.isDark());
        }
    }

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java
================================================
package org.fordes.subtitles.view.config;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONUtil;
import cn.hutool.setting.yaml.YamlUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import javafx.scene.text.Font;
import lombok.Data;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.nio.charset.Charset;

/**
 * @author fordes on 2022/4/17
 */
@Data
@Component
@ConfigurationProperties(prefix = "config")
public class ApplicationConfig implements Serializable {

    /**
     * 主题模式 false-浅色、true-深色、null-跟随系统
     */
    private Boolean theme = null;

    /**
     * 字体
     */
    private String fontFace = Font.getDefault().getFamily();;

    /**
     * 字体大小
     */
    private Integer fontSize = 18;

    /**
     * 编辑模式 false-简洁模式、true-完整模式
     */
    private Boolean editMode = Boolean.FALSE;

    /**
     * 退出模式 false-直接退出、true-最小化至托盘
     */
    private Boolean exitMode = Boolean.FALSE;

    /**
     * 默认文件输出路径
     */
    private String outPath = CommonConstant.PATH_HOME;

    /**
     * 语言列表选项 false-完整、true-精简
     */
    private Boolean languageListMode = Boolean.TRUE;

    @TableField(exist = false)
    private boolean currentTheme;

    private static final long serialVersionUID = 1L;

    private static final ClassPathResource resource = new ClassPathResource("application.yml");

    /**
     * 写入配置文件
     */
    public void dump() {
        Dict all = YamlUtil.load(resource.getReader(Charset.defaultCharset()));
        all.put("config", JSONUtil.parseObj(this));
        YamlUtil.dump(all, FileUtil.getWriter(resource.getFile(), Charset.defaultCharset(), false));
    }
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java
================================================
package org.fordes.subtitles.view.config;

import cn.hutool.core.thread.ExecutorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author fordes on 2022/7/11
 */
@Configuration
public class ExecutorConfig {

    private final static int core = Runtime.getRuntime().availableProcessors();

    @Bean("globalExecutor")
    public ThreadPoolExecutor globalExecutor() {
        return ExecutorBuilder.create()
                .setCorePoolSize(2 * core)
                .setMaxPoolSize(2 * core)
                .setHandler(new ThreadPoolExecutor.CallerRunsPolicy())
                .build();
    }


}


================================================
FILE: src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java
================================================
package org.fordes.subtitles.view.constant;

/**
 * @author fordes on 2022/1/24
 */
public class CommonConstant {

    public static final double SCENE_MIN_WIDTH = 1050.0;

    public static final double SCENE_MIN_HEIGHT = 700.0;

    public static final double SIDE_BAR_WIDTH = 250.0;

    public static final String PREFIX = "*.";

    public static final String TITLE_ALL_FILE = "选择文件以开始";

    public static final String TITLE_PATH = "选择文件路径";

    public static final String PATH_HOME = System.getProperty("user.home");

    public static final String ROOT_PATH = System.getProperty("user.dir");

    public static final String TEMP_PATH = ROOT_PATH+ "\\temp\\";

    public static final String DOWNLOAD_PATH = TEMP_PATH+ "download\\";

//    public static final String FILE_PATH = TEMP_PATH+ "file\\";

//    public static final String LIB_PATH = ROOT_PATH+  "\\lib\\";
//
//    public static final String SEVEN_ZIP_PATH = LIB_PATH+ "7z";

    /**
     * 7z解压命令 递归子目录、全部解压到指定文件夹、只解压指定格式文件
     */
//    public static final String UN_ARCHIVE_COMMAND_FORMAT = SEVEN_ZIP_PATH+ " e -aoa -bse0 -r {} -o{} {} -y";

    public static final String CONCISE_MODE = "简洁模式";

    public static final String FULL_MODE = "完整模式";

    public static final String TRANSLATE_REPLACE =  "替换模式";

    public static final String TRANSLATE_BILINGUAL =  "双语模式";

    public static final String URL_HOME = "https://github.com/fordes123/subtitles-view";

    public static final String URL_ISSUES = URL_HOME + "/issues";
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java
================================================
package org.fordes.subtitles.view.constant;

/**
 * @author fordes on 2022/1/23
 */
public class StyleClassConstant {

    public static final String NORMAL_SCREEN = "normal-screen";

    public static final String FULL_SCREEN = "full-screen";

    public static final String SUBTITLE_SEARCH_ENGINE_ITEM = "item";

    public static final String SUBTITLE_SEARCH_ENGINE = "engine";

    public static final String QUICK_START_FILE_CHOOSE_WARNING = "warning";

    public static final String QUICK_START_FILE_CHOOSE_ERROR = "error";

    public static final String QUICK_START_FILE_CHOOSE_SUCCESS = "success";

    public static final String SUBTITLE_SEARCH_ITEM = "search-item";

    public static final String SUBTITLE_SEARCH_ITEM_CAPTION = "caption";

    public static final String SUBTITLE_SEARCH_ITEM_TEXT = "text";

    public static final String CONTENT_EXCLUSIVE = "content-exclusive";

    public static final String FONT_STYLE_TEMPLATE = "-fx-font-size: {};-fx-font-family: {}";

    public static final String ERROR = "error";
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java
================================================
package org.fordes.subtitles.view.controller;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 控制器抽象,继承并实现delayInit()方法即可在面板首次显示时进行初始化操作
 *
 * @author fordes on 2022/4/22
 */
@Component
public abstract class DelayInitController implements Initializable {

    @FXML
    public Pane root;

    @Resource
    public ThreadPoolExecutor globalExecutor;

    private boolean isInit = false;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
            if (!isInit && t1) {
                delay();
                isInit = true;
            }
        });
        globalExecutor.execute(this::async);
    }

    public Scene getScene() {
        return root.getScene();
    }

    /**
     * 懒加载,在面板首次显示时执行
     */
    public void delay() {};

    /**
     * 异步方法,在线程池中执行,避免主线程阻塞
     */
    public void async() {};
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/EditTool.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jfoenix.controls.JFXComboBox;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.config.ApplicationConfig;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.enums.EditToolEventEnum;
import org.fordes.subtitles.view.enums.FileEnum;
import org.fordes.subtitles.view.enums.ServiceType;
import org.fordes.subtitles.view.event.EditToolEvent;
import org.fordes.subtitles.view.event.LoadingEvent;
import org.fordes.subtitles.view.event.ToastChooseEvent;
import org.fordes.subtitles.view.event.ToastConfirmEvent;
import org.fordes.subtitles.view.factory.TranslateServiceFactory;
import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.model.PO.Language;
import org.fordes.subtitles.view.service.InterfaceService;
import org.fordes.subtitles.view.service.translate.TranslateService;
import org.fordes.subtitles.view.utils.CacheUtil;
import org.fordes.subtitles.view.utils.SubtitleUtil;
import org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSTime;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;
import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.TwoDimensional;
import org.mozilla.universalchardet.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 编辑工具 控制器
 *
 * @author fordes on 2022/7/15
 */
@Slf4j
@Component
public class EditTool extends DelayInitController {

    private static Subtitle subtitle;

    private static StyleClassedTextArea area;

    private static ToggleButton editMode;

    private static int max;

    private static final Map<EditToolEventEnum, GridPane> bindMap = MapUtil.newHashMap();

    @FXML
    private CheckMenuItem search_case, search_regex, replace_case, replace_regex;

    @FXML
    private JFXComboBox<String> code_choice, font_family;

    @FXML
    private ChoiceBox<AvailableServiceInfo> translate_source;

    @FXML
    private ChoiceBox<String> translate_mode;

    @FXML
    private JFXComboBox<Language> translate_original, translate_target;

    @FXML
    private JFXComboBox<Integer> font_size;

    @FXML
    private ChoiceBox<TimelineType> timeline_option;

    @FXML
    private TextField timeline_input, jump_input, search_input, replace_input, replace_find_input;

    private final InterfaceService interfaceService;

    private final SidebarBottom sidebarBottom;

    private final ApplicationConfig config;

    @Autowired
    public EditTool(InterfaceService interfaceService,
                    SidebarBottom sidebarBottom, ApplicationConfig config) {
        this.interfaceService = interfaceService;
        this.sidebarBottom = sidebarBottom;
        this.config = config;
    }

    @Override
    public void delay() {

        //编码选择框
        code_choice.getItems().addAll(Arrays.stream(ReflectUtil.getFieldsValue(Constants.class))
                .map(Object::toString).toArray(String[]::new));
        //初始化字体大小
        font_size.getItems().addAll(CollUtil.newArrayList(12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72));
        //初始化字体列表
        font_family.getItems().addAll(Font.getFontNames());
        //时间轴校正选项
        timeline_option.getItems().addAll(TimelineType.values());
        timeline_option.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1)
                -> timeline_input.setPromptText(t1.desc));
        timeline_option.getSelectionModel().select(0);
        timeline_input.textProperty().addListener((observableValue, s, t1)
                -> timeline_input.getStyleClass().remove("error"));
        //翻译相关
        translate_original.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1) -> {
            if (t1 != null) {
                Collection<Language> gap = CollUtil.subtract(config.getLanguageListMode() ?
                        t1.getTarget().stream().filter(e -> e.isGeneral() == config.getLanguageListMode()).collect(Collectors.toList()) :
                        t1.getTarget(), translate_target.getItems());
                Collection<Language> neg = CollUtil.subtract(translate_target.getItems(), t1.getTarget());
                if (!gap.isEmpty()) {
                    translate_target.getItems().addAll(gap);
                }
                if (!neg.isEmpty()) {
                    translate_target.getItems().removeAll(neg);
                }
            } else translate_target.getItems().clear();
        });
        translate_source.getSelectionModel().selectedItemProperty()
                .addListener((observableValue, availableServiceInfo, t1) -> {
                    if (t1 != null) {
                        translate_original.getItems().clear();
                        translate_original.getItems().addAll(CacheUtil.getLanguageDict(ServiceType.TRANSLATE, t1.getProvider(), config.getLanguageListMode()));
                        translate_original.getSelectionModel().selectFirst();
                    }
                });
        translate_source.getItems().clear();
        translate_source.getItems().addAll(interfaceService.getAvailableService(ServiceType.TRANSLATE));
        translate_source.getSelectionModel().selectFirst();
        translate_mode.getItems().addAll(CommonConstant.TRANSLATE_REPLACE, CommonConstant.TRANSLATE_BILINGUAL);
        translate_mode.getSelectionModel().selectFirst();
        //回车提交操作
        timeline_input.setOnAction(this::applyTimeline);
        jump_input.setOnAction(this::applyJump);
        search_input.setOnAction(this::applySearch);
        replace_find_input.setOnAction(this::applyReplaceFind);
        replace_input.setOnAction(this::applyReplaceNext);
        //错误输入
        jump_input.textProperty().addListener((observableValue, s, t1)
                -> jump_input.getStyleClass().remove(StyleClassConstant.ERROR));
        search_input.textProperty().addListener((observableValue, s, t1)
                -> search_input.getStyleClass().remove(StyleClassConstant.ERROR));
        timeline_input.textProperty().addListener((observableValue, s, t1)
                -> timeline_input.getStyleClass().remove(StyleClassConstant.ERROR));

    }

    @Override
    public void async() {
        Stage stage = Singleton.get(Stage.class);
        //各工具面板互斥
        root.getChildren().forEach(node -> {
                    if (node instanceof GridPane) {
                        EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData());
                        bindMap.put(type, (GridPane) node);
                        node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
                            if (t1) {
                                bindMap.values().forEach(e -> e.setVisible(node.equals(e)));
                                root.setVisible(true);
                            }
                        });
                    }
                }
        );

        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
            if (!t1) {
                bindMap.values().forEach(e -> e.setVisible(false));
            }
        });
        //监听编辑工具事件 唤起对应功能面板
        stage.addEventHandler(EditToolEvent.EVENT_TYPE, event -> {

            subtitle = event.getSubtitle();
            area = event.getSource();
            editMode = event.getEditMode();
            Parent parent = bindMap.get(event.getType());

            switch (event.getType()) {
                case SEARCH: //搜索
                    search_input.requestFocus();
                    parent.setVisible(true);
                    break;

                case REPLACE://替换
                    replace_find_input.requestFocus();
                    parent.setVisible(true);
                    break;

                case JUMP://跳转
                    jump_input.requestFocus();
                    max = 0;
                    for (TimedLine timedLine : subtitle.getTimedTextFile().getTimedLines()) {
                        max += timedLine.getTextLines().size();
                    }
                    parent.setVisible(true);
                    break;

                case FONT: //字体(样式)
//                    font_family.getSelectionModel().select(config.getFontFace());
                    font_size.getSelectionModel().select(config.getFontSize());
                    parent.setVisible(true);
                    break;

                case TIMELINE: //时间轴
                    TimedLine start = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines());
                    timeline_input.setPromptText(start.getTime().getStart().toString());
                    timeline_input.requestFocus();
                    parent.setVisible(true);
                    break;

                case CODE://编码
                    code_choice.getSelectionModel().select(subtitle.getCharset());
                    parent.setVisible(true);
                    break;

                case TRANSLATE:
                    List<AvailableServiceInfo> list = interfaceService.getAvailableService(ServiceType.TRANSLATE);
                    if (list.isEmpty()) {
                        stage.fireEvent(new ToastChooseEvent("未配置翻译服务", "是否立即转到设置?",
                                "确定", () -> sidebarBottom.getSetting().getOnAction().handle(null)));
                        parent.setVisible(false);
                    } else {
                        Collection<AvailableServiceInfo> gap = CollUtil.subtract(list, translate_source.getItems());
                        Collection<AvailableServiceInfo> neg = CollUtil.subtract(translate_source.getItems(), list);
                        if (!gap.isEmpty()) {
                            translate_source.getItems().addAll(gap);
                        }
                        if (!neg.isEmpty()) {
                            translate_source.getItems().removeAll(neg);
                        }
                        parent.setVisible(true);
                    }
                    break;

                case REF: //刷新
                    try {
                        SubtitleUtil.parse(subtitle);
                        area.clear();
                        area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), "styled-text-area");
                        area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,config.getFontSize(), config.getFontFace()));
                    } catch (Exception e) {
                        log.error(ExceptionUtil.stacktraceToString(e));
                        stage.fireEvent(new ToastConfirmEvent("编码更改出错", "已切换回原编码~"));
                    }
                    break;
            }
        });

    }

    @FXML
    private void onClose(ActionEvent actionEvent) {
        actionEvent.consume();
        area = null;
        subtitle = null;
        editMode = null;
        root.setVisible(false);
    }

    @FXML
    private void applyCode(ActionEvent actionEvent) {
        String original = subtitle.getCharset();
        try {
            subtitle.setCharset(code_choice.getSelectionModel().getSelectedItem());
            SubtitleUtil.parse(subtitle);
            area.clear();
            area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), StrUtil.EMPTY);
        } catch (Exception e) {
            log.error(ExceptionUtil.stacktraceToString(e));
            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("编码更改出错", "已切换回原编码~"));
            subtitle.setCharset(original);
            code_choice.getSelectionModel().select(original);
        }
        actionEvent.consume();
    }

    @FXML
    private void applyFont(ActionEvent actionEvent) {
        Stage stage = Singleton.get(Stage.class);

        String originalFontFamily = config.getFontFace();
        Integer originalFontSize = config.getFontSize();
        try {
            config.setFontSize(Convert.toInt(font_size.getValue()));
            config.setFontFace(font_family.getValue());
            area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,
                    config.getFontSize(), config.getFontFace()));
            area.requestFocus();
        } catch (Exception e) {
            log.error(ExceptionUtil.stacktraceToString(e));
            config.setFontSize(originalFontSize);
            config.setFontFace(originalFontFamily);
            font_family.setValue(originalFontFamily);
            font_size.setValue(originalFontSize);
            stage.fireEvent(new ToastConfirmEvent("字体更改出错", "已切换回原字体~"));
        }
        actionEvent.consume();
    }

    @FXML
    private void applyTimeline(ActionEvent actionEvent) {
        LocalTime newTime = null;
        String timeLine = timeline_input.getText();
        TimelineType option = timeline_option.getValue();
        if (TimelineType.TIMELINE.equals(option)) {
            try {
                newTime = FileEnum.SRT.equals(subtitle.getFormat()) ?
                        SRTTime.fromString(timeLine) : ASSTime.fromString(timeLine);
            } catch (Exception ignored) {
            }

        } else {
            if (NumberUtil.isInteger(timeLine)) {
                int offset = Convert.toInt(timeLine);
                LocalTime date = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines()).getTime().getStart();
                newTime = date.plus(offset, option.rate);
            }
        }
        if (newTime != null) {
            //TODO 按选中范围处理 待支持
            TimedTextFile original = subtitle.getTimedTextFile();
            try {
                TimedTextFile target = SubtitleUtil
                        .revise(subtitle.getTimedTextFile(), newTime, null, editMode.isSelected());
                subtitle.setTimedTextFile(target);
                SubtitleUtil.write(subtitle, success -> {
                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(!success));
                    if (success) {
                        area.clear();
                        area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(),
                                editMode.isSelected()), StrUtil.EMPTY);
                    } else throw new RuntimeException("写入失败");
                });
            } catch (Exception e) {
                log.error(ExceptionUtil.stacktraceToString(e));
                subtitle.setTimedTextFile(original);
                Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("时间轴更改出错", "已切换回原时间轴~"));
            }
        } else timeline_input.getStyleClass().add(StyleClassConstant.ERROR);
        actionEvent.consume();
    }

    @FXML
    private void applyJump(ActionEvent actionEvent) {
        String text = jump_input.getText();
        int value = NumberUtil.isInteger(text) ? NumberUtil.parseInt(text) : 0;

        if (value > 0 && value <= max) {
            TwoDimensional.Position position = area.position(value, 1);
            area.moveTo(position.toOffset());
            area.requestFollowCaret();
        } else {
            jump_input.getStyleClass().add(StyleClassConstant.ERROR);
        }
        actionEvent.consume();
    }

    @FXML
    private void applySearch(ActionEvent actionEvent) {
        String str = search_input.getText();
        if (StrUtil.isNotBlank(str)) {
            SubtitleUtil.search(area, str, search_case.isSelected(), search_regex.isSelected());
        } else search_input.getStyleClass().add(StyleClassConstant.ERROR);
        actionEvent.consume();
    }

    @FXML
    private void applyReplaceNext(ActionEvent actionEvent) {
        applyReplace(false);
        actionEvent.consume();
    }

    @FXML
    private void applyReplaceAll(ActionEvent actionEvent) {
        applyReplace(true);
        actionEvent.consume();
    }

    @FXML
    private void applyReplaceFind(ActionEvent actionEvent) {
        String str = replace_find_input.getText();
        if (StrUtil.isNotBlank(str)) {
            SubtitleUtil.find(area, str, replace_case.isSelected(), replace_regex.isSelected());
        }
        actionEvent.consume();
    }

    private void applyReplace(boolean isAll) {
        if (editMode.isSelected()) {
            String replaceText = replace_input.getText();
            String searchText = replace_find_input.getText();
            if (StrUtil.isAllNotBlank(replaceText, searchText)) {
                try {
                    SubtitleUtil.replace(area, subtitle, searchText, replaceText, isAll,
                            replace_case.isSelected(), replace_regex.isSelected());
                } catch (Exception e) {
                    log.error(ExceptionUtil.stacktraceToString(e));
                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("替换出错", "已切换回原文本~"));
                }
            }
        } else Singleton.get(Stage.class).fireEvent(new ToastChooseEvent("操作受限", "是否切换至完整模式?",
                "切换", () -> editMode.setSelected(true)));
    }

    @FXML
    private void applyTranslate(ActionEvent actionEvent) {
        AvailableServiceInfo source = translate_source.getValue();
        boolean mode = StrUtil.equals(CommonConstant.TRANSLATE_BILINGUAL, translate_mode.getValue());
        Language origin = translate_original.getValue();
        Language target = translate_target.getValue();
        if (source != null && origin != null && target != null) {

            TranslateService service = TranslateServiceFactory.getService(source.getProvider().getValue());
            Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));
            globalExecutor.execute(() -> service.translate(subtitle, target.getCode(), origin.getCode(),
                    source.getVersionInfo(), mode, JSONUtil.parseObj(source.getAuth())));
        }
        actionEvent.consume();
    }


    /**
     * 时间轴校正 操作类型枚举
     */
    @AllArgsConstructor
    enum TimelineType {

        TIMELINE("时间轴", null, "形如: xx:xx:xx:xx"),
        SECOND("秒", ChronoUnit.SECONDS, "整数,时间偏移量"),
        MILLISECOND("毫秒", ChronoUnit.MILLIS, "整数,时间偏移量"),
        MINUTE("分钟", ChronoUnit.MINUTES, "整数,时间偏移量"),
        HOUR("小时", ChronoUnit.HOURS, "整数,的时间偏移量");

        public final String name;
        public final ChronoUnit rate;
        public final String desc;

        @Override
        public String toString() {
            return this.name;
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/Export.java
================================================
package org.fordes.subtitles.view.controller;

import org.springframework.stereotype.Component;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Component
public class Export {
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/MainController.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.lang.Singleton;
import javafx.fxml.FXML;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.enums.FontIcon;
import org.fordes.subtitles.view.event.FileOpenEvent;
import org.fordes.subtitles.view.event.LoadingEvent;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/1/19
 */
@Slf4j
@Component
public class MainController extends DelayInitController {

    @FXML
    private StackPane loading;

    @FXML
    private ColumnConstraints sidebarColumn;

    @FXML
    private Label drawer;

    @FXML
    private SidebarBefore sidebarBeforeController;

    @FXML
    private SidebarAfter sidebarAfterController;

    @FXML
    private SidebarBottom sidebarBottomController;

    @FXML
    private GridPane content;

    @FXML
    private Parent quickStart, subtitleSearch, toolBox, setting, export, mainEditor, syncEditor, voiceConvert,
            sidebarBefore, sidebarAfter;

    private static double xOffset = 0;
    private static double yOffset = 0;
    private static int bit = 0;
    private final static double RESIZE_WIDTH = 5.00;

    @Override
    public void delay() {
        content.getChildren().forEach(node ->
                node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
                    if (t1) {
                        content.getChildren().forEach(e -> e.setVisible(e.equals(node)));
                    }
                }));
    }

    @Override
    public void async() {
        //绑定侧边按键和对应面板显示
        sidebarBeforeController.getQuickStart().setOnAction(event -> {
            sidebarBeforeController.getQuickStart().setSelected(true);
            quickStart.setVisible(true);
        });
        sidebarBeforeController.getSubtitleSearch().setOnAction(event -> {
            sidebarBeforeController.getSubtitleSearch().setSelected(true);
            subtitleSearch.setVisible(true);
        });
        sidebarBeforeController.getToolBox().setOnAction(event -> {
            sidebarBeforeController.getToolBox().setSelected(true);
            toolBox.setVisible(true);
        });
        sidebarAfterController.getMainEditor().setOnAction(event -> {
            sidebarAfterController.getMainEditor().setSelected(true);
            mainEditor.setVisible(true);
        });
        sidebarAfterController.getSyncEditor().setOnAction(event -> {
            sidebarAfterController.getSyncEditor().setSelected(true);
            syncEditor.setVisible(true);
        });
        sidebarAfterController.getExport().setOnAction(event -> {
            sidebarAfterController.getExport().setSelected(true);
            export.setVisible(true);
        });
        sidebarBottomController.getSetting().setOnAction(event -> {
            setting.setVisible(true);
            sidebarAfterController.getItemGroup().selectToggle(null);
            sidebarBeforeController.getItemGroup().selectToggle(null);
        });

        content.getChildren().forEach(node ->
                node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
                    if (t1) {
                        content.getChildren().forEach(e -> e.setVisible(e.equals(node)));
                    }
                }));


        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {
            if (fileOpenEvent.getRecord().getFormat().media) {
                sidebarAfterController.getItemGroup().selectToggle(null);
                sidebarBeforeController.getItemGroup().selectToggle(null);
            }else {
                sidebarBefore.setVisible(false);
                sidebarAfter.setVisible(true);
            }
        });

        Singleton.get(Stage.class).addEventHandler(LoadingEvent.EVENT_TYPE, loadingEvent
                -> loading.setVisible(loadingEvent.isAlive()));
    }

    @FXML
    private void mousePressedHandle(MouseEvent event) {
        event.consume();
        xOffset = event.getSceneX();
        yOffset = event.getSceneY();
    }

    @FXML
    private void mouseMoveHandle(MouseEvent event) {
        event.consume();
        double x = event.getSceneX();
        double y = event.getSceneY();
        double width = Singleton.get(Stage.class).getWidth() - 20;
        double height = Singleton.get(Stage.class).getHeight() - 20;
        Cursor cursorType = Cursor.DEFAULT;
        bit = 0;
        if (y >= height - RESIZE_WIDTH) {
            if (x <= RESIZE_WIDTH) {
                bit |= 1 << 3;
            } else if (x >= width - RESIZE_WIDTH) {
                bit |= 1;
                bit |= 1 << 2;
                cursorType = Cursor.SE_RESIZE;
            } else {
                bit |= 1;
                cursorType = Cursor.S_RESIZE;
            }
        } else if (x >= width - RESIZE_WIDTH) {
            bit |= 1 << 2;
            cursorType = Cursor.E_RESIZE;
        }
        getScene().getRoot().setCursor(cursorType);
    }

    @FXML
    private void mouseDraggedHandle(MouseEvent event) {
        Stage stage = Singleton.get(Stage.class);
        event.consume();
        double x = event.getSceneX();
        double y = event.getSceneY();
        double nextX = stage.getX();
        double nextY = stage.getY();
        double nextWidth = stage.getWidth();
        double nextHeight = stage.getHeight();
        if ((bit & 1 << 2) != 0) {
            nextWidth = x;
        }
        if ((bit & 1) != 0) {
            nextHeight = y;
        }
        if (nextWidth <= CommonConstant.SCENE_MIN_WIDTH) {
            nextWidth = CommonConstant.SCENE_MIN_WIDTH;
        }
        if (nextHeight <= CommonConstant.SCENE_MIN_HEIGHT) {
            nextHeight = CommonConstant.SCENE_MIN_HEIGHT;
        }
        stage.setX(nextX);
        stage.setY(nextY);
        stage.setWidth(nextWidth);
        stage.setHeight(nextHeight);
    }

    @FXML
    private void titleBarDraggedHandle(MouseEvent event) {
        Stage stage = Singleton.get(Stage.class);
        stage.setX(event.getScreenX() - xOffset);
        stage.setY(event.getScreenY() - yOffset);
        event.consume();
    }

    @FXML
    private void onDrawer(MouseEvent event) {
        if (sidebarColumn.getPrefWidth() > 0) {
            sidebarColumn.setPrefWidth(0);
            drawer.setText(FontIcon.PLACE_THE_LEFT.toString());
            content.getStyleClass().add(StyleClassConstant.CONTENT_EXCLUSIVE);
        } else {
            sidebarColumn.setPrefWidth(CommonConstant.SIDE_BAR_WIDTH);
            drawer.setText(FontIcon.PLACE_THE_RIGHT.toString());
            content.getStyleClass().remove(StyleClassConstant.CONTENT_EXCLUSIVE);
        }
        event.consume();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/MainEditor.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.config.ApplicationConfig;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.enums.EditToolEventEnum;
import org.fordes.subtitles.view.enums.FontIcon;
import org.fordes.subtitles.view.event.*;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.utils.SubtitleUtil;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.TwoDimensional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Slf4j
@Component
public class MainEditor extends DelayInitController {

    @FXML
    private GridPane editTool;

    @FXML
    private Label indicator, editModeIcon;

    @FXML
    private ToggleButton editMode;

    @FXML
    private StyleClassedTextArea editor;

    @FXML
    private HBox toolbarPanel;

    @FXML
    private RowConstraints toolbarRow;

    private Subtitle subtitle;

    private final ApplicationConfig config;

    @Autowired
    public MainEditor(ApplicationConfig config) {
        this.config = config;
    }

    @Override
    public void delay() {
        Stage stage = Singleton.get(Stage.class);

        //工具栏按钮,点击按钮发送编辑工具事件 唤起编辑工具
        toolbarPanel.getChildren().forEach(node -> {
            if (node.getUserData() != null) {
                node.setOnMouseClicked(event -> {
                    if (node.getUserData() != null) {
                        EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData());
                        stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, type));
                    }
                });
            }
        });

        //编辑模式监听
        editMode.selectedProperty().addListener((observableValue, aBoolean, t1) -> {
            ctrlEditMode(t1);
            editor.clear();
            editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), t1), StrUtil.EMPTY);
        });
        //行列号监听
        editor.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
            TwoDimensional.Position position = editor.offsetToPosition(newValue, TwoDimensional.Bias.Backward);
            indicator.setText(StrUtil.format((String) indicator.getUserData(), position.getMajor(), position.getMinor()));
        });

//        stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event -> {
//            editor.setStyleClass(0, editor.getLength(),  config.isCurrentTheme()? "richtext_dark":"richtext_light");
//        });

        stage.addEventHandler(TranslateEvent.EVENT_TYPE, event -> {

            if (TranslateEvent.SUCCESS.equals(event.getMsg())) {
                editor.clear();
                editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(),
                        editMode.isSelected()), "styled-text-area");
                editor.moveTo(0);
            }
            Platform.runLater(() -> {
                stage.fireEvent(new ToastConfirmEvent(event.getMsg(), event.getDetail()));
                stage.fireEvent(new LoadingEvent(false));
            });

        });

        //快捷键
        KeyCodeCombination ctrlT = new KeyCodeCombination(KeyCode.T, KeyCodeCombination.CONTROL_DOWN);
        stage.getScene().getAccelerators().put(ctrlT, this::ctrlToolbar);

        KeyCodeCombination ctrlF = new KeyCodeCombination(KeyCode.F, KeyCodeCombination.CONTROL_DOWN);
        stage.getScene().getAccelerators().put(ctrlF, ()
                -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.SEARCH)));

        KeyCodeCombination ctrlR = new KeyCodeCombination(KeyCode.R, KeyCodeCombination.CONTROL_DOWN);
        stage.getScene().getAccelerators().put(ctrlR, ()
                -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.REPLACE)));

    }

    @Override
    public void async() {
        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {
            if (fileOpenEvent.getRecord().getFormat().subtitle) {
                subtitle = (Subtitle) fileOpenEvent.getRecord();
                log.debug("主编辑器 => {}", subtitle.getFile().getPath());
                try {
                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));
                    SubtitleUtil.parse(subtitle);
                    root.setVisible(true);
                } catch (Exception e) {
                    log.error(ExceptionUtil.stacktraceToString(e));
                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("读取失败!", "字幕文件已经损坏"));
                } finally {
                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(false));
                }
            }
        });

        //载入设置
        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
            if (t1) {
                editor.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,
                        config.getFontSize(), config.getFontFace()));
                editor.clear();
                editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), "styled-text-area");
                //编辑器模式
                ctrlEditMode(config.getEditMode());
            }else {
                editTool.setVisible(false);
            }
        });
    }

    @FXML
    private void hideToolbar(ActionEvent actionEvent) {
        ctrlToolbar(false);
        actionEvent.consume();
    }

    /**
     * 控制工具栏显示/隐藏
     *
     * @param state 状态
     */
    private void ctrlToolbar(boolean state) {
        toolbarRow.setMaxHeight(state ? 60 : 0);
        toolbarRow.setMinHeight(state ? 60 : 0);
        toolbarRow.setPrefHeight(state ? 60 : 0);
        toolbarPanel.setVisible(state);
    }

    private void ctrlToolbar() {
        ctrlToolbar(!toolbarPanel.isVisible());
    }

    private void ctrlEditMode(Boolean mode) {
        if (mode == null) {
            mode = config.getEditMode();
        } else {
            config.setEditMode(mode);
        }
        editModeIcon.setText(mode ?
                FontIcon.SWITCH_ON_DARK.toString() :
                FontIcon.SWITCH_OFF_DARK.toString());
        editMode.setText(mode ? CommonConstant.FULL_MODE : CommonConstant.CONCISE_MODE);
        editMode.setSelected(mode);
    }

    @FXML
    private void changeEditMode(ActionEvent actionEvent) {
        actionEvent.consume();
    }

    @FXML
    private void onIndicatorClicked(MouseEvent mouseEvent) {
        Singleton.get(Stage.class).fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.JUMP));
        mouseEvent.consume();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/QuickStart.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Singleton;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.enums.FileEnum;
import org.fordes.subtitles.view.event.FileOpenEvent;
import org.fordes.subtitles.view.utils.FileUtils;
import org.springframework.stereotype.Component;

import java.io.File;

/**
 * @author fordes on 2022/2/6
 */
@Slf4j
@Component
public class QuickStart {
    @FXML
    private Label clues;

    @FXML
    private GridPane root;

    private static File dragFile;

    private static final String UNSUPPORTED_FILE_TYPE = "不支持的文件类型";

    private static final String DRAG_SUPPORT = "松手以打开文件";

    private static final String TIPS_DEFAULT = "拖放或选择文件以继续";

    private static final String OPEN_FILE_ERROR = "打开文件出错";

    @FXML
    private void chooseFile(ActionEvent event) {
        File file = FileUtils.chooseFile(CommonConstant.TITLE_ALL_FILE, FileEnum.values())
                .showOpenDialog(Singleton.get(Stage.class));

        //读取文件信息
        if (FileUtil.exist(file) && FileEnum.isSupport(FileUtil.getSuffix(file))) {
            Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile));
        } else {
            root.getStyleClass().clear();
            clues.setText(TIPS_DEFAULT);
        }
        event.consume();
    }

    @FXML
    private void onDragOver(DragEvent dragEvent) {
        Dragboard db = dragEvent.getDragboard();
        if (db.hasFiles()) {
            dragFile = db.getFiles().get(0);
            if (FileUtil.exist(dragFile) && FileEnum.isSupport(FileUtil.getSuffix(dragFile))) {
                dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                clues.setText(DRAG_SUPPORT);
                root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_SUCCESS);
            } else {
                clues.setText(UNSUPPORTED_FILE_TYPE);
                root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_WARNING);
                dragFile = null;
            }
        }
        dragEvent.consume();
    }

    @FXML
    private void onDragExited(DragEvent dragEvent) {
        clues.setText(TIPS_DEFAULT);
        root.getStyleClass().clear();
        dragEvent.consume();
    }

    @FXML
    private void onDragDropped(DragEvent dragEvent) {
        try {
            if (dragFile != null) {
                Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile));
            }
        } catch (Exception e) {
            clues.setText(OPEN_FILE_ERROR);
            root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_ERROR);
        } finally {
            dragEvent.consume();
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/Setting.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.swing.DesktopUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.config.ApplicationConfig;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.enums.ServiceType;
import org.fordes.subtitles.view.event.ThemeChangeEvent;
import org.fordes.subtitles.view.event.ToastConfirmEvent;
import org.fordes.subtitles.view.model.PO.ServiceInterface;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.InterfaceService;
import org.fordes.subtitles.view.utils.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Slf4j
@Component
public class Setting extends DelayInitController {

    @FXML
    private VBox infoPanel;

    @FXML
    private TextFlow tips;

    @FXML
    private ToggleGroup themeGroup, editorModeGroup, exitModeGroup, languageListGroup;

    @FXML
    private JFXComboBox<Version> version;

    @FXML
    private JFXComboBox<ServiceType> type;

    @FXML
    private JFXComboBox<ServiceProvider> provider;

    @FXML
    private JFXComboBox<String> fontFace;

    @FXML
    private JFXComboBox<Integer> fontSize;

    @FXML
    private TextField outPath;

    private final InterfaceService interfaceService;

    private final ApplicationConfig config;

    @Autowired
    public Setting(ApplicationConfig config, InterfaceService interfaceService) {
        this.config = config;
        this.interfaceService = interfaceService;
    }

    @Override
    public void delay() {
        Stage stage = Singleton.get(Stage.class);
        //初始化首选项
        fontFace.getItems().addAll(Font.getFontNames());
        fontSize.getItems().addAll(CollUtil.newArrayList(10, 12, 14, 16, 18, 20, 24, 36));
        applyConfig();

        //首选项监听事件
        themeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> {
            Boolean value = Convert.toBool(t1.getUserData());
            config.setTheme(value);
            stage.fireEvent(new ThemeChangeEvent(value));
        });

        editorModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1)
                -> config.setEditMode(Convert.toBool(t1.getUserData())));
        exitModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1)
                -> config.setExitMode(Convert.toBool(t1.getUserData())));
        fontFace.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1)
                -> config.setFontFace(t1));
        fontSize.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1)
                -> config.setFontSize(t1));
        outPath.textProperty().addListener((observableValue, s, t1)
                -> config.setOutPath(StrUtil.trim(t1)));
        languageListGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) ->
                config.setLanguageListMode(Convert.toBool(t1.getUserData())));

        //接口类型
        type.getItems().addAll(ServiceType.values());
        type.getSelectionModel().selectedItemProperty().addListener((observableValue, type, t1) -> {
            if (null != t1 && provider.getValue() != null) {
                version.getItems().clear();
                version.getItems().addAll(interfaceService.getVersions(t1, provider.getValue()));
            }
        });

        //服务商
        provider.getItems().addAll(ServiceProvider.values());
        provider.getSelectionModel().selectedItemProperty().addListener((observableValue, supportDto, t1) -> {
            if (null != t1 && type.getValue() != null) {
                version.getItems().clear();
                version.getItems().addAll(interfaceService.getVersions(type.getValue(), t1));
            }
        });

        //版本
        version.getSelectionModel().selectedItemProperty().addListener((observableValue, serviceVersion, t1) -> {
            if (null != t1) {
                tips.setVisible(false);
                version.setTooltip(new Tooltip(t1.getRemark()));
                buildInfoFrame(interfaceService.getInterface(type.getValue(), provider.getValue()));
            } else {
                tips.setVisible(true);
            }
        });
        //提示区
        tips.visibleProperty().addListener((observableValue, aBoolean, t1) -> infoPanel.setVisible(!t1));
    }

    @Override
    public void async() {
        //监听器用于保存配置
        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
            if (!t1) {
                if (FileUtil.exist(config.getOutPath())) {
                    config.setOutPath(outPath.getText().trim());
                } else {
                    outPath.setText(config.getOutPath());
                }
                config.dump();
            } else {
                //每次显示前重新初始化一次
                applyConfig();
            }
        });
    }


    /**
     * 从配置文件应用设置项
     */
    void applyConfig() {
        //读取配置设置默认值
        fontFace.getSelectionModel().select(config.getFontFace());
        fontSize.getSelectionModel().select(config.getFontSize());
        editorModeGroup.getToggles().forEach(item -> {
            if (Convert.toBool(item.getUserData()).equals(config.getEditMode())) {
                item.setSelected(true);
            }
        });
        themeGroup.getToggles().forEach(item -> {
            if (ObjectUtil.equal(config.getTheme(), Convert.toBool(item.getUserData()))) {
                item.setSelected(true);
            }
        });
        exitModeGroup.getToggles().forEach(item -> {
            if (Convert.toBool(item.getUserData()).equals(config.getExitMode())) {
                item.setSelected(true);
            }
        });
        outPath.setText(config.getOutPath());

    }

    void buildInfoFrame(ServiceInterface info) {
        infoPanel.getChildren().clear();
        JSONUtil.parseObj(StrUtil.isBlank(info.getAuth()) ?
                        info.getTemplate() :
                        info.getAuth())
                .forEach((k, v) -> {

                    HBox hBox = new HBox();
                    hBox.setMinHeight(90);
                    hBox.setAlignment(Pos.CENTER_LEFT);

                    Label label = new Label(k);
                    label.setMinSize(120, 90);
                    label.getStyleClass().add("item");
                    HBox.setMargin(label, new Insets(0, 0, 0, 30));
                    hBox.getChildren().add(label);

                    TextField textField = new TextField(ObjectUtil.isNotEmpty(v) ? v.toString() : StrUtil.EMPTY);
                    textField.getStyleClass().add("item");
                    textField.setUserData(k);
                    textField.setMinSize(140, 90);
                    hBox.getChildren().add(textField);
                    infoPanel.getChildren().add(hBox);
                });

        JFXButton save = new JFXButton("保存");
        save.setPrefSize(80, 30);
        save.getStyleClass().add("normal-button");
        save.setUserData(info);
        save.setOnAction(event -> {

            JSONObject param = new JSONObject();
            infoPanel.getChildren().forEach(e -> {
                if (e instanceof TextField) {
                    param.putOpt((String) e.getUserData(), ((TextField) e).getText());
                }
            });
            ServiceInterface data = (ServiceInterface) save.getUserData();
            data.setAuth(param.toString());
            try {
                if (interfaceService.updateById(info)) {
                    tips.setVisible(true);
                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("保存成功", "接口信息已经保存"));
                    return;
                }
            } catch (Exception e) {
                log.error("接口信息保存失败 => {}", JSONUtil.toJsonStr(info));
                log.error(ExceptionUtil.stacktraceToString(e));
            }
            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("保存失败", "数据操作失败,错误已记录"));
        });
        HBox hBox = new HBox();
        hBox.setMinHeight(90);
        hBox.setAlignment(Pos.CENTER_RIGHT);
        HBox.setMargin(save, new Insets(0, 30, 0, 0));
        hBox.getChildren().add(save);

        if (StrUtil.isNotEmpty(info.getPage())) {
            JFXButton applyFor = new JFXButton("去申请");
            applyFor.setPrefSize(80, 30);
            applyFor.getStyleClass().add("normal-button");
            applyFor.setTooltip(new Tooltip(info.getPage()));
            applyFor.setOnAction(event -> DesktopUtil.browse(info.getPage()));
            hBox.getChildren().add(applyFor);
        }
        infoPanel.getChildren().add(hBox);
    }

    @FXML
    private void onChooseOutPath(MouseEvent event) {
        File path = FileUtils.choosePath(outPath.getText().trim()).showDialog(Singleton.get(Stage.class));
        if (path != null && StrUtil.isNotEmpty(path.getPath())) {
            outPath.setText(path.getPath());
        }
        event.consume();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java
================================================
package org.fordes.subtitles.view.controller;

import javafx.fxml.FXML;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import lombok.Getter;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/4/8
 */
@Component
public class SidebarAfter {

    @FXML
    @Getter
    private ToggleButton mainEditor, syncEditor, export;

    @FXML
    @Getter
    private ToggleGroup itemGroup;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java
================================================
package org.fordes.subtitles.view.controller;

import javafx.fxml.FXML;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import lombok.Getter;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/1/27
 */
@Component
public class SidebarBefore {

    @FXML
    @Getter
    private ToggleButton quickStart, subtitleSearch, toolBox;

    @FXML
    @Getter
    private ToggleGroup itemGroup;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java
================================================
package org.fordes.subtitles.view.controller;

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/2/1
 */
@Slf4j
@Component
public class SidebarBottom {

    @FXML
    @Getter
    private Button setting;


}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java
================================================
package org.fordes.subtitles.view.controller;

import org.springframework.stereotype.Component;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Component
public class SpeechConversion {
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.swing.DesktopUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXNodesList;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.skins.JFXListViewSkin;
import com.sun.javafx.scene.control.VirtualScrollBar;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.constant.StyleClassConstant;
import org.fordes.subtitles.view.event.FileOpenEvent;
import org.fordes.subtitles.view.event.LoadingEvent;
import org.fordes.subtitles.view.event.ToastChooseEvent;
import org.fordes.subtitles.view.event.ToastConfirmEvent;
import org.fordes.subtitles.view.mapper.SearchCasesMapper;
import org.fordes.subtitles.view.model.PO.SearchCases;
import org.fordes.subtitles.view.model.search.Cases;
import org.fordes.subtitles.view.model.search.Result;
import org.fordes.subtitles.view.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/2/6
 */
@Slf4j
@Component
public class SubtitleSearch extends DelayInitController {

    @FXML
    private JFXListView<StackPane> listView;

    @FXML
    private JFXTextField searchField;

    @FXML
    private JFXNodesList nodesList;

    private ToggleGroup engineGroup;

    private static final SearchService SERVICE = new SearchService();

    private static final Dict SEARCH_KEY = Dict.create();

    static final String KEYWORD = "keyword";

    private final SearchCasesMapper casesMapper;

    @Autowired
    public SubtitleSearch(SearchCasesMapper casesMapper) {
        this.casesMapper = casesMapper;
    }

    @Override
    public void delay() {
        //选择默认接口
        if (engineGroup.getToggles().isEmpty()) {
            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("未找到搜索源", "字幕搜索无法使用!"));
            searchField.setDisable(true);
        } else {
            Toggle engine = CollUtil.getFirst(engineGroup.getToggles());
            engine.setSelected(true);
            searchField.setPromptText(StrUtil
                    .format("从{}搜索", ((SearchCases) engine.getUserData()).getName()));
        }
    }

    @Override
    public void async() {
        //读取字幕搜索接口
        engineGroup = new ToggleGroup();
        casesMapper.selectList(new QueryWrapper<>()).forEach(e -> {
            ToggleButton engine = new ToggleButton();
            engine.getStyleClass().addAll(StyleClassConstant.SUBTITLE_SEARCH_ENGINE,
                    StyleClassConstant.SUBTITLE_SEARCH_ENGINE_ITEM);
            engine.setToggleGroup(engineGroup);
            engine.setUserData(e);
            engine.setTooltip(new Tooltip(e.getName()));
            engine.setText(e.getIcon());
            engine.selectedProperty().addListener((observableValue, aBoolean, t1) -> {
                if (t1) {
                    SearchCases cases = (SearchCases) engine.getUserData();
                    searchField.setPromptText(StrUtil.format("从{}搜索", cases.getName()));
                    listView.getItems().clear();
                    SERVICE.cancel();
                    nodesList.animateList(false);
                }
            });
            nodesList.addAnimatedNode(engine);
        });

        //监听搜索服务运行状态,控制loading
        SERVICE.runningProperty().addListener((observableValue, aBoolean, t1)
                -> Singleton.get(Stage.class).fireEvent(new LoadingEvent(t1)));
        //搜索完成,载入新结果
        SERVICE.setOnSucceeded(event -> {
            Result val = SERVICE.getValue();
            if (ObjectUtil.isNotNull(val) && !val.getData().isEmpty()) {
                if (Result.Type.SEARCH.equals(val.getType())) {
                    listView.getItems().clear();
                }
                listView.setUserData(val.getPage());
                val.getData().forEach(result -> listView.getItems().add(buildItem(result)));
            }else {
                Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("暂无结果", "换一个资源试试吧~", "确定", () -> {}));
            }
        });
        //搜索出错
        SERVICE.setOnFailed(event -> Singleton.get(Stage.class).fireEvent(new ToastChooseEvent("搜索出错",
                "请等待后尝试重试\n或者前往项目主页反馈", "去反馈",
                () -> DesktopUtil.browse(CommonConstant.URL_ISSUES))));

        //为listview添加skin,反射获取垂直滚动条,监听滚动条判断分页
        JFXListViewSkin<StackPane> skin = new JFXListViewSkin<>(listView);
        listView.setSkin(skin);

        VirtualFlow<?> virtualFlow = (VirtualFlow<?>) ReflectUtil.getFieldValue(skin, "flow");
        VirtualScrollBar vbar = (VirtualScrollBar) ReflectUtil.getFieldValue(virtualFlow, "vbar");
        vbar.valueProperty().addListener((observableValue, number, t1) -> {
            if (t1.floatValue() == 1 && listView.getUserData() != null) {
                SERVICE.search(Result.Type.PAGE, (Cases) listView.getUserData(), SEARCH_KEY);
            }
        });
    }

    /**
     * 输入框监听,提交新的搜索
     * @param event source
     */
    @FXML
    private void searchBeginHandle(ActionEvent event) {
        JFXTextField field = (JFXTextField) event.getSource();
        if (StrUtil.isNotBlank(field.getText())) {
            SearchCases cases = (SearchCases) engineGroup.getSelectedToggle().getUserData();
            SEARCH_KEY.clear();
            SEARCH_KEY.set(KEYWORD, field.getText());
            SERVICE.search(Result.Type.SEARCH, cases.getCases(), SEARCH_KEY);
        }
    }


    private StackPane buildItem(Result.Item rsi) {
        StackPane root = new StackPane();
        root.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM);
        Label caption = new Label(rsi.caption);
        caption.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_CAPTION);
        Label text = new Label(rsi.text);
        text.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_TEXT);
        root.getChildren().addAll(caption, text);

        StackPane.setAlignment(caption, Pos.TOP_LEFT);
        StackPane.setMargin(caption, new Insets(5, 0, 0, 0));
        StackPane.setAlignment(text, Pos.BOTTOM_LEFT);
        StackPane.setMargin(caption, new Insets(0, 0, 5, 0));
        root.setUserData(rsi);
        root.setOnMouseClicked(e -> {
            if (MouseButton.PRIMARY.equals(e.getButton()) && 2 == e.getClickCount()) {
                StackPane item = (StackPane) e.getSource();
                Result.Item data = (Result.Item)item.getUserData();
                if (ObjectUtil.isNull(data.next)) {
                    if (StrUtil.isNotEmpty(data.text)) {
                        Singleton.get(Stage.class).fireEvent(new FileOpenEvent(data.text));
                    }
                }else {
                    SERVICE.search(Result.Type.SEARCH, data.next, data.params);
                }
            }
        });
        return root;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java
================================================
package org.fordes.subtitles.view.controller;

import org.springframework.stereotype.Component;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Component
public class SyncEditor {
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/TitleBar.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.lang.Singleton;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.enums.FontIcon;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/1/19
 */
@Slf4j
@Component
public class TitleBar {

    @FXML
    private Button closed, maximize, minimize;

    @FXML
    private VBox root;

    @FXML
    private Label title;

    @FXML
    private void closed(ActionEvent actionEvent) {
        //TODO
        Singleton.get(Stage.class).close();
        Platform.exit();
        System.exit(0);
    }

    @FXML
    private void maximize(ActionEvent actionEvent) {
        Stage stage = Singleton.get(Stage.class);
        stage.setFullScreen(!stage.isFullScreen());
        maximize.setText(stage.isFullScreen() ?
                FontIcon.EXIT_FULL_SCREEN.toString() : FontIcon.FULL_SCREEN.toString());
        actionEvent.consume();

    }

    @FXML
    private void minimize(ActionEvent actionEvent) {
        Singleton.get(Stage.class).setIconified(true);
        actionEvent.consume();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/Toast.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import org.fordes.subtitles.view.event.AbstractToastEvent;
import org.fordes.subtitles.view.handler.ToastEventHandler;
import org.fordes.subtitles.view.handler.ToastHandler;
import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/1/28
 */
@Component
public class Toast extends DelayInitController {

    @FXML
    private JFXButton _perform, _choose1, _choose2;

    @FXML
    private Label _caption, _text;

    @FXML
    private GridPane root;

    @Override
    public void async() {
        //选择型toast和确认型toast互斥
        _perform.visibleProperty().addListener((observableValue, aBoolean, t1) -> {
            _choose1.setVisible(!t1);
            _choose2.setVisible(!t1);
        });
        //为stage添加toast事件处理
        Singleton.get(Stage.class).addEventHandler(AbstractToastEvent.TOAST_EVENT_TYPE, new ToastEventHandler() {
            @Override
            public void onConfirmEvent(String caption, String text, String perform, ToastHandler handler) {
                _caption.setText(caption);
                _text.setText(text);
                if (StrUtil.isNotEmpty(perform)) {
                    _perform.setText(perform);
                }
                _perform.setOnAction(actionEvent -> {
                    handler.handle();
                    root.setVisible(false);
                });
                _perform.setVisible(true);
                root.setVisible(true);
            }

            @Override
            public void onChooseEvent(String caption, String text, String choose1, String choose2,
                                      ToastHandler handler1, ToastHandler handler2) {
                _caption.setText(caption);
                _text.setText(text);
                if (StrUtil.isNotEmpty(choose1)) {
                    _choose1.setText(choose1);
                }
                if (StrUtil.isNotEmpty(choose2)) {
                    _choose2.setText(choose2);
                }
                _choose1.setOnAction(event -> {
                    handler1.handle();
                    root.setVisible(false);
                });
                _choose2.setOnAction(event -> {
                    handler2.handle();
                    root.setVisible(false);
                });
                _perform.setVisible(false);
                root.setVisible(true);
            }
        });
    }


}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/ToolBox.java
================================================
package org.fordes.subtitles.view.controller;

import org.springframework.stereotype.Component;

/**
 * @author fordes on 2022/2/6
 */
@Component
public class ToolBox {
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java
================================================
package org.fordes.subtitles.view.controller;

import cn.hutool.core.lang.Singleton;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.event.FileOpenEvent;
import org.fordes.subtitles.view.model.DTO.Video;
import org.springframework.stereotype.Component;

/**
 * 语音转换 控制器
 *
 * @author fordes on 2022/4/8
 */
@Slf4j
@Component
public class VoiceConvert extends DelayInitController {

    private Video video;


    @Override
    public void delay() {

    }

    @Override
    public void async() {
        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {
            if (fileOpenEvent.getRecord().getFormat().media) {
                video = (Video) fileOpenEvent.getRecord();
                root.setVisible(true);
            }
        });
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java
================================================
package org.fordes.subtitles.view.enums;

/**
 * 编辑工具 事件类型枚举
 *
 * @author fordes on 2022/7/15
 */
public enum EditToolEventEnum {

    SEARCH, //搜索
    REPLACE,//替换
    JUMP,//跳转
    FONT, //字体(样式)
    TIMELINE, //时间轴
    CODE,//编码
    REF, //刷新
    TRANSLATE //翻译
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/FileEnum.java
================================================
package org.fordes.subtitles.view.enums;

import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;

import java.util.Arrays;

/**
 * 文件类型枚举
 *
 * @author fordes on 2022/2/9
 */
@AllArgsConstructor
public enum FileEnum {

    //视频
    MP4("mp4", true, true, false),
    MKV("mkv", true, true, false),
    AVI("avi", true, true, false),
    RMVB("rmvb", true, true, false),
    TS("ts", true, true, false),

    //音频
    MP3("mp3", true, false, false),
    FLAC("flac", true, false, false),
    AAC("aac", true, false, false),

    //字幕
    LRC("lrc", true, false, true),
    SRT("srt", true, false, true),
    ASS("ass", true, false, true);

    public final String suffix;

    public final boolean support;

    public final boolean media;

    public final boolean subtitle;

    public static final String[] SUPPORT_SUBTITLE = Arrays.stream(FileEnum.values())
            .filter(e -> e.subtitle && e.support).map(e -> e.suffix).toArray(String[]::new);

    public static final String[] SUPPORT_MEDIA = Arrays.stream(FileEnum.values())
            .filter(e -> e.media && e.support).map(e -> e.suffix).toArray(String[]::new);

    public static boolean isMedia(String suffix) {
        return Arrays.stream(FileEnum.values())
                .filter(e -> e.media)
                .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix));
    }

    public static boolean isSupport(String suffix) {
        return Arrays.stream(FileEnum.values())
                .filter(e -> e.support)
                .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix));
    }

    public static boolean check(String suffix, boolean isSupport, boolean isMedia, boolean isSubtitle) {
        FileEnum val = of(suffix);
        return val != null && (val.support == isSupport) && (val.media == isMedia) && (val.subtitle == isSubtitle);
    }

    public static FileEnum of(String name) {
        for (FileEnum value : FileEnum.values()) {
            if (StrUtil.equalsIgnoreCase(name, value.suffix)) {
                return value;
            }
        }
        return null;
    }
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/FontIcon.java
================================================
package org.fordes.subtitles.view.enums;

import lombok.AllArgsConstructor;

/**
 * 图标枚举
 *
 * @author fordes on 2022/1/23
 */
@AllArgsConstructor
public enum FontIcon {

    SCENE_CLOSE("\ue648"),
    SCENE_MINIMIZE("\ue634"),
    EXIT_FULL_SCREEN("\ue61f"),
    FULL_SCREEN("\ue628"),
    ITEM_START("\ue669"),
    ITEM_SEARCH("\uec6f"),
    ITEM_TOOL("\ue64a"),
    LOGO("\ue69f"),
    SETTING("\ue711"),
    CHOOSE_FILE("\ue64e"),

    ENGINE_DDZM("\ue63b"),
    ENGINE_ASSRT("\ue609"),
    ENGINE_ZMK("\ue623"),
    ENGINE("\ue60f"),

    PLACE_THE_LEFT("\uec70"),
    PLACE_THE_RIGHT("\ue61a"),

    SETTING_PREFERENCES("\ue63c"),
    SETTING_INTERFACE("\ue62d"),

    EDIT_BAR_SEARCH("\ue754"),
    EDIT_BAR_REPLACE("\ue674"),
    EDIT_BAR_JUMP("\ue695"),
    EDIT_BAR_FONT("\ue61d"),
    EDIT_BAR_HIDE("\ue60b"),
    EDIT_BAR_TIMELINE("\ue64f"),
    EDIT_BAR_CODE("\ue629"),

    EDIT_BAR_REF("\ue62c"),

    EDIT_BAR_OPTION("\ue86c"),

    EDIT_BAR_REPLACE_ITEM("\ue63e"),

    EDIT_BAR_REPLACE_ALL("\ue642"),

    SWITCH_OFF_LIGHT("\ue612"),
    SWITCH_ON_LIGHT("\ue611"),
    SWITCH_OFF_DARK("\ue613"),
    SWITCH_ON_DARK("\ue615"),

    EDIT_BAR_TRANSLATE("\ue6fb");

    private final String unicode;

    @Override
    public String toString() {
        return unicode;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java
================================================
package org.fordes.subtitles.view.enums;

import com.baomidou.mybatisplus.annotation.IEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ServiceProvider implements IEnum<String> {

    BAIDU("百度"),

    TENCENT("腾讯"),
    ALI("阿里"),

    HUOSHAN("火山");

    private final String desc;

    @Override
    public String toString() {
        return this.desc;
    }

    @Override
    public String getValue() {
        return this.name();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceType.java
================================================
package org.fordes.subtitles.view.enums;

import com.baomidou.mybatisplus.annotation.IEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 服务类型枚举
 *
 * @author fordes on 2022/4/17
 */
@Getter
@AllArgsConstructor
public enum ServiceType implements IEnum<String> {

    VOICE("语音转写"),

    TRANSLATE("翻译");

    private final String desc;

    @Override
    public String toString() {
        return this.getDesc();
    }

    public String getValue() {
        return this.name();
    }
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java
================================================
package org.fordes.subtitles.view.enums;

import lombok.Getter;

/**
 * 7zip结束码枚举
 *
 * @author fordes on 2021/1/7
 */
@Getter
public enum SevenZipEnum {

    NORMAL(0, "未发生错误"),
    WARNING(1,"警告,发生部分错误"),
    FATAL_ERROR(2, "致命错误"),
    COMMAND_ERROR(7, "命令错误"),
    OUT_OF_MEMORY_ERROR(8, "内存不足"),
    TERMINATION(255, "操作终止"),
    UNKNOWN_ERROR(-1, "未知错误");

    SevenZipEnum(int code, String status) {
        this.code = code;
        this.status = status;
    }

    private final int code;

    private final String status;

    public static String getStatus(int code){
        switch (code){
            case 0:
                return NORMAL.status;
            case 1:
                return WARNING.status;
            case 2:
                return FATAL_ERROR.status;
            case  7:
                return COMMAND_ERROR.status;
            case 8:
                return OUT_OF_MEMORY_ERROR.status;
            case 255:
                return TERMINATION.status;
            default:
                return UNKNOWN_ERROR.status;
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.Event;
import javafx.event.EventType;
import org.fordes.subtitles.view.handler.ToastEventHandler;

/**
 * @author fordes on 2022/2/2
 */
public abstract class AbstractToastEvent extends Event {

    public static final String CONFIRM = "确定";

    public static final String CANCEL = "取消";

    public static final EventType<AbstractToastEvent> TOAST_EVENT_TYPE = new EventType(ANY);

    public AbstractToastEvent(EventType<? extends Event> eventType) {
        super(eventType);
    }

    public abstract void invokeHandler(ToastEventHandler handler);
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.ToggleButton;
import lombok.Getter;
import lombok.NonNull;
import org.fordes.subtitles.view.enums.EditToolEventEnum;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fxmisc.richtext.StyleClassedTextArea;


/**
 * 编辑工具 事件
 *
 * @author fordes on 2022/7/15
 */
public class EditToolEvent extends Event {

    public static final EventType<EditToolEvent> EVENT_TYPE = new EventType<>(ANY, "editToolEvent");

    @Getter
    private final StyleClassedTextArea source;

    @Getter
    private final Subtitle subtitle;

    @Getter
    private final ToggleButton editMode;

    @Getter
    private final EditToolEventEnum type;

    public EditToolEvent(@NonNull StyleClassedTextArea source,
                         @NonNull Subtitle subtitle,
                         @NonNull ToggleButton editMode,
                         @NonNull EditToolEventEnum type) {
        super(EVENT_TYPE);
        this.source = source;
        this.subtitle = subtitle;
        this.editMode = editMode;
        this.type = type;
    }

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java
================================================
package org.fordes.subtitles.view.event;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Singleton;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.stage.Stage;
import lombok.Getter;
import org.fordes.subtitles.view.model.PO.FileRecord;
import org.fordes.subtitles.view.utils.FileUtils;

import java.io.File;
import java.io.IOException;

/**
 * @author fordes on 2022/4/8
 */
public class FileOpenEvent extends Event {

    public static final EventType<FileOpenEvent> FILE_OPEN_EVENT = new EventType(ANY, "fileOpenEvent");

    @Getter
    private FileRecord record;

    public FileOpenEvent(File openFile) {
        super(FILE_OPEN_EVENT);
        try {
            this.record = FileUtils.readFileInfo(openFile);
        }catch (IOException e) {
            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("出错了","打开文件失败!"));
        }
    }

    public FileOpenEvent(String filePath) {
        super(FILE_OPEN_EVENT);
        try {
            this.record = FileUtils.readFileInfo(FileUtil.file(filePath));
        }catch (IOException e) {
            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("出错了","打开文件失败!"));
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.Event;
import javafx.event.EventType;
import lombok.Getter;

/**
 * loading事件
 *
 * @author fordes on 2022/7/20
 */
public class LoadingEvent extends Event {

    public final static EventType<LoadingEvent> EVENT_TYPE = new EventType<>(ANY, "loadingEvent");

    @Getter
    private final boolean alive;

    public LoadingEvent(boolean alive) {
        super(EVENT_TYPE);
        this.alive = alive;
    }

    public LoadingEvent() {
        super(EVENT_TYPE);
        this.alive = false;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.Event;
import javafx.event.EventType;

/**
 * 主题切换事件
 *
 * @author fordes on 2022/4/13
 */
public class ThemeChangeEvent extends Event {

    public static final EventType<ThemeChangeEvent> EVENT_TYPE = new EventType(ANY, "themeChangeEvent");

    private Boolean dark;

    public Boolean isDark() {
        return dark;
    }

    public ThemeChangeEvent(Boolean dark) {
        super(EVENT_TYPE);
        this.dark = dark;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.EventType;
import org.fordes.subtitles.view.handler.ToastEventHandler;
import org.fordes.subtitles.view.handler.ToastHandler;

/**
 * toast选择事件
 *
 * @author fordes on 2022/2/2
 */
public class ToastChooseEvent extends AbstractToastEvent {

    public static final EventType<AbstractToastEvent> TOAST_CHOOSE_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, "toastChooseEvent");

    private final String caption;

    private final String text;

    private final String choose1;

    private final String choose2;

    private final ToastHandler handler1;

    private final ToastHandler handler2;

    public ToastChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2) {
        super(TOAST_CHOOSE_EVENT_TYPE);
        this.caption = caption;
        this.text = text;
        this.choose1 = choose1;
        this.choose2 = choose2;
        this.handler1 = handler1;
        this.handler2 = handler2;
    }

    public ToastChooseEvent(String caption, String text, String choose1, ToastHandler handler1) {
        super(TOAST_CHOOSE_EVENT_TYPE);
        this.caption = caption;
        this.text = text;
        this.choose1 = choose1;
        this.choose2 = AbstractToastEvent.CANCEL;
        this.handler1 = handler1;
        this.handler2 = () -> {};
    }

    @Override
    public void invokeHandler(ToastEventHandler handler) {
        handler.onChooseEvent(caption, text, choose1, choose2, handler1, handler2);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.EventType;
import org.fordes.subtitles.view.handler.ToastEventHandler;
import org.fordes.subtitles.view.handler.ToastHandler;

/**
 * @author fordes on 2022/2/2
 */
public class ToastConfirmEvent extends AbstractToastEvent {

    public static final EventType<AbstractToastEvent> TOAST_CONFIRM_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, "confirmToastEvent");

    private final String caption;

    private final String text;

    private final String perform;

    private final ToastHandler handler;

    public ToastConfirmEvent(String caption, String text, String perform, ToastHandler handler) {
        super(TOAST_CONFIRM_EVENT_TYPE);
        this.caption = caption;
        this.text = text;
        this.perform = perform;
        this.handler = handler;
    }

    public ToastConfirmEvent(String caption, String text) {
        super(TOAST_CONFIRM_EVENT_TYPE);
        this.caption = caption;
        this.text = text;
        this.perform = AbstractToastEvent.CONFIRM;
        this.handler = () -> {};
    }

    @Override
    public void invokeHandler(ToastEventHandler handler) {
        handler.onConfirmEvent(caption, text, perform, this.handler);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java
================================================
package org.fordes.subtitles.view.event;

import javafx.event.Event;
import javafx.event.EventType;
import lombok.Getter;

/**
 * 翻译服务事件
 *
 * @author fordes on 2022/8/1
 */
public class TranslateEvent extends Event {

    public static final EventType<TranslateEvent> EVENT_TYPE = new EventType<>(ANY, "translateEvent");

    public static final String SUCCESS = "翻译完成";

    public static final String FAIL = "翻译失败";

    public TranslateEvent(EventType<? extends Event> eventType) {
        super(eventType);
    }

    @Getter
    private String msg;

    @Getter
    private String detail;

    public TranslateEvent(String msg, String detail) {
        super(EVENT_TYPE);
        this.msg = msg;
        this.detail = detail;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java
================================================
package org.fordes.subtitles.view.factory;

import org.fordes.subtitles.view.service.translate.TranslateService;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author fordes on 2022/7/11
 */
public class TranslateServiceFactory {

    private static final Map<String, TranslateService> services = new ConcurrentHashMap<>();

    public static TranslateService getService(String provider) {
        return services.getOrDefault(provider, null);
    }

    public static void register(TranslateService service, String provider) {
        services.put(provider, service);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java
================================================
package org.fordes.subtitles.view.handler;

/**
 * @author fordes on 2022/7/27
 */
@FunctionalInterface
public interface CallBackHandler<T> {

    void handle(T value);
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java
================================================
package org.fordes.subtitles.view.handler;

import javafx.event.EventHandler;
import org.fordes.subtitles.view.event.EditToolEvent;

/**
 * 编辑工具 事件处理器
 *
 * @author fordes on 2022/7/15
 */
public abstract class EditToolEventHandler implements EventHandler<EditToolEvent> {
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java
================================================
package org.fordes.subtitles.view.handler;

import javafx.event.EventHandler;
import org.fordes.subtitles.view.event.FileOpenEvent;

/**
 * @author fordes on 2022/4/8
 */
public abstract class FileOpenEventHandler implements EventHandler<FileOpenEvent> {

    public final static String ERROR_MESSAGE = "文件打开失败";

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java
================================================
package org.fordes.subtitles.view.handler;

import javafx.event.EventHandler;
import org.fordes.subtitles.view.event.AbstractToastEvent;

/**
 * toast事件抽象
 *
 * @author fordes on 2022/2/2
 */
public abstract class ToastEventHandler implements EventHandler<AbstractToastEvent> {

    /**
     * 确认型 toast事件
     * @param caption   标题
     * @param text  内容
     * @param perform   确认按钮文本
     * @param handler   回调
     */
    public abstract void onConfirmEvent(String caption, String text, String perform, ToastHandler handler);

    /**
     * 选择型 toast事件
     * @param caption   标题
     * @param text  内容
     * @param choose1  选择1
     * @param choose2  选择2
     * @param handler1  选择1回调
     * @param handler2  选择2回调
     */
    public abstract void onChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2);

    @Override
    public void handle(AbstractToastEvent event) {
        event.invokeHandler(this);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java
================================================
package org.fordes.subtitles.view.handler;

/**
 * toast回调事件处理器接口
 *
 * @author fordes on 2022/2/2
 */
@FunctionalInterface
public interface ToastHandler {

    void handle();
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java
================================================
package org.fordes.subtitles.view.mapper;

import cn.hutool.core.lang.Dict;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;
import org.fordes.subtitles.view.model.PO.ServiceInterface;
import org.fordes.subtitles.view.model.PO.Version;

import java.util.List;

/**
 * @author fordes on 2022/4/17
 */
@Mapper
public interface InterfaceMapper extends BaseMapper<ServiceInterface> {


    List<AvailableServiceInfo> serviceInfo(@Param("type") String type);

    List<Version> getVersions(@Param("type") String serviceType, @Param("provider") String provider);

    List<Dict> getLanguageList();
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java
================================================
package org.fordes.subtitles.view.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.fordes.subtitles.view.model.PO.Language;

@Mapper
public interface LanguageMapper extends BaseMapper<Language> {

}

================================================
FILE: src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java
================================================
package org.fordes.subtitles.view.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.fordes.subtitles.view.model.PO.SearchCases;

/**
 * @author fordes on 2022/2/15
 */
@Mapper
public interface SearchCasesMapper extends BaseMapper<SearchCases> {
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java
================================================
package org.fordes.subtitles.view.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.fordes.subtitles.view.model.PO.Version;

/**
 * @author fordes on 2022/4/17
 */
@Mapper
public interface VersionMapper extends BaseMapper<Version> {
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java
================================================
package org.fordes.subtitles.view.model.DTO;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.fordes.subtitles.view.model.PO.ServiceInterface;
import org.fordes.subtitles.view.model.PO.Version;

/**
 * @author fordes on 2022/4/20
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AvailableServiceInfo extends ServiceInterface {

    private Version versionInfo;

    @Override
    public String toString() {
        return getProvider().getDesc() + getType().getDesc();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java
================================================
package org.fordes.subtitles.view.model.DTO;

/**
 * @author fordes on 2022/7/19
 */

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.fordes.subtitles.view.model.PO.FileRecord;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;

/**
 * @author fordes on 2021/6/30
 */
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class Subtitle extends FileRecord {

    private TimedTextFile timedTextFile;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java
================================================
package org.fordes.subtitles.view.model.DTO;

import lombok.Builder;
import lombok.Data;

/**
 * 翻译
 *
 * @author fordes on 2022/7/27
 */
@Data
@Builder
public class TranslateResult {

    private Integer serial;

    private boolean success;

    private String data;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Video.java
================================================
package org.fordes.subtitles.view.model.DTO;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.fordes.subtitles.view.model.PO.FileRecord;

/**
 * 视频类
 *
 * @author fordes on 2020/12/4
 */
@Data
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class Video extends FileRecord {

    /**
     * 帧宽
     */
    private int width;

    /**
     * 帧高
     */
    private int height;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java
================================================
package org.fordes.subtitles.view.model.PO;


import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.experimental.Accessors;
import org.fordes.subtitles.view.enums.FileEnum;

import java.io.File;
import java.util.Date;

/**
 * 文件抽象类
 *
 * @author fordes on 2020/12/4
 */
@Data
@Accessors(chain = true)
public class FileRecord {

    @JsonIgnore
    private File file;

    /**
     * 名称
     */
    private String file_name;

    /**
     * 格式
     */
    private FileEnum format;

    /**
     * 语言
     */
    private String language;

    /**
     * 文件路径
     */
    private String path;


    /**
     * 文件字节大小
     */
    private Long size_byte;

    /**
     * 文件大小
     */
    private String size;

    /**
     * 长度,字幕为时间轴起止,视频为时长
     */
    private Long duration;

    /**
     * 编码
     */
    private String charset;

    /**
     * 文件最后修改时间
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date file_modify_time;

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/Interface.java
================================================
package org.fordes.subtitles.view.model.PO;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName(value = "interface")
public class Interface implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField(value = "provider")
    private String provider;

    @TableField(value = "\"type\"")
    private String type;

    @TableField(value = "auth")
    private String auth;

    @TableField(value = "page")
    private String page;

    @TableField(value = "\"template\"")
    private String template;

    private static final long serialVersionUID = 1L;
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/Language.java
================================================
package org.fordes.subtitles.view.model.PO;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.List;

@Data
@Accessors(chain = true)
@NoArgsConstructor
@TableName(value = "\"language\"")
public class Language implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String name;

    @TableField(exist = false)
    private String code;

    @TableField(exist = false)
    private boolean general;

    @TableField(exist = false)
    private List<String> _target;

    @TableField(exist = false)
    private List<Language> target;

    @TableField("huoshan")
    private String huoshan;


    public String toString() {
        return this.name;
    }

    private static final long serialVersionUID = 1L;

    public static final String COL_ID = "id";

    public static final String COL_TYPE = "type";

    public static final String COL_NAME = "name";

    public static final String COL_GENERAL = "general";

    public static final String TARGET = "_target";
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java
================================================
package org.fordes.subtitles.view.model.PO;

import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.fordes.subtitles.view.model.search.Cases;

/**
 * @author fordes on 2022/2/15
 */
@Data
@Accessors(chain = true)
public class SearchCases {

    /**
     * 自增主键
     */
    private Integer id;

    /**
     * 名称
     */
    private String name;

    /**
     * 图标 {@link org.fordes.subtitles.view.enums.FontIcon}
     */
    private String icon;

    /**
     * 用例
     */
    private Cases cases;

    /**
     * 备注
     */
    private String remark;


    public void setCases(String cases) {
        this.cases = JSONUtil.toBean(cases, Cases.class);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java
================================================
package org.fordes.subtitles.view.model.PO;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.enums.ServiceType;

import java.io.Serializable;

/**
 * @author fordes on 2022/4/19
 */
@Data
@Accessors(chain = true)
@TableName(value = "interface")
public class ServiceInterface implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 服务提供商 {@link ServiceProvider}
     */

    @TableField(value = "provider")
    private ServiceProvider provider;

    /**
     * 服务类型 {@link ServiceType}
     */
    @TableField(value = "type")
    private ServiceType type;

    /**
     * 授权信息
     */
    @TableField(value = "auth")
    private String auth;

    /**
     * 授权信息模板
     */
    @TableField(value = "template")
    private String template;


    /**
     * 主页
     */
    @TableField(value = "page")
    private String page;

    private static final long serialVersionUID = 1L;
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/model/PO/Version.java
================================================
package org.fordes.subtitles.view.model.PO;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * @author fordes on 2022/4/17
 */
@Data
@Accessors(chain = true)
@TableName(value = "version")
public class Version implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField(value = "interface_id")
    private Integer interfaceId;

    @TableField(value = "\"name\"")
    private String name;

    @TableField(value = "concurrent")
    private Integer concurrent;

    @TableField(value = "carrying")
    private Integer carrying;

    @TableField(value = "server_url")
    private String serverUrl;

    @TableField(value = "remark")
    private String remark;

    public String toString() {
        return this.name;
    }

    private static final long serialVersionUID = 1L;
}

================================================
FILE: src/main/java/org/fordes/subtitles/view/model/search/Cases.java
================================================
package org.fordes.subtitles.view.model.search;

import cn.hutool.http.ContentType;
import lombok.Builder;

import java.io.Serializable;
import java.util.Map;

/**
 * @author fordes on 2022/3/28
 */
@Builder
public class Cases implements Serializable {

    public static final String CAPTION = "caption";

    public static final String TEXT = "text";

    public static final String PAGE = "page";

    public String[] keys;

    public Object url;

    public ContentType type;

    public Map<String, Selector> params;

    public Cases next;

    public void setType(String val) {
        this.type = ContentType.valueOf(val);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/search/Engine.java
================================================
package org.fordes.subtitles.view.model.search;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author fordes on 2022/2/12
 */
@Data
@Accessors(chain = true)
public class Engine {

    private String id;

    private String name;

    private String url;

    private Cases cases;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/search/Result.java
================================================
package org.fordes.subtitles.view.model.search;

import cn.hutool.core.map.MapUtil;
import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.Map;

/**
 * @author fordes on 2022/2/12
 */
@Data
@Builder
public class Result {

    private Type type;

    private Cases page;

    private List<Item> data;

    @Builder
    public static class Item {

        public Cases next;

        public String caption;

        public String text;

        public boolean isFile = false;

        public Map<String, Object> params = MapUtil.newHashMap();
    }

    public static enum Type {
        //普通搜索
        SEARCH(),
        //分页
        PAGE()
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/model/search/Selector.java
================================================
package org.fordes.subtitles.view.model.search;

import java.io.Serializable;

/**
 * 字段解析器
 * @author fordes on 2022/3/28
 */
public class Selector implements Serializable {

    /**
     * 唯一性标识
     * false:按条件提取结果集,true:按条件提取唯一结果
     */
    public boolean only = false;

    /**
     * 正则提取,提取匹配正则的内容
     * 高优先级
     */
    public String regular;

    /**
     * 内容格式化模板,参考{@see cn.hutool.core.util.StrUtil.format()}
     */
    public String format;

    /**
     * key选择 多层级使用"."连接 如:a.c.b
     */
    public String jsonKey;

    /**
     * css选择器 参考Jsoup css选择器
     * 高优先级
     */
    public String css;

    /**
     * 属性选择 提取指定属性,为空时使用text()
     */
    public String attr;

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/ConfigService.java
================================================
package org.fordes.subtitles.view.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.fordes.subtitles.view.config.ApplicationConfig;

/**
 * @author fordes on 2022/4/17
 */
public interface ConfigService extends IService<ApplicationConfig> {


}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java
================================================
package org.fordes.subtitles.view.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.AllArgsConstructor;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.enums.ServiceType;
import org.fordes.subtitles.view.mapper.InterfaceMapper;
import org.fordes.subtitles.view.mapper.VersionMapper;
import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;
import org.fordes.subtitles.view.model.PO.ServiceInterface;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.InterfaceService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 接口服务
 *
 * @author fordes on 2022/4/17
 */
@Service
@AllArgsConstructor
public class InterfaceServiceImpl extends ServiceImpl<InterfaceMapper, ServiceInterface> implements InterfaceService {

    private final InterfaceMapper interfaceMapper;

    private final VersionMapper versionMapper;

//    private final DictMapper dictMapper;


    @Override
    public List<Version> getVersions(ServiceType type, ServiceProvider provider) {
        return interfaceMapper.getVersions(type.name(), provider.name());
    }

    @Override
    public ServiceInterface getInterface(ServiceType type, ServiceProvider provider) {
        return interfaceMapper.selectOne(new LambdaQueryWrapper<ServiceInterface>()
                .eq(ServiceInterface::getType, type)
                .eq(ServiceInterface::getProvider, provider));
    }


    @Override
    public List<AvailableServiceInfo> getAvailableService(ServiceType type) {
        return interfaceMapper.serviceInfo(type.name());
    }
}



================================================
FILE: src/main/java/org/fordes/subtitles/view/service/InterfaceService.java
================================================
package org.fordes.subtitles.view.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.enums.ServiceType;
import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;
import org.fordes.subtitles.view.model.PO.ServiceInterface;
import org.fordes.subtitles.view.model.PO.Version;

import java.util.List;

/**
 * 接口服务
 *
 * @author fordes on 2022/4/17
 */
public interface InterfaceService extends IService<ServiceInterface> {


    /**
     * 获取指定接口的版本信息
     *
     * @param type     服务类型 {@link ServiceType}
     * @param provider 服务提供商 {@link ServiceProvider}
     * @return { @link Version}
     */
    List<Version> getVersions(ServiceType type, ServiceProvider provider);

    ServiceInterface getInterface(ServiceType type, ServiceProvider provider);


    /**
     * 获取可用的服务接口
     *
     * @param type 服务类型 {@link ServiceType}
     * @return {@link AvailableServiceInfo}
     */
    List<AvailableServiceInfo> getAvailableService(ServiceType type);
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/SearchService.java
================================================
package org.fordes.subtitles.view.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.model.search.Cases;
import org.fordes.subtitles.view.model.search.Result;
import org.fordes.subtitles.view.utils.ArchiveUtil;
import org.fordes.subtitles.view.utils.search.ParsingFactory;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * 在线字幕搜索服务
 *
 * @author fordes on 2022/2/15
 */
@Slf4j
public class SearchService extends Service<Result> {

    private Result.Type type;

    private Cases cases;

    private Map<String, Object> params = MapUtil.newHashMap();

    @Override
    protected Task<Result> createTask() {
        return new Task<>() {
            @Override
            protected Result call() {
                Result result = Result.builder()
                        .type(type)
                        .data(CollUtil.newArrayList()).build();
                try {
                    List<Object> paramList = new ArrayList<>(params.size());
                    if (null != cases.keys) {
                        for (String key : cases.keys) {
                            paramList.add(params.get(key));
                        }
                    }
                    String url = StrUtil.format((CharSequence) cases.url, paramList.toArray());
                    HttpResponse response = HttpUtil
                            .createGet(url, true)
                            .execute();
                    if (ObjectUtil.isEmpty(cases.next)) {
                        File outFile = response.completeFileNameFromHeader(FileUtil.mkdir(CommonConstant.DOWNLOAD_PATH));
                        FileUtil.writeFromStream(response.bodyStream(), outFile);
                        log.debug("下载文件成功!{}", outFile.getPath());
                        for (File l : ArchiveUtil.unArchiveToCurrentPath(outFile)) {
                            result.getData().add(Result.Item.builder()
                                    .caption(l.getName())
                                    .text(l.getPath())
                                    .isFile(true)
                                    .build());
                        }
                    } else {

                        ContentType contentType = ContentType.get(StrUtil.trimStart(response.body()));
                        if (contentType != null) {
                            //根据类型,创建解析器
                            ParsingFactory parsing = new ParsingFactory(response.body(), contentType);
                            //遍历解析,获取结果
                            Map<String, List<String>> displayMap = MapUtil.newHashMap();
                            Map<String, Object> otherMap = MapUtil.newHashMap();
                            cases.params.forEach((k, v) -> {
                                switch (k) {
                                    case Cases.CAPTION:
                                    case Cases.TEXT:
                                        displayMap.put(k, Convert.toList(String.class, parsing.getResult(v)));
                                        break;
                                    case Cases.PAGE:
                                        if (ObjectUtil.isNotEmpty(parsing.getResult(v))) {
                                            result.setPage(Cases.builder()
                                                    .keys(cases.keys)
                                                    .next(cases.next)
                                                    .type(cases.type)
                                                    .params(cases.params)
                                                    .url(parsing.getResult(v))
                                                    .build());
                                        }
                                        break;
                                    default:
                                        otherMap.put(k, parsing.getResult(v));
                                }
                            });
                            //拼装结果
                            List<String> captions = displayMap.get("caption");
                            List<String> texts = displayMap.get("text");
                            for (int i = 0; i < captions.size(); i++) {
                                result.getData().add(Result.Item.builder()
                                        .caption(CollUtil.get(captions, i))
                                        .text(CollUtil.get(texts, i))
                                        .params(MapUtil.newHashMap())
                                        .next(cases.next)
                                        .build());
                            }

                            otherMap.forEach((k, v) -> {
                                if (v instanceof Collection) {
                                    List<String> list = Convert.toList(String.class, v);
                                    for (int i = 0; i < result.getData().size(); i++) {
                                        result.getData().get(i).params.put(k, list.get(i));
                                    }
                                } else {
                                    for (Result.Item value : result.getData()) {
                                        value.params.put(k, v);
                                    }
                                }
                            });
                        }
                    }

                } catch (Exception e) {
                    log.error(ExceptionUtil.stacktraceToString(e));
                    throw new RuntimeException();
                }
                return result;
            }
        };
    }


    public void search(Result.Type type, Cases cases, Map<String, Object> params) {
        this.type = type;
        this.cases = cases;
        this.params = params;
        this.restart();
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java
================================================
package org.fordes.subtitles.view.service.translate;

import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.factory.TranslateServiceFactory;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.translate.thread.AliTranslateThread;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author fordes on 2022/7/25
 */
@Slf4j
@Service
public class AliTranslateService extends TranslateService implements InitializingBean {


    static final String APP_ID = "Accesskey ID";

    static final String APP_KEY = "AccessKey Secret";


    @Override
    public void afterPropertiesSet() {
        TranslateServiceFactory.register(this, ServiceProvider.ALI.name());
    }


    @Override
    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {
        String id = MapUtil.getStr(config, APP_ID);
        String secret = MapUtil.getStr(config, APP_KEY);
        return new AliTranslateThread(id, secret, serial, version.getServerUrl(), target, original, segment);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java
================================================
package org.fordes.subtitles.view.service.translate;

import cn.hutool.core.map.MapUtil;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.factory.TranslateServiceFactory;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.translate.thread.BaiduTranslateThread;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author fordes on 2022/7/11
 */
@Service
public class BaiduTranslateService extends TranslateService implements InitializingBean {

    static final String APP_ID = "APP_ID";

    static final String APP_KEY = "APP_KEY";

    @Override
    public void afterPropertiesSet() {
        TranslateServiceFactory.register(this, ServiceProvider.BAIDU.name());
    }

    @Override
    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {
        String app_id = MapUtil.getStr(config, APP_ID);
        String app_key = MapUtil.getStr(config, APP_KEY);
        return new BaiduTranslateThread(app_id, app_key, serial, version.getServerUrl(), target, original, segment);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java
================================================
package org.fordes.subtitles.view.service.translate;

import cn.hutool.core.map.MapUtil;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.factory.TranslateServiceFactory;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.translate.thread.HuoShanTranslateThread;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author fordes on 2022/7/31
 */
@Service
public class HuoShanTranslateService extends TranslateService implements InitializingBean {

    static final String AccessKeyID = "AccessKeyID";

    static final String SecretAccessKey = "SecretAccessKey";

    @Value("${service.translate.huoshan.region: cn-north-1}")
    private String region;

    @Value("${service.translate.huoshan.version-date: 2020-06-01}")
    private String versionDate;

    @Override
    public void afterPropertiesSet() {
        TranslateServiceFactory.register(this, ServiceProvider.HUOSHAN.name());
    }

    @Override
    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target,
                                                String original, Version version, Map<String, Object> config) {
        return new HuoShanTranslateThread(versionDate, region, MapUtil.getStr(config, AccessKeyID), MapUtil.getStr(config, SecretAccessKey),
                serial, version.getServerUrl(), target, original, segment);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java
================================================
package org.fordes.subtitles.view.service.translate;

import cn.hutool.core.map.MapUtil;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.factory.TranslateServiceFactory;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.service.translate.thread.TencentTranslateThread;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author fordes on 2022/7/11
 */
@Service
public class TencentTranslateService extends TranslateService implements InitializingBean {

    static final String SECRET_ID = "Secret Id";

    static final String SECRET_KEY = "Secret Key";


    @Value("${service.translate.tencent.region: ap-shanghai}")
    private String region;

    @Override
    public void afterPropertiesSet() {
        TranslateServiceFactory.register(this, ServiceProvider.TENCENT.name());
    }


    @Override
    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {
        String id = MapUtil.getStr(config, SECRET_ID);
        String key = MapUtil.getStr(config, SECRET_KEY);
        return new TencentTranslateThread(id ,key, region, serial, version.getServerUrl(), target, original, segment);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java
================================================
package org.fordes.subtitles.view.service.translate;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import javafx.application.Platform;
import javafx.stage.Stage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.event.LoadingEvent;
import org.fordes.subtitles.view.event.TranslateEvent;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.model.PO.Version;
import org.fordes.subtitles.view.utils.TranslateUtil;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

/**
 * @author fordes on 2022/7/29
 */
@Slf4j
@Service
public abstract class TranslateService {

    @Resource
    private ThreadPoolExecutor globalExecutor;

    @Async
    public void translate(Subtitle subtitle, String target, String original, Version version,
                          boolean mode, Map<String, Object> config) {
        TimeInterval interval = DateUtil.timer();
        Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));
        //根据接口限制,重设线程池
        int threadNum = Math.min(globalExecutor.getMaximumPoolSize(), version.getConcurrent() - 1);
        globalExecutor.setCorePoolSize(threadNum);
        globalExecutor.setMaximumPoolSize(threadNum);
        //根据接口限制对数据整合分段
        List<String> segmented = TranslateUtil.segmented(subtitle, version.getCarrying());

        //延迟队列
        DelayQueue<Segment> queue = new DelayQueue<>();
        for (int i = 0; i < segmented.size(); i++) {
            queue.put(new Segment(segmented.get(i), i, ((i + 1) % version.getCarrying()) - 1));
        }
        //添加任务, 提交至线程池
        Collection<Future<TranslateResult>> futures = CollUtil.newArrayList();
        try {
            while (!queue.isEmpty()) {
                Segment part = queue.take();
                Integer serial = part.getSerial();
                String segment = part.getData();

                Future<TranslateResult> task = globalExecutor
                        .submit(createTask(globalExecutor, serial, segment, target, original, version, config));
                futures.add(task);
            }

            //遍历获取结果
            for (Future<TranslateResult> e : futures) {
                TranslateResult item = e.get();
                if (item.isSuccess()) {
                    segmented.set(item.getSerial(), item.getData());
                } else {
                    throw new RuntimeException(item.getData());
                }
            }

            //合并结果
            TranslateUtil.reduction(subtitle, segmented, mode);
        } catch (Exception ex) {
            log.error(ExceptionUtil.stacktraceToString(ex));
//            ApplicationInfo.stage.fireEvent(new ToastConfirmEvent("翻译失败", ex.getMessage()));
            Platform.runLater(() ->
                    Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.FAIL, ex.getMessage())));
            return;
        } finally {
            log.debug("翻译线程结束,耗时:{} ms", interval.intervalMs());
        }
//        ApplicationInfo.stage.fireEvent(new ToastConfirmEvent("翻译完成", StrUtil.format("总耗时:{} ms", interval.intervalMs())));
//        Platform.runLater(() -> ApplicationInfo.stage.fireEvent(new LoadingEvent(false)));
        Platform.runLater(() -> Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.SUCCESS,
                StrUtil.format("总耗时:{} ms", interval.intervalMs()))));
    }

    /**
     * 创建翻译线程
     *
     * @param executor 线程池 {@link ThreadPoolExecutor}
     * @param serial   序号,用于再整合结果时维持内容顺序
     * @param segment  待翻译内容
     * @param target   目标语言
     * @param original 源语言
     * @param version  接口版本
     * @param config   接口配置
     * @return 线程
     */
    public abstract Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial,
                                                         String segment, String target, String original,
                                                         Version version, Map<String, Object> config);


    static class Segment implements Delayed {

        private final long executeTime;

        @Getter
        private final Integer serial;

        @Getter
        private final String data;

        public Segment(String data, Integer serial, long delay) {
            this.data = data;
            this.serial = serial;
            this.executeTime = System.nanoTime()+ TimeUnit.NANOSECONDS.convert(delay, TimeUnit.SECONDS);
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.executeTime - System.nanoTime(), TimeUnit.NANOSECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            Segment that = (Segment) o;
            return Long.compare(executeTime, that.executeTime);
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java
================================================
package org.fordes.subtitles.view.service.translate.thread;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.http.*;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.model.DTO.TranslateResult;

import java.net.URL;
import java.util.UUID;
import java.util.concurrent.Callable;

/**
 * @author fordes on 2022/7/26
 */
@Slf4j
public class AliTranslateThread extends TranslateThread  implements Callable<TranslateResult> {

    static final String CONTENT_MD5 = "Content-MD5";

    static final String CONTENT_TYPE = "application/json;chrset=utf-8";

    static final String X_ACS_SIGNATURE_NONCE = "x-acs-signature-nonce";

    static final String X_ACS_SIGNATURE_METHOD = "x-acs-signature-method";

    static final String X_ACS_VERSION = "x-acs-version";

    static final String HMAC_SHA1 = "HMAC-SHA1";

    static final String VERSION = "2019-01-02";

    private final String ak_id;

    private final String ak_secret;

    public AliTranslateThread(String ak_id, String ak_secret, Integer serial, String serviceURL,
                              String target, String original, String content) {
        super(serial, serviceURL, target, original, content);
        this.ak_id = ak_id;
        this.ak_secret = ak_secret;
    }


    @Override
    public TranslateResult call() {
        TimeInterval interval = DateUtil.timer();
        URL url = URLUtil.url(serviceURL);
        Dict param = Dict.of(
                "FormatType", "text",
                "SourceLanguage", original,
                "TargetLanguage", target,
                "SourceText", content,
                "Scene", "general"
        );
        String postBody = JSONUtil.parseObj(param).toString();
        String bodyMd5 = Base64.encode(MD5.create().digest(postBody));
        String uuid = UUID.randomUUID().toString();
        String date = DateTime.now().toString(DatePattern.HTTP_DATETIME_FORMAT);


        String stringToSign = Method.POST.name() + StrUtil.LF +
                ContentType.JSON.getValue() + StrUtil.LF +
                bodyMd5 + StrUtil.LF +
                CONTENT_TYPE + StrUtil.LF +
                date + StrUtil.LF +
                X_ACS_SIGNATURE_METHOD + StrUtil.COLON + HMAC_SHA1 + StrUtil.LF +
                X_ACS_SIGNATURE_NONCE + StrUtil.COLON + uuid + StrUtil.LF +
                X_ACS_VERSION + StrUtil.COLON + VERSION + StrUtil.LF +
                url.getFile();

        String signature = new HMac(HmacAlgorithm.HmacSHA1, ak_secret.getBytes()).digestBase64(stringToSign, false);
        String authHeader = "acs " + ak_id + ":" + signature;

        HttpResponse response = HttpUtil.createPost(serviceURL)
                .header(Header.ACCEPT, ContentType.JSON.getValue())
                .header(Header.CONTENT_TYPE, CONTENT_TYPE)
                .header(CONTENT_MD5, bodyMd5)
                .header(Header.DATE, date)
                .header(Header.HOST, url.getHost())
                .header(Header.AUTHORIZATION, authHeader)
                .header(X_ACS_SIGNATURE_NONCE, uuid)
                .header(X_ACS_SIGNATURE_METHOD, HMAC_SHA1)
                .header(X_ACS_VERSION, VERSION)
                .setFollowRedirects(true)
                .body(postBody)
                .execute();

        JSONObject resp = JSONUtil.parseObj(response.body());
        TranslateResult result = TranslateResult.builder().serial(serial).build();
        if (response.isOk() && resp.containsKey("Data")) {
            result.setSuccess(true);
            result.setData(resp.getJSONObject("Data").getStr("Translated"));
        } else {
            result.setSuccess(false);
            result.setData(resp.getStr("errorMsg"));
        }
        log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", interval.intervalMs());
//        log.debug(resp.toStringPretty());
        return result;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java
================================================
package org.fordes.subtitles.view.service.translate.thread;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.model.DTO.TranslateResult;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

/**
 * @author fordes on 2022/7/27
 */
@Slf4j
public class BaiduTranslateThread extends TranslateThread implements Callable<TranslateResult> {

    static final String SALT = "subview-proxy";

    private final String app_id;

    private final String app_key;

    public BaiduTranslateThread(String app_id, String app_key, Integer serial, String serviceURL,
                                String target, String original, String content) {
        super(serial, serviceURL, target, original, content);
        this.app_id = app_id;
        this.app_key = app_key;
    }


    @Override
    public TranslateResult call() {
        TimeInterval interval = DateUtil.timer();
        HttpResponse response = HttpUtil.createPost(serviceURL)
                .form("q", content)
                .form("from", original)
                .form("to", target)
                .form("appid", app_id)
                .form("salt", SALT)
                .form("sign", MD5.create().digestHex(app_id+ content + SALT + app_key))
                .contentType(ContentType.FORM_URLENCODED.getValue())
                .charset("UTF-8")
                .setFollowRedirects(true)
                .execute();
        JSONObject resp = JSONUtil.parseObj(response.body());
        TranslateResult result = TranslateResult.builder().serial(serial).build();
        if (response.isOk() && !resp.containsKey("error_code")) {
            result.setSuccess(true);
            List<JSONObject> dataList = resp.getJSONArray("trans_result").toList(JSONObject.class);
            result.setData(dataList.stream().map(e -> e.getStr("dst")).collect(Collectors.joining()));
        } else {
            result.setSuccess(false);
            result.setData(resp.getStr("error_msg"));
        }
        log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", interval.intervalMs());
//        log.debug(resp.toStringPretty());
        return result;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java
================================================
package org.fordes.subtitles.view.service.translate.thread;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.utils.TranslateUtil;

import java.net.URL;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

/**
 * @author fordes on 2022/7/31
 */
@Slf4j
public class HuoShanTranslateThread extends TranslateThread implements Callable<TranslateResult> {

    private final String accessKeyId;

    private final String secretAccessKey;

    private  final String versionDate;

    private final String region;

    static final String Action = "TranslateText";

    static final String Service = "translate";

    static final String Version = "1.0.16";

    static final String Algorithm = "HMAC-SHA256";


    public HuoShanTranslateThread(String versionDate, String region, String accessKeyId, String secretAccessKey,
                                  Integer serial, String serviceURL, String target, String original, String content) {
        super(serial, serviceURL, target, original, content);
        this.versionDate = versionDate;
        this.region = region;
        this.accessKeyId = accessKeyId;
        this.secretAccessKey = secretAccessKey;
    }


    @Override
    public TranslateResult call() throws Exception {
        TimeInterval interval = DateUtil.timer();
        //请求路径
        URL url = URLUtil.url(serviceURL);

        //请求体
        String body = new JSONObject()
                .putOnce("SourceLanguage", original) //原语言
                .putOnce("TargetLanguage", target) //目标语言
                .putOnce("TextList", CollUtil.newArrayList(content)).toString(); //待翻译文本列表,长度不大于128
        String bodyHash = SecureUtil.sha256(body);

        //时间 (必须使用UTC时间)
        DateTime now = DateTime.now();
        String nowDate = now.toString(FastDateFormat.getInstance(DatePattern.PURE_DATE_PATTERN, TimeZone.getTimeZone("UTC")));
        String nowTime = now.toString(FastDateFormat.getInstance(DatePattern.PURE_TIME_PATTERN, TimeZone.getTimeZone("UTC")));
        String requestDate = nowDate + "T" + nowTime + "Z";

        //构造需要计入签名的部分请求头
        Map<String, String> signHeadMap = MapUtil.newHashMap();
        signHeadMap.put(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue());
        signHeadMap.put(Header.HOST.getValue(), url.getHost());
        signHeadMap.put("X-Date", requestDate);
        signHeadMap.put("X-Content-Sha256", bodyHash);
        //按照ASCII也即字母序排序
        TreeMap<String, String> signHeadMapSort = MapUtil.sort(signHeadMap);

        // 正规化请求
        String requestMethod = "POST";
        String canonicalURI = "/";
        String canonicalQueryString = StrUtil.format("Action={}&Version={}", Action, versionDate);
        StringBuilder canonicalHeaders = new StringBuilder();
        signHeadMapSort.forEach((key, value) -> canonicalHeaders.append(key.trim().toLowerCase())
                .append(StrUtil.COLON).append(value.trim()).append(StrUtil.LF));
        String SignedHeaders = CollUtil.join(signHeadMapSort.keySet(), ";").trim().toLowerCase();
        String canonicalRequest = StrUtil.concat(false, requestMethod, StrUtil.LF, canonicalURI, StrUtil.LF,
                canonicalQueryString, StrUtil.LF, canonicalHeaders, StrUtil.LF, SignedHeaders, StrUtil.LF, bodyHash);

        // 签名
        String CredentialScope = StrUtil.concat(false, nowDate, StrUtil.SLASH, region,
                StrUtil.SLASH, Service, "/request");
        String StringToSign = StrUtil.concat(false, Algorithm, StrUtil.LF, requestDate, StrUtil.LF,
                CredentialScope, StrUtil.LF, SecureUtil.sha256(canonicalRequest));

        //计算签名密钥
        byte[] kDate = TranslateUtil.hmac256(secretAccessKey, nowDate);
        byte[] kRegion = TranslateUtil.hmac256(kDate, region);
        byte[] kService = TranslateUtil.hmac256(kRegion, Service);
        byte[] kSigning = TranslateUtil.hmac256(kService, "request");

        //计算签名
        String Signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(kSigning, StringToSign));
        //拼接出授权头
        String Authorization = StrUtil.format("{} Credential={}/{}, SignedHeaders={}, Signature={}",
                Algorithm, accessKeyId, CredentialScope, SignedHeaders, Signature);

        //创建真实请求
        HttpResponse response = HttpUtil.createPost(serviceURL)
                .header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
                .header(Header.ACCEPT, ContentType.JSON.getValue())
                .header(Header.HOST, url.getHost())
                .header(Header.USER_AGENT, "volc-sdk-java/v" + Version)
                .header("X-Date", requestDate)
                .header("X-Content-Sha256", bodyHash)
                .header(Header.AUTHORIZATION, Authorization)
                .setFollowRedirects(true)
                .body(body)
                .execute();
        //解析结果
        JSONObject resp = JSONUtil.parseObj(response.body());
        TranslateResult result = TranslateResult.builder().serial(serial).build();
        if (response.isOk() && resp.containsKey("TranslationList")) {
            result.setSuccess(true);
            result.setData(resp.getJSONArray("TranslationList").stream()
                    .map(e -> JSONUtil.parseObj(e).getStr("Translation"))
                    .collect(Collectors.joining(StrUtil.LF)));
        } else {
            result.setSuccess(false);
            result.setData(resp.getJSONObject("ResponseMetadata")
                    .getJSONObject("Error").getStr("Message"));
        }
        log.debug("序号:{} 请求 {},耗时:{} ms", serial, result.isSuccess() ? "成功" : "失败", interval.intervalMs());
//        log.debug(resp.toStringPretty());
        return result;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java
================================================
package org.fordes.subtitles.view.service.translate.thread;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.model.DTO.TranslateResult;
import org.fordes.subtitles.view.utils.TranslateUtil;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.TimeZone;
import java.util.concurrent.Callable;

/**
 * @author fordes on 2022/7/29
 */
@Slf4j
public class TencentTranslateThread extends TranslateThread implements Callable<TranslateResult> {


    static final String CT_JSON = "application/json; charset=utf-8";

    static final String SERVICE = "tmt";

    static final String ACTION = "TextTranslate";

    static final String VERSION = "2018-03-21";

    static final String ALGORITHM = "TC3-HMAC-SHA256";

    private final String secretId;

    private final String secretKey;

    private final String region;

    public TencentTranslateThread(String secretId, String secretKey, String region, Integer serial, String serviceURL,
                                  String target, String original, String content) {
        super(serial, serviceURL, target, original, content);
        this.region = region;
        this.secretId = secretId;
        this.secretKey = secretKey;
    }

    @Override
    public TranslateResult call() throws Exception {
        TimeInterval interval = DateUtil.timer();
        URL url = URLUtil.url(serviceURL);

        //时间
        long now = DateUtil.currentSeconds();
        String timestamp = String.valueOf(now);
        String date = DateTime.of(now * 1000)
                .toString(FastDateFormat.getInstance(DatePattern.NORM_DATE_PATTERN, TimeZone.getTimeZone("UTC")));

        //拼接规范请求串
        String httpRequestMethod = "POST";
        String canonicalUri = "/";
        String canonicalQueryString = "";
        String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + url.getHost() + "\n";
        String signedHeaders = "content-type;host";

        //整合参数
        Dict param = Dict.of(
                "SourceText", content,
                "Source", original,
                "Target", target,
                "ProjectId", 0);
        String payload = JSONUtil.toJsonStr(param);
        String hashedRequestPayload = TranslateUtil.sha256Hex(payload);
        String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
                + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;

        //拼接待签名字符串
        String credentialScope = date + "/" + SERVICE + "/" + "tc3_request";
        String hashedCanonicalRequest = TranslateUtil.sha256Hex(canonicalRequest);
        String stringToSign = ALGORITHM + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;

        //计算签名
        byte[] secretDate = TranslateUtil.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
        byte[] secretService = TranslateUtil.hmac256(secretDate, SERVICE);
        byte[] secretSigning = TranslateUtil.hmac256(secretService, "tc3_request");
        String signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(secretSigning, stringToSign)).toLowerCase();

        //拼接 Authorization
        String authorization = ALGORITHM + " " + "Credential=" + secretId + "/" + credentialScope + ", "
                + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;

        HttpResponse response = HttpUtil.createPost(serviceURL)
                .header("Authorization", authorization)
                .header("Content-Type", CT_JSON)
                .header("Host", url.getHost())
                .header("X-TC-Action", ACTION)
                .header("X-TC-Timestamp", timestamp)
                .header("X-TC-Version", VERSION)
                .header("X-TC-Region", region)
                .setFollowRedirects(true)
                .body(payload)
                .execute();
        JSONObject resp = JSONUtil.parseObj(response.body());
        TranslateResult result = TranslateResult.builder().serial(serial)
                .success(false).data("翻译失败!").build();
        if (response.isOk() && resp.containsKey("Response")) {
            JSONObject respJson = resp.getJSONObject("Response");
            if (respJson.containsKey("TargetText")) {
                result.setSuccess(true);
                result.setData(respJson.getStr("TargetText"));
            }else {
                result.setSuccess(false);
                result.setData(respJson.getJSONObject("Error").getStr("Message"));
            }
        }
        long intervalTime = interval.intervalMs();
        log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", intervalTime);
//        log.debug(resp.toStringPretty());
        return result;
    }

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java
================================================
package org.fordes.subtitles.view.service.translate.thread;

import lombok.AllArgsConstructor;

/**
 * 翻译线程抽象
 *
 * @author fordes on 2022/7/27
 */
@AllArgsConstructor
public abstract class TranslateThread {

    /**
     * 序号,将随结果返回,用于还原顺序
     */
    public Integer serial;

    /**
     * 服务地址,即api调用地址
     */
    public String serviceURL;

    /**
     * 目标语言 通常为代码
     */
    public String target;

    /**
     * 原语言 通常为代码
     */
    public String original;

    /**
     * 待翻译内容
     */
    public String content;
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java
================================================
package org.fordes.subtitles.view.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
import org.fordes.subtitles.view.enums.FileEnum;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;

/**
 * 文件解压工具类
 *
 * @author fordes on 2022/4/4
 */
@Slf4j
public class ArchiveUtil {

    /**
     * 解压文件至当前路径下uuid命名路径 并删除原文件
     * 将排除不受支持的文件
     *
     * @param file 压缩文件
     * @return 文件路径
     */
    public static Collection<File> unArchiveToCurrentPath(File file) {
        Collection<File> result = Collections.emptyList();
        if (FileUtil.exist(file)) {
            String outPath = StrUtil.concat(false, file.getParent(), File.separator, UUID.fastUUID().toString());
            //创建目标文件夹
            if (!FileUtil.exist(outPath)) {
                FileUtil.mkdir(outPath);
            }
            String suffix = FileUtil.getSuffix(file);
            if (StrUtil.equalsAnyIgnoreCase(suffix, FileEnum.SUPPORT_SUBTITLE)) {
                File newFile = FileUtil.file(StrUtil.concat(false, outPath, File.separator, URLUtil.decode(file.getName(), Charset.defaultCharset())));
                FileUtil.move(file, newFile, true);
                result = CollUtil.newArrayList(newFile);
            } else {
                result = unArchiveFile(file, outPath, FileEnum.SUPPORT_SUBTITLE);
            }
            FileUtil.del(file);
        }
        return result;
    }


    /**
     * 解压文件,不保留内部结构
     *
     * @param in      压缩文件路径
     * @param outPath 输出路径
     * @param filter  指定需要提取的文件后缀 如 ass
     */
    public static Collection<File> unArchiveFile(File in, String outPath, String... filter) {
        Collection<File> result = CollUtil.newArrayList();
        TimeInterval interval = DateUtil.timer();

        RandomAccessFile randomAccessFile = null;
        IInArchive inArchive = null;
        try {
            randomAccessFile = new RandomAccessFile(in.getPath(), "r");
            inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));
            ISimpleInArchive archive = inArchive.getSimpleInterface();
            for (ISimpleInArchiveItem item : archive.getArchiveItems()) {
                if (!item.isFolder() && StrUtil.equalsAnyIgnoreCase(FileUtil.getSuffix(item.getPath()), filter)) {
                    File file = FileUtil.file(StrUtil.concat(false, outPath, File.separator, item.getPath()));
                    ExtractOperationResult operationResult = item.extractSlow(data -> {
                        FileUtil.writeBytes(data, file);
                        return data.length;
                    });
                    if (operationResult == ExtractOperationResult.OK) {
                        result.add(file);
                        log.debug("提取成功 => {}", item.getPath());
                    } else {
                        log.error("提取失败 => {}\n{}", item.getPath(), operationResult);
                    }
                }
            }
        } catch (FileNotFoundException | SevenZipException e) {
            log.error("解压文件出错!{} => {}", in.getPath(), outPath);
            log.error(ExceptionUtil.stacktraceToString(e));
        } finally {
            if (inArchive != null) {
                try {
                    inArchive.close();
                } catch (SevenZipException e) {
                    log.error(ExceptionUtil.stacktraceToString(e));
                }
            }
            if (randomAccessFile != null) {
                try {
                    randomAccessFile.close();
                } catch (IOException e) {
                    log.error(ExceptionUtil.stacktraceToString(e));
                }
            }
            if (result.isEmpty()) {
                FileUtil.del(outPath);
            }

        }
        log.debug("解压文件:{} => {},耗时:{} ms", in.getPath(), outPath, interval.intervalMs());
        return result;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java
================================================
package org.fordes.subtitles.view.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.fordes.subtitles.view.enums.ServiceProvider;
import org.fordes.subtitles.view.enums.ServiceType;
import org.fordes.subtitles.view.mapper.InterfaceMapper;
import org.fordes.subtitles.view.model.PO.Language;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 缓存
 *
 * @author fordes on 2022/7/27
 */
public class CacheUtil {

    public static final Map<ServiceType, Map<ServiceProvider, List<Language>>> languageMap = MapUtil.newHashMap();

    /**
     * 初始化语言字典
     *
     * @param data 数据
     */
    public static void initLanguageDict(List<Dict> data) {
        data.stream().collect(Collectors.groupingBy(e -> ServiceType.valueOf(e.getStr(Language.COL_TYPE))))
                .forEach((k, v) -> {
                    Map<ServiceProvider, List<Language>> providerMap = MapUtil.newHashMap();
                    Arrays.stream(ServiceProvider.values()).forEach(p -> {
                        Map<String, List<Language>> idMap = v.stream()
                                .filter(q -> q.containsKey(p.name().toLowerCase()))
                                .map(q -> {
                                    String target = MapUtil.getStr(q, p.name().toLowerCase() + Language.TARGET);
                                    return new Language()
                                            .setId(q.getInt(Language.COL_ID))
                                            .setName(q.getStr(Language.COL_NAME))
                                            .setCode(q.getStr(p.name().toLowerCase()))
                                            .setGeneral(q.getBool(Language.COL_GENERAL))
                                            .set_target(StrUtil.split(target, StrUtil.COMMA, true, true));
                                })
                                .collect(Collectors.groupingBy(Language::getCode, Collectors.toList()));

                        List<Language> languageList = CollUtil.newArrayList();
                        idMap.forEach((x, y) -> {
                            Language item = y.get(0);
                            if (item.get_target().isEmpty()) {
                                item.setTarget(idMap.values().stream().map(e -> e.get(0)).collect(Collectors.toList()));
                            } else {
                                item.setTarget(item.get_target().stream()
                                        .map(e -> idMap.get(e).get(0))
                                        .collect(Collectors.toList()));
                            }
                            languageList.add(item);
                        });
                        providerMap.put(p, languageList);
                    });
                    languageMap.put(k, providerMap);
                });
    }

    /**
     * 获取语言字典
     *
     * @param type     {@link ServiceType}
     * @param provider {@link ServiceProvider}
     * @param general  是否只获取常用语言
     * @return {@link List<Language>}
     */
    public static List<Language> getLanguageDict(ServiceType type, ServiceProvider provider, boolean general) {
        if (languageMap.isEmpty()) {
            CacheUtil.initLanguageDict(SpringUtil.getBean(InterfaceMapper.class).getLanguageList());
        }
        List<Language> result = languageMap.get(type).get(provider);
        return general ?
                result.stream().filter(Language::isGeneral).collect(Collectors.toList()) : result;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/FileUtils.java
================================================
package org.fordes.subtitles.view.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.constant.CommonConstant;
import org.fordes.subtitles.view.enums.FileEnum;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.model.DTO.Video;
import org.fordes.subtitles.view.model.PO.FileRecord;
import org.fordes.subtitles.view.utils.submerge.utils.EncodeUtils;
import org.springframework.lang.NonNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;

import static cn.hutool.core.thread.ThreadUtil.sleep;

/**
 * 文件工具类
 *
 * @author fordes on 2022/1/23
 */
@Slf4j
public class FileUtils {

    /**
     * 根据路径获取文件流,支持http和resource
     * @param path
     * @return
     */
    public static InputStream getStream(@NonNull String path) {
        if (ReUtil.isMatch("^http[s]?://.*", path)) {
            HttpResponse response = HttpUtil.createGet(path, true).execute();
            if (response.isOk()) {
                return response.bodyStream();
            }
        }else {
            ClassPathResource resource = new ClassPathResource(path);
            return resource.getStream();
        }

        throw new RuntimeException(StrUtil.format("resource: {} not found", path));
    }

    /**
     * 选择文件
     * @param title 选择框标题内容
     * @param items 选项
     * @return 返回指定文件选择器
     */
    public static FileChooser chooseFile(String title, FileEnum... items) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle(title);
        fileChooser.setInitialDirectory(new File(CommonConstant.PATH_HOME));
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("全部文件", "*.*"));
        if (ArrayUtil.isNotEmpty(items)) {
            fileChooser.getExtensionFilters().addAll(Arrays.stream(items)
                    .filter(e -> e.support)
                    .map(e -> new FileChooser.ExtensionFilter(e.suffix, CommonConstant.PREFIX + e.suffix))
                    .collect(Collectors.toList()));
        }
        return fileChooser;
    }


    /**
     * 选择路径
     * @return 文件夹选择器
     */
    public static DirectoryChooser choosePath(String path) {
        DirectoryChooser directoryChooser = new DirectoryChooser();
        directoryChooser.setTitle(CommonConstant.TITLE_PATH);
        directoryChooser.setInitialDirectory(FileUtil.file(StrUtil.isNotEmpty(path)? path: CommonConstant.PATH_HOME));
        return directoryChooser;
    }

    /**
     * 读取文件信息
     * @param file 文件
     * @return 文件信息实例
     */
    public static <T> FileRecord readFileInfo(File file) throws IOException {
        String suffix = FileUtil.extName(file);
        FileRecord info;
        FileEnum type = FileEnum.of(FileUtil.getSuffix(file));

        assert type != null;
        if (type.media) {
            info =  new Video().setFormat(type);
        }else {
            info = new Subtitle().setCharset(EncodeUtils.guessEncoding(file)).setFormat(type);
        }

        return info.setFile(file)
                .setFile_name(file.getName())
                .setPath(file.getPath())
                .setSize(FileUtil.readableFileSize(file))
                .setFile_modify_time(FileUtil.lastModifiedTime(file));
    }

    /**
     * 加锁将集合按行写入文件
     *
     * @param file    目标文件
     * @param content 内容集合
     */
    public static void write(File file, Collection<String> content, String charset) {
        write(file,CollUtil.join(content, StrUtil.CRLF), charset);
    }

    public static void write(File file, String content, String charset) {
        if (StrUtil.isNotEmpty(content)) {
            try (RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
                 FileChannel channel = accessFile.getChannel()) {
                //加锁写入文件,如获取不到锁则休眠
                FileLock fileLock = null;
                while (true) {
                    try {
                        fileLock = channel.tryLock();
                        break;
                    } catch (Exception e) {
                        sleep(1000);
                    }
                }
                accessFile.seek(accessFile.length());
                accessFile.write(content.getBytes(charset));
                accessFile.write(StrUtil.CRLF.getBytes(charset));
            } catch (IOException ioException) {
                log.error("写入文件出错,{} => {}", file.getPath(), ioException.getMessage());
                throw new RuntimeException("写入文件出错");
            }
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java
================================================
package org.fordes.subtitles.view.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import javafx.scene.control.IndexRange;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.enums.FileEnum;
import org.fordes.subtitles.view.handler.CallBackHandler;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.utils.submerge.parser.ParserFactory;
import org.fordes.subtitles.view.utils.submerge.parser.SubtitleParser;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;
import org.fxmisc.richtext.StyleClassedTextArea;

import java.time.LocalTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author fordes on 2022/7/19
 */
@Slf4j
public class SubtitleUtil {


    /**
     * 纯文本搜索 使用 {@link SearchCache} 单例作为缓存
     *
     * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean)
     */
    public static void search(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) {
        search(Singleton.get(SearchCache.class), area, target, isIgnoreCase, isRegular);
    }

    /**
     * 文本替换 前置 搜索 使用 {@link ReplaceCache} 单例作为缓存
     *
     * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean) (ReplaceCache, StyleClassedTextArea, String, String, boolean, boolean)
     */
    public static void find(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) {
        search(Singleton.get(ReplaceCache.class), area, target, isIgnoreCase, isRegular);
    }

    /**
     * 简易文本搜索
     *
     * @param area         被搜索文本
     * @param target       目标关键字
     * @param isIgnoreCase 忽略大小写
     * @param isRegular    正则搜索
     */
    public static <T extends SearchCache> void search(T cache, StyleClassedTextArea area, String target,
                                                      boolean isIgnoreCase, boolean isRegular) {
        int cursor;
        String text;
        if (StrUtil.equals(cache.getTarget(), target)) {
            cursor = cache.getAnchor() + 1;
            text = area.getText(cursor, area.getLength());
        } else {
            cache.reset();
            cursor = 0;
            text = area.getText();
        }
        int start = 0, end = 0;
        for (String line : text.split(StrUtil.LF)) {
            if (isRegular) {
                Matcher matcher = Pattern.compile(target).matcher(line);
                if (matcher.find()) {
                    start = cursor + line.indexOf(matcher.group(0));
                    end = cursor + line.indexOf(matcher.group(0)) + matcher.group(0).length();
                    break;
                }
            } else {
                int pos = StrUtil.indexOf(line, target, 0, isIgnoreCase);
                if (pos >= 0) {
                    start = cursor + pos;
                    end = cursor + pos + target.length();
                    break;
                }
            }
            cursor += line.length() + 1;
        }
        if (start != 0 && end != 0) {
            area.moveTo(end);
            area.requestFollowCaret();
            area.selectRange(start, end);
            cache.setAnchor(start);
            cache.setTarget(target);
            if (cache instanceof ReplaceCache) {
                ((ReplaceCache) cache).setCaretPosition(end);
            }
        } else cache.reset();
    }


    /**
     * 文本替换
     *
     * @param area         被处理文本区
     * @param subtitle     对应字幕文件
     * @param searchStr    被替换内容
     * @param replaceStr   替换内容
     * @param isAll        是否替换全部
     * @param isIgnoreCase 是否忽略大小写
     * @param isRegular    (searchStr)是否为正则表达式
     */
    public static void replace(StyleClassedTextArea area, Subtitle subtitle, String searchStr, String replaceStr,
                               boolean isAll, boolean isIgnoreCase, boolean isRegular) throws Exception {
        if (isAll) {
            String text = area.getText();
            if (isRegular) {
                Matcher matcher = Pattern.compile(searchStr).matcher(text);
                if (matcher.find()) {
                    text = matcher.replaceAll(replaceStr);
                }
            } else {
                text = isIgnoreCase ?
                        StrUtil.replaceIgnoreCase(text, searchStr, replaceStr) :
                        StrUtil.replace(text, searchStr, replaceStr);
            }
            area.clear();
            area.append(text, StrUtil.EMPTY);
        } else {
            ReplaceCache cache = Singleton.get(ReplaceCache.class);
            if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) {
                area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY);
            } else {
                search(cache, area, searchStr, isIgnoreCase, isRegular);
                if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) {
                    area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY);
                }else return;
            }
        }

        TimedTextFile timedTextFile = SubtitleUtil.parse(area.getText(), subtitle.getFormat());
        subtitle.setTimedTextFile(timedTextFile);
    }

    /**
     * 时间轴位移
     *
     * @param timedTextFile 字幕
     * @param begin         开始时间
     * @param range         位移范围
     * @param mode          显示模式
     * @return 时间轴位移后的字幕
     */
    public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, IndexRange range, boolean mode) {
        LocalTime start = CollUtil.getFirst(timedTextFile.getTimedLines()).getTime().getStart();
        long poor = begin.toNanoOfDay() - start.toNanoOfDay();
        if (range != null) {
            long sort = 0;
            for (TimedLine item : timedTextFile.getTimedLines()) {
                sort += toStr(item, mode).length();
                if (sort > range.getEnd()) {
                    break;
                } else if (sort >= range.getStart()) {
                    item.getTime().setStart(LocalTime.ofNanoOfDay(item.getTime().getStart().toNanoOfDay() + poor));
                    item.getTime().setEnd(LocalTime.ofNanoOfDay(item.getTime().getEnd().toNanoOfDay() + poor));
                }
            }
        } else {
            for (TimedLine item : timedTextFile.getTimedLines()) {
                revise(item.getTime(), poor);
            }
        }
        return timedTextFile;
    }

    /**
     * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean)
     */
    private static void revise(TimedObject timedLine, long poor) {
        timedLine.setStart(LocalTime.ofNanoOfDay(timedLine.getStart().toNanoOfDay() + poor));
        timedLine.setEnd(LocalTime.ofNanoOfDay(timedLine.getEnd().toNanoOfDay() + poor));
    }

    /**
     * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean)
     */
    public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, boolean mode) {
        return revise(timedTextFile, begin, null, mode);
    }

    /**
     * 从文件解析字幕
     *
     * @param subtitle 字幕文件
     * @throws Exception 异常
     */
    public static void parse(Subtitle subtitle) throws Exception {
        TimeInterval timer = DateUtil.timer();
        SubtitleParser parser = ParserFactory.getParser(subtitle.getFormat().suffix);
        TimedTextFile content = parser.parse(subtitle.getFile(), subtitle.getCharset());
        log.debug("解析字幕耗时:{} ms", timer.interval());
        subtitle.setTimedTextFile(content);
    }

    /**
     * 从文本字幕解析字幕
     *
     * @param str  字幕文本
     * @param type 字幕格式
     * @return 字幕结构
     * @throws Exception 异常
     */
    public static TimedTextFile parse(String str, FileEnum type) throws Exception {
        return ParserFactory.getParser(type.suffix).parse(str, StrUtil.EMPTY);
    }

    /**
     * 字幕结构转换为字符串
     *
     * @param mode 解析模式 f-简洁模式 t-完整模式
     * @return 字符串
     */
    public static String toStr(TimedTextFile subtitle, boolean mode) {
        if (!mode) {
            StringBuilder content = new StringBuilder();
            subtitle.getTimedLines().forEach(item
                    -> content.append(CollUtil.join(item.getTextLines(), StrUtil.CRLF)).append(StrUtil.CRLF));
            return content.toString();
        } else {
            return subtitle.toString();
        }
    }

    /**
     * 字幕结构转换为字符串
     *
     * @param mode 解析模式 f-简洁模式 t-完整模式
     * @return 字符串
     */
    public static String toStr(TimedLine timedLine, boolean mode) {
        return mode ? timedLine.toString() : CollUtil.join(timedLine.getTextLines(), StrUtil.CRLF);
    }

    /**
     * 写入字幕结构到源文件
     *
     * @param subtitle 字幕
     * @param handler  回调
     */
    public static void write(Subtitle subtitle, CallBackHandler<Boolean> handler) {
        try {
            FileUtils.write(subtitle.getFile(), subtitle.getTimedTextFile().toString(), subtitle.getCharset());
        } catch (RuntimeException e) {
            handler.handle(false);
        }
        handler.handle(true);
    }

    /**
     * 搜索操作 上一步结果缓存
     */
    @Data
    static class SearchCache {

        private String target;
        private int anchor;

        public SearchCache() {
            reset();
        }

        public void reset() {
            this.target = StrUtil.EMPTY;
            this.anchor = 0;
        }

    }

    /**
     * 替换操作 上一步结果缓存
     */
    @Data
    @EqualsAndHashCode(callSuper = false)
    static class ReplaceCache extends SearchCache {
        private int caretPosition;

        @Override
        public void reset() {
            this.caretPosition = 0;
            super.reset();
        }
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java
================================================
package org.fordes.subtitles.view.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.fordes.subtitles.view.model.DTO.Subtitle;
import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.StringReader;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;

/**
 * 翻译工具
 *
 * @author fordes on 2022/7/26
 */
@Slf4j
public class TranslateUtil {

    private final static String QUESTION_MARK = "?";

    public final static String SEPARATIST = "><";

    /**
     * 字符串切分,按照指定的分隔符切分字符串为长度不超过{@code maxLength}的集合
     * @param content 内容
     * @param maxLength 最大长度
     * @return 切分后的集合
     */
    public static List<String> segmented(String content, int maxLength) {
        List<String> result = CollUtil.newArrayList();
        try (StringReader reader = StrUtil.getReader(content)) {
            StringBuilder builder = StrUtil.builder();
            StringBuilder temp = StrUtil.builder();
            CharBuffer buffer = CharBuffer.allocate(1);
            while (-1 != reader.read(buffer)) {
                CharSequence s = buffer.flip();
                temp.append(s);
                if (StrUtil.equalsAny(s, StrUtil.COMMA, StrUtil.DOT, StrUtil.LF, QUESTION_MARK)) {
                    if (builder.length() + temp.length() >= maxLength) {
                        result.add(builder.toString());
                        builder.setLength(0);
                    }
                    builder.append(temp);
                    temp.setLength(0);
                }
            }
            result.add(builder.toString());
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
        return result;
    }


    /**
     * @see #segmented(String, int)
     * 重载方法 多行文本使用\n分割
     */
    public static List<String> segmented(List<String> content, int maxLength) {
        return segmented(CollUtil.join(content, StrUtil.LF), maxLength);
    }

    /**
     * @see #segmented(String, int)
     * 重载方法 每段字幕使用 {@link #SEPARATIST} 分割
     */
    public static List<String> segmented(Subtitle subtitle, int maxLength) {
        List<String> data = CollUtil.newArrayList();
        subtitle.getTimedTextFile().getTimedLines().forEach(e
                -> data.add(CollUtil.join(e.getTextLines(), SEPARATIST)));
        return segmented(data, maxLength);
    }

    /**
     * 将经过 {@link #segmented(Subtitle, int)} 切分后的结构还原至字幕文件中
     * @param subtitle  字幕
     * @param data  数据
     * @param mode  模式 f-覆盖模式,t-追加模式
     */
    public static void reduction(Subtitle subtitle, List<String> data, boolean mode) {
        StringBuilder builder = StrUtil.builder();
        data.forEach(builder::append);

        List<String> lines = StrUtil.split(builder.toString(), StrUtil.LF);
        for (int part = 0; part < lines.size(); part++) {
            TimedLine line = CollUtil.get(subtitle.getTimedTextFile().getTimedLines(), part);
            List<String> second =  StrUtil.split(lines.get(part), SEPARATIST);
            if (mode) {
                List<String> temp = new ArrayList<>(line.getTextLines().size());
                List<String> first = line.getTextLines();
                for (int i = 0; i < first.size(); i++) {
                    temp.add(CollUtil.get(first, i));
                    temp.add(CollUtil.get(second, i));
                }
                line.setTextLines(temp);
            }else {
                line.setTextLines(second);
            }
        }
    }

    public static byte[] hmac256(byte[] key, String msg) throws Exception {
        return hmac256(key, msg.getBytes(StandardCharsets.UTF_8));
    }

    public static byte[] hmac256(byte[] key, byte[] msg) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
        mac.init(secretKeySpec);
        return mac.doFinal(msg);
    }

    public static byte[] hmac256(String key, String msg) throws Exception {
        return hmac256(key.getBytes(StandardCharsets.UTF_8), msg.getBytes(StandardCharsets.UTF_8));
    }

    public static String sha256Hex(String s) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] d = md.digest(s.getBytes(StandardCharsets.UTF_8));
        return HexUtil.encodeHexStr(d).toLowerCase();
    }

}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java
================================================
package org.fordes.subtitles.view.utils.search;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import org.fordes.subtitles.view.model.search.Selector;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * HTML解析器
 *
 * @author fordes on 2022/3/29
 */
public class HTMLParsing extends Parsing {

    private Document doc;

    public HTMLParsing(Object data) {
        super(data);
        this.doc = Jsoup.parse((String) data);
    }

    @Override
    public Object parsing(Selector selector) {
        List<String> fields = getFields(doc, selector);
        return selector.only ? CollUtil.getFirst(fields): fields;
    }

    private static List<String> getFields(Document doc, Selector selector) {
        if (ObjectUtil.isNotEmpty(selector)) {
            return doc.select(selector.css).stream()
                    .map(e -> getField(e, selector.attr, selector.regular, selector.format))
                    .collect(Collectors.toList());
        }else {
            return Collections.emptyList();
        }
    }


    private static String getField(Element element, String attr, String regular, String format) {
        String attrField = StrUtil.isBlank(attr) ?
                element.text() : element.attr(attr);
        String regField = StrUtil.isBlank(regular) ?
                StrUtil.trim(attrField) : CollUtil.join(ReUtil.findAll(regular, attrField, 1), StrUtil.EMPTY);
        return StrUtil.isBlank(format) ? regField : StrUtil.format(format, regField);
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java
================================================
package org.fordes.subtitles.view.utils.search;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.fordes.subtitles.view.model.search.Selector;

import java.util.List;

/**
 * JSON解析器
 *
 * @author fordes on 2022/3/29
 */
public class JSONParsing extends Parsing {

    private JSONObject json;

    public JSONParsing(Object data) {
        super(data);
        this.json = JSONUtil.parseObj(data);
    }

    //jsonKey > regular > foramt
    //TODO 未测试
    @Override
    public Object parsing(Selector selector) {
        List<String> keys = StrUtil.split(selector.jsonKey, StrUtil.C_DOT);
        for (int i = 0; i < keys.size(); i++) {
            if (i == keys.size()-1) {
                return json.get(keys.get(i));
            }else {
                json = json.getJSONObject(keys.get(i));
            }
        }
        return json;
    }
}


================================================
FILE: src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java
================================================
package org.fordes.subtitles.view.utils.search;

import org.fordes.subtitles.view.model.search.Selector;

/**
 * 解析器
Download .txt
gitextract__0rcvlyu/

├── .github/
│   └── workflows/
│       └── subtitles-view.yml
├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src/
    └── main/
        ├── java/
        │   └── org/
        │       └── fordes/
        │           └── subtitles/
        │               └── view/
        │                   ├── SubtitlesViewApplication.java
        │                   ├── config/
        │                   │   ├── ApplicationConfig.java
        │                   │   └── ExecutorConfig.java
        │                   ├── constant/
        │                   │   ├── CommonConstant.java
        │                   │   └── StyleClassConstant.java
        │                   ├── controller/
        │                   │   ├── DelayInitController.java
        │                   │   ├── EditTool.java
        │                   │   ├── Export.java
        │                   │   ├── MainController.java
        │                   │   ├── MainEditor.java
        │                   │   ├── QuickStart.java
        │                   │   ├── Setting.java
        │                   │   ├── SidebarAfter.java
        │                   │   ├── SidebarBefore.java
        │                   │   ├── SidebarBottom.java
        │                   │   ├── SpeechConversion.java
        │                   │   ├── SubtitleSearch.java
        │                   │   ├── SyncEditor.java
        │                   │   ├── TitleBar.java
        │                   │   ├── Toast.java
        │                   │   ├── ToolBox.java
        │                   │   └── VoiceConvert.java
        │                   ├── enums/
        │                   │   ├── EditToolEventEnum.java
        │                   │   ├── FileEnum.java
        │                   │   ├── FontIcon.java
        │                   │   ├── ServiceProvider.java
        │                   │   ├── ServiceType.java
        │                   │   └── SevenZipEnum.java
        │                   ├── event/
        │                   │   ├── AbstractToastEvent.java
        │                   │   ├── EditToolEvent.java
        │                   │   ├── FileOpenEvent.java
        │                   │   ├── LoadingEvent.java
        │                   │   ├── ThemeChangeEvent.java
        │                   │   ├── ToastChooseEvent.java
        │                   │   ├── ToastConfirmEvent.java
        │                   │   └── TranslateEvent.java
        │                   ├── factory/
        │                   │   └── TranslateServiceFactory.java
        │                   ├── handler/
        │                   │   ├── CallBackHandler.java
        │                   │   ├── EditToolEventHandler.java
        │                   │   ├── FileOpenEventHandler.java
        │                   │   ├── ToastEventHandler.java
        │                   │   └── ToastHandler.java
        │                   ├── mapper/
        │                   │   ├── InterfaceMapper.java
        │                   │   ├── LanguageMapper.java
        │                   │   ├── SearchCasesMapper.java
        │                   │   └── VersionMapper.java
        │                   ├── model/
        │                   │   ├── DTO/
        │                   │   │   ├── AvailableServiceInfo.java
        │                   │   │   ├── Subtitle.java
        │                   │   │   ├── TranslateResult.java
        │                   │   │   └── Video.java
        │                   │   ├── PO/
        │                   │   │   ├── FileRecord.java
        │                   │   │   ├── Interface.java
        │                   │   │   ├── Language.java
        │                   │   │   ├── SearchCases.java
        │                   │   │   ├── ServiceInterface.java
        │                   │   │   └── Version.java
        │                   │   └── search/
        │                   │       ├── Cases.java
        │                   │       ├── Engine.java
        │                   │       ├── Result.java
        │                   │       └── Selector.java
        │                   ├── service/
        │                   │   ├── ConfigService.java
        │                   │   ├── Impl/
        │                   │   │   └── InterfaceServiceImpl.java
        │                   │   ├── InterfaceService.java
        │                   │   ├── SearchService.java
        │                   │   └── translate/
        │                   │       ├── AliTranslateService.java
        │                   │       ├── BaiduTranslateService.java
        │                   │       ├── HuoShanTranslateService.java
        │                   │       ├── TencentTranslateService.java
        │                   │       ├── TranslateService.java
        │                   │       └── thread/
        │                   │           ├── AliTranslateThread.java
        │                   │           ├── BaiduTranslateThread.java
        │                   │           ├── HuoShanTranslateThread.java
        │                   │           ├── TencentTranslateThread.java
        │                   │           └── TranslateThread.java
        │                   └── utils/
        │                       ├── ArchiveUtil.java
        │                       ├── CacheUtil.java
        │                       ├── FileUtils.java
        │                       ├── SubtitleUtil.java
        │                       ├── TranslateUtil.java
        │                       ├── search/
        │                       │   ├── HTMLParsing.java
        │                       │   ├── JSONParsing.java
        │                       │   ├── Parsing.java
        │                       │   └── ParsingFactory.java
        │                       └── submerge/
        │                           ├── SubmergeAPI.java
        │                           ├── TimedLinesAPI.java
        │                           ├── constant/
        │                           │   └── FontName.java
        │                           ├── parser/
        │                           │   ├── ASSParser.java
        │                           │   ├── BaseParser.java
        │                           │   ├── LRCParser.java
        │                           │   ├── ParserFactory.java
        │                           │   ├── SRTParser.java
        │                           │   ├── SubtitleParser.java
        │                           │   └── exception/
        │                           │       ├── InvalidAssSubException.java
        │                           │       ├── InvalidColorCode.java
        │                           │       ├── InvalidFileException.java
        │                           │       ├── InvalidSRTSubException.java
        │                           │       └── InvalidSubException.java
        │                           ├── subtitle/
        │                           │   ├── ass/
        │                           │   │   ├── ASSSub.java
        │                           │   │   ├── ASSTime.java
        │                           │   │   ├── Events.java
        │                           │   │   ├── ScriptInfo.java
        │                           │   │   └── V4Style.java
        │                           │   ├── common/
        │                           │   │   ├── SubtitleLine.java
        │                           │   │   ├── SubtitleTime.java
        │                           │   │   ├── TimedLine.java
        │                           │   │   ├── TimedObject.java
        │                           │   │   └── TimedTextFile.java
        │                           │   ├── config/
        │                           │   │   ├── Font.java
        │                           │   │   └── SimpleSubConfig.java
        │                           │   ├── lrc/
        │                           │   │   ├── LRCLine.java
        │                           │   │   ├── LRCSub.java
        │                           │   │   └── LRCTime.java
        │                           │   └── srt/
        │                           │       ├── SRTLine.java
        │                           │       ├── SRTSub.java
        │                           │       └── SRTTime.java
        │                           └── utils/
        │                               ├── ColorUtils.java
        │                               ├── ConvertUtils.java
        │                               └── EncodeUtils.java
        └── resources/
            ├── application.yml
            ├── banner.txt
            ├── css/
            │   ├── edit-tool.css
            │   ├── font.css
            │   ├── main-editor.css
            │   ├── quick-start.css
            │   ├── setting.css
            │   ├── speech-conversion.css
            │   ├── styles.css
            │   ├── subtitle-search.css
            │   ├── title-bar.css
            │   ├── toast.css
            │   └── tool-box.css
            ├── font/
            │   └── buttersans-Rounded.otf
            ├── fxml/
            │   ├── edit-tool.fxml
            │   ├── export.fxml
            │   ├── main-editor.fxml
            │   ├── main-view.fxml
            │   ├── quick-start.fxml
            │   ├── setting.fxml
            │   ├── sidebar-after.fxml
            │   ├── sidebar-before.fxml
            │   ├── sidebar-bottom.fxml
            │   ├── speech-conversion.fxml
            │   ├── subtitle-search.fxml
            │   ├── sync-editor.fxml
            │   ├── title-bar.fxml
            │   ├── toast.fxml
            │   ├── tool-box.fxml
            │   └── voice-convert.fxml
            ├── logback/
            │   └── logback-spring.xml
            └── mapper/
                └── InterfaceMapper.xml
Download .txt
SYMBOL INDEX (466 symbols across 118 files)

FILE: src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java
  class SubtitlesViewApplication (line 28) | @Slf4j
    method setApplicationName (line 42) | @Value("${spring.application.name}")
    method main (line 47) | public static void main(String[] args) {
    method handleEvent (line 51) | @Override
    method loadFXMLBefore (line 57) | @Override
    method initAfter (line 64) | @Override
    method registerOsThemeDetector (line 77) | @Override
    method switchTheme (line 94) | private void switchTheme(OsThemeDetector detector, Parent root, JFXApp...

FILE: src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java
  class ApplicationConfig (line 21) | @Data
    method dump (line 71) | public void dump() {

FILE: src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java
  class ExecutorConfig (line 12) | @Configuration
    method globalExecutor (line 17) | @Bean("globalExecutor")

FILE: src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java
  class CommonConstant (line 6) | public class CommonConstant {

FILE: src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java
  class StyleClassConstant (line 6) | public class StyleClassConstant {

FILE: src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java
  class DelayInitController (line 19) | @Component
    method initialize (line 30) | @Override
    method getScene (line 41) | public Scene getScene() {
    method delay (line 48) | public void delay() {}
    method async (line 53) | public void async() {}

FILE: src/main/java/org/fordes/subtitles/view/controller/EditTool.java
  class EditTool (line 66) | @Slf4j
    method EditTool (line 110) | @Autowired
    method delay (line 118) | @Override
    method async (line 179) | @Override
    method onClose (line 283) | @FXML
    method applyCode (line 292) | @FXML
    method applyFont (line 309) | @FXML
    method applyTimeline (line 332) | @FXML
    method applyJump (line 375) | @FXML
    method applySearch (line 390) | @FXML
    method applyReplaceNext (line 399) | @FXML
    method applyReplaceAll (line 405) | @FXML
    method applyReplaceFind (line 411) | @FXML
    method applyReplace (line 420) | private void applyReplace(boolean isAll) {
    method applyTranslate (line 437) | @FXML
    type TimelineType (line 457) | @AllArgsConstructor
      method toString (line 470) | @Override

FILE: src/main/java/org/fordes/subtitles/view/controller/Export.java
  class Export (line 10) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/MainController.java
  class MainController (line 24) | @Slf4j
    method delay (line 58) | @Override
    method async (line 68) | @Override
    method mousePressedHandle (line 123) | @FXML
    method mouseMoveHandle (line 130) | @FXML
    method mouseDraggedHandle (line 157) | @FXML
    method titleBarDraggedHandle (line 185) | @FXML
    method onDrawer (line 193) | @FXML

FILE: src/main/java/org/fordes/subtitles/view/controller/MainEditor.java
  class MainEditor (line 37) | @Slf4j
    method MainEditor (line 63) | @Autowired
    method delay (line 68) | @Override
    method async (line 129) | @Override
    method hideToolbar (line 163) | @FXML
    method ctrlToolbar (line 174) | private void ctrlToolbar(boolean state) {
    method ctrlToolbar (line 181) | private void ctrlToolbar() {
    method ctrlEditMode (line 185) | private void ctrlEditMode(Boolean mode) {
    method changeEditMode (line 198) | @FXML
    method onIndicatorClicked (line 203) | @FXML

FILE: src/main/java/org/fordes/subtitles/view/controller/QuickStart.java
  class QuickStart (line 26) | @Slf4j
    method chooseFile (line 45) | @FXML
    method onDragOver (line 60) | @FXML
    method onDragExited (line 78) | @FXML
    method onDragDropped (line 85) | @FXML

FILE: src/main/java/org/fordes/subtitles/view/controller/Setting.java
  class Setting (line 48) | @Slf4j
    method Setting (line 83) | @Autowired
    method delay (line 89) | @Override
    method async (line 149) | @Override
    method applyConfig (line 171) | void applyConfig() {
    method buildInfoFrame (line 194) | void buildInfoFrame(ServiceInterface info) {
    method onChooseOutPath (line 262) | @FXML

FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java
  class SidebarAfter (line 12) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java
  class SidebarBefore (line 12) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java
  class SidebarBottom (line 12) | @Slf4j

FILE: src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java
  class SpeechConversion (line 10) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java
  class SubtitleSearch (line 43) | @Slf4j
    method SubtitleSearch (line 66) | @Autowired
    method delay (line 71) | @Override
    method async (line 85) | @Override
    method searchBeginHandle (line 147) | @FXML
    method buildItem (line 159) | private StackPane buildItem(Result.Item rsi) {

FILE: src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java
  class SyncEditor (line 10) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/TitleBar.java
  class TitleBar (line 18) | @Slf4j
    method closed (line 31) | @FXML
    method maximize (line 39) | @FXML
    method minimize (line 49) | @FXML

FILE: src/main/java/org/fordes/subtitles/view/controller/Toast.java
  class Toast (line 18) | @Component
    method async (line 30) | @Override

FILE: src/main/java/org/fordes/subtitles/view/controller/ToolBox.java
  class ToolBox (line 8) | @Component

FILE: src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java
  class VoiceConvert (line 15) | @Slf4j
    method delay (line 22) | @Override
    method async (line 27) | @Override

FILE: src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java
  type EditToolEventEnum (line 8) | public enum EditToolEventEnum {

FILE: src/main/java/org/fordes/subtitles/view/enums/FileEnum.java
  type FileEnum (line 13) | @AllArgsConstructor
    method isMedia (line 47) | public static boolean isMedia(String suffix) {
    method isSupport (line 53) | public static boolean isSupport(String suffix) {
    method check (line 59) | public static boolean check(String suffix, boolean isSupport, boolean ...
    method of (line 64) | public static FileEnum of(String name) {

FILE: src/main/java/org/fordes/subtitles/view/enums/FontIcon.java
  type FontIcon (line 10) | @AllArgsConstructor
    method toString (line 60) | @Override

FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java
  type ServiceProvider (line 7) | @Getter
    method toString (line 20) | @Override
    method getValue (line 25) | @Override

FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceType.java
  type ServiceType (line 12) | @Getter
    method toString (line 22) | @Override
    method getValue (line 27) | public String getValue() {

FILE: src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java
  type SevenZipEnum (line 10) | @Getter
    method SevenZipEnum (line 21) | SevenZipEnum(int code, String status) {
    method getStatus (line 30) | public static String getStatus(int code){

FILE: src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java
  class AbstractToastEvent (line 10) | public abstract class AbstractToastEvent extends Event {
    method AbstractToastEvent (line 18) | public AbstractToastEvent(EventType<? extends Event> eventType) {
    method invokeHandler (line 22) | public abstract void invokeHandler(ToastEventHandler handler);

FILE: src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java
  class EditToolEvent (line 18) | public class EditToolEvent extends Event {
    method EditToolEvent (line 34) | public EditToolEvent(@NonNull StyleClassedTextArea source,

FILE: src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java
  class FileOpenEvent (line 18) | public class FileOpenEvent extends Event {
    method FileOpenEvent (line 25) | public FileOpenEvent(File openFile) {
    method FileOpenEvent (line 34) | public FileOpenEvent(String filePath) {

FILE: src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java
  class LoadingEvent (line 12) | public class LoadingEvent extends Event {
    method LoadingEvent (line 19) | public LoadingEvent(boolean alive) {
    method LoadingEvent (line 24) | public LoadingEvent() {

FILE: src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java
  class ThemeChangeEvent (line 11) | public class ThemeChangeEvent extends Event {
    method isDark (line 17) | public Boolean isDark() {
    method ThemeChangeEvent (line 21) | public ThemeChangeEvent(Boolean dark) {

FILE: src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java
  class ToastChooseEvent (line 12) | public class ToastChooseEvent extends AbstractToastEvent {
    method ToastChooseEvent (line 28) | public ToastChooseEvent(String caption, String text, String choose1, S...
    method ToastChooseEvent (line 38) | public ToastChooseEvent(String caption, String text, String choose1, T...
    method invokeHandler (line 48) | @Override

FILE: src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java
  class ToastConfirmEvent (line 10) | public class ToastConfirmEvent extends AbstractToastEvent {
    method ToastConfirmEvent (line 22) | public ToastConfirmEvent(String caption, String text, String perform, ...
    method ToastConfirmEvent (line 30) | public ToastConfirmEvent(String caption, String text) {
    method invokeHandler (line 38) | @Override

FILE: src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java
  class TranslateEvent (line 12) | public class TranslateEvent extends Event {
    method TranslateEvent (line 20) | public TranslateEvent(EventType<? extends Event> eventType) {
    method TranslateEvent (line 30) | public TranslateEvent(String msg, String detail) {

FILE: src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java
  class TranslateServiceFactory (line 11) | public class TranslateServiceFactory {
    method getService (line 15) | public static TranslateService getService(String provider) {
    method register (line 19) | public static void register(TranslateService service, String provider) {

FILE: src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java
  type CallBackHandler (line 6) | @FunctionalInterface
    method handle (line 9) | void handle(T value);

FILE: src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java
  class EditToolEventHandler (line 11) | public abstract class EditToolEventHandler implements EventHandler<EditT...

FILE: src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java
  class FileOpenEventHandler (line 9) | public abstract class FileOpenEventHandler implements EventHandler<FileO...

FILE: src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java
  class ToastEventHandler (line 11) | public abstract class ToastEventHandler implements EventHandler<Abstract...
    method onConfirmEvent (line 20) | public abstract void onConfirmEvent(String caption, String text, Strin...
    method onChooseEvent (line 31) | public abstract void onChooseEvent(String caption, String text, String...
    method handle (line 33) | @Override

FILE: src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java
  type ToastHandler (line 8) | @FunctionalInterface
    method handle (line 11) | void handle();

FILE: src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java
  type InterfaceMapper (line 16) | @Mapper
    method serviceInfo (line 20) | List<AvailableServiceInfo> serviceInfo(@Param("type") String type);
    method getVersions (line 22) | List<Version> getVersions(@Param("type") String serviceType, @Param("p...
    method getLanguageList (line 24) | List<Dict> getLanguageList();

FILE: src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java
  type LanguageMapper (line 7) | @Mapper

FILE: src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java
  type SearchCasesMapper (line 10) | @Mapper

FILE: src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java
  type VersionMapper (line 10) | @Mapper

FILE: src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java
  class AvailableServiceInfo (line 11) | @Data
    method toString (line 17) | @Override

FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java
  class Subtitle (line 16) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java
  class TranslateResult (line 11) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Video.java
  class Video (line 14) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java
  class FileRecord (line 18) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/PO/Interface.java
  class Interface (line 11) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/PO/Language.java
  class Language (line 14) | @Data
    method toString (line 41) | public String toString() {

FILE: src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java
  class SearchCases (line 11) | @Data
    method setCases (line 41) | public void setCases(String cases) {

FILE: src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java
  class ServiceInterface (line 17) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/PO/Version.java
  class Version (line 15) | @Data
    method toString (line 41) | public String toString() {

FILE: src/main/java/org/fordes/subtitles/view/model/search/Cases.java
  class Cases (line 12) | @Builder
    method setType (line 31) | public void setType(String val) {

FILE: src/main/java/org/fordes/subtitles/view/model/search/Engine.java
  class Engine (line 9) | @Data

FILE: src/main/java/org/fordes/subtitles/view/model/search/Result.java
  class Result (line 13) | @Data
    class Item (line 23) | @Builder
    type Type (line 37) | public static enum Type {

FILE: src/main/java/org/fordes/subtitles/view/model/search/Selector.java
  class Selector (line 9) | public class Selector implements Serializable {

FILE: src/main/java/org/fordes/subtitles/view/service/ConfigService.java
  type ConfigService (line 9) | public interface ConfigService extends IService<ApplicationConfig> {

FILE: src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java
  class InterfaceServiceImpl (line 23) | @Service
    method getVersions (line 34) | @Override
    method getInterface (line 39) | @Override
    method getAvailableService (line 47) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/InterfaceService.java
  type InterfaceService (line 17) | public interface InterfaceService extends IService<ServiceInterface> {
    method getVersions (line 27) | List<Version> getVersions(ServiceType type, ServiceProvider provider);
    method getInterface (line 29) | ServiceInterface getInterface(ServiceType type, ServiceProvider provid...
    method getAvailableService (line 38) | List<AvailableServiceInfo> getAvailableService(ServiceType type);

FILE: src/main/java/org/fordes/subtitles/view/service/SearchService.java
  class SearchService (line 33) | @Slf4j
    method createTask (line 42) | @Override
    method search (line 139) | public void search(Result.Type type, Cases cases, Map<String, Object> ...

FILE: src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java
  class AliTranslateService (line 20) | @Slf4j
    method afterPropertiesSet (line 30) | @Override
    method createTask (line 36) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java
  class BaiduTranslateService (line 19) | @Service
    method afterPropertiesSet (line 26) | @Override
    method createTask (line 31) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java
  class HuoShanTranslateService (line 20) | @Service
    method afterPropertiesSet (line 33) | @Override
    method createTask (line 38) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java
  class TencentTranslateService (line 20) | @Service
    method afterPropertiesSet (line 31) | @Override
    method createTask (line 37) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java
  class TranslateService (line 31) | @Slf4j
    method translate (line 38) | @Async
    method createTask (line 107) | public abstract Callable<TranslateResult> createTask(ThreadPoolExecuto...
    class Segment (line 112) | static class Segment implements Delayed {
      method Segment (line 122) | public Segment(String data, Integer serial, long delay) {
      method getDelay (line 128) | @Override
      method compareTo (line 133) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java
  class AliTranslateThread (line 27) | @Slf4j
    method AliTranslateThread (line 48) | public AliTranslateThread(String ak_id, String ak_secret, Integer seri...
    method call (line 56) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java
  class BaiduTranslateThread (line 21) | @Slf4j
    method BaiduTranslateThread (line 30) | public BaiduTranslateThread(String app_id, String app_key, Integer ser...
    method call (line 38) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java
  class HuoShanTranslateThread (line 34) | @Slf4j
    method HuoShanTranslateThread (line 54) | public HuoShanTranslateThread(String versionDate, String region, Strin...
    method call (line 64) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java
  class TencentTranslateThread (line 27) | @Slf4j
    method TencentTranslateThread (line 47) | public TencentTranslateThread(String secretId, String secretKey, Strin...
    method call (line 55) | @Override

FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java
  class TranslateThread (line 10) | @AllArgsConstructor

FILE: src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java
  class ArchiveUtil (line 34) | @Slf4j
    method unArchiveToCurrentPath (line 44) | public static Collection<File> unArchiveToCurrentPath(File file) {
    method unArchiveFile (line 73) | public static Collection<File> unArchiveFile(File in, String outPath, ...

FILE: src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java
  class CacheUtil (line 23) | public class CacheUtil {
    method initLanguageDict (line 32) | public static void initLanguageDict(List<Dict> data) {
    method getLanguageDict (line 76) | public static List<Language> getLanguageDict(ServiceType type, Service...

FILE: src/main/java/org/fordes/subtitles/view/utils/FileUtils.java
  class FileUtils (line 39) | @Slf4j
    method getStream (line 47) | public static InputStream getStream(@NonNull String path) {
    method chooseFile (line 67) | public static FileChooser chooseFile(String title, FileEnum... items) {
    method choosePath (line 86) | public static DirectoryChooser choosePath(String path) {
    method readFileInfo (line 98) | public static <T> FileRecord readFileInfo(File file) throws IOException {
    method write (line 123) | public static void write(File file, Collection<String> content, String...
    method write (line 127) | public static void write(File file, String content, String charset) {

FILE: src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java
  class SubtitleUtil (line 29) | @Slf4j
    method search (line 38) | public static void search(StyleClassedTextArea area, String target, bo...
    method find (line 47) | public static void find(StyleClassedTextArea area, String target, bool...
    method search (line 59) | public static <T extends SearchCache> void search(T cache, StyleClasse...
    method replace (line 114) | public static void replace(StyleClassedTextArea area, Subtitle subtitl...
    method revise (line 155) | public static TimedTextFile revise(TimedTextFile timedTextFile, LocalT...
    method revise (line 180) | private static void revise(TimedObject timedLine, long poor) {
    method revise (line 188) | public static TimedTextFile revise(TimedTextFile timedTextFile, LocalT...
    method parse (line 198) | public static void parse(Subtitle subtitle) throws Exception {
    method parse (line 214) | public static TimedTextFile parse(String str, FileEnum type) throws Ex...
    method toStr (line 224) | public static String toStr(TimedTextFile subtitle, boolean mode) {
    method toStr (line 241) | public static String toStr(TimedLine timedLine, boolean mode) {
    method write (line 251) | public static void write(Subtitle subtitle, CallBackHandler<Boolean> h...
    class SearchCache (line 263) | @Data
      method SearchCache (line 269) | public SearchCache() {
      method reset (line 273) | public void reset() {
    class ReplaceCache (line 283) | @Data
      method reset (line 288) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java
  class TranslateUtil (line 26) | @Slf4j
    method segmented (line 39) | public static List<String> segmented(String content, int maxLength) {
    method segmented (line 69) | public static List<String> segmented(List<String> content, int maxLeng...
    method segmented (line 77) | public static List<String> segmented(Subtitle subtitle, int maxLength) {
    method reduction (line 90) | public static void reduction(Subtitle subtitle, List<String> data, boo...
    method hmac256 (line 112) | public static byte[] hmac256(byte[] key, String msg) throws Exception {
    method hmac256 (line 116) | public static byte[] hmac256(byte[] key, byte[] msg) throws Exception {
    method hmac256 (line 123) | public static byte[] hmac256(String key, String msg) throws Exception {
    method sha256Hex (line 127) | public static String sha256Hex(String s) throws Exception {

FILE: src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java
  class HTMLParsing (line 21) | public class HTMLParsing extends Parsing {
    method HTMLParsing (line 25) | public HTMLParsing(Object data) {
    method parsing (line 30) | @Override
    method getFields (line 36) | private static List<String> getFields(Document doc, Selector selector) {
    method getField (line 47) | private static String getField(Element element, String attr, String re...

FILE: src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java
  class JSONParsing (line 15) | public class JSONParsing extends Parsing {
    method JSONParsing (line 19) | public JSONParsing(Object data) {
    method parsing (line 26) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java
  class Parsing (line 10) | public abstract class Parsing {
    method Parsing (line 12) | public Parsing(Object data) {
    method parsing (line 16) | public abstract Object parsing(Selector selector);

FILE: src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java
  class ParsingFactory (line 11) | public class ParsingFactory {
    method ParsingFactory (line 15) | public ParsingFactory(Object data, ContentType contentType) {
    method getResult (line 20) | public Object getResult(Selector selector) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java
  class SubmergeAPI (line 24) | public class SubmergeAPI {
    method convertFramerate (line 33) | public void convertFramerate(TimedTextFile timedFile, double sourceFra...
    method toSRT (line 52) | public SRTSub toSRT(TimedTextFile timedFile) {
    method toASS (line 84) | public ASSSub toASS(SimpleSubConfig config) {
    method mergeToAss (line 95) | public ASSSub mergeToAss(SimpleSubConfig... configs) {
    method mergeTextLines (line 114) | public void mergeTextLines(TimedTextFile timedFile) {
    method adjustTimecodes (line 131) | public void adjustTimecodes(TimedTextFile fileToAdjust, TimedTextFile ...
    method expandLongLines (line 181) | private static void expandLongLines(List<? extends TimedLine> adjusted...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java
  class TimedLinesAPI (line 13) | public class TimedLinesAPI {
    method closestByStart (line 25) | public TimedLine closestByStart(List<? extends TimedLine> lines, final...
    method getDelay (line 76) | public int getDelay(LocalTime start, LocalTime end) {
    method isEqualsOrAfter (line 88) | public boolean isEqualsOrAfter(TimedObject elementToCompare, TimedObje...
    method intersected (line 101) | public TimedLine intersected(List<? extends TimedLine> lines, LocalTim...
    method intersected (line 129) | public TimedLine intersected(List<? extends TimedLine> lines, LocalTim...
    method findByTime (line 157) | public int findByTime(List<? extends TimedLine> lines, TimedObject tim...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java
  type FontName (line 7) | public enum FontName {
    method FontName (line 37) | FontName(String name) {
    method getName (line 44) | public String getName() {
    method toString (line 48) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java
  class ASSParser (line 21) | public class ASSParser extends BaseParser<ASSSub> {
    method parse (line 28) | @Override
    method parseEvents (line 74) | private static Set<Events> parseEvents(BufferedReader br) throws IOExc...
    method parseStyle (line 128) | private static List<V4Style> parseStyle(BufferedReader br) throws IOEx...
    method parseDialog (line 169) | private static Events parseDialog(String[] eventsFormat, String[] dial...
    method parseV4Style (line 213) | private static V4Style parseV4Style(String[] styleFormat, String[] sty...
    method getBGR (line 258) | private static int getBGR(String value) {
    method parseScriptInfo (line 289) | private static ScriptInfo parseScriptInfo(BufferedReader br) throws IO...
    method callProperty (line 330) | private static String callProperty(Object object, String property, Str...
    method findFormat (line 369) | private static String[] findFormat(BufferedReader br, String sectionNa...
    method findInfo (line 389) | private static String findInfo(String line, String search) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java
  class BaseParser (line 17) | public abstract class BaseParser<T extends TimedTextFile> implements Sub...
    method parse (line 24) | @Override
    method parse (line 33) | @Override
    method parse (line 47) | @SuppressWarnings("unchecked")
    method parse (line 57) | @Override
    method parse (line 74) | @Override
    method parse (line 99) | protected abstract void parse(BufferedReader br, T sub) throws IOExcep...
    method readFirstTextLine (line 107) | protected static String readFirstTextLine(BufferedReader br) throws IO...
    method skipBom (line 124) | private static void skipBom(BufferedReader br) throws IOException {
    method reset (line 139) | protected static void reset(BufferedReader br, String line) throws IOE...
    method markAndRead (line 152) | protected static String markAndRead(BufferedReader br) throws IOExcept...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java
  class LRCParser (line 18) | @Slf4j
    method parse (line 20) | @Override
    method readFirstTimeLine (line 59) | private String readFirstTimeLine(BufferedReader br) throws IOException {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java
  class ParserFactory (line 6) | public final class ParserFactory {
    method getParser (line 14) | public static SubtitleParser getParser(String extension) throws Except...
    method ParserFactory (line 30) | private ParserFactory() {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java
  class SRTParser (line 23) | public final class SRTParser extends BaseParser<SRTSub> {
    method parse (line 25) | @Override
    method firstIn (line 53) | private static SRTLine firstIn(BufferedReader br) throws IOException, ...
    method parseId (line 84) | private static int parseId(String textLine) throws InvalidSRTSubExcept...
    method parseTime (line 103) | public static SRTTime parseTime(String timeLine) throws InvalidSRTSubE...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java
  type SubtitleParser (line 13) | public interface SubtitleParser {
    method parse (line 23) | TimedTextFile parse(File file);
    method parse (line 35) | TimedTextFile parse(InputStream is, String fileName);
    method parse (line 46) | TimedTextFile parse(File file, String charset);
    method parse (line 59) | TimedTextFile parse(InputStream is, String fileName, String charset);
    method parse (line 68) | TimedTextFile parse(String str, String fileName);

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java
  class InvalidAssSubException (line 4) | public class InvalidAssSubException extends InvalidSubException {
    method InvalidAssSubException (line 8) | public InvalidAssSubException() {
    method InvalidAssSubException (line 11) | public InvalidAssSubException(String arg0) {
    method InvalidAssSubException (line 15) | public InvalidAssSubException(Throwable arg0) {
    method InvalidAssSubException (line 19) | public InvalidAssSubException(String arg0, Throwable arg1) {
    method InvalidAssSubException (line 23) | public InvalidAssSubException(String arg0, Throwable arg1, boolean arg...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java
  class InvalidColorCode (line 3) | public class InvalidColorCode extends RuntimeException {
    method InvalidColorCode (line 7) | public InvalidColorCode() {
    method InvalidColorCode (line 10) | public InvalidColorCode(String message) {
    method InvalidColorCode (line 14) | public InvalidColorCode(Throwable cause) {
    method InvalidColorCode (line 18) | public InvalidColorCode(String message, Throwable cause) {
    method InvalidColorCode (line 22) | public InvalidColorCode(String message, Throwable cause, boolean enabl...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java
  class InvalidFileException (line 3) | public class InvalidFileException extends RuntimeException {
    method InvalidFileException (line 7) | public InvalidFileException() {
    method InvalidFileException (line 10) | public InvalidFileException(String message) {
    method InvalidFileException (line 14) | public InvalidFileException(Throwable cause) {
    method InvalidFileException (line 18) | public InvalidFileException(String message, Throwable cause) {
    method InvalidFileException (line 22) | public InvalidFileException(String message, Throwable cause, boolean e...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java
  class InvalidSRTSubException (line 4) | public class InvalidSRTSubException extends InvalidSubException {
    method InvalidSRTSubException (line 8) | public InvalidSRTSubException() {
    method InvalidSRTSubException (line 11) | public InvalidSRTSubException(String arg0) {
    method InvalidSRTSubException (line 15) | public InvalidSRTSubException(Throwable arg0) {
    method InvalidSRTSubException (line 19) | public InvalidSRTSubException(String arg0, Throwable arg1) {
    method InvalidSRTSubException (line 23) | public InvalidSRTSubException(String arg0, Throwable arg1, boolean arg...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java
  class InvalidSubException (line 3) | public class InvalidSubException extends RuntimeException {
    method InvalidSubException (line 7) | public InvalidSubException() {
    method InvalidSubException (line 10) | public InvalidSubException(String arg0) {
    method InvalidSubException (line 14) | public InvalidSubException(Throwable arg0) {
    method InvalidSubException (line 18) | public InvalidSubException(String arg0, Throwable arg1) {
    method InvalidSubException (line 22) | public InvalidSubException(String arg0, Throwable arg1, boolean arg2, ...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java
  class ASSSub (line 19) | @Data
    method toString (line 79) | @Override
    method toInputStream (line 105) | public InputStream toInputStream() {
    method setFileName (line 110) | @Override
    method getFileName (line 115) | @Override
    method getTimedLines (line 120) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java
  class ASSTime (line 14) | public class ASSTime extends SubtitleTime {
    method ASSTime (line 34) | public ASSTime(LocalTime start, LocalTime end) {
    method ASSTime (line 41) | public ASSTime() {
    method format (line 51) | public static String format(LocalTime time) {
    method fromString (line 63) | public static LocalTime fromString(String time) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java
  class Events (line 27) | @Data
    method Events (line 120) | public Events(String style, ASSTime time, List<String> textLines) {
    method Events (line 130) | public Events() {
    method toString (line 136) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java
  class ScriptInfo (line 12) | @Data
    type Collision (line 105) | public enum Collision {
      method Collision (line 120) | Collision(String type) {
      method toString (line 124) | @Override
    method toString (line 235) | @Override
    method appendNotNull (line 264) | private static void appendNotNull(StringBuilder sb, String desc, Strin...
    method appendPositive (line 277) | private static void appendPositive(StringBuilder sb, String desc, int ...

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java
  class V4Style (line 29) | @Data
    method V4Style (line 217) | public V4Style() {
    method V4Style (line 225) | public V4Style(String name) {
    method toString (line 229) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java
  class SubtitleLine (line 9) | public class SubtitleLine<T extends TimedObject> implements TimedLine, S...
    method SubtitleLine (line 37) | public SubtitleLine() {
    method SubtitleLine (line 44) | public SubtitleLine(T time) {
    method equals (line 50) | @Override
    method compare (line 64) | @Override
    method compareTo (line 70) | @Override
    method getTime (line 88) | @Override
    method setTime (line 93) | public void setTime(T time) {
    method getTextLines (line 97) | @Override
    method setTextLines (line 102) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java
  class SubtitleTime (line 7) | public class SubtitleTime implements TimedObject, Serializable {
    method SubtitleTime (line 23) | public SubtitleTime() {
    method SubtitleTime (line 26) | public SubtitleTime(LocalTime start, LocalTime end) {
    method compare (line 33) | @Override
    method equals (line 39) | @Override
    method compareTo (line 53) | @Override
    method getStart (line 65) | @Override
    method setStart (line 70) | @Override
    method getEnd (line 75) | @Override
    method setEnd (line 80) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java
  type TimedLine (line 11) | public interface TimedLine extends Serializable, Comparable<TimedLine>, ...
    method getTextLines (line 18) | List<String> getTextLines();
    method setTextLines (line 24) | void setTextLines(List<String> textLines);
    method getTime (line 31) | TimedObject getTime();

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java
  type TimedObject (line 10) | public interface TimedObject extends Serializable, Comparable<TimedObjec...
    method getStart (line 18) | LocalTime getStart();
    method getEnd (line 26) | LocalTime getEnd();
    method setStart (line 33) | void setStart(LocalTime start);
    method setEnd (line 41) | void setEnd(LocalTime end);

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java
  type TimedTextFile (line 10) | public interface TimedTextFile extends Serializable {
    method getFileName (line 17) | String getFileName();
    method setFileName (line 24) | void setFileName(String fileName);
    method getTimedLines (line 31) | Set<? extends TimedLine> getTimedLines();

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java
  class Font (line 9) | @Data

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java
  class SimpleSubConfig (line 10) | @Data
    method SimpleSubConfig (line 21) | public SimpleSubConfig() {
    method SimpleSubConfig (line 24) | public SimpleSubConfig(TimedTextFile sub, Font fontConfig) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java
  class LRCLine (line 12) | @NoArgsConstructor
    method LRCLine (line 18) | public LRCLine(LRCTime time, List<String> textLines) {
    method toString (line 23) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java
  class LRCSub (line 17) | @Data
    method add (line 26) | public void add(LRCLine line) {
    method remove (line 30) | public void remove(TimedLine line) {
    method toString (line 34) | public String toString() {
    method getTimedLines (line 38) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java
  class LRCTime (line 18) | @Slf4j
    method LRCTime (line 30) | public LRCTime(LocalTime start) {
    method toString (line 34) | @Override
    method format (line 39) | public static String format(LocalTime time) {
    method fromString (line 47) | public static LRCTime fromString(String times) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java
  class SRTLine (line 14) | @Data
    method SRTLine (line 24) | public SRTLine(int id, SRTTime time, List<String> textLines) {
    method toString (line 31) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java
  class SRTSub (line 15) | public class SRTSub implements TimedTextFile, Serializable {
    method add (line 24) | public void add(SRTLine line) {
    method remove (line 29) | public void remove(TimedLine line) {
    method toString (line 34) | @Override
    method getLines (line 44) | public Set<SRTLine> getLines() {
    method getTimedLines (line 48) | @Override
    method setLines (line 53) | public void setLines(Set<SRTLine> lines) {
    method getFileName (line 57) | @Override
    method setFileName (line 62) | @Override

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java
  class SRTTime (line 13) | public class SRTTime extends SubtitleTime implements Serializable {
    method SRTTime (line 22) | public SRTTime() {
    method SRTTime (line 26) | public SRTTime(LocalTime start, LocalTime end) {
    method toString (line 31) | @Override
    method format (line 47) | public static String format(LocalTime time) {
    method fromString (line 65) | public static LocalTime fromString(String times) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java
  class ColorUtils (line 10) | public final class ColorUtils {
    method hexToBGR (line 18) | public static int hexToBGR(String hex) {
    method HAABBGGRRToHex (line 34) | public static String HAABBGGRRToHex(String haabbggrr) {
    method HBBGGRRToHex (line 53) | public static String HBBGGRRToHex(String hbbggrr) {
    method HAABBGGRRToBGR (line 68) | public static int HAABBGGRRToBGR(String haabbggrr) {
    method HBBGGRRToBGR (line 79) | public static int HBBGGRRToBGR(String hbbggrr) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java
  class ConvertUtils (line 16) | public class ConvertUtils {
    method createEvent (line 32) | public static Events createEvent(TimedLine line, String style) {
    method createV4Style (line 49) | public static V4Style createV4Style(SimpleSubConfig config) {
    method toSRTString (line 69) | public static String toSRTString(String textLine) {
    method toASSString (line 84) | public static String toASSString(String textLine) {

FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java
  class EncodeUtils (line 16) | @Slf4j
    method guessEncoding (line 26) | public static String guessEncoding(File file) throws IOException {
    method guessEncoding (line 39) | public static String guessEncoding(InputStream is) throws IOException {
    method guessEncoding (line 61) | public static String guessEncoding(byte[] bytes) {
Condensed preview — 155 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (408K chars).
[
  {
    "path": ".github/workflows/subtitles-view.yml",
    "chars": 2993,
    "preview": "# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow executi"
  },
  {
    "path": ".gitignore",
    "chars": 376,
    "preview": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n."
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2021 fordes123\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 1922,
    "preview": "# Subtitles-View\n\n[![stars](https://img.shields.io/github/stars/fordes123/Subtitles-View?color=%23e74c3c)]()\n[![forks](h"
  },
  {
    "path": "pom.xml",
    "chars": 7735,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java",
    "chars": 4104,
    "preview": "package org.fordes.subtitles.view;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport com"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java",
    "chars": 1885,
    "preview": "package org.fordes.subtitles.view.config;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ClassPat"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java",
    "chars": 727,
    "preview": "package org.fordes.subtitles.view.config;\n\nimport cn.hutool.core.thread.ExecutorBuilder;\nimport org.springframework.cont"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java",
    "chars": 1504,
    "preview": "package org.fordes.subtitles.view.constant;\n\n/**\n * @author fordes on 2022/1/24\n */\npublic class CommonConstant {\n\n    p"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java",
    "chars": 1039,
    "preview": "package org.fordes.subtitles.view.constant;\n\n/**\n * @author fordes on 2022/1/23\n */\npublic class StyleClassConstant {\n\n "
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java",
    "chars": 1192,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.fxml.Initializable;\nimport javafx."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/EditTool.java",
    "chars": 19658,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Export.java",
    "chars": 185,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/MainController.java",
    "chars": 7109,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.fxml.FXML;\nimport jav"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/MainEditor.java",
    "chars": 7434,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lan"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/QuickStart.java",
    "chars": 3121,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Singleton;\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Setting.java",
    "chars": 9996,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java",
    "chars": 444,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.ToggleButton;\nimport"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java",
    "chars": 451,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.ToggleButton;\nimport"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java",
    "chars": 354,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Button;\nimport lombo"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java",
    "chars": 195,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java",
    "chars": 7492,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Dic"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java",
    "chars": 189,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/TitleBar.java",
    "chars": 1327,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.application.Platform;"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Toast.java",
    "chars": 2658,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/ToolBox.java",
    "chars": 171,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes o"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java",
    "chars": 839,
    "preview": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.stage.Stage;\nimport l"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java",
    "chars": 268,
    "preview": "package org.fordes.subtitles.view.enums;\n\n/**\n * 编辑工具 事件类型枚举\n *\n * @author fordes on 2022/7/15\n */\npublic enum EditToolE"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/FileEnum.java",
    "chars": 2092,
    "preview": "package org.fordes.subtitles.view.enums;\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.AllArgsConstructor;\n\nimport "
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/FontIcon.java",
    "chars": 1292,
    "preview": "package org.fordes.subtitles.view.enums;\n\nimport lombok.AllArgsConstructor;\n\n/**\n * 图标枚举\n *\n * @author fordes on 2022/1/"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java",
    "chars": 497,
    "preview": "package org.fordes.subtitles.view.enums;\n\nimport com.baomidou.mybatisplus.annotation.IEnum;\nimport lombok.AllArgsConstru"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/ServiceType.java",
    "chars": 504,
    "preview": "package org.fordes.subtitles.view.enums;\n\nimport com.baomidou.mybatisplus.annotation.IEnum;\nimport lombok.AllArgsConstru"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java",
    "chars": 1068,
    "preview": "package org.fordes.subtitles.view.enums;\n\nimport lombok.Getter;\n\n/**\n * 7zip结束码枚举\n *\n * @author fordes on 2021/1/7\n */\n@"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java",
    "chars": 618,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport org.fordes.su"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java",
    "chars": 1160,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport javafx.scene."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java",
    "chars": 1202,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Singleton;\nimpor"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java",
    "chars": 561,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport lombok.Getter"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java",
    "chars": 496,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\n\n/**\n * 主题切换事件\n *\n *"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java",
    "chars": 1550,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.EventType;\nimport org.fordes.subtitles.view.handler.ToastE"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java",
    "chars": 1236,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.EventType;\nimport org.fordes.subtitles.view.handler.ToastE"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java",
    "chars": 740,
    "preview": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport lombok.Getter"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java",
    "chars": 609,
    "preview": "package org.fordes.subtitles.view.factory;\n\nimport org.fordes.subtitles.view.service.translate.TranslateService;\n\nimport"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java",
    "chars": 171,
    "preview": "package org.fordes.subtitles.view.handler;\n\n/**\n * @author fordes on 2022/7/27\n */\n@FunctionalInterface\npublic interface"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java",
    "chars": 275,
    "preview": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.Edi"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java",
    "chars": 316,
    "preview": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.Fil"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java",
    "chars": 986,
    "preview": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.Abs"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java",
    "chars": 178,
    "preview": "package org.fordes.subtitles.view.handler;\n\n/**\n * toast回调事件处理器接口\n *\n * @author fordes on 2022/2/2\n */\n@FunctionalInterf"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java",
    "chars": 754,
    "preview": "package org.fordes.subtitles.view.mapper;\n\nimport cn.hutool.core.lang.Dict;\nimport com.baomidou.mybatisplus.core.mapper."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java",
    "chars": 270,
    "preview": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.iba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java",
    "chars": 317,
    "preview": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.iba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java",
    "chars": 305,
    "preview": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.iba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java",
    "chars": 505,
    "preview": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.fordes.sub"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java",
    "chars": 505,
    "preview": "package org.fordes.subtitles.view.model.DTO;\n\n/**\n * @author fordes on 2022/7/19\n */\n\nimport lombok.Data;\nimport lombok."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java",
    "chars": 271,
    "preview": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * 翻译\n *\n * @author fordes"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/Video.java",
    "chars": 504,
    "preview": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsC"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java",
    "chars": 1085,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.j"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Interface.java",
    "chars": 835,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.myba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Language.java",
    "chars": 1318,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.myba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java",
    "chars": 707,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\nimport cn.hutool.json.JSONUtil;\nimport lombok.Data;\nimport lombok.experimen"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java",
    "chars": 1246,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.myba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Version.java",
    "chars": 1089,
    "preview": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.myba"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Cases.java",
    "chars": 640,
    "preview": "package org.fordes.subtitles.view.model.search;\n\nimport cn.hutool.http.ContentType;\nimport lombok.Builder;\n\nimport java."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Engine.java",
    "chars": 303,
    "preview": "package org.fordes.subtitles.view.model.search;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n/**\n * @auth"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Result.java",
    "chars": 676,
    "preview": "package org.fordes.subtitles.view.model.search;\n\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Builder;\nimport lombok"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Selector.java",
    "chars": 689,
    "preview": "package org.fordes.subtitles.view.model.search;\n\nimport java.io.Serializable;\n\n/**\n * 字段解析器\n * @author fordes on 2022/3/"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/ConfigService.java",
    "chars": 276,
    "preview": "package org.fordes.subtitles.view.service;\n\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport org.forde"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java",
    "chars": 1741,
    "preview": "package org.fordes.subtitles.view.service.Impl;\n\nimport com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrappe"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/InterfaceService.java",
    "chars": 1068,
    "preview": "package org.fordes.subtitles.view.service;\n\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport org.forde"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/SearchService.java",
    "chars": 6456,
    "preview": "package org.fordes.subtitles.view.service;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Con"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java",
    "chars": 1463,
    "preview": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport lombok.extern.slf4j.Slf4"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java",
    "chars": 1422,
    "preview": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.vie"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java",
    "chars": 1738,
    "preview": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.vie"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java",
    "chars": 1592,
    "preview": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.vie"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java",
    "chars": 5234,
    "preview": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.d"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java",
    "chars": 4305,
    "preview": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.d"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java",
    "chars": 2472,
    "preview": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java",
    "chars": 6449,
    "preview": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java",
    "chars": 5226,
    "preview": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.co"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java",
    "chars": 526,
    "preview": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport lombok.AllArgsConstructor;\n\n/**\n * 翻译线程抽象\n *\n * @aut"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java",
    "chars": 4689,
    "preview": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java",
    "chars": 3684,
    "preview": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Dict;\nim"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/FileUtils.java",
    "chars": 5087,
    "preview": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java",
    "chars": 10108,
    "preview": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java",
    "chars": 4687,
    "preview": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IORuntimeE"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java",
    "chars": 1724,
    "preview": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.O"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java",
    "chars": 916,
    "preview": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONObject;\ni"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java",
    "chars": 289,
    "preview": "package org.fordes.subtitles.view.utils.search;\n\nimport org.fordes.subtitles.view.model.search.Selector;\n\n/**\n * 解析器抽象\n "
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java",
    "chars": 543,
    "preview": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.http.ContentType;\nimport org.fordes.subtitles.view.mod"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java",
    "chars": 7586,
    "preview": "package org.fordes.subtitles.view.utils.submerge;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java",
    "chars": 4509,
    "preview": "package org.fordes.subtitles.view.utils.submerge;\n\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.Subt"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java",
    "chars": 947,
    "preview": "package org.fordes.subtitles.view.utils.submerge.constant;\n\n/**\n * Enum all the supported font names of the application\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java",
    "chars": 13951,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.co"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java",
    "chars": 4661,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.Re"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java",
    "chars": 2054,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.co"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java",
    "chars": 973,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.util.StrUtil;\n\npublic final class Parse"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java",
    "chars": 3084,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitl"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java",
    "chars": 2112,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport org.fordes.subtitles.view.utils.submerge.parser.except"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java",
    "chars": 593,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\n\npublic class InvalidAssSubException extends Invalid"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java",
    "chars": 632,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidColorCode extends RuntimeExcepti"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java",
    "chars": 655,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidFileException extends RuntimeExc"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java",
    "chars": 594,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\n\npublic class InvalidSRTSubException extends Invalid"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java",
    "chars": 571,
    "preview": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidSubException extends RuntimeExce"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java",
    "chars": 2721,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\nimport org.fordes.subtitles.view.uti"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java",
    "chars": 1396,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java",
    "chars": 4700,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Data;"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java",
    "chars": 7823,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java",
    "chars": 7755,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n "
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java",
    "chars": 2268,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.ArrayL"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java",
    "chars": 1969,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.time.LocalT"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java",
    "chars": 619,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.Compar"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java",
    "chars": 1027,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\nimport java.io.Serializable;\nimport java.time.LocalTi"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java",
    "chars": 589,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.Set;\n\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java",
    "chars": 646,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.config;\n\n\nimport lombok.Data;\nimport org.fordes.subtitles.view"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java",
    "chars": 633,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.config;\n\n\nimport lombok.Data;\nimport org.fordes.subtitles.view"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java",
    "chars": 817,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.NoArgs"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java",
    "chars": 1065,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hut"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java",
    "chars": 1648,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.date.LocalDateTimeUtil;\nimport cn."
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java",
    "chars": 978,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimp"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java",
    "chars": 1416,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java",
    "chars": 1751,
    "preview": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\n\nimport org.fordes.subtitles.view.utils.submerge.subtitl"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java",
    "chars": 2310,
    "preview": "package org.fordes.subtitles.view.utils.submerge.utils;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitle"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java",
    "chars": 3079,
    "preview": "package org.fordes.subtitles.view.utils.submerge.utils;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java",
    "chars": 1768,
    "preview": "package org.fordes.subtitles.view.utils.submerge.utils;\n\nimport cn.hutool.core.io.CharsetDetector;\nimport cn.hutool.core"
  },
  {
    "path": "src/main/resources/application.yml",
    "chars": 927,
    "preview": "logging:\n  config: classpath:logback/logback-spring.xml\n  file:\n    path: ./logs\n\n\nspring:\n  application:\n    name: subt"
  },
  {
    "path": "src/main/resources/banner.txt",
    "chars": 1438,
    "preview": "\n${AnsiColor.BRIGHT_GREEN}  $$$$$$\\            $$\\        $$\\     $$\\   $$\\     $$\\                           $$\\    $$\\"
  },
  {
    "path": "src/main/resources/css/edit-tool.css",
    "chars": 1906,
    "preview": ".toolPanel {\n    -fx-font-size: 15;\n    -fx-text-fill: -fx-dark-0;\n    -fx-background-radius: 5;\n    -fx-border-radius: "
  },
  {
    "path": "src/main/resources/css/font.css",
    "chars": 219,
    "preview": "@font-face {\n    font-family: \"iconfont\";\n    src: url('/font/iconfont.ttf') format('truetype');\n}\n\n@font-face {\n    fon"
  },
  {
    "path": "src/main/resources/css/main-editor.css",
    "chars": 1618,
    "preview": ".bar .item {\n    -fx-font-family: iconfont;\n    -fx-text-fill: -fx-white-5;\n    -fx-text-alignment: center;\n    -fx-padd"
  },
  {
    "path": "src/main/resources/css/quick-start.css",
    "chars": 1521,
    "preview": "#root {\n    -fx-background-color: transparent;\n    -fx-border-style: dashed;\n    -fx-border-radius: 10;\n    -fx-border-w"
  },
  {
    "path": "src/main/resources/css/setting.css",
    "chars": 1513,
    "preview": ".item {\n    -fx-font-size: 15;\n}\n\n.sub-title {\n    -fx-font-size: 20;\n}\n\n.item, .sub-title {\n    -fx-text-fill: -fx-dark"
  },
  {
    "path": "src/main/resources/css/speech-conversion.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/main/resources/css/styles.css",
    "chars": 7842,
    "preview": "* {\n    -fx-white-0: #ffffff;\n    -fx-white-1: #f5f5f5;\n    -fx-white-2: #ebebed;\n    -fx-white-3: #e8e8e8;\n    -fx-whit"
  },
  {
    "path": "src/main/resources/css/subtitle-search.css",
    "chars": 1348,
    "preview": ".engine {\n    -fx-background-radius: 50px;\n    -fx-pref-height: 50px;\n    -fx-pref-width: 50px;\n    -fx-min-width: -fx-p"
  },
  {
    "path": "src/main/resources/css/title-bar.css",
    "chars": 992,
    "preview": "#root {\n    -fx-background-color: -fx-white-3;\n}\n\n.dark #root {\n    -fx-background-color: -fx-dark-2;\n}\n\n.full-screen #r"
  },
  {
    "path": "src/main/resources/css/toast.css",
    "chars": 1164,
    "preview": ".toast {\n    -fx-cursor: hand;\n    -fx-background-color: -fx-white-0;\n    -fx-border-insets: 0;\n    -fx-background-radiu"
  },
  {
    "path": "src/main/resources/css/tool-box.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/main/resources/fxml/edit-tool.fxml",
    "chars": 22098,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.JFXComboBox?>\n<?import javafx.geometry.Insets?>\n<?"
  },
  {
    "path": "src/main/resources/fxml/export.fxml",
    "chars": 605,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import"
  },
  {
    "path": "src/main/resources/fxml/main-editor.fxml",
    "chars": 8563,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import jav"
  },
  {
    "path": "src/main/resources/fxml/main-view.fxml",
    "chars": 3889,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import "
  },
  {
    "path": "src/main/resources/fxml/quick-start.fxml",
    "chars": 1873,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.Button?>\n<?impor"
  },
  {
    "path": "src/main/resources/fxml/setting.fxml",
    "chars": 10231,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.*?>\n<?import java.lang.*?>\n<?import javafx.geometr"
  },
  {
    "path": "src/main/resources/fxml/sidebar-after.fxml",
    "chars": 3399,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import jav"
  },
  {
    "path": "src/main/resources/fxml/sidebar-before.fxml",
    "chars": 2962,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import jav"
  },
  {
    "path": "src/main/resources/fxml/sidebar-bottom.fxml",
    "chars": 1641,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Button?>\n<?import javafx.scene.control.Label?>\n<?i"
  },
  {
    "path": "src/main/resources/fxml/speech-conversion.fxml",
    "chars": 650,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import "
  },
  {
    "path": "src/main/resources/fxml/subtitle-search.fxml",
    "chars": 2667,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.*?>\n<?import javafx.geometry.Insets?>\n<?import jav"
  },
  {
    "path": "src/main/resources/fxml/sync-editor.fxml",
    "chars": 600,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import "
  },
  {
    "path": "src/main/resources/fxml/title-bar.fxml",
    "chars": 1755,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Button?>\n<?import javafx.scene.control.Label?>\n<?i"
  },
  {
    "path": "src/main/resources/fxml/toast.fxml",
    "chars": 2222,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.JFXButton?>\n<?import javafx.geometry.Insets?>\n<?im"
  },
  {
    "path": "src/main/resources/fxml/tool-box.fxml",
    "chars": 1035,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import "
  },
  {
    "path": "src/main/resources/fxml/voice-convert.fxml",
    "chars": 611,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import"
  },
  {
    "path": "src/main/resources/logback/logback-spring.xml",
    "chars": 4473,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--\n    scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。\n    scanPeriod:设置监测配置文件是"
  },
  {
    "path": "src/main/resources/mapper/InterfaceMapper.xml",
    "chars": 1641,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the fordes123/subtitles-view GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 155 files (358.4 KB), approximately 93.3k tokens, and a symbol index with 466 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!