[
  {
    "path": ".github/workflows/subtitles-view.yml",
    "content": "# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time\n# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven\n\nname: CI Build\n\non:\n  push:\n    paths-ignore:\n      - 'README.md'\n    branches: [ main ]\n  pull_request:\n    paths-ignore:\n      - 'README.md'\n    branches: [ main ]\n\n  workflow_dispatch:\n    inputs:\n      generateInstaller:\n        description: 'generateInstaller'\n        required: true\n        type: choice\n        options:\n          - 'true'\n          - 'false'\n        default: 'false'\n      customizedJre:\n        description: 'customizedJre'\n        required: true\n        type: choice\n        options:\n          - 'true'\n          - 'false'\n        default: 'false'\n\njobs:\n  bundling-for-windows:\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up JDK 11\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n          cache: maven\n      - name: Build with Maven\n        run: mvn --file pom.xml -Dplatform=windows -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=true -DcreateTarball=false -B package\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v3\n        with:\n          name: windows\n          path: |\n            target/subtitles-view-*.*\n            !target/subtitles-view-*.jar\n  bundling-for-linux:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up JDK 11\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n          cache: maven\n      - name: Build with Maven\n        run: mvn --file pom.xml -Dplatform=linux -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v3\n        with:\n          name: linux\n          path: |\n            target/subtitles-view-*.*\n            !target/subtitles-view-*.jar\n  bundling-for-mac:\n    runs-on: macos-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up JDK 11\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n          cache: maven\n      - name: Build with Maven\n        run: mvn --file pom.xml -Dplatform=mac -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v3\n        with:\n          name: mac\n          path: |\n            target/subtitles-view-*.*\n            !target/subtitles-view-*.jar"
  },
  {
    "path": ".gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n\n### VS Code ###\n.vscode/\n/src/test\n\n### custom ###\n/logs\n/src/test/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 fordes123\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Subtitles-View\n\n[![stars](https://img.shields.io/github/stars/fordes123/Subtitles-View?color=%23e74c3c)]()\n[![forks](https://img.shields.io/github/forks/fordes123/Subtitles-View?color=%232ecc71)]()\n[![release](https://img.shields.io/github/v/release/fordes123/Subtitles-View.svg)](https://github.com/fordes123/Subtitles-View/releases)\n[![license](https://img.shields.io/github/license/fordes123/Subtitles-View?color=%239b59b6)](https://opensource.org/licenses/MIT)\n&nbsp;\n\n这是一个基于`JavaFX`的程序，致力于简单、优雅、高效处理和编辑字幕。适配SRT、ASS等字幕格式，并且支持视频语音转换与字幕翻译，欢迎体验.\n\n> ⚠️ 很遗憾，此仓库已停止维护\n\n## ✨ 特性\n\n- 🎁 现代化的界面，简洁明快\n- 🦄 在线语音转换，简单为视频生成字幕并翻译\n- ☑️ 多种视频与字幕格式支持\n- ✏ 便捷化字幕编辑功能，帮助快速修正机器翻译\n- 🎯 在线的字幕搜索与下载\n- 🎈 深色浅色模式一键切换\n- ⛏ 更多特性待开发...\n\n## 🎉 应用界面\n\n![浅色模式](./screenshot/home.png \"⚠️界面可能已经更新，请以具体程序为准\")\n\n## ☑️ TODO\n\n- [x] 框架搭建以及迁移重构\n- [x] UI调整，深浅色跟随系统等\n- [x] 字幕搜索、下载支持：`字幕库`、`伪射手网`、`A4k字幕网`\n- [x] 文字翻译服务适配：`百度翻译`、`阿里翻译`、`腾讯翻译`、`火山翻译`\n- [ ] 语音转换服务适配\n- [ ] 简单的视频处理支持，如字幕分离、水印、格式转换等\n\n## 🧑🏻‍🔧技术栈\n\n- `Maven`\n- `JavaFX`\n- `SpringBoot`\n- `SQLite`\n- `Mybatis-Plus`\n\n## 📢 项目说明\n\n- 兴趣之作，欢迎提出任何修改意见，但不保证任何更新以及功能的可靠性\n- 设计支持跨平台，但未经测试，现阶段以`Windows`平台为主\n- 程序无任何收费和用户信息收集行为。所有在线服务如：语音转写、在线翻译均为第三方提供，与本程序无关\n\n## 🛠 快速开始\n\n### 从源代码构建\n\n```shell\n# 请保证你的JDK版本不低于11，否则无法通过编译\ngit clone https://github.com/fordes123/subtitles-view.git\ncd subtitles-view\nmvn clean install\nmvn run\n```\n\n或者\n\nfork 本项目, 在 `WorkFlows` 中运行 `CI Build`, Github Action 将根据配置自动为你构建对应程序包\n<details>\n<summary>查看引导</summary>\n<img src=\"./screenshot/action.png\" alt=\"\">\n</details>\n\n### 获取可执行文件\n\n- 正式发行版 [🚀 Releases](https://github.com/fordes123/Subtitles-View/releases/)\n- 自动构建的测试版 [🤖 CI](https://github.com/fordes123/subtitles-view/actions)\n\n（由于正在积极开发中，暂时没有 Release 版本，预览以及体验可使用 CI 版本）\n\n## 🤝 交流反馈\n\n- 提交 [📌Issues](https://github.com/fordes123/Subtitles-View/issues)\n- 博客评论区 [📌Blog Page](https://blog.fordes.top/archives/subtitles-view.html)\n\n## 📜 开源许可\n\n- 基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>subtitles-view</name>\n    <groupId>org.fordes</groupId>\n    <artifactId>subtitles-view</artifactId>\n    <version>2.0.0-Alpha</version>\n    <description>subtitles-view</description>\n\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <spring-boot.version>2.6.2</spring-boot.version>\n        <javafx-weaver.version>1.3.0</javafx-weaver.version>\n        <lombok.version>1.18.22</lombok.version>\n        <hutool.version>5.7.18</hutool.version>\n        <javafx.version>11.0.2</javafx.version>\n        <jfoenix.version>9.0.9</jfoenix.version>\n        <jSystemThemeDetector.version>3.8</jSystemThemeDetector.version>\n        <jsoup.version>1.14.3</jsoup.version>\n        <sqlite.version>3.36.0.3</sqlite.version>\n        <mybatis-plus.version>3.5.1</mybatis-plus.version>\n        <sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>\n        <javapackager.version>1.6.7</javapackager.version>\n        <juniversalchardet.version>2.4.0</juniversalchardet.version>\n        <richtextfx.version>0.10.9</richtextfx.version>\n\n        <!--javapackager configuration-->\n        <platform>windows</platform>\n        <bundleJre>true</bundleJre>\n        <generateInstaller>false</generateInstaller>\n        <customizedJre>false</customizedJre>\n        <createZipball>true</createZipball>\n        <createTarball>false</createTarball>\n    </properties>\n\n    <repositories>\n        <repository>\n            <id>jitpack.io</id>\n            <url>https://jitpack.io</url>\n        </repository>\n    </repositories>\n\n    <dependencies>\n\n        <!--javafx springboot 整合-->\n        <dependency>\n            <groupId>com.github.fordes123</groupId>\n            <artifactId>spring-boot-jfx</artifactId>\n            <version>0.0.1</version>\n        </dependency>\n\n        <!--test-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!--lombok-->\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!--hutool工具类-->\n        <dependency>\n            <groupId>cn.hutool</groupId>\n            <artifactId>hutool-all</artifactId>\n            <version>${hutool.version}</version>\n        </dependency>\n\n        <!--javafx组件-->\n        <dependency>\n            <groupId>org.openjfx</groupId>\n            <artifactId>javafx-controls</artifactId>\n            <version>${javafx.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.openjfx</groupId>\n            <artifactId>javafx-fxml</artifactId>\n            <version>${javafx.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.openjfx</groupId>\n            <artifactId>javafx-base</artifactId>\n            <version>${javafx.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.openjfx</groupId>\n            <artifactId>javafx-graphics</artifactId>\n            <version>${javafx.version}</version>\n        </dependency>\n\n        <!--Material Design 控件-->\n        <dependency>\n            <groupId>com.jfoenix</groupId>\n            <artifactId>jfoenix</artifactId>\n            <version>${jfoenix.version}</version>\n        </dependency>\n\n        <!--jsoup，html解析-->\n        <dependency>\n            <groupId>org.jsoup</groupId>\n            <artifactId>jsoup</artifactId>\n            <version>${jsoup.version}</version>\n        </dependency>\n\n        <!--sqlite 数据库-->\n        <dependency>\n            <groupId>org.xerial</groupId>\n            <artifactId>sqlite-jdbc</artifactId>\n            <version>${sqlite.version}</version>\n        </dependency>\n\n        <!--mybatis-plus ORM-->\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-boot-starter</artifactId>\n            <version>${mybatis-plus.version}</version>\n        </dependency>\n\n        <!--7z 解压-->\n        <dependency>\n            <groupId>net.sf.sevenzipjbinding</groupId>\n            <artifactId>sevenzipjbinding</artifactId>\n            <version>${sevenzipjbinding.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>net.sf.sevenzipjbinding</groupId>\n            <artifactId>sevenzipjbinding-all-platforms</artifactId>\n            <version>${sevenzipjbinding.version}</version>\n        </dependency>\n\n        <!--文件编码检测-->\n        <dependency>\n            <groupId>com.github.albfernandez</groupId>\n            <artifactId>juniversalchardet</artifactId>\n            <version>${juniversalchardet.version}</version>\n        </dependency>\n\n        <!--jackson-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-json</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.fxmisc.richtext</groupId>\n            <artifactId>richtextfx</artifactId>\n            <version>${richtextfx.version}</version>\n        </dependency>\n\n    </dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>${spring-boot.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.8.1</version>\n                <configuration>\n                    <source>11</source>\n                    <target>11</target>\n                    <encoding>UTF-8</encoding>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>io.github.fvarrui</groupId>\n                <artifactId>javapackager</artifactId>\n                <version>${javapackager.version}</version>\n                <executions>\n                    <execution>\n                        <id>package</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>package</goal>\n                        </goals>\n                        <configuration>\n                            <platform>${platform}</platform>\n                            <mainClass>org.fordes.subtitles.view.SubtitlesViewApplication</mainClass>\n                            <bundleJre>${bundleJre}</bundleJre>\n                            <customizedJre>${customizedJre}</customizedJre>\n                            <generateInstaller>${generateInstaller}</generateInstaller>\n                            <administratorRequired>false</administratorRequired>\n                            <createZipball>${createZipball}</createZipball>\n                            <createTarball>${createTarball}</createTarball>\n                            <vmArgs>-XX:TieredStopAtLevel=1 -noverify</vmArgs>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java",
    "content": "package org.fordes.subtitles.view;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport com.jthemedetecor.OsThemeDetector;\nimport javafx.scene.Parent;\nimport javafx.stage.Stage;\nimport javafx.stage.StageStyle;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.jfx.annotation.JFXApplication;\nimport org.fordes.jfx.annotation.Tray;\nimport org.fordes.jfx.core.ProxyApplication;\nimport org.fordes.jfx.core.ProxyLauncher;\nimport org.fordes.jfx.core.StageReadyEvent;\nimport org.fordes.subtitles.view.config.ApplicationConfig;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.event.ThemeChangeEvent;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport java.awt.*;\nimport java.io.IOException;\n\n/**\n * @author fordes\n */\n@Slf4j\n@AllArgsConstructor\n@SpringBootApplication\n@JFXApplication(value = \"/fxml/main-view.fxml\", title = \"SubtitlesView Alpha\", style = StageStyle.TRANSPARENT,\n        css = {\"/css/styles.css\", \"/css/font.css\"}, osThemeDetector = true, darkStyleClass = \"dark\", icons = {\"/icon/logo.ico\"},\n        systemTray = @Tray(value = true, image = \"/icon/logo.png\", toolTip = \"SubtitlesView\"))\npublic class SubtitlesViewApplication extends ProxyApplication {\n\n    private final ApplicationConfig config;\n\n    public static String applicationName;\n\n    private static final long timeMillis = System.currentTimeMillis();\n\n    @Value(\"${spring.application.name}\")\n    public void setApplicationName(String applicationName) {\n        SubtitlesViewApplication.applicationName = applicationName;\n    }\n\n    public static void main(String[] args) {\n        ProxyLauncher.run(SubtitlesViewApplication.class, args);\n    }\n\n    @Override\n    public void handleEvent(StageReadyEvent event) throws IOException, AWTException {\n        super.handleEvent(event);\n        log.info(\"{} 启动成功! 耗时: {} ms\", applicationName, System.currentTimeMillis() - timeMillis);\n    }\n\n    @Override\n    public void loadFXMLBefore(Stage stage, JFXApplication property) {\n        //stage存入单例池\n        Singleton.put(stage);\n        super.loadFXMLBefore(stage, property);\n    }\n\n    @Override\n    public void initAfter(Stage stage) {\n        stage.getScene().setFill(null);\n        //监听全屏状态，切换样式\n        stage.fullScreenProperty().addListener((observableValue, aBoolean, t1) -> {\n            stage.getScene().getRoot().getStyleClass().remove(t1 ?\n                    StyleClassConstant.NORMAL_SCREEN : StyleClassConstant.FULL_SCREEN);\n            stage.getScene().getRoot().getStyleClass().add(t1 ?\n                    StyleClassConstant.FULL_SCREEN : StyleClassConstant.NORMAL_SCREEN);\n        });\n        super.initAfter(stage);\n    }\n\n    @Override\n    public void registerOsThemeDetector(OsThemeDetector detector, Stage stage, JFXApplication property) {\n        Parent root = stage.getScene().getRoot();\n        if (StrUtil.isNotEmpty(property.darkStyleClass())) {\n            detector.registerListener(isDark -> {\n                if (config.getTheme() == null) {\n                    switchTheme(detector, root, property, isDark);\n                }\n            });\n            //监听主题切换事件\n            stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event ->\n                    switchTheme(detector, root, property, event.isDark()));\n            //初始主题\n            switchTheme(detector, root, property, config.getTheme());\n        }\n    }\n\n    private void switchTheme(OsThemeDetector detector, Parent root, JFXApplication property, Boolean isDark) {\n        if (isDark != null) {\n            if (isDark) {\n                if (!root.getStyleClass().contains(property.darkStyleClass())) {\n                    root.getStyleClass().add(property.darkStyleClass());\n                }\n            } else {\n                root.getStyleClass().remove(property.darkStyleClass());\n            }\n            config.setCurrentTheme(isDark);\n        } else {\n            switchTheme(detector, root, property, detector.isDark());\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java",
    "content": "package org.fordes.subtitles.view.config;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ClassPathResource;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.json.JSONUtil;\nimport cn.hutool.setting.yaml.YamlUtil;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport javafx.scene.text.Font;\nimport lombok.Data;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\n\n/**\n * @author fordes on 2022/4/17\n */\n@Data\n@Component\n@ConfigurationProperties(prefix = \"config\")\npublic class ApplicationConfig implements Serializable {\n\n    /**\n     * 主题模式 false-浅色、true-深色、null-跟随系统\n     */\n    private Boolean theme = null;\n\n    /**\n     * 字体\n     */\n    private String fontFace = Font.getDefault().getFamily();;\n\n    /**\n     * 字体大小\n     */\n    private Integer fontSize = 18;\n\n    /**\n     * 编辑模式 false-简洁模式、true-完整模式\n     */\n    private Boolean editMode = Boolean.FALSE;\n\n    /**\n     * 退出模式 false-直接退出、true-最小化至托盘\n     */\n    private Boolean exitMode = Boolean.FALSE;\n\n    /**\n     * 默认文件输出路径\n     */\n    private String outPath = CommonConstant.PATH_HOME;\n\n    /**\n     * 语言列表选项 false-完整、true-精简\n     */\n    private Boolean languageListMode = Boolean.TRUE;\n\n    @TableField(exist = false)\n    private boolean currentTheme;\n\n    private static final long serialVersionUID = 1L;\n\n    private static final ClassPathResource resource = new ClassPathResource(\"application.yml\");\n\n    /**\n     * 写入配置文件\n     */\n    public void dump() {\n        Dict all = YamlUtil.load(resource.getReader(Charset.defaultCharset()));\n        all.put(\"config\", JSONUtil.parseObj(this));\n        YamlUtil.dump(all, FileUtil.getWriter(resource.getFile(), Charset.defaultCharset(), false));\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java",
    "content": "package org.fordes.subtitles.view.config;\n\nimport cn.hutool.core.thread.ExecutorBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * @author fordes on 2022/7/11\n */\n@Configuration\npublic class ExecutorConfig {\n\n    private final static int core = Runtime.getRuntime().availableProcessors();\n\n    @Bean(\"globalExecutor\")\n    public ThreadPoolExecutor globalExecutor() {\n        return ExecutorBuilder.create()\n                .setCorePoolSize(2 * core)\n                .setMaxPoolSize(2 * core)\n                .setHandler(new ThreadPoolExecutor.CallerRunsPolicy())\n                .build();\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java",
    "content": "package org.fordes.subtitles.view.constant;\n\n/**\n * @author fordes on 2022/1/24\n */\npublic class CommonConstant {\n\n    public static final double SCENE_MIN_WIDTH = 1050.0;\n\n    public static final double SCENE_MIN_HEIGHT = 700.0;\n\n    public static final double SIDE_BAR_WIDTH = 250.0;\n\n    public static final String PREFIX = \"*.\";\n\n    public static final String TITLE_ALL_FILE = \"选择文件以开始\";\n\n    public static final String TITLE_PATH = \"选择文件路径\";\n\n    public static final String PATH_HOME = System.getProperty(\"user.home\");\n\n    public static final String ROOT_PATH = System.getProperty(\"user.dir\");\n\n    public static final String TEMP_PATH = ROOT_PATH+ \"\\\\temp\\\\\";\n\n    public static final String DOWNLOAD_PATH = TEMP_PATH+ \"download\\\\\";\n\n//    public static final String FILE_PATH = TEMP_PATH+ \"file\\\\\";\n\n//    public static final String LIB_PATH = ROOT_PATH+  \"\\\\lib\\\\\";\n//\n//    public static final String SEVEN_ZIP_PATH = LIB_PATH+ \"7z\";\n\n    /**\n     * 7z解压命令 递归子目录、全部解压到指定文件夹、只解压指定格式文件\n     */\n//    public static final String UN_ARCHIVE_COMMAND_FORMAT = SEVEN_ZIP_PATH+ \" e -aoa -bse0 -r {} -o{} {} -y\";\n\n    public static final String CONCISE_MODE = \"简洁模式\";\n\n    public static final String FULL_MODE = \"完整模式\";\n\n    public static final String TRANSLATE_REPLACE =  \"替换模式\";\n\n    public static final String TRANSLATE_BILINGUAL =  \"双语模式\";\n\n    public static final String URL_HOME = \"https://github.com/fordes123/subtitles-view\";\n\n    public static final String URL_ISSUES = URL_HOME + \"/issues\";\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java",
    "content": "package org.fordes.subtitles.view.constant;\n\n/**\n * @author fordes on 2022/1/23\n */\npublic class StyleClassConstant {\n\n    public static final String NORMAL_SCREEN = \"normal-screen\";\n\n    public static final String FULL_SCREEN = \"full-screen\";\n\n    public static final String SUBTITLE_SEARCH_ENGINE_ITEM = \"item\";\n\n    public static final String SUBTITLE_SEARCH_ENGINE = \"engine\";\n\n    public static final String QUICK_START_FILE_CHOOSE_WARNING = \"warning\";\n\n    public static final String QUICK_START_FILE_CHOOSE_ERROR = \"error\";\n\n    public static final String QUICK_START_FILE_CHOOSE_SUCCESS = \"success\";\n\n    public static final String SUBTITLE_SEARCH_ITEM = \"search-item\";\n\n    public static final String SUBTITLE_SEARCH_ITEM_CAPTION = \"caption\";\n\n    public static final String SUBTITLE_SEARCH_ITEM_TEXT = \"text\";\n\n    public static final String CONTENT_EXCLUSIVE = \"content-exclusive\";\n\n    public static final String FONT_STYLE_TEMPLATE = \"-fx-font-size: {};-fx-font-family: {}\";\n\n    public static final String ERROR = \"error\";\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.fxml.Initializable;\nimport javafx.scene.Scene;\nimport javafx.scene.layout.Pane;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.Resource;\nimport java.net.URL;\nimport java.util.ResourceBundle;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * 控制器抽象，继承并实现delayInit()方法即可在面板首次显示时进行初始化操作\n *\n * @author fordes on 2022/4/22\n */\n@Component\npublic abstract class DelayInitController implements Initializable {\n\n    @FXML\n    public Pane root;\n\n    @Resource\n    public ThreadPoolExecutor globalExecutor;\n\n    private boolean isInit = false;\n\n    @Override\n    public void initialize(URL url, ResourceBundle resourceBundle) {\n        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n            if (!isInit && t1) {\n                delay();\n                isInit = true;\n            }\n        });\n        globalExecutor.execute(this::async);\n    }\n\n    public Scene getScene() {\n        return root.getScene();\n    }\n\n    /**\n     * 懒加载，在面板首次显示时执行\n     */\n    public void delay() {};\n\n    /**\n     * 异步方法，在线程池中执行，避免主线程阻塞\n     */\n    public void async() {};\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/EditTool.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONUtil;\nimport com.jfoenix.controls.JFXComboBox;\nimport javafx.event.ActionEvent;\nimport javafx.fxml.FXML;\nimport javafx.scene.Parent;\nimport javafx.scene.control.CheckMenuItem;\nimport javafx.scene.control.ChoiceBox;\nimport javafx.scene.control.TextField;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.layout.GridPane;\nimport javafx.scene.text.Font;\nimport javafx.stage.Stage;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.config.ApplicationConfig;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.enums.EditToolEventEnum;\nimport org.fordes.subtitles.view.enums.FileEnum;\nimport org.fordes.subtitles.view.enums.ServiceType;\nimport org.fordes.subtitles.view.event.EditToolEvent;\nimport org.fordes.subtitles.view.event.LoadingEvent;\nimport org.fordes.subtitles.view.event.ToastChooseEvent;\nimport org.fordes.subtitles.view.event.ToastConfirmEvent;\nimport org.fordes.subtitles.view.factory.TranslateServiceFactory;\nimport org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.model.PO.Language;\nimport org.fordes.subtitles.view.service.InterfaceService;\nimport org.fordes.subtitles.view.service.translate.TranslateService;\nimport org.fordes.subtitles.view.utils.CacheUtil;\nimport org.fordes.subtitles.view.utils.SubtitleUtil;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSTime;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime;\nimport org.fxmisc.richtext.StyleClassedTextArea;\nimport org.fxmisc.richtext.model.TwoDimensional;\nimport org.mozilla.universalchardet.Constants;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\n\nimport java.time.LocalTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * 编辑工具 控制器\n *\n * @author fordes on 2022/7/15\n */\n@Slf4j\n@Component\npublic class EditTool extends DelayInitController {\n\n    private static Subtitle subtitle;\n\n    private static StyleClassedTextArea area;\n\n    private static ToggleButton editMode;\n\n    private static int max;\n\n    private static final Map<EditToolEventEnum, GridPane> bindMap = MapUtil.newHashMap();\n\n    @FXML\n    private CheckMenuItem search_case, search_regex, replace_case, replace_regex;\n\n    @FXML\n    private JFXComboBox<String> code_choice, font_family;\n\n    @FXML\n    private ChoiceBox<AvailableServiceInfo> translate_source;\n\n    @FXML\n    private ChoiceBox<String> translate_mode;\n\n    @FXML\n    private JFXComboBox<Language> translate_original, translate_target;\n\n    @FXML\n    private JFXComboBox<Integer> font_size;\n\n    @FXML\n    private ChoiceBox<TimelineType> timeline_option;\n\n    @FXML\n    private TextField timeline_input, jump_input, search_input, replace_input, replace_find_input;\n\n    private final InterfaceService interfaceService;\n\n    private final SidebarBottom sidebarBottom;\n\n    private final ApplicationConfig config;\n\n    @Autowired\n    public EditTool(InterfaceService interfaceService,\n                    SidebarBottom sidebarBottom, ApplicationConfig config) {\n        this.interfaceService = interfaceService;\n        this.sidebarBottom = sidebarBottom;\n        this.config = config;\n    }\n\n    @Override\n    public void delay() {\n\n        //编码选择框\n        code_choice.getItems().addAll(Arrays.stream(ReflectUtil.getFieldsValue(Constants.class))\n                .map(Object::toString).toArray(String[]::new));\n        //初始化字体大小\n        font_size.getItems().addAll(CollUtil.newArrayList(12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72));\n        //初始化字体列表\n        font_family.getItems().addAll(Font.getFontNames());\n        //时间轴校正选项\n        timeline_option.getItems().addAll(TimelineType.values());\n        timeline_option.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1)\n                -> timeline_input.setPromptText(t1.desc));\n        timeline_option.getSelectionModel().select(0);\n        timeline_input.textProperty().addListener((observableValue, s, t1)\n                -> timeline_input.getStyleClass().remove(\"error\"));\n        //翻译相关\n        translate_original.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1) -> {\n            if (t1 != null) {\n                Collection<Language> gap = CollUtil.subtract(config.getLanguageListMode() ?\n                        t1.getTarget().stream().filter(e -> e.isGeneral() == config.getLanguageListMode()).collect(Collectors.toList()) :\n                        t1.getTarget(), translate_target.getItems());\n                Collection<Language> neg = CollUtil.subtract(translate_target.getItems(), t1.getTarget());\n                if (!gap.isEmpty()) {\n                    translate_target.getItems().addAll(gap);\n                }\n                if (!neg.isEmpty()) {\n                    translate_target.getItems().removeAll(neg);\n                }\n            } else translate_target.getItems().clear();\n        });\n        translate_source.getSelectionModel().selectedItemProperty()\n                .addListener((observableValue, availableServiceInfo, t1) -> {\n                    if (t1 != null) {\n                        translate_original.getItems().clear();\n                        translate_original.getItems().addAll(CacheUtil.getLanguageDict(ServiceType.TRANSLATE, t1.getProvider(), config.getLanguageListMode()));\n                        translate_original.getSelectionModel().selectFirst();\n                    }\n                });\n        translate_source.getItems().clear();\n        translate_source.getItems().addAll(interfaceService.getAvailableService(ServiceType.TRANSLATE));\n        translate_source.getSelectionModel().selectFirst();\n        translate_mode.getItems().addAll(CommonConstant.TRANSLATE_REPLACE, CommonConstant.TRANSLATE_BILINGUAL);\n        translate_mode.getSelectionModel().selectFirst();\n        //回车提交操作\n        timeline_input.setOnAction(this::applyTimeline);\n        jump_input.setOnAction(this::applyJump);\n        search_input.setOnAction(this::applySearch);\n        replace_find_input.setOnAction(this::applyReplaceFind);\n        replace_input.setOnAction(this::applyReplaceNext);\n        //错误输入\n        jump_input.textProperty().addListener((observableValue, s, t1)\n                -> jump_input.getStyleClass().remove(StyleClassConstant.ERROR));\n        search_input.textProperty().addListener((observableValue, s, t1)\n                -> search_input.getStyleClass().remove(StyleClassConstant.ERROR));\n        timeline_input.textProperty().addListener((observableValue, s, t1)\n                -> timeline_input.getStyleClass().remove(StyleClassConstant.ERROR));\n\n    }\n\n    @Override\n    public void async() {\n        Stage stage = Singleton.get(Stage.class);\n        //各工具面板互斥\n        root.getChildren().forEach(node -> {\n                    if (node instanceof GridPane) {\n                        EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData());\n                        bindMap.put(type, (GridPane) node);\n                        node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n                            if (t1) {\n                                bindMap.values().forEach(e -> e.setVisible(node.equals(e)));\n                                root.setVisible(true);\n                            }\n                        });\n                    }\n                }\n        );\n\n        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n            if (!t1) {\n                bindMap.values().forEach(e -> e.setVisible(false));\n            }\n        });\n        //监听编辑工具事件 唤起对应功能面板\n        stage.addEventHandler(EditToolEvent.EVENT_TYPE, event -> {\n\n            subtitle = event.getSubtitle();\n            area = event.getSource();\n            editMode = event.getEditMode();\n            Parent parent = bindMap.get(event.getType());\n\n            switch (event.getType()) {\n                case SEARCH: //搜索\n                    search_input.requestFocus();\n                    parent.setVisible(true);\n                    break;\n\n                case REPLACE://替换\n                    replace_find_input.requestFocus();\n                    parent.setVisible(true);\n                    break;\n\n                case JUMP://跳转\n                    jump_input.requestFocus();\n                    max = 0;\n                    for (TimedLine timedLine : subtitle.getTimedTextFile().getTimedLines()) {\n                        max += timedLine.getTextLines().size();\n                    }\n                    parent.setVisible(true);\n                    break;\n\n                case FONT: //字体（样式）\n//                    font_family.getSelectionModel().select(config.getFontFace());\n                    font_size.getSelectionModel().select(config.getFontSize());\n                    parent.setVisible(true);\n                    break;\n\n                case TIMELINE: //时间轴\n                    TimedLine start = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines());\n                    timeline_input.setPromptText(start.getTime().getStart().toString());\n                    timeline_input.requestFocus();\n                    parent.setVisible(true);\n                    break;\n\n                case CODE://编码\n                    code_choice.getSelectionModel().select(subtitle.getCharset());\n                    parent.setVisible(true);\n                    break;\n\n                case TRANSLATE:\n                    List<AvailableServiceInfo> list = interfaceService.getAvailableService(ServiceType.TRANSLATE);\n                    if (list.isEmpty()) {\n                        stage.fireEvent(new ToastChooseEvent(\"未配置翻译服务\", \"是否立即转到设置?\",\n                                \"确定\", () -> sidebarBottom.getSetting().getOnAction().handle(null)));\n                        parent.setVisible(false);\n                    } else {\n                        Collection<AvailableServiceInfo> gap = CollUtil.subtract(list, translate_source.getItems());\n                        Collection<AvailableServiceInfo> neg = CollUtil.subtract(translate_source.getItems(), list);\n                        if (!gap.isEmpty()) {\n                            translate_source.getItems().addAll(gap);\n                        }\n                        if (!neg.isEmpty()) {\n                            translate_source.getItems().removeAll(neg);\n                        }\n                        parent.setVisible(true);\n                    }\n                    break;\n\n                case REF: //刷新\n                    try {\n                        SubtitleUtil.parse(subtitle);\n                        area.clear();\n                        area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), \"styled-text-area\");\n                        area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,config.getFontSize(), config.getFontFace()));\n                    } catch (Exception e) {\n                        log.error(ExceptionUtil.stacktraceToString(e));\n                        stage.fireEvent(new ToastConfirmEvent(\"编码更改出错\", \"已切换回原编码~\"));\n                    }\n                    break;\n            }\n        });\n\n    }\n\n    @FXML\n    private void onClose(ActionEvent actionEvent) {\n        actionEvent.consume();\n        area = null;\n        subtitle = null;\n        editMode = null;\n        root.setVisible(false);\n    }\n\n    @FXML\n    private void applyCode(ActionEvent actionEvent) {\n        String original = subtitle.getCharset();\n        try {\n            subtitle.setCharset(code_choice.getSelectionModel().getSelectedItem());\n            SubtitleUtil.parse(subtitle);\n            area.clear();\n            area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), StrUtil.EMPTY);\n        } catch (Exception e) {\n            log.error(ExceptionUtil.stacktraceToString(e));\n            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"编码更改出错\", \"已切换回原编码~\"));\n            subtitle.setCharset(original);\n            code_choice.getSelectionModel().select(original);\n        }\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyFont(ActionEvent actionEvent) {\n        Stage stage = Singleton.get(Stage.class);\n\n        String originalFontFamily = config.getFontFace();\n        Integer originalFontSize = config.getFontSize();\n        try {\n            config.setFontSize(Convert.toInt(font_size.getValue()));\n            config.setFontFace(font_family.getValue());\n            area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,\n                    config.getFontSize(), config.getFontFace()));\n            area.requestFocus();\n        } catch (Exception e) {\n            log.error(ExceptionUtil.stacktraceToString(e));\n            config.setFontSize(originalFontSize);\n            config.setFontFace(originalFontFamily);\n            font_family.setValue(originalFontFamily);\n            font_size.setValue(originalFontSize);\n            stage.fireEvent(new ToastConfirmEvent(\"字体更改出错\", \"已切换回原字体~\"));\n        }\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyTimeline(ActionEvent actionEvent) {\n        LocalTime newTime = null;\n        String timeLine = timeline_input.getText();\n        TimelineType option = timeline_option.getValue();\n        if (TimelineType.TIMELINE.equals(option)) {\n            try {\n                newTime = FileEnum.SRT.equals(subtitle.getFormat()) ?\n                        SRTTime.fromString(timeLine) : ASSTime.fromString(timeLine);\n            } catch (Exception ignored) {\n            }\n\n        } else {\n            if (NumberUtil.isInteger(timeLine)) {\n                int offset = Convert.toInt(timeLine);\n                LocalTime date = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines()).getTime().getStart();\n                newTime = date.plus(offset, option.rate);\n            }\n        }\n        if (newTime != null) {\n            //TODO 按选中范围处理 待支持\n            TimedTextFile original = subtitle.getTimedTextFile();\n            try {\n                TimedTextFile target = SubtitleUtil\n                        .revise(subtitle.getTimedTextFile(), newTime, null, editMode.isSelected());\n                subtitle.setTimedTextFile(target);\n                SubtitleUtil.write(subtitle, success -> {\n                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(!success));\n                    if (success) {\n                        area.clear();\n                        area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(),\n                                editMode.isSelected()), StrUtil.EMPTY);\n                    } else throw new RuntimeException(\"写入失败\");\n                });\n            } catch (Exception e) {\n                log.error(ExceptionUtil.stacktraceToString(e));\n                subtitle.setTimedTextFile(original);\n                Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"时间轴更改出错\", \"已切换回原时间轴~\"));\n            }\n        } else timeline_input.getStyleClass().add(StyleClassConstant.ERROR);\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyJump(ActionEvent actionEvent) {\n        String text = jump_input.getText();\n        int value = NumberUtil.isInteger(text) ? NumberUtil.parseInt(text) : 0;\n\n        if (value > 0 && value <= max) {\n            TwoDimensional.Position position = area.position(value, 1);\n            area.moveTo(position.toOffset());\n            area.requestFollowCaret();\n        } else {\n            jump_input.getStyleClass().add(StyleClassConstant.ERROR);\n        }\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applySearch(ActionEvent actionEvent) {\n        String str = search_input.getText();\n        if (StrUtil.isNotBlank(str)) {\n            SubtitleUtil.search(area, str, search_case.isSelected(), search_regex.isSelected());\n        } else search_input.getStyleClass().add(StyleClassConstant.ERROR);\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyReplaceNext(ActionEvent actionEvent) {\n        applyReplace(false);\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyReplaceAll(ActionEvent actionEvent) {\n        applyReplace(true);\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void applyReplaceFind(ActionEvent actionEvent) {\n        String str = replace_find_input.getText();\n        if (StrUtil.isNotBlank(str)) {\n            SubtitleUtil.find(area, str, replace_case.isSelected(), replace_regex.isSelected());\n        }\n        actionEvent.consume();\n    }\n\n    private void applyReplace(boolean isAll) {\n        if (editMode.isSelected()) {\n            String replaceText = replace_input.getText();\n            String searchText = replace_find_input.getText();\n            if (StrUtil.isAllNotBlank(replaceText, searchText)) {\n                try {\n                    SubtitleUtil.replace(area, subtitle, searchText, replaceText, isAll,\n                            replace_case.isSelected(), replace_regex.isSelected());\n                } catch (Exception e) {\n                    log.error(ExceptionUtil.stacktraceToString(e));\n                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"替换出错\", \"已切换回原文本~\"));\n                }\n            }\n        } else Singleton.get(Stage.class).fireEvent(new ToastChooseEvent(\"操作受限\", \"是否切换至完整模式?\",\n                \"切换\", () -> editMode.setSelected(true)));\n    }\n\n    @FXML\n    private void applyTranslate(ActionEvent actionEvent) {\n        AvailableServiceInfo source = translate_source.getValue();\n        boolean mode = StrUtil.equals(CommonConstant.TRANSLATE_BILINGUAL, translate_mode.getValue());\n        Language origin = translate_original.getValue();\n        Language target = translate_target.getValue();\n        if (source != null && origin != null && target != null) {\n\n            TranslateService service = TranslateServiceFactory.getService(source.getProvider().getValue());\n            Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));\n            globalExecutor.execute(() -> service.translate(subtitle, target.getCode(), origin.getCode(),\n                    source.getVersionInfo(), mode, JSONUtil.parseObj(source.getAuth())));\n        }\n        actionEvent.consume();\n    }\n\n\n    /**\n     * 时间轴校正 操作类型枚举\n     */\n    @AllArgsConstructor\n    enum TimelineType {\n\n        TIMELINE(\"时间轴\", null, \"形如: xx:xx:xx:xx\"),\n        SECOND(\"秒\", ChronoUnit.SECONDS, \"整数，时间偏移量\"),\n        MILLISECOND(\"毫秒\", ChronoUnit.MILLIS, \"整数，时间偏移量\"),\n        MINUTE(\"分钟\", ChronoUnit.MINUTES, \"整数，时间偏移量\"),\n        HOUR(\"小时\", ChronoUnit.HOURS, \"整数，的时间偏移量\");\n\n        public final String name;\n        public final ChronoUnit rate;\n        public final String desc;\n\n        @Override\n        public String toString() {\n            return this.name;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Export.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Component\npublic class Export {\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/MainController.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.fxml.FXML;\nimport javafx.scene.Cursor;\nimport javafx.scene.Parent;\nimport javafx.scene.control.Label;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.ColumnConstraints;\nimport javafx.scene.layout.GridPane;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.enums.FontIcon;\nimport org.fordes.subtitles.view.event.FileOpenEvent;\nimport org.fordes.subtitles.view.event.LoadingEvent;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/1/19\n */\n@Slf4j\n@Component\npublic class MainController extends DelayInitController {\n\n    @FXML\n    private StackPane loading;\n\n    @FXML\n    private ColumnConstraints sidebarColumn;\n\n    @FXML\n    private Label drawer;\n\n    @FXML\n    private SidebarBefore sidebarBeforeController;\n\n    @FXML\n    private SidebarAfter sidebarAfterController;\n\n    @FXML\n    private SidebarBottom sidebarBottomController;\n\n    @FXML\n    private GridPane content;\n\n    @FXML\n    private Parent quickStart, subtitleSearch, toolBox, setting, export, mainEditor, syncEditor, voiceConvert,\n            sidebarBefore, sidebarAfter;\n\n    private static double xOffset = 0;\n    private static double yOffset = 0;\n    private static int bit = 0;\n    private final static double RESIZE_WIDTH = 5.00;\n\n    @Override\n    public void delay() {\n        content.getChildren().forEach(node ->\n                node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n                    if (t1) {\n                        content.getChildren().forEach(e -> e.setVisible(e.equals(node)));\n                    }\n                }));\n    }\n\n    @Override\n    public void async() {\n        //绑定侧边按键和对应面板显示\n        sidebarBeforeController.getQuickStart().setOnAction(event -> {\n            sidebarBeforeController.getQuickStart().setSelected(true);\n            quickStart.setVisible(true);\n        });\n        sidebarBeforeController.getSubtitleSearch().setOnAction(event -> {\n            sidebarBeforeController.getSubtitleSearch().setSelected(true);\n            subtitleSearch.setVisible(true);\n        });\n        sidebarBeforeController.getToolBox().setOnAction(event -> {\n            sidebarBeforeController.getToolBox().setSelected(true);\n            toolBox.setVisible(true);\n        });\n        sidebarAfterController.getMainEditor().setOnAction(event -> {\n            sidebarAfterController.getMainEditor().setSelected(true);\n            mainEditor.setVisible(true);\n        });\n        sidebarAfterController.getSyncEditor().setOnAction(event -> {\n            sidebarAfterController.getSyncEditor().setSelected(true);\n            syncEditor.setVisible(true);\n        });\n        sidebarAfterController.getExport().setOnAction(event -> {\n            sidebarAfterController.getExport().setSelected(true);\n            export.setVisible(true);\n        });\n        sidebarBottomController.getSetting().setOnAction(event -> {\n            setting.setVisible(true);\n            sidebarAfterController.getItemGroup().selectToggle(null);\n            sidebarBeforeController.getItemGroup().selectToggle(null);\n        });\n\n        content.getChildren().forEach(node ->\n                node.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n                    if (t1) {\n                        content.getChildren().forEach(e -> e.setVisible(e.equals(node)));\n                    }\n                }));\n\n\n        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {\n            if (fileOpenEvent.getRecord().getFormat().media) {\n                sidebarAfterController.getItemGroup().selectToggle(null);\n                sidebarBeforeController.getItemGroup().selectToggle(null);\n            }else {\n                sidebarBefore.setVisible(false);\n                sidebarAfter.setVisible(true);\n            }\n        });\n\n        Singleton.get(Stage.class).addEventHandler(LoadingEvent.EVENT_TYPE, loadingEvent\n                -> loading.setVisible(loadingEvent.isAlive()));\n    }\n\n    @FXML\n    private void mousePressedHandle(MouseEvent event) {\n        event.consume();\n        xOffset = event.getSceneX();\n        yOffset = event.getSceneY();\n    }\n\n    @FXML\n    private void mouseMoveHandle(MouseEvent event) {\n        event.consume();\n        double x = event.getSceneX();\n        double y = event.getSceneY();\n        double width = Singleton.get(Stage.class).getWidth() - 20;\n        double height = Singleton.get(Stage.class).getHeight() - 20;\n        Cursor cursorType = Cursor.DEFAULT;\n        bit = 0;\n        if (y >= height - RESIZE_WIDTH) {\n            if (x <= RESIZE_WIDTH) {\n                bit |= 1 << 3;\n            } else if (x >= width - RESIZE_WIDTH) {\n                bit |= 1;\n                bit |= 1 << 2;\n                cursorType = Cursor.SE_RESIZE;\n            } else {\n                bit |= 1;\n                cursorType = Cursor.S_RESIZE;\n            }\n        } else if (x >= width - RESIZE_WIDTH) {\n            bit |= 1 << 2;\n            cursorType = Cursor.E_RESIZE;\n        }\n        getScene().getRoot().setCursor(cursorType);\n    }\n\n    @FXML\n    private void mouseDraggedHandle(MouseEvent event) {\n        Stage stage = Singleton.get(Stage.class);\n        event.consume();\n        double x = event.getSceneX();\n        double y = event.getSceneY();\n        double nextX = stage.getX();\n        double nextY = stage.getY();\n        double nextWidth = stage.getWidth();\n        double nextHeight = stage.getHeight();\n        if ((bit & 1 << 2) != 0) {\n            nextWidth = x;\n        }\n        if ((bit & 1) != 0) {\n            nextHeight = y;\n        }\n        if (nextWidth <= CommonConstant.SCENE_MIN_WIDTH) {\n            nextWidth = CommonConstant.SCENE_MIN_WIDTH;\n        }\n        if (nextHeight <= CommonConstant.SCENE_MIN_HEIGHT) {\n            nextHeight = CommonConstant.SCENE_MIN_HEIGHT;\n        }\n        stage.setX(nextX);\n        stage.setY(nextY);\n        stage.setWidth(nextWidth);\n        stage.setHeight(nextHeight);\n    }\n\n    @FXML\n    private void titleBarDraggedHandle(MouseEvent event) {\n        Stage stage = Singleton.get(Stage.class);\n        stage.setX(event.getScreenX() - xOffset);\n        stage.setY(event.getScreenY() - yOffset);\n        event.consume();\n    }\n\n    @FXML\n    private void onDrawer(MouseEvent event) {\n        if (sidebarColumn.getPrefWidth() > 0) {\n            sidebarColumn.setPrefWidth(0);\n            drawer.setText(FontIcon.PLACE_THE_LEFT.toString());\n            content.getStyleClass().add(StyleClassConstant.CONTENT_EXCLUSIVE);\n        } else {\n            sidebarColumn.setPrefWidth(CommonConstant.SIDE_BAR_WIDTH);\n            drawer.setText(FontIcon.PLACE_THE_RIGHT.toString());\n            content.getStyleClass().remove(StyleClassConstant.CONTENT_EXCLUSIVE);\n        }\n        event.consume();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/MainEditor.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport javafx.application.Platform;\nimport javafx.event.ActionEvent;\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.GridPane;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.RowConstraints;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.config.ApplicationConfig;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.enums.EditToolEventEnum;\nimport org.fordes.subtitles.view.enums.FontIcon;\nimport org.fordes.subtitles.view.event.*;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.utils.SubtitleUtil;\nimport org.fxmisc.richtext.StyleClassedTextArea;\nimport org.fxmisc.richtext.model.TwoDimensional;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Slf4j\n@Component\npublic class MainEditor extends DelayInitController {\n\n    @FXML\n    private GridPane editTool;\n\n    @FXML\n    private Label indicator, editModeIcon;\n\n    @FXML\n    private ToggleButton editMode;\n\n    @FXML\n    private StyleClassedTextArea editor;\n\n    @FXML\n    private HBox toolbarPanel;\n\n    @FXML\n    private RowConstraints toolbarRow;\n\n    private Subtitle subtitle;\n\n    private final ApplicationConfig config;\n\n    @Autowired\n    public MainEditor(ApplicationConfig config) {\n        this.config = config;\n    }\n\n    @Override\n    public void delay() {\n        Stage stage = Singleton.get(Stage.class);\n\n        //工具栏按钮，点击按钮发送编辑工具事件 唤起编辑工具\n        toolbarPanel.getChildren().forEach(node -> {\n            if (node.getUserData() != null) {\n                node.setOnMouseClicked(event -> {\n                    if (node.getUserData() != null) {\n                        EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData());\n                        stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, type));\n                    }\n                });\n            }\n        });\n\n        //编辑模式监听\n        editMode.selectedProperty().addListener((observableValue, aBoolean, t1) -> {\n            ctrlEditMode(t1);\n            editor.clear();\n            editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), t1), StrUtil.EMPTY);\n        });\n        //行列号监听\n        editor.caretPositionProperty().addListener((observable, oldValue, newValue) -> {\n            TwoDimensional.Position position = editor.offsetToPosition(newValue, TwoDimensional.Bias.Backward);\n            indicator.setText(StrUtil.format((String) indicator.getUserData(), position.getMajor(), position.getMinor()));\n        });\n\n//        stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event -> {\n//            editor.setStyleClass(0, editor.getLength(),  config.isCurrentTheme()? \"richtext_dark\":\"richtext_light\");\n//        });\n\n        stage.addEventHandler(TranslateEvent.EVENT_TYPE, event -> {\n\n            if (TranslateEvent.SUCCESS.equals(event.getMsg())) {\n                editor.clear();\n                editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(),\n                        editMode.isSelected()), \"styled-text-area\");\n                editor.moveTo(0);\n            }\n            Platform.runLater(() -> {\n                stage.fireEvent(new ToastConfirmEvent(event.getMsg(), event.getDetail()));\n                stage.fireEvent(new LoadingEvent(false));\n            });\n\n        });\n\n        //快捷键\n        KeyCodeCombination ctrlT = new KeyCodeCombination(KeyCode.T, KeyCodeCombination.CONTROL_DOWN);\n        stage.getScene().getAccelerators().put(ctrlT, this::ctrlToolbar);\n\n        KeyCodeCombination ctrlF = new KeyCodeCombination(KeyCode.F, KeyCodeCombination.CONTROL_DOWN);\n        stage.getScene().getAccelerators().put(ctrlF, ()\n                -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.SEARCH)));\n\n        KeyCodeCombination ctrlR = new KeyCodeCombination(KeyCode.R, KeyCodeCombination.CONTROL_DOWN);\n        stage.getScene().getAccelerators().put(ctrlR, ()\n                -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.REPLACE)));\n\n    }\n\n    @Override\n    public void async() {\n        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {\n            if (fileOpenEvent.getRecord().getFormat().subtitle) {\n                subtitle = (Subtitle) fileOpenEvent.getRecord();\n                log.debug(\"主编辑器 => {}\", subtitle.getFile().getPath());\n                try {\n                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));\n                    SubtitleUtil.parse(subtitle);\n                    root.setVisible(true);\n                } catch (Exception e) {\n                    log.error(ExceptionUtil.stacktraceToString(e));\n                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"读取失败！\", \"字幕文件已经损坏\"));\n                } finally {\n                    Singleton.get(Stage.class).fireEvent(new LoadingEvent(false));\n                }\n            }\n        });\n\n        //载入设置\n        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n            if (t1) {\n                editor.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,\n                        config.getFontSize(), config.getFontFace()));\n                editor.clear();\n                editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), \"styled-text-area\");\n                //编辑器模式\n                ctrlEditMode(config.getEditMode());\n            }else {\n                editTool.setVisible(false);\n            }\n        });\n    }\n\n    @FXML\n    private void hideToolbar(ActionEvent actionEvent) {\n        ctrlToolbar(false);\n        actionEvent.consume();\n    }\n\n    /**\n     * 控制工具栏显示/隐藏\n     *\n     * @param state 状态\n     */\n    private void ctrlToolbar(boolean state) {\n        toolbarRow.setMaxHeight(state ? 60 : 0);\n        toolbarRow.setMinHeight(state ? 60 : 0);\n        toolbarRow.setPrefHeight(state ? 60 : 0);\n        toolbarPanel.setVisible(state);\n    }\n\n    private void ctrlToolbar() {\n        ctrlToolbar(!toolbarPanel.isVisible());\n    }\n\n    private void ctrlEditMode(Boolean mode) {\n        if (mode == null) {\n            mode = config.getEditMode();\n        } else {\n            config.setEditMode(mode);\n        }\n        editModeIcon.setText(mode ?\n                FontIcon.SWITCH_ON_DARK.toString() :\n                FontIcon.SWITCH_OFF_DARK.toString());\n        editMode.setText(mode ? CommonConstant.FULL_MODE : CommonConstant.CONCISE_MODE);\n        editMode.setSelected(mode);\n    }\n\n    @FXML\n    private void changeEditMode(ActionEvent actionEvent) {\n        actionEvent.consume();\n    }\n\n    @FXML\n    private void onIndicatorClicked(MouseEvent mouseEvent) {\n        Singleton.get(Stage.class).fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.JUMP));\n        mouseEvent.consume();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/QuickStart.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Singleton;\nimport javafx.event.ActionEvent;\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Label;\nimport javafx.scene.input.DragEvent;\nimport javafx.scene.input.Dragboard;\nimport javafx.scene.input.TransferMode;\nimport javafx.scene.layout.GridPane;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.enums.FileEnum;\nimport org.fordes.subtitles.view.event.FileOpenEvent;\nimport org.fordes.subtitles.view.utils.FileUtils;\nimport org.springframework.stereotype.Component;\n\nimport java.io.File;\n\n/**\n * @author fordes on 2022/2/6\n */\n@Slf4j\n@Component\npublic class QuickStart {\n    @FXML\n    private Label clues;\n\n    @FXML\n    private GridPane root;\n\n    private static File dragFile;\n\n    private static final String UNSUPPORTED_FILE_TYPE = \"不支持的文件类型\";\n\n    private static final String DRAG_SUPPORT = \"松手以打开文件\";\n\n    private static final String TIPS_DEFAULT = \"拖放或选择文件以继续\";\n\n    private static final String OPEN_FILE_ERROR = \"打开文件出错\";\n\n    @FXML\n    private void chooseFile(ActionEvent event) {\n        File file = FileUtils.chooseFile(CommonConstant.TITLE_ALL_FILE, FileEnum.values())\n                .showOpenDialog(Singleton.get(Stage.class));\n\n        //读取文件信息\n        if (FileUtil.exist(file) && FileEnum.isSupport(FileUtil.getSuffix(file))) {\n            Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile));\n        } else {\n            root.getStyleClass().clear();\n            clues.setText(TIPS_DEFAULT);\n        }\n        event.consume();\n    }\n\n    @FXML\n    private void onDragOver(DragEvent dragEvent) {\n        Dragboard db = dragEvent.getDragboard();\n        if (db.hasFiles()) {\n            dragFile = db.getFiles().get(0);\n            if (FileUtil.exist(dragFile) && FileEnum.isSupport(FileUtil.getSuffix(dragFile))) {\n                dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);\n                clues.setText(DRAG_SUPPORT);\n                root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_SUCCESS);\n            } else {\n                clues.setText(UNSUPPORTED_FILE_TYPE);\n                root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_WARNING);\n                dragFile = null;\n            }\n        }\n        dragEvent.consume();\n    }\n\n    @FXML\n    private void onDragExited(DragEvent dragEvent) {\n        clues.setText(TIPS_DEFAULT);\n        root.getStyleClass().clear();\n        dragEvent.consume();\n    }\n\n    @FXML\n    private void onDragDropped(DragEvent dragEvent) {\n        try {\n            if (dragFile != null) {\n                Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile));\n            }\n        } catch (Exception e) {\n            clues.setText(OPEN_FILE_ERROR);\n            root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_ERROR);\n        } finally {\n            dragEvent.consume();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Setting.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.swing.DesktopUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.jfoenix.controls.JFXButton;\nimport com.jfoenix.controls.JFXComboBox;\nimport javafx.fxml.FXML;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.control.ToggleGroup;\nimport javafx.scene.control.Tooltip;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.text.Font;\nimport javafx.scene.text.TextFlow;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.config.ApplicationConfig;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.enums.ServiceType;\nimport org.fordes.subtitles.view.event.ThemeChangeEvent;\nimport org.fordes.subtitles.view.event.ToastConfirmEvent;\nimport org.fordes.subtitles.view.model.PO.ServiceInterface;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.InterfaceService;\nimport org.fordes.subtitles.view.utils.FileUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\n\nimport java.io.File;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Slf4j\n@Component\npublic class Setting extends DelayInitController {\n\n    @FXML\n    private VBox infoPanel;\n\n    @FXML\n    private TextFlow tips;\n\n    @FXML\n    private ToggleGroup themeGroup, editorModeGroup, exitModeGroup, languageListGroup;\n\n    @FXML\n    private JFXComboBox<Version> version;\n\n    @FXML\n    private JFXComboBox<ServiceType> type;\n\n    @FXML\n    private JFXComboBox<ServiceProvider> provider;\n\n    @FXML\n    private JFXComboBox<String> fontFace;\n\n    @FXML\n    private JFXComboBox<Integer> fontSize;\n\n    @FXML\n    private TextField outPath;\n\n    private final InterfaceService interfaceService;\n\n    private final ApplicationConfig config;\n\n    @Autowired\n    public Setting(ApplicationConfig config, InterfaceService interfaceService) {\n        this.config = config;\n        this.interfaceService = interfaceService;\n    }\n\n    @Override\n    public void delay() {\n        Stage stage = Singleton.get(Stage.class);\n        //初始化首选项\n        fontFace.getItems().addAll(Font.getFontNames());\n        fontSize.getItems().addAll(CollUtil.newArrayList(10, 12, 14, 16, 18, 20, 24, 36));\n        applyConfig();\n\n        //首选项监听事件\n        themeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> {\n            Boolean value = Convert.toBool(t1.getUserData());\n            config.setTheme(value);\n            stage.fireEvent(new ThemeChangeEvent(value));\n        });\n\n        editorModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1)\n                -> config.setEditMode(Convert.toBool(t1.getUserData())));\n        exitModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1)\n                -> config.setExitMode(Convert.toBool(t1.getUserData())));\n        fontFace.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1)\n                -> config.setFontFace(t1));\n        fontSize.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1)\n                -> config.setFontSize(t1));\n        outPath.textProperty().addListener((observableValue, s, t1)\n                -> config.setOutPath(StrUtil.trim(t1)));\n        languageListGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) ->\n                config.setLanguageListMode(Convert.toBool(t1.getUserData())));\n\n        //接口类型\n        type.getItems().addAll(ServiceType.values());\n        type.getSelectionModel().selectedItemProperty().addListener((observableValue, type, t1) -> {\n            if (null != t1 && provider.getValue() != null) {\n                version.getItems().clear();\n                version.getItems().addAll(interfaceService.getVersions(t1, provider.getValue()));\n            }\n        });\n\n        //服务商\n        provider.getItems().addAll(ServiceProvider.values());\n        provider.getSelectionModel().selectedItemProperty().addListener((observableValue, supportDto, t1) -> {\n            if (null != t1 && type.getValue() != null) {\n                version.getItems().clear();\n                version.getItems().addAll(interfaceService.getVersions(type.getValue(), t1));\n            }\n        });\n\n        //版本\n        version.getSelectionModel().selectedItemProperty().addListener((observableValue, serviceVersion, t1) -> {\n            if (null != t1) {\n                tips.setVisible(false);\n                version.setTooltip(new Tooltip(t1.getRemark()));\n                buildInfoFrame(interfaceService.getInterface(type.getValue(), provider.getValue()));\n            } else {\n                tips.setVisible(true);\n            }\n        });\n        //提示区\n        tips.visibleProperty().addListener((observableValue, aBoolean, t1) -> infoPanel.setVisible(!t1));\n    }\n\n    @Override\n    public void async() {\n        //监听器用于保存配置\n        root.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n            if (!t1) {\n                if (FileUtil.exist(config.getOutPath())) {\n                    config.setOutPath(outPath.getText().trim());\n                } else {\n                    outPath.setText(config.getOutPath());\n                }\n                config.dump();\n            } else {\n                //每次显示前重新初始化一次\n                applyConfig();\n            }\n        });\n    }\n\n\n    /**\n     * 从配置文件应用设置项\n     */\n    void applyConfig() {\n        //读取配置设置默认值\n        fontFace.getSelectionModel().select(config.getFontFace());\n        fontSize.getSelectionModel().select(config.getFontSize());\n        editorModeGroup.getToggles().forEach(item -> {\n            if (Convert.toBool(item.getUserData()).equals(config.getEditMode())) {\n                item.setSelected(true);\n            }\n        });\n        themeGroup.getToggles().forEach(item -> {\n            if (ObjectUtil.equal(config.getTheme(), Convert.toBool(item.getUserData()))) {\n                item.setSelected(true);\n            }\n        });\n        exitModeGroup.getToggles().forEach(item -> {\n            if (Convert.toBool(item.getUserData()).equals(config.getExitMode())) {\n                item.setSelected(true);\n            }\n        });\n        outPath.setText(config.getOutPath());\n\n    }\n\n    void buildInfoFrame(ServiceInterface info) {\n        infoPanel.getChildren().clear();\n        JSONUtil.parseObj(StrUtil.isBlank(info.getAuth()) ?\n                        info.getTemplate() :\n                        info.getAuth())\n                .forEach((k, v) -> {\n\n                    HBox hBox = new HBox();\n                    hBox.setMinHeight(90);\n                    hBox.setAlignment(Pos.CENTER_LEFT);\n\n                    Label label = new Label(k);\n                    label.setMinSize(120, 90);\n                    label.getStyleClass().add(\"item\");\n                    HBox.setMargin(label, new Insets(0, 0, 0, 30));\n                    hBox.getChildren().add(label);\n\n                    TextField textField = new TextField(ObjectUtil.isNotEmpty(v) ? v.toString() : StrUtil.EMPTY);\n                    textField.getStyleClass().add(\"item\");\n                    textField.setUserData(k);\n                    textField.setMinSize(140, 90);\n                    hBox.getChildren().add(textField);\n                    infoPanel.getChildren().add(hBox);\n                });\n\n        JFXButton save = new JFXButton(\"保存\");\n        save.setPrefSize(80, 30);\n        save.getStyleClass().add(\"normal-button\");\n        save.setUserData(info);\n        save.setOnAction(event -> {\n\n            JSONObject param = new JSONObject();\n            infoPanel.getChildren().forEach(e -> {\n                if (e instanceof TextField) {\n                    param.putOpt((String) e.getUserData(), ((TextField) e).getText());\n                }\n            });\n            ServiceInterface data = (ServiceInterface) save.getUserData();\n            data.setAuth(param.toString());\n            try {\n                if (interfaceService.updateById(info)) {\n                    tips.setVisible(true);\n                    Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"保存成功\", \"接口信息已经保存\"));\n                    return;\n                }\n            } catch (Exception e) {\n                log.error(\"接口信息保存失败 => {}\", JSONUtil.toJsonStr(info));\n                log.error(ExceptionUtil.stacktraceToString(e));\n            }\n            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"保存失败\", \"数据操作失败，错误已记录\"));\n        });\n        HBox hBox = new HBox();\n        hBox.setMinHeight(90);\n        hBox.setAlignment(Pos.CENTER_RIGHT);\n        HBox.setMargin(save, new Insets(0, 30, 0, 0));\n        hBox.getChildren().add(save);\n\n        if (StrUtil.isNotEmpty(info.getPage())) {\n            JFXButton applyFor = new JFXButton(\"去申请\");\n            applyFor.setPrefSize(80, 30);\n            applyFor.getStyleClass().add(\"normal-button\");\n            applyFor.setTooltip(new Tooltip(info.getPage()));\n            applyFor.setOnAction(event -> DesktopUtil.browse(info.getPage()));\n            hBox.getChildren().add(applyFor);\n        }\n        infoPanel.getChildren().add(hBox);\n    }\n\n    @FXML\n    private void onChooseOutPath(MouseEvent event) {\n        File path = FileUtils.choosePath(outPath.getText().trim()).showDialog(Singleton.get(Stage.class));\n        if (path != null && StrUtil.isNotEmpty(path.getPath())) {\n            outPath.setText(path.getPath());\n        }\n        event.consume();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.control.ToggleGroup;\nimport lombok.Getter;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/4/8\n */\n@Component\npublic class SidebarAfter {\n\n    @FXML\n    @Getter\n    private ToggleButton mainEditor, syncEditor, export;\n\n    @FXML\n    @Getter\n    private ToggleGroup itemGroup;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.control.ToggleGroup;\nimport lombok.Getter;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/1/27\n */\n@Component\npublic class SidebarBefore {\n\n    @FXML\n    @Getter\n    private ToggleButton quickStart, subtitleSearch, toolBox;\n\n    @FXML\n    @Getter\n    private ToggleGroup itemGroup;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Button;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/2/1\n */\n@Slf4j\n@Component\npublic class SidebarBottom {\n\n    @FXML\n    @Getter\n    private Button setting;\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Component\npublic class SpeechConversion {\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.swing.DesktopUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.jfoenix.controls.JFXListView;\nimport com.jfoenix.controls.JFXNodesList;\nimport com.jfoenix.controls.JFXTextField;\nimport com.jfoenix.skins.JFXListViewSkin;\nimport com.sun.javafx.scene.control.VirtualScrollBar;\nimport javafx.event.ActionEvent;\nimport javafx.fxml.FXML;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.control.skin.VirtualFlow;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.constant.StyleClassConstant;\nimport org.fordes.subtitles.view.event.FileOpenEvent;\nimport org.fordes.subtitles.view.event.LoadingEvent;\nimport org.fordes.subtitles.view.event.ToastChooseEvent;\nimport org.fordes.subtitles.view.event.ToastConfirmEvent;\nimport org.fordes.subtitles.view.mapper.SearchCasesMapper;\nimport org.fordes.subtitles.view.model.PO.SearchCases;\nimport org.fordes.subtitles.view.model.search.Cases;\nimport org.fordes.subtitles.view.model.search.Result;\nimport org.fordes.subtitles.view.service.SearchService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/2/6\n */\n@Slf4j\n@Component\npublic class SubtitleSearch extends DelayInitController {\n\n    @FXML\n    private JFXListView<StackPane> listView;\n\n    @FXML\n    private JFXTextField searchField;\n\n    @FXML\n    private JFXNodesList nodesList;\n\n    private ToggleGroup engineGroup;\n\n    private static final SearchService SERVICE = new SearchService();\n\n    private static final Dict SEARCH_KEY = Dict.create();\n\n    static final String KEYWORD = \"keyword\";\n\n    private final SearchCasesMapper casesMapper;\n\n    @Autowired\n    public SubtitleSearch(SearchCasesMapper casesMapper) {\n        this.casesMapper = casesMapper;\n    }\n\n    @Override\n    public void delay() {\n        //选择默认接口\n        if (engineGroup.getToggles().isEmpty()) {\n            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"未找到搜索源\", \"字幕搜索无法使用！\"));\n            searchField.setDisable(true);\n        } else {\n            Toggle engine = CollUtil.getFirst(engineGroup.getToggles());\n            engine.setSelected(true);\n            searchField.setPromptText(StrUtil\n                    .format(\"从{}搜索\", ((SearchCases) engine.getUserData()).getName()));\n        }\n    }\n\n    @Override\n    public void async() {\n        //读取字幕搜索接口\n        engineGroup = new ToggleGroup();\n        casesMapper.selectList(new QueryWrapper<>()).forEach(e -> {\n            ToggleButton engine = new ToggleButton();\n            engine.getStyleClass().addAll(StyleClassConstant.SUBTITLE_SEARCH_ENGINE,\n                    StyleClassConstant.SUBTITLE_SEARCH_ENGINE_ITEM);\n            engine.setToggleGroup(engineGroup);\n            engine.setUserData(e);\n            engine.setTooltip(new Tooltip(e.getName()));\n            engine.setText(e.getIcon());\n            engine.selectedProperty().addListener((observableValue, aBoolean, t1) -> {\n                if (t1) {\n                    SearchCases cases = (SearchCases) engine.getUserData();\n                    searchField.setPromptText(StrUtil.format(\"从{}搜索\", cases.getName()));\n                    listView.getItems().clear();\n                    SERVICE.cancel();\n                    nodesList.animateList(false);\n                }\n            });\n            nodesList.addAnimatedNode(engine);\n        });\n\n        //监听搜索服务运行状态，控制loading\n        SERVICE.runningProperty().addListener((observableValue, aBoolean, t1)\n                -> Singleton.get(Stage.class).fireEvent(new LoadingEvent(t1)));\n        //搜索完成，载入新结果\n        SERVICE.setOnSucceeded(event -> {\n            Result val = SERVICE.getValue();\n            if (ObjectUtil.isNotNull(val) && !val.getData().isEmpty()) {\n                if (Result.Type.SEARCH.equals(val.getType())) {\n                    listView.getItems().clear();\n                }\n                listView.setUserData(val.getPage());\n                val.getData().forEach(result -> listView.getItems().add(buildItem(result)));\n            }else {\n                Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"暂无结果\", \"换一个资源试试吧~\", \"确定\", () -> {}));\n            }\n        });\n        //搜索出错\n        SERVICE.setOnFailed(event -> Singleton.get(Stage.class).fireEvent(new ToastChooseEvent(\"搜索出错\",\n                \"请等待后尝试重试\\n或者前往项目主页反馈\", \"去反馈\",\n                () -> DesktopUtil.browse(CommonConstant.URL_ISSUES))));\n\n        //为listview添加skin，反射获取垂直滚动条，监听滚动条判断分页\n        JFXListViewSkin<StackPane> skin = new JFXListViewSkin<>(listView);\n        listView.setSkin(skin);\n\n        VirtualFlow<?> virtualFlow = (VirtualFlow<?>) ReflectUtil.getFieldValue(skin, \"flow\");\n        VirtualScrollBar vbar = (VirtualScrollBar) ReflectUtil.getFieldValue(virtualFlow, \"vbar\");\n        vbar.valueProperty().addListener((observableValue, number, t1) -> {\n            if (t1.floatValue() == 1 && listView.getUserData() != null) {\n                SERVICE.search(Result.Type.PAGE, (Cases) listView.getUserData(), SEARCH_KEY);\n            }\n        });\n    }\n\n    /**\n     * 输入框监听，提交新的搜索\n     * @param event source\n     */\n    @FXML\n    private void searchBeginHandle(ActionEvent event) {\n        JFXTextField field = (JFXTextField) event.getSource();\n        if (StrUtil.isNotBlank(field.getText())) {\n            SearchCases cases = (SearchCases) engineGroup.getSelectedToggle().getUserData();\n            SEARCH_KEY.clear();\n            SEARCH_KEY.set(KEYWORD, field.getText());\n            SERVICE.search(Result.Type.SEARCH, cases.getCases(), SEARCH_KEY);\n        }\n    }\n\n\n    private StackPane buildItem(Result.Item rsi) {\n        StackPane root = new StackPane();\n        root.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM);\n        Label caption = new Label(rsi.caption);\n        caption.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_CAPTION);\n        Label text = new Label(rsi.text);\n        text.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_TEXT);\n        root.getChildren().addAll(caption, text);\n\n        StackPane.setAlignment(caption, Pos.TOP_LEFT);\n        StackPane.setMargin(caption, new Insets(5, 0, 0, 0));\n        StackPane.setAlignment(text, Pos.BOTTOM_LEFT);\n        StackPane.setMargin(caption, new Insets(0, 0, 5, 0));\n        root.setUserData(rsi);\n        root.setOnMouseClicked(e -> {\n            if (MouseButton.PRIMARY.equals(e.getButton()) && 2 == e.getClickCount()) {\n                StackPane item = (StackPane) e.getSource();\n                Result.Item data = (Result.Item)item.getUserData();\n                if (ObjectUtil.isNull(data.next)) {\n                    if (StrUtil.isNotEmpty(data.text)) {\n                        Singleton.get(Stage.class).fireEvent(new FileOpenEvent(data.text));\n                    }\n                }else {\n                    SERVICE.search(Result.Type.SEARCH, data.next, data.params);\n                }\n            }\n        });\n        return root;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Component\npublic class SyncEditor {\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/TitleBar.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.application.Platform;\nimport javafx.event.ActionEvent;\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.enums.FontIcon;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/1/19\n */\n@Slf4j\n@Component\npublic class TitleBar {\n\n    @FXML\n    private Button closed, maximize, minimize;\n\n    @FXML\n    private VBox root;\n\n    @FXML\n    private Label title;\n\n    @FXML\n    private void closed(ActionEvent actionEvent) {\n        //TODO\n        Singleton.get(Stage.class).close();\n        Platform.exit();\n        System.exit(0);\n    }\n\n    @FXML\n    private void maximize(ActionEvent actionEvent) {\n        Stage stage = Singleton.get(Stage.class);\n        stage.setFullScreen(!stage.isFullScreen());\n        maximize.setText(stage.isFullScreen() ?\n                FontIcon.EXIT_FULL_SCREEN.toString() : FontIcon.FULL_SCREEN.toString());\n        actionEvent.consume();\n\n    }\n\n    @FXML\n    private void minimize(ActionEvent actionEvent) {\n        Singleton.get(Stage.class).setIconified(true);\n        actionEvent.consume();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/Toast.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport com.jfoenix.controls.JFXButton;\nimport javafx.fxml.FXML;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.GridPane;\nimport javafx.stage.Stage;\nimport org.fordes.subtitles.view.event.AbstractToastEvent;\nimport org.fordes.subtitles.view.handler.ToastEventHandler;\nimport org.fordes.subtitles.view.handler.ToastHandler;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/1/28\n */\n@Component\npublic class Toast extends DelayInitController {\n\n    @FXML\n    private JFXButton _perform, _choose1, _choose2;\n\n    @FXML\n    private Label _caption, _text;\n\n    @FXML\n    private GridPane root;\n\n    @Override\n    public void async() {\n        //选择型toast和确认型toast互斥\n        _perform.visibleProperty().addListener((observableValue, aBoolean, t1) -> {\n            _choose1.setVisible(!t1);\n            _choose2.setVisible(!t1);\n        });\n        //为stage添加toast事件处理\n        Singleton.get(Stage.class).addEventHandler(AbstractToastEvent.TOAST_EVENT_TYPE, new ToastEventHandler() {\n            @Override\n            public void onConfirmEvent(String caption, String text, String perform, ToastHandler handler) {\n                _caption.setText(caption);\n                _text.setText(text);\n                if (StrUtil.isNotEmpty(perform)) {\n                    _perform.setText(perform);\n                }\n                _perform.setOnAction(actionEvent -> {\n                    handler.handle();\n                    root.setVisible(false);\n                });\n                _perform.setVisible(true);\n                root.setVisible(true);\n            }\n\n            @Override\n            public void onChooseEvent(String caption, String text, String choose1, String choose2,\n                                      ToastHandler handler1, ToastHandler handler2) {\n                _caption.setText(caption);\n                _text.setText(text);\n                if (StrUtil.isNotEmpty(choose1)) {\n                    _choose1.setText(choose1);\n                }\n                if (StrUtil.isNotEmpty(choose2)) {\n                    _choose2.setText(choose2);\n                }\n                _choose1.setOnAction(event -> {\n                    handler1.handle();\n                    root.setVisible(false);\n                });\n                _choose2.setOnAction(event -> {\n                    handler2.handle();\n                    root.setVisible(false);\n                });\n                _perform.setVisible(false);\n                root.setVisible(true);\n            }\n        });\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/ToolBox.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * @author fordes on 2022/2/6\n */\n@Component\npublic class ToolBox {\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java",
    "content": "package org.fordes.subtitles.view.controller;\n\nimport cn.hutool.core.lang.Singleton;\nimport javafx.stage.Stage;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.event.FileOpenEvent;\nimport org.fordes.subtitles.view.model.DTO.Video;\nimport org.springframework.stereotype.Component;\n\n/**\n * 语音转换 控制器\n *\n * @author fordes on 2022/4/8\n */\n@Slf4j\n@Component\npublic class VoiceConvert extends DelayInitController {\n\n    private Video video;\n\n\n    @Override\n    public void delay() {\n\n    }\n\n    @Override\n    public void async() {\n        Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> {\n            if (fileOpenEvent.getRecord().getFormat().media) {\n                video = (Video) fileOpenEvent.getRecord();\n                root.setVisible(true);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java",
    "content": "package org.fordes.subtitles.view.enums;\n\n/**\n * 编辑工具 事件类型枚举\n *\n * @author fordes on 2022/7/15\n */\npublic enum EditToolEventEnum {\n\n    SEARCH, //搜索\n    REPLACE,//替换\n    JUMP,//跳转\n    FONT, //字体（样式）\n    TIMELINE, //时间轴\n    CODE,//编码\n    REF, //刷新\n    TRANSLATE //翻译\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/FileEnum.java",
    "content": "package org.fordes.subtitles.view.enums;\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.AllArgsConstructor;\n\nimport java.util.Arrays;\n\n/**\n * 文件类型枚举\n *\n * @author fordes on 2022/2/9\n */\n@AllArgsConstructor\npublic enum FileEnum {\n\n    //视频\n    MP4(\"mp4\", true, true, false),\n    MKV(\"mkv\", true, true, false),\n    AVI(\"avi\", true, true, false),\n    RMVB(\"rmvb\", true, true, false),\n    TS(\"ts\", true, true, false),\n\n    //音频\n    MP3(\"mp3\", true, false, false),\n    FLAC(\"flac\", true, false, false),\n    AAC(\"aac\", true, false, false),\n\n    //字幕\n    LRC(\"lrc\", true, false, true),\n    SRT(\"srt\", true, false, true),\n    ASS(\"ass\", true, false, true);\n\n    public final String suffix;\n\n    public final boolean support;\n\n    public final boolean media;\n\n    public final boolean subtitle;\n\n    public static final String[] SUPPORT_SUBTITLE = Arrays.stream(FileEnum.values())\n            .filter(e -> e.subtitle && e.support).map(e -> e.suffix).toArray(String[]::new);\n\n    public static final String[] SUPPORT_MEDIA = Arrays.stream(FileEnum.values())\n            .filter(e -> e.media && e.support).map(e -> e.suffix).toArray(String[]::new);\n\n    public static boolean isMedia(String suffix) {\n        return Arrays.stream(FileEnum.values())\n                .filter(e -> e.media)\n                .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix));\n    }\n\n    public static boolean isSupport(String suffix) {\n        return Arrays.stream(FileEnum.values())\n                .filter(e -> e.support)\n                .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix));\n    }\n\n    public static boolean check(String suffix, boolean isSupport, boolean isMedia, boolean isSubtitle) {\n        FileEnum val = of(suffix);\n        return val != null && (val.support == isSupport) && (val.media == isMedia) && (val.subtitle == isSubtitle);\n    }\n\n    public static FileEnum of(String name) {\n        for (FileEnum value : FileEnum.values()) {\n            if (StrUtil.equalsIgnoreCase(name, value.suffix)) {\n                return value;\n            }\n        }\n        return null;\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/FontIcon.java",
    "content": "package org.fordes.subtitles.view.enums;\n\nimport lombok.AllArgsConstructor;\n\n/**\n * 图标枚举\n *\n * @author fordes on 2022/1/23\n */\n@AllArgsConstructor\npublic enum FontIcon {\n\n    SCENE_CLOSE(\"\\ue648\"),\n    SCENE_MINIMIZE(\"\\ue634\"),\n    EXIT_FULL_SCREEN(\"\\ue61f\"),\n    FULL_SCREEN(\"\\ue628\"),\n    ITEM_START(\"\\ue669\"),\n    ITEM_SEARCH(\"\\uec6f\"),\n    ITEM_TOOL(\"\\ue64a\"),\n    LOGO(\"\\ue69f\"),\n    SETTING(\"\\ue711\"),\n    CHOOSE_FILE(\"\\ue64e\"),\n\n    ENGINE_DDZM(\"\\ue63b\"),\n    ENGINE_ASSRT(\"\\ue609\"),\n    ENGINE_ZMK(\"\\ue623\"),\n    ENGINE(\"\\ue60f\"),\n\n    PLACE_THE_LEFT(\"\\uec70\"),\n    PLACE_THE_RIGHT(\"\\ue61a\"),\n\n    SETTING_PREFERENCES(\"\\ue63c\"),\n    SETTING_INTERFACE(\"\\ue62d\"),\n\n    EDIT_BAR_SEARCH(\"\\ue754\"),\n    EDIT_BAR_REPLACE(\"\\ue674\"),\n    EDIT_BAR_JUMP(\"\\ue695\"),\n    EDIT_BAR_FONT(\"\\ue61d\"),\n    EDIT_BAR_HIDE(\"\\ue60b\"),\n    EDIT_BAR_TIMELINE(\"\\ue64f\"),\n    EDIT_BAR_CODE(\"\\ue629\"),\n\n    EDIT_BAR_REF(\"\\ue62c\"),\n\n    EDIT_BAR_OPTION(\"\\ue86c\"),\n\n    EDIT_BAR_REPLACE_ITEM(\"\\ue63e\"),\n\n    EDIT_BAR_REPLACE_ALL(\"\\ue642\"),\n\n    SWITCH_OFF_LIGHT(\"\\ue612\"),\n    SWITCH_ON_LIGHT(\"\\ue611\"),\n    SWITCH_OFF_DARK(\"\\ue613\"),\n    SWITCH_ON_DARK(\"\\ue615\"),\n\n    EDIT_BAR_TRANSLATE(\"\\ue6fb\");\n\n    private final String unicode;\n\n    @Override\n    public String toString() {\n        return unicode;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java",
    "content": "package org.fordes.subtitles.view.enums;\n\nimport com.baomidou.mybatisplus.annotation.IEnum;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\n@Getter\n@AllArgsConstructor\npublic enum ServiceProvider implements IEnum<String> {\n\n    BAIDU(\"百度\"),\n\n    TENCENT(\"腾讯\"),\n    ALI(\"阿里\"),\n\n    HUOSHAN(\"火山\");\n\n    private final String desc;\n\n    @Override\n    public String toString() {\n        return this.desc;\n    }\n\n    @Override\n    public String getValue() {\n        return this.name();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/ServiceType.java",
    "content": "package org.fordes.subtitles.view.enums;\n\nimport com.baomidou.mybatisplus.annotation.IEnum;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\n/**\n * 服务类型枚举\n *\n * @author fordes on 2022/4/17\n */\n@Getter\n@AllArgsConstructor\npublic enum ServiceType implements IEnum<String> {\n\n    VOICE(\"语音转写\"),\n\n    TRANSLATE(\"翻译\");\n\n    private final String desc;\n\n    @Override\n    public String toString() {\n        return this.getDesc();\n    }\n\n    public String getValue() {\n        return this.name();\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java",
    "content": "package org.fordes.subtitles.view.enums;\n\nimport lombok.Getter;\n\n/**\n * 7zip结束码枚举\n *\n * @author fordes on 2021/1/7\n */\n@Getter\npublic enum SevenZipEnum {\n\n    NORMAL(0, \"未发生错误\"),\n    WARNING(1,\"警告，发生部分错误\"),\n    FATAL_ERROR(2, \"致命错误\"),\n    COMMAND_ERROR(7, \"命令错误\"),\n    OUT_OF_MEMORY_ERROR(8, \"内存不足\"),\n    TERMINATION(255, \"操作终止\"),\n    UNKNOWN_ERROR(-1, \"未知错误\");\n\n    SevenZipEnum(int code, String status) {\n        this.code = code;\n        this.status = status;\n    }\n\n    private final int code;\n\n    private final String status;\n\n    public static String getStatus(int code){\n        switch (code){\n            case 0:\n                return NORMAL.status;\n            case 1:\n                return WARNING.status;\n            case 2:\n                return FATAL_ERROR.status;\n            case  7:\n                return COMMAND_ERROR.status;\n            case 8:\n                return OUT_OF_MEMORY_ERROR.status;\n            case 255:\n                return TERMINATION.status;\n            default:\n                return UNKNOWN_ERROR.status;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport org.fordes.subtitles.view.handler.ToastEventHandler;\n\n/**\n * @author fordes on 2022/2/2\n */\npublic abstract class AbstractToastEvent extends Event {\n\n    public static final String CONFIRM = \"确定\";\n\n    public static final String CANCEL = \"取消\";\n\n    public static final EventType<AbstractToastEvent> TOAST_EVENT_TYPE = new EventType(ANY);\n\n    public AbstractToastEvent(EventType<? extends Event> eventType) {\n        super(eventType);\n    }\n\n    public abstract void invokeHandler(ToastEventHandler handler);\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport javafx.scene.control.ToggleButton;\nimport lombok.Getter;\nimport lombok.NonNull;\nimport org.fordes.subtitles.view.enums.EditToolEventEnum;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fxmisc.richtext.StyleClassedTextArea;\n\n\n/**\n * 编辑工具 事件\n *\n * @author fordes on 2022/7/15\n */\npublic class EditToolEvent extends Event {\n\n    public static final EventType<EditToolEvent> EVENT_TYPE = new EventType<>(ANY, \"editToolEvent\");\n\n    @Getter\n    private final StyleClassedTextArea source;\n\n    @Getter\n    private final Subtitle subtitle;\n\n    @Getter\n    private final ToggleButton editMode;\n\n    @Getter\n    private final EditToolEventEnum type;\n\n    public EditToolEvent(@NonNull StyleClassedTextArea source,\n                         @NonNull Subtitle subtitle,\n                         @NonNull ToggleButton editMode,\n                         @NonNull EditToolEventEnum type) {\n        super(EVENT_TYPE);\n        this.source = source;\n        this.subtitle = subtitle;\n        this.editMode = editMode;\n        this.type = type;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Singleton;\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport javafx.stage.Stage;\nimport lombok.Getter;\nimport org.fordes.subtitles.view.model.PO.FileRecord;\nimport org.fordes.subtitles.view.utils.FileUtils;\n\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * @author fordes on 2022/4/8\n */\npublic class FileOpenEvent extends Event {\n\n    public static final EventType<FileOpenEvent> FILE_OPEN_EVENT = new EventType(ANY, \"fileOpenEvent\");\n\n    @Getter\n    private FileRecord record;\n\n    public FileOpenEvent(File openFile) {\n        super(FILE_OPEN_EVENT);\n        try {\n            this.record = FileUtils.readFileInfo(openFile);\n        }catch (IOException e) {\n            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"出错了\",\"打开文件失败！\"));\n        }\n    }\n\n    public FileOpenEvent(String filePath) {\n        super(FILE_OPEN_EVENT);\n        try {\n            this.record = FileUtils.readFileInfo(FileUtil.file(filePath));\n        }catch (IOException e) {\n            Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent(\"出错了\",\"打开文件失败！\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport lombok.Getter;\n\n/**\n * loading事件\n *\n * @author fordes on 2022/7/20\n */\npublic class LoadingEvent extends Event {\n\n    public final static EventType<LoadingEvent> EVENT_TYPE = new EventType<>(ANY, \"loadingEvent\");\n\n    @Getter\n    private final boolean alive;\n\n    public LoadingEvent(boolean alive) {\n        super(EVENT_TYPE);\n        this.alive = alive;\n    }\n\n    public LoadingEvent() {\n        super(EVENT_TYPE);\n        this.alive = false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\n\n/**\n * 主题切换事件\n *\n * @author fordes on 2022/4/13\n */\npublic class ThemeChangeEvent extends Event {\n\n    public static final EventType<ThemeChangeEvent> EVENT_TYPE = new EventType(ANY, \"themeChangeEvent\");\n\n    private Boolean dark;\n\n    public Boolean isDark() {\n        return dark;\n    }\n\n    public ThemeChangeEvent(Boolean dark) {\n        super(EVENT_TYPE);\n        this.dark = dark;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.EventType;\nimport org.fordes.subtitles.view.handler.ToastEventHandler;\nimport org.fordes.subtitles.view.handler.ToastHandler;\n\n/**\n * toast选择事件\n *\n * @author fordes on 2022/2/2\n */\npublic class ToastChooseEvent extends AbstractToastEvent {\n\n    public static final EventType<AbstractToastEvent> TOAST_CHOOSE_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, \"toastChooseEvent\");\n\n    private final String caption;\n\n    private final String text;\n\n    private final String choose1;\n\n    private final String choose2;\n\n    private final ToastHandler handler1;\n\n    private final ToastHandler handler2;\n\n    public ToastChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2) {\n        super(TOAST_CHOOSE_EVENT_TYPE);\n        this.caption = caption;\n        this.text = text;\n        this.choose1 = choose1;\n        this.choose2 = choose2;\n        this.handler1 = handler1;\n        this.handler2 = handler2;\n    }\n\n    public ToastChooseEvent(String caption, String text, String choose1, ToastHandler handler1) {\n        super(TOAST_CHOOSE_EVENT_TYPE);\n        this.caption = caption;\n        this.text = text;\n        this.choose1 = choose1;\n        this.choose2 = AbstractToastEvent.CANCEL;\n        this.handler1 = handler1;\n        this.handler2 = () -> {};\n    }\n\n    @Override\n    public void invokeHandler(ToastEventHandler handler) {\n        handler.onChooseEvent(caption, text, choose1, choose2, handler1, handler2);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.EventType;\nimport org.fordes.subtitles.view.handler.ToastEventHandler;\nimport org.fordes.subtitles.view.handler.ToastHandler;\n\n/**\n * @author fordes on 2022/2/2\n */\npublic class ToastConfirmEvent extends AbstractToastEvent {\n\n    public static final EventType<AbstractToastEvent> TOAST_CONFIRM_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, \"confirmToastEvent\");\n\n    private final String caption;\n\n    private final String text;\n\n    private final String perform;\n\n    private final ToastHandler handler;\n\n    public ToastConfirmEvent(String caption, String text, String perform, ToastHandler handler) {\n        super(TOAST_CONFIRM_EVENT_TYPE);\n        this.caption = caption;\n        this.text = text;\n        this.perform = perform;\n        this.handler = handler;\n    }\n\n    public ToastConfirmEvent(String caption, String text) {\n        super(TOAST_CONFIRM_EVENT_TYPE);\n        this.caption = caption;\n        this.text = text;\n        this.perform = AbstractToastEvent.CONFIRM;\n        this.handler = () -> {};\n    }\n\n    @Override\n    public void invokeHandler(ToastEventHandler handler) {\n        handler.onConfirmEvent(caption, text, perform, this.handler);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java",
    "content": "package org.fordes.subtitles.view.event;\n\nimport javafx.event.Event;\nimport javafx.event.EventType;\nimport lombok.Getter;\n\n/**\n * 翻译服务事件\n *\n * @author fordes on 2022/8/1\n */\npublic class TranslateEvent extends Event {\n\n    public static final EventType<TranslateEvent> EVENT_TYPE = new EventType<>(ANY, \"translateEvent\");\n\n    public static final String SUCCESS = \"翻译完成\";\n\n    public static final String FAIL = \"翻译失败\";\n\n    public TranslateEvent(EventType<? extends Event> eventType) {\n        super(eventType);\n    }\n\n    @Getter\n    private String msg;\n\n    @Getter\n    private String detail;\n\n    public TranslateEvent(String msg, String detail) {\n        super(EVENT_TYPE);\n        this.msg = msg;\n        this.detail = detail;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java",
    "content": "package org.fordes.subtitles.view.factory;\n\nimport org.fordes.subtitles.view.service.translate.TranslateService;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * @author fordes on 2022/7/11\n */\npublic class TranslateServiceFactory {\n\n    private static final Map<String, TranslateService> services = new ConcurrentHashMap<>();\n\n    public static TranslateService getService(String provider) {\n        return services.getOrDefault(provider, null);\n    }\n\n    public static void register(TranslateService service, String provider) {\n        services.put(provider, service);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java",
    "content": "package org.fordes.subtitles.view.handler;\n\n/**\n * @author fordes on 2022/7/27\n */\n@FunctionalInterface\npublic interface CallBackHandler<T> {\n\n    void handle(T value);\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java",
    "content": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.EditToolEvent;\n\n/**\n * 编辑工具 事件处理器\n *\n * @author fordes on 2022/7/15\n */\npublic abstract class EditToolEventHandler implements EventHandler<EditToolEvent> {\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java",
    "content": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.FileOpenEvent;\n\n/**\n * @author fordes on 2022/4/8\n */\npublic abstract class FileOpenEventHandler implements EventHandler<FileOpenEvent> {\n\n    public final static String ERROR_MESSAGE = \"文件打开失败\";\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java",
    "content": "package org.fordes.subtitles.view.handler;\n\nimport javafx.event.EventHandler;\nimport org.fordes.subtitles.view.event.AbstractToastEvent;\n\n/**\n * toast事件抽象\n *\n * @author fordes on 2022/2/2\n */\npublic abstract class ToastEventHandler implements EventHandler<AbstractToastEvent> {\n\n    /**\n     * 确认型 toast事件\n     * @param caption   标题\n     * @param text  内容\n     * @param perform   确认按钮文本\n     * @param handler   回调\n     */\n    public abstract void onConfirmEvent(String caption, String text, String perform, ToastHandler handler);\n\n    /**\n     * 选择型 toast事件\n     * @param caption   标题\n     * @param text  内容\n     * @param choose1  选择1\n     * @param choose2  选择2\n     * @param handler1  选择1回调\n     * @param handler2  选择2回调\n     */\n    public abstract void onChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2);\n\n    @Override\n    public void handle(AbstractToastEvent event) {\n        event.invokeHandler(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java",
    "content": "package org.fordes.subtitles.view.handler;\n\n/**\n * toast回调事件处理器接口\n *\n * @author fordes on 2022/2/2\n */\n@FunctionalInterface\npublic interface ToastHandler {\n\n    void handle();\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java",
    "content": "package org.fordes.subtitles.view.mapper;\n\nimport cn.hutool.core.lang.Dict;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.apache.ibatis.annotations.Param;\nimport org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;\nimport org.fordes.subtitles.view.model.PO.ServiceInterface;\nimport org.fordes.subtitles.view.model.PO.Version;\n\nimport java.util.List;\n\n/**\n * @author fordes on 2022/4/17\n */\n@Mapper\npublic interface InterfaceMapper extends BaseMapper<ServiceInterface> {\n\n\n    List<AvailableServiceInfo> serviceInfo(@Param(\"type\") String type);\n\n    List<Version> getVersions(@Param(\"type\") String serviceType, @Param(\"provider\") String provider);\n\n    List<Dict> getLanguageList();\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java",
    "content": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.fordes.subtitles.view.model.PO.Language;\n\n@Mapper\npublic interface LanguageMapper extends BaseMapper<Language> {\n\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java",
    "content": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.fordes.subtitles.view.model.PO.SearchCases;\n\n/**\n * @author fordes on 2022/2/15\n */\n@Mapper\npublic interface SearchCasesMapper extends BaseMapper<SearchCases> {\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java",
    "content": "package org.fordes.subtitles.view.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.fordes.subtitles.view.model.PO.Version;\n\n/**\n * @author fordes on 2022/4/17\n */\n@Mapper\npublic interface VersionMapper extends BaseMapper<Version> {\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java",
    "content": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.fordes.subtitles.view.model.PO.ServiceInterface;\nimport org.fordes.subtitles.view.model.PO.Version;\n\n/**\n * @author fordes on 2022/4/20\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\npublic class AvailableServiceInfo extends ServiceInterface {\n\n    private Version versionInfo;\n\n    @Override\n    public String toString() {\n        return getProvider().getDesc() + getType().getDesc();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java",
    "content": "package org.fordes.subtitles.view.model.DTO;\n\n/**\n * @author fordes on 2022/7/19\n */\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport org.fordes.subtitles.view.model.PO.FileRecord;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\n/**\n * @author fordes on 2021/6/30\n */\n@Data\n@Accessors(chain = true)\n@EqualsAndHashCode(callSuper = true)\npublic class Subtitle extends FileRecord {\n\n    private TimedTextFile timedTextFile;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java",
    "content": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * 翻译\n *\n * @author fordes on 2022/7/27\n */\n@Data\n@Builder\npublic class TranslateResult {\n\n    private Integer serial;\n\n    private boolean success;\n\n    private String data;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/DTO/Video.java",
    "content": "package org.fordes.subtitles.view.model.DTO;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\nimport lombok.experimental.Accessors;\nimport org.fordes.subtitles.view.model.PO.FileRecord;\n\n/**\n * 视频类\n *\n * @author fordes on 2020/12/4\n */\n@Data\n@NoArgsConstructor\n@Accessors(chain = true)\n@EqualsAndHashCode(callSuper = true)\npublic class Video extends FileRecord {\n\n    /**\n     * 帧宽\n     */\n    private int width;\n\n    /**\n     * 帧高\n     */\n    private int height;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.fordes.subtitles.view.enums.FileEnum;\n\nimport java.io.File;\nimport java.util.Date;\n\n/**\n * 文件抽象类\n *\n * @author fordes on 2020/12/4\n */\n@Data\n@Accessors(chain = true)\npublic class FileRecord {\n\n    @JsonIgnore\n    private File file;\n\n    /**\n     * 名称\n     */\n    private String file_name;\n\n    /**\n     * 格式\n     */\n    private FileEnum format;\n\n    /**\n     * 语言\n     */\n    private String language;\n\n    /**\n     * 文件路径\n     */\n    private String path;\n\n\n    /**\n     * 文件字节大小\n     */\n    private Long size_byte;\n\n    /**\n     * 文件大小\n     */\n    private String size;\n\n    /**\n     * 长度，字幕为时间轴起止，视频为时长\n     */\n    private Long duration;\n\n    /**\n     * 编码\n     */\n    private String charset;\n\n    /**\n     * 文件最后修改时间\n     */\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd HH:mm:ss\", timezone = \"GMT+8\")\n    private Date file_modify_time;\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Interface.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n@Data\n@TableName(value = \"interface\")\npublic class Interface implements Serializable {\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Integer id;\n\n    @TableField(value = \"provider\")\n    private String provider;\n\n    @TableField(value = \"\\\"type\\\"\")\n    private String type;\n\n    @TableField(value = \"auth\")\n    private String auth;\n\n    @TableField(value = \"page\")\n    private String page;\n\n    @TableField(value = \"\\\"template\\\"\")\n    private String template;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Language.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport lombok.experimental.Accessors;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n@Data\n@Accessors(chain = true)\n@NoArgsConstructor\n@TableName(value = \"\\\"language\\\"\")\npublic class Language implements Serializable {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Integer id;\n\n    private String name;\n\n    @TableField(exist = false)\n    private String code;\n\n    @TableField(exist = false)\n    private boolean general;\n\n    @TableField(exist = false)\n    private List<String> _target;\n\n    @TableField(exist = false)\n    private List<Language> target;\n\n    @TableField(\"huoshan\")\n    private String huoshan;\n\n\n    public String toString() {\n        return this.name;\n    }\n\n    private static final long serialVersionUID = 1L;\n\n    public static final String COL_ID = \"id\";\n\n    public static final String COL_TYPE = \"type\";\n\n    public static final String COL_NAME = \"name\";\n\n    public static final String COL_GENERAL = \"general\";\n\n    public static final String TARGET = \"_target\";\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\nimport cn.hutool.json.JSONUtil;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.fordes.subtitles.view.model.search.Cases;\n\n/**\n * @author fordes on 2022/2/15\n */\n@Data\n@Accessors(chain = true)\npublic class SearchCases {\n\n    /**\n     * 自增主键\n     */\n    private Integer id;\n\n    /**\n     * 名称\n     */\n    private String name;\n\n    /**\n     * 图标 {@link org.fordes.subtitles.view.enums.FontIcon}\n     */\n    private String icon;\n\n    /**\n     * 用例\n     */\n    private Cases cases;\n\n    /**\n     * 备注\n     */\n    private String remark;\n\n\n    public void setCases(String cases) {\n        this.cases = JSONUtil.toBean(cases, Cases.class);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.enums.ServiceType;\n\nimport java.io.Serializable;\n\n/**\n * @author fordes on 2022/4/19\n */\n@Data\n@Accessors(chain = true)\n@TableName(value = \"interface\")\npublic class ServiceInterface implements Serializable {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Integer id;\n\n    /**\n     * 服务提供商 {@link ServiceProvider}\n     */\n\n    @TableField(value = \"provider\")\n    private ServiceProvider provider;\n\n    /**\n     * 服务类型 {@link ServiceType}\n     */\n    @TableField(value = \"type\")\n    private ServiceType type;\n\n    /**\n     * 授权信息\n     */\n    @TableField(value = \"auth\")\n    private String auth;\n\n    /**\n     * 授权信息模板\n     */\n    @TableField(value = \"template\")\n    private String template;\n\n\n    /**\n     * 主页\n     */\n    @TableField(value = \"page\")\n    private String page;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/PO/Version.java",
    "content": "package org.fordes.subtitles.view.model.PO;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport java.io.Serializable;\n\n/**\n * @author fordes on 2022/4/17\n */\n@Data\n@Accessors(chain = true)\n@TableName(value = \"version\")\npublic class Version implements Serializable {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Integer id;\n\n    @TableField(value = \"interface_id\")\n    private Integer interfaceId;\n\n    @TableField(value = \"\\\"name\\\"\")\n    private String name;\n\n    @TableField(value = \"concurrent\")\n    private Integer concurrent;\n\n    @TableField(value = \"carrying\")\n    private Integer carrying;\n\n    @TableField(value = \"server_url\")\n    private String serverUrl;\n\n    @TableField(value = \"remark\")\n    private String remark;\n\n    public String toString() {\n        return this.name;\n    }\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Cases.java",
    "content": "package org.fordes.subtitles.view.model.search;\n\nimport cn.hutool.http.ContentType;\nimport lombok.Builder;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n/**\n * @author fordes on 2022/3/28\n */\n@Builder\npublic class Cases implements Serializable {\n\n    public static final String CAPTION = \"caption\";\n\n    public static final String TEXT = \"text\";\n\n    public static final String PAGE = \"page\";\n\n    public String[] keys;\n\n    public Object url;\n\n    public ContentType type;\n\n    public Map<String, Selector> params;\n\n    public Cases next;\n\n    public void setType(String val) {\n        this.type = ContentType.valueOf(val);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Engine.java",
    "content": "package org.fordes.subtitles.view.model.search;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n/**\n * @author fordes on 2022/2/12\n */\n@Data\n@Accessors(chain = true)\npublic class Engine {\n\n    private String id;\n\n    private String name;\n\n    private String url;\n\n    private Cases cases;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Result.java",
    "content": "package org.fordes.subtitles.view.model.search;\n\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Builder;\nimport lombok.Data;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author fordes on 2022/2/12\n */\n@Data\n@Builder\npublic class Result {\n\n    private Type type;\n\n    private Cases page;\n\n    private List<Item> data;\n\n    @Builder\n    public static class Item {\n\n        public Cases next;\n\n        public String caption;\n\n        public String text;\n\n        public boolean isFile = false;\n\n        public Map<String, Object> params = MapUtil.newHashMap();\n    }\n\n    public static enum Type {\n        //普通搜索\n        SEARCH(),\n        //分页\n        PAGE()\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/model/search/Selector.java",
    "content": "package org.fordes.subtitles.view.model.search;\n\nimport java.io.Serializable;\n\n/**\n * 字段解析器\n * @author fordes on 2022/3/28\n */\npublic class Selector implements Serializable {\n\n    /**\n     * 唯一性标识\n     * false：按条件提取结果集，true：按条件提取唯一结果\n     */\n    public boolean only = false;\n\n    /**\n     * 正则提取，提取匹配正则的内容\n     * 高优先级\n     */\n    public String regular;\n\n    /**\n     * 内容格式化模板，参考{@see cn.hutool.core.util.StrUtil.format()}\n     */\n    public String format;\n\n    /**\n     * key选择 多层级使用\".\"连接 如：a.c.b\n     */\n    public String jsonKey;\n\n    /**\n     * css选择器 参考Jsoup css选择器\n     * 高优先级\n     */\n    public String css;\n\n    /**\n     * 属性选择 提取指定属性，为空时使用text()\n     */\n    public String attr;\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/ConfigService.java",
    "content": "package org.fordes.subtitles.view.service;\n\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport org.fordes.subtitles.view.config.ApplicationConfig;\n\n/**\n * @author fordes on 2022/4/17\n */\npublic interface ConfigService extends IService<ApplicationConfig> {\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java",
    "content": "package org.fordes.subtitles.view.service.Impl;\n\nimport com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport lombok.AllArgsConstructor;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.enums.ServiceType;\nimport org.fordes.subtitles.view.mapper.InterfaceMapper;\nimport org.fordes.subtitles.view.mapper.VersionMapper;\nimport org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;\nimport org.fordes.subtitles.view.model.PO.ServiceInterface;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.InterfaceService;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n/**\n * 接口服务\n *\n * @author fordes on 2022/4/17\n */\n@Service\n@AllArgsConstructor\npublic class InterfaceServiceImpl extends ServiceImpl<InterfaceMapper, ServiceInterface> implements InterfaceService {\n\n    private final InterfaceMapper interfaceMapper;\n\n    private final VersionMapper versionMapper;\n\n//    private final DictMapper dictMapper;\n\n\n    @Override\n    public List<Version> getVersions(ServiceType type, ServiceProvider provider) {\n        return interfaceMapper.getVersions(type.name(), provider.name());\n    }\n\n    @Override\n    public ServiceInterface getInterface(ServiceType type, ServiceProvider provider) {\n        return interfaceMapper.selectOne(new LambdaQueryWrapper<ServiceInterface>()\n                .eq(ServiceInterface::getType, type)\n                .eq(ServiceInterface::getProvider, provider));\n    }\n\n\n    @Override\n    public List<AvailableServiceInfo> getAvailableService(ServiceType type) {\n        return interfaceMapper.serviceInfo(type.name());\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/InterfaceService.java",
    "content": "package org.fordes.subtitles.view.service;\n\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.enums.ServiceType;\nimport org.fordes.subtitles.view.model.DTO.AvailableServiceInfo;\nimport org.fordes.subtitles.view.model.PO.ServiceInterface;\nimport org.fordes.subtitles.view.model.PO.Version;\n\nimport java.util.List;\n\n/**\n * 接口服务\n *\n * @author fordes on 2022/4/17\n */\npublic interface InterfaceService extends IService<ServiceInterface> {\n\n\n    /**\n     * 获取指定接口的版本信息\n     *\n     * @param type     服务类型 {@link ServiceType}\n     * @param provider 服务提供商 {@link ServiceProvider}\n     * @return { @link Version}\n     */\n    List<Version> getVersions(ServiceType type, ServiceProvider provider);\n\n    ServiceInterface getInterface(ServiceType type, ServiceProvider provider);\n\n\n    /**\n     * 获取可用的服务接口\n     *\n     * @param type 服务类型 {@link ServiceType}\n     * @return {@link AvailableServiceInfo}\n     */\n    List<AvailableServiceInfo> getAvailableService(ServiceType type);\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/SearchService.java",
    "content": "package org.fordes.subtitles.view.service;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.http.HttpUtil;\nimport javafx.concurrent.Service;\nimport javafx.concurrent.Task;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.model.search.Cases;\nimport org.fordes.subtitles.view.model.search.Result;\nimport org.fordes.subtitles.view.utils.ArchiveUtil;\nimport org.fordes.subtitles.view.utils.search.ParsingFactory;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 在线字幕搜索服务\n *\n * @author fordes on 2022/2/15\n */\n@Slf4j\npublic class SearchService extends Service<Result> {\n\n    private Result.Type type;\n\n    private Cases cases;\n\n    private Map<String, Object> params = MapUtil.newHashMap();\n\n    @Override\n    protected Task<Result> createTask() {\n        return new Task<>() {\n            @Override\n            protected Result call() {\n                Result result = Result.builder()\n                        .type(type)\n                        .data(CollUtil.newArrayList()).build();\n                try {\n                    List<Object> paramList = new ArrayList<>(params.size());\n                    if (null != cases.keys) {\n                        for (String key : cases.keys) {\n                            paramList.add(params.get(key));\n                        }\n                    }\n                    String url = StrUtil.format((CharSequence) cases.url, paramList.toArray());\n                    HttpResponse response = HttpUtil\n                            .createGet(url, true)\n                            .execute();\n                    if (ObjectUtil.isEmpty(cases.next)) {\n                        File outFile = response.completeFileNameFromHeader(FileUtil.mkdir(CommonConstant.DOWNLOAD_PATH));\n                        FileUtil.writeFromStream(response.bodyStream(), outFile);\n                        log.debug(\"下载文件成功！{}\", outFile.getPath());\n                        for (File l : ArchiveUtil.unArchiveToCurrentPath(outFile)) {\n                            result.getData().add(Result.Item.builder()\n                                    .caption(l.getName())\n                                    .text(l.getPath())\n                                    .isFile(true)\n                                    .build());\n                        }\n                    } else {\n\n                        ContentType contentType = ContentType.get(StrUtil.trimStart(response.body()));\n                        if (contentType != null) {\n                            //根据类型，创建解析器\n                            ParsingFactory parsing = new ParsingFactory(response.body(), contentType);\n                            //遍历解析，获取结果\n                            Map<String, List<String>> displayMap = MapUtil.newHashMap();\n                            Map<String, Object> otherMap = MapUtil.newHashMap();\n                            cases.params.forEach((k, v) -> {\n                                switch (k) {\n                                    case Cases.CAPTION:\n                                    case Cases.TEXT:\n                                        displayMap.put(k, Convert.toList(String.class, parsing.getResult(v)));\n                                        break;\n                                    case Cases.PAGE:\n                                        if (ObjectUtil.isNotEmpty(parsing.getResult(v))) {\n                                            result.setPage(Cases.builder()\n                                                    .keys(cases.keys)\n                                                    .next(cases.next)\n                                                    .type(cases.type)\n                                                    .params(cases.params)\n                                                    .url(parsing.getResult(v))\n                                                    .build());\n                                        }\n                                        break;\n                                    default:\n                                        otherMap.put(k, parsing.getResult(v));\n                                }\n                            });\n                            //拼装结果\n                            List<String> captions = displayMap.get(\"caption\");\n                            List<String> texts = displayMap.get(\"text\");\n                            for (int i = 0; i < captions.size(); i++) {\n                                result.getData().add(Result.Item.builder()\n                                        .caption(CollUtil.get(captions, i))\n                                        .text(CollUtil.get(texts, i))\n                                        .params(MapUtil.newHashMap())\n                                        .next(cases.next)\n                                        .build());\n                            }\n\n                            otherMap.forEach((k, v) -> {\n                                if (v instanceof Collection) {\n                                    List<String> list = Convert.toList(String.class, v);\n                                    for (int i = 0; i < result.getData().size(); i++) {\n                                        result.getData().get(i).params.put(k, list.get(i));\n                                    }\n                                } else {\n                                    for (Result.Item value : result.getData()) {\n                                        value.params.put(k, v);\n                                    }\n                                }\n                            });\n                        }\n                    }\n\n                } catch (Exception e) {\n                    log.error(ExceptionUtil.stacktraceToString(e));\n                    throw new RuntimeException();\n                }\n                return result;\n            }\n        };\n    }\n\n\n    public void search(Result.Type type, Cases cases, Map<String, Object> params) {\n        this.type = type;\n        this.cases = cases;\n        this.params = params;\n        this.restart();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java",
    "content": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.factory.TranslateServiceFactory;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.translate.thread.AliTranslateThread;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * @author fordes on 2022/7/25\n */\n@Slf4j\n@Service\npublic class AliTranslateService extends TranslateService implements InitializingBean {\n\n\n    static final String APP_ID = \"Accesskey ID\";\n\n    static final String APP_KEY = \"AccessKey Secret\";\n\n\n    @Override\n    public void afterPropertiesSet() {\n        TranslateServiceFactory.register(this, ServiceProvider.ALI.name());\n    }\n\n\n    @Override\n    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {\n        String id = MapUtil.getStr(config, APP_ID);\n        String secret = MapUtil.getStr(config, APP_KEY);\n        return new AliTranslateThread(id, secret, serial, version.getServerUrl(), target, original, segment);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java",
    "content": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.factory.TranslateServiceFactory;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.translate.thread.BaiduTranslateThread;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * @author fordes on 2022/7/11\n */\n@Service\npublic class BaiduTranslateService extends TranslateService implements InitializingBean {\n\n    static final String APP_ID = \"APP_ID\";\n\n    static final String APP_KEY = \"APP_KEY\";\n\n    @Override\n    public void afterPropertiesSet() {\n        TranslateServiceFactory.register(this, ServiceProvider.BAIDU.name());\n    }\n\n    @Override\n    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {\n        String app_id = MapUtil.getStr(config, APP_ID);\n        String app_key = MapUtil.getStr(config, APP_KEY);\n        return new BaiduTranslateThread(app_id, app_key, serial, version.getServerUrl(), target, original, segment);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java",
    "content": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.factory.TranslateServiceFactory;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.translate.thread.HuoShanTranslateThread;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * @author fordes on 2022/7/31\n */\n@Service\npublic class HuoShanTranslateService extends TranslateService implements InitializingBean {\n\n    static final String AccessKeyID = \"AccessKeyID\";\n\n    static final String SecretAccessKey = \"SecretAccessKey\";\n\n    @Value(\"${service.translate.huoshan.region: cn-north-1}\")\n    private String region;\n\n    @Value(\"${service.translate.huoshan.version-date: 2020-06-01}\")\n    private String versionDate;\n\n    @Override\n    public void afterPropertiesSet() {\n        TranslateServiceFactory.register(this, ServiceProvider.HUOSHAN.name());\n    }\n\n    @Override\n    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target,\n                                                String original, Version version, Map<String, Object> config) {\n        return new HuoShanTranslateThread(versionDate, region, MapUtil.getStr(config, AccessKeyID), MapUtil.getStr(config, SecretAccessKey),\n                serial, version.getServerUrl(), target, original, segment);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java",
    "content": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.factory.TranslateServiceFactory;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.service.translate.thread.TencentTranslateThread;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * @author fordes on 2022/7/11\n */\n@Service\npublic class TencentTranslateService extends TranslateService implements InitializingBean {\n\n    static final String SECRET_ID = \"Secret Id\";\n\n    static final String SECRET_KEY = \"Secret Key\";\n\n\n    @Value(\"${service.translate.tencent.region: ap-shanghai}\")\n    private String region;\n\n    @Override\n    public void afterPropertiesSet() {\n        TranslateServiceFactory.register(this, ServiceProvider.TENCENT.name());\n    }\n\n\n    @Override\n    public Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map<String, Object> config) {\n        String id = MapUtil.getStr(config, SECRET_ID);\n        String key = MapUtil.getStr(config, SECRET_KEY);\n        return new TencentTranslateThread(id ,key, region, serial, version.getServerUrl(), target, original, segment);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java",
    "content": "package org.fordes.subtitles.view.service.translate;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport javafx.application.Platform;\nimport javafx.stage.Stage;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.event.LoadingEvent;\nimport org.fordes.subtitles.view.event.TranslateEvent;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.model.PO.Version;\nimport org.fordes.subtitles.view.utils.TranslateUtil;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.*;\n\n/**\n * @author fordes on 2022/7/29\n */\n@Slf4j\n@Service\npublic abstract class TranslateService {\n\n    @Resource\n    private ThreadPoolExecutor globalExecutor;\n\n    @Async\n    public void translate(Subtitle subtitle, String target, String original, Version version,\n                          boolean mode, Map<String, Object> config) {\n        TimeInterval interval = DateUtil.timer();\n        Singleton.get(Stage.class).fireEvent(new LoadingEvent(true));\n        //根据接口限制，重设线程池\n        int threadNum = Math.min(globalExecutor.getMaximumPoolSize(), version.getConcurrent() - 1);\n        globalExecutor.setCorePoolSize(threadNum);\n        globalExecutor.setMaximumPoolSize(threadNum);\n        //根据接口限制对数据整合分段\n        List<String> segmented = TranslateUtil.segmented(subtitle, version.getCarrying());\n\n        //延迟队列\n        DelayQueue<Segment> queue = new DelayQueue<>();\n        for (int i = 0; i < segmented.size(); i++) {\n            queue.put(new Segment(segmented.get(i), i, ((i + 1) % version.getCarrying()) - 1));\n        }\n        //添加任务, 提交至线程池\n        Collection<Future<TranslateResult>> futures = CollUtil.newArrayList();\n        try {\n            while (!queue.isEmpty()) {\n                Segment part = queue.take();\n                Integer serial = part.getSerial();\n                String segment = part.getData();\n\n                Future<TranslateResult> task = globalExecutor\n                        .submit(createTask(globalExecutor, serial, segment, target, original, version, config));\n                futures.add(task);\n            }\n\n            //遍历获取结果\n            for (Future<TranslateResult> e : futures) {\n                TranslateResult item = e.get();\n                if (item.isSuccess()) {\n                    segmented.set(item.getSerial(), item.getData());\n                } else {\n                    throw new RuntimeException(item.getData());\n                }\n            }\n\n            //合并结果\n            TranslateUtil.reduction(subtitle, segmented, mode);\n        } catch (Exception ex) {\n            log.error(ExceptionUtil.stacktraceToString(ex));\n//            ApplicationInfo.stage.fireEvent(new ToastConfirmEvent(\"翻译失败\", ex.getMessage()));\n            Platform.runLater(() ->\n                    Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.FAIL, ex.getMessage())));\n            return;\n        } finally {\n            log.debug(\"翻译线程结束，耗时：{} ms\", interval.intervalMs());\n        }\n//        ApplicationInfo.stage.fireEvent(new ToastConfirmEvent(\"翻译完成\", StrUtil.format(\"总耗时：{} ms\", interval.intervalMs())));\n//        Platform.runLater(() -> ApplicationInfo.stage.fireEvent(new LoadingEvent(false)));\n        Platform.runLater(() -> Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.SUCCESS,\n                StrUtil.format(\"总耗时：{} ms\", interval.intervalMs()))));\n    }\n\n    /**\n     * 创建翻译线程\n     *\n     * @param executor 线程池 {@link ThreadPoolExecutor}\n     * @param serial   序号，用于再整合结果时维持内容顺序\n     * @param segment  待翻译内容\n     * @param target   目标语言\n     * @param original 源语言\n     * @param version  接口版本\n     * @param config   接口配置\n     * @return 线程\n     */\n    public abstract Callable<TranslateResult> createTask(ThreadPoolExecutor executor, int serial,\n                                                         String segment, String target, String original,\n                                                         Version version, Map<String, Object> config);\n\n\n    static class Segment implements Delayed {\n\n        private final long executeTime;\n\n        @Getter\n        private final Integer serial;\n\n        @Getter\n        private final String data;\n\n        public Segment(String data, Integer serial, long delay) {\n            this.data = data;\n            this.serial = serial;\n            this.executeTime = System.nanoTime()+ TimeUnit.NANOSECONDS.convert(delay, TimeUnit.SECONDS);\n        }\n\n        @Override\n        public long getDelay(TimeUnit unit) {\n            return unit.convert(this.executeTime - System.nanoTime(), TimeUnit.NANOSECONDS);\n        }\n\n        @Override\n        public int compareTo(Delayed o) {\n            Segment that = (Segment) o;\n            return Long.compare(executeTime, that.executeTime);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java",
    "content": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.crypto.digest.HMac;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\nimport cn.hutool.crypto.digest.MD5;\nimport cn.hutool.http.*;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\n\nimport java.net.URL;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\n/**\n * @author fordes on 2022/7/26\n */\n@Slf4j\npublic class AliTranslateThread extends TranslateThread  implements Callable<TranslateResult> {\n\n    static final String CONTENT_MD5 = \"Content-MD5\";\n\n    static final String CONTENT_TYPE = \"application/json;chrset=utf-8\";\n\n    static final String X_ACS_SIGNATURE_NONCE = \"x-acs-signature-nonce\";\n\n    static final String X_ACS_SIGNATURE_METHOD = \"x-acs-signature-method\";\n\n    static final String X_ACS_VERSION = \"x-acs-version\";\n\n    static final String HMAC_SHA1 = \"HMAC-SHA1\";\n\n    static final String VERSION = \"2019-01-02\";\n\n    private final String ak_id;\n\n    private final String ak_secret;\n\n    public AliTranslateThread(String ak_id, String ak_secret, Integer serial, String serviceURL,\n                              String target, String original, String content) {\n        super(serial, serviceURL, target, original, content);\n        this.ak_id = ak_id;\n        this.ak_secret = ak_secret;\n    }\n\n\n    @Override\n    public TranslateResult call() {\n        TimeInterval interval = DateUtil.timer();\n        URL url = URLUtil.url(serviceURL);\n        Dict param = Dict.of(\n                \"FormatType\", \"text\",\n                \"SourceLanguage\", original,\n                \"TargetLanguage\", target,\n                \"SourceText\", content,\n                \"Scene\", \"general\"\n        );\n        String postBody = JSONUtil.parseObj(param).toString();\n        String bodyMd5 = Base64.encode(MD5.create().digest(postBody));\n        String uuid = UUID.randomUUID().toString();\n        String date = DateTime.now().toString(DatePattern.HTTP_DATETIME_FORMAT);\n\n\n        String stringToSign = Method.POST.name() + StrUtil.LF +\n                ContentType.JSON.getValue() + StrUtil.LF +\n                bodyMd5 + StrUtil.LF +\n                CONTENT_TYPE + StrUtil.LF +\n                date + StrUtil.LF +\n                X_ACS_SIGNATURE_METHOD + StrUtil.COLON + HMAC_SHA1 + StrUtil.LF +\n                X_ACS_SIGNATURE_NONCE + StrUtil.COLON + uuid + StrUtil.LF +\n                X_ACS_VERSION + StrUtil.COLON + VERSION + StrUtil.LF +\n                url.getFile();\n\n        String signature = new HMac(HmacAlgorithm.HmacSHA1, ak_secret.getBytes()).digestBase64(stringToSign, false);\n        String authHeader = \"acs \" + ak_id + \":\" + signature;\n\n        HttpResponse response = HttpUtil.createPost(serviceURL)\n                .header(Header.ACCEPT, ContentType.JSON.getValue())\n                .header(Header.CONTENT_TYPE, CONTENT_TYPE)\n                .header(CONTENT_MD5, bodyMd5)\n                .header(Header.DATE, date)\n                .header(Header.HOST, url.getHost())\n                .header(Header.AUTHORIZATION, authHeader)\n                .header(X_ACS_SIGNATURE_NONCE, uuid)\n                .header(X_ACS_SIGNATURE_METHOD, HMAC_SHA1)\n                .header(X_ACS_VERSION, VERSION)\n                .setFollowRedirects(true)\n                .body(postBody)\n                .execute();\n\n        JSONObject resp = JSONUtil.parseObj(response.body());\n        TranslateResult result = TranslateResult.builder().serial(serial).build();\n        if (response.isOk() && resp.containsKey(\"Data\")) {\n            result.setSuccess(true);\n            result.setData(resp.getJSONObject(\"Data\").getStr(\"Translated\"));\n        } else {\n            result.setSuccess(false);\n            result.setData(resp.getStr(\"errorMsg\"));\n        }\n        log.debug(\"序号：{} 请求 {}，耗时：{} ms\", serial,result.isSuccess()? \"成功\":\"失败\", interval.intervalMs());\n//        log.debug(resp.toStringPretty());\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java",
    "content": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.crypto.digest.MD5;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\n\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\n\n/**\n * @author fordes on 2022/7/27\n */\n@Slf4j\npublic class BaiduTranslateThread extends TranslateThread implements Callable<TranslateResult> {\n\n    static final String SALT = \"subview-proxy\";\n\n    private final String app_id;\n\n    private final String app_key;\n\n    public BaiduTranslateThread(String app_id, String app_key, Integer serial, String serviceURL,\n                                String target, String original, String content) {\n        super(serial, serviceURL, target, original, content);\n        this.app_id = app_id;\n        this.app_key = app_key;\n    }\n\n\n    @Override\n    public TranslateResult call() {\n        TimeInterval interval = DateUtil.timer();\n        HttpResponse response = HttpUtil.createPost(serviceURL)\n                .form(\"q\", content)\n                .form(\"from\", original)\n                .form(\"to\", target)\n                .form(\"appid\", app_id)\n                .form(\"salt\", SALT)\n                .form(\"sign\", MD5.create().digestHex(app_id+ content + SALT + app_key))\n                .contentType(ContentType.FORM_URLENCODED.getValue())\n                .charset(\"UTF-8\")\n                .setFollowRedirects(true)\n                .execute();\n        JSONObject resp = JSONUtil.parseObj(response.body());\n        TranslateResult result = TranslateResult.builder().serial(serial).build();\n        if (response.isOk() && !resp.containsKey(\"error_code\")) {\n            result.setSuccess(true);\n            List<JSONObject> dataList = resp.getJSONArray(\"trans_result\").toList(JSONObject.class);\n            result.setData(dataList.stream().map(e -> e.getStr(\"dst\")).collect(Collectors.joining()));\n        } else {\n            result.setSuccess(false);\n            result.setData(resp.getStr(\"error_msg\"));\n        }\n        log.debug(\"序号：{} 请求 {}，耗时：{} ms\", serial,result.isSuccess()? \"成功\":\"失败\", interval.intervalMs());\n//        log.debug(resp.toStringPretty());\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java",
    "content": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.date.format.FastDateFormat;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.Header;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.utils.TranslateUtil;\n\nimport java.net.URL;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.TreeMap;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\n\n/**\n * @author fordes on 2022/7/31\n */\n@Slf4j\npublic class HuoShanTranslateThread extends TranslateThread implements Callable<TranslateResult> {\n\n    private final String accessKeyId;\n\n    private final String secretAccessKey;\n\n    private  final String versionDate;\n\n    private final String region;\n\n    static final String Action = \"TranslateText\";\n\n    static final String Service = \"translate\";\n\n    static final String Version = \"1.0.16\";\n\n    static final String Algorithm = \"HMAC-SHA256\";\n\n\n    public HuoShanTranslateThread(String versionDate, String region, String accessKeyId, String secretAccessKey,\n                                  Integer serial, String serviceURL, String target, String original, String content) {\n        super(serial, serviceURL, target, original, content);\n        this.versionDate = versionDate;\n        this.region = region;\n        this.accessKeyId = accessKeyId;\n        this.secretAccessKey = secretAccessKey;\n    }\n\n\n    @Override\n    public TranslateResult call() throws Exception {\n        TimeInterval interval = DateUtil.timer();\n        //请求路径\n        URL url = URLUtil.url(serviceURL);\n\n        //请求体\n        String body = new JSONObject()\n                .putOnce(\"SourceLanguage\", original) //原语言\n                .putOnce(\"TargetLanguage\", target) //目标语言\n                .putOnce(\"TextList\", CollUtil.newArrayList(content)).toString(); //待翻译文本列表，长度不大于128\n        String bodyHash = SecureUtil.sha256(body);\n\n        //时间 (必须使用UTC时间)\n        DateTime now = DateTime.now();\n        String nowDate = now.toString(FastDateFormat.getInstance(DatePattern.PURE_DATE_PATTERN, TimeZone.getTimeZone(\"UTC\")));\n        String nowTime = now.toString(FastDateFormat.getInstance(DatePattern.PURE_TIME_PATTERN, TimeZone.getTimeZone(\"UTC\")));\n        String requestDate = nowDate + \"T\" + nowTime + \"Z\";\n\n        //构造需要计入签名的部分请求头\n        Map<String, String> signHeadMap = MapUtil.newHashMap();\n        signHeadMap.put(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue());\n        signHeadMap.put(Header.HOST.getValue(), url.getHost());\n        signHeadMap.put(\"X-Date\", requestDate);\n        signHeadMap.put(\"X-Content-Sha256\", bodyHash);\n        //按照ASCII也即字母序排序\n        TreeMap<String, String> signHeadMapSort = MapUtil.sort(signHeadMap);\n\n        // 正规化请求\n        String requestMethod = \"POST\";\n        String canonicalURI = \"/\";\n        String canonicalQueryString = StrUtil.format(\"Action={}&Version={}\", Action, versionDate);\n        StringBuilder canonicalHeaders = new StringBuilder();\n        signHeadMapSort.forEach((key, value) -> canonicalHeaders.append(key.trim().toLowerCase())\n                .append(StrUtil.COLON).append(value.trim()).append(StrUtil.LF));\n        String SignedHeaders = CollUtil.join(signHeadMapSort.keySet(), \";\").trim().toLowerCase();\n        String canonicalRequest = StrUtil.concat(false, requestMethod, StrUtil.LF, canonicalURI, StrUtil.LF,\n                canonicalQueryString, StrUtil.LF, canonicalHeaders, StrUtil.LF, SignedHeaders, StrUtil.LF, bodyHash);\n\n        // 签名\n        String CredentialScope = StrUtil.concat(false, nowDate, StrUtil.SLASH, region,\n                StrUtil.SLASH, Service, \"/request\");\n        String StringToSign = StrUtil.concat(false, Algorithm, StrUtil.LF, requestDate, StrUtil.LF,\n                CredentialScope, StrUtil.LF, SecureUtil.sha256(canonicalRequest));\n\n        //计算签名密钥\n        byte[] kDate = TranslateUtil.hmac256(secretAccessKey, nowDate);\n        byte[] kRegion = TranslateUtil.hmac256(kDate, region);\n        byte[] kService = TranslateUtil.hmac256(kRegion, Service);\n        byte[] kSigning = TranslateUtil.hmac256(kService, \"request\");\n\n        //计算签名\n        String Signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(kSigning, StringToSign));\n        //拼接出授权头\n        String Authorization = StrUtil.format(\"{} Credential={}/{}, SignedHeaders={}, Signature={}\",\n                Algorithm, accessKeyId, CredentialScope, SignedHeaders, Signature);\n\n        //创建真实请求\n        HttpResponse response = HttpUtil.createPost(serviceURL)\n                .header(Header.CONTENT_TYPE, ContentType.JSON.getValue())\n                .header(Header.ACCEPT, ContentType.JSON.getValue())\n                .header(Header.HOST, url.getHost())\n                .header(Header.USER_AGENT, \"volc-sdk-java/v\" + Version)\n                .header(\"X-Date\", requestDate)\n                .header(\"X-Content-Sha256\", bodyHash)\n                .header(Header.AUTHORIZATION, Authorization)\n                .setFollowRedirects(true)\n                .body(body)\n                .execute();\n        //解析结果\n        JSONObject resp = JSONUtil.parseObj(response.body());\n        TranslateResult result = TranslateResult.builder().serial(serial).build();\n        if (response.isOk() && resp.containsKey(\"TranslationList\")) {\n            result.setSuccess(true);\n            result.setData(resp.getJSONArray(\"TranslationList\").stream()\n                    .map(e -> JSONUtil.parseObj(e).getStr(\"Translation\"))\n                    .collect(Collectors.joining(StrUtil.LF)));\n        } else {\n            result.setSuccess(false);\n            result.setData(resp.getJSONObject(\"ResponseMetadata\")\n                    .getJSONObject(\"Error\").getStr(\"Message\"));\n        }\n        log.debug(\"序号：{} 请求 {}，耗时：{} ms\", serial, result.isSuccess() ? \"成功\" : \"失败\", interval.intervalMs());\n//        log.debug(resp.toStringPretty());\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java",
    "content": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.date.format.FastDateFormat;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.model.DTO.TranslateResult;\nimport org.fordes.subtitles.view.utils.TranslateUtil;\n\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.util.TimeZone;\nimport java.util.concurrent.Callable;\n\n/**\n * @author fordes on 2022/7/29\n */\n@Slf4j\npublic class TencentTranslateThread extends TranslateThread implements Callable<TranslateResult> {\n\n\n    static final String CT_JSON = \"application/json; charset=utf-8\";\n\n    static final String SERVICE = \"tmt\";\n\n    static final String ACTION = \"TextTranslate\";\n\n    static final String VERSION = \"2018-03-21\";\n\n    static final String ALGORITHM = \"TC3-HMAC-SHA256\";\n\n    private final String secretId;\n\n    private final String secretKey;\n\n    private final String region;\n\n    public TencentTranslateThread(String secretId, String secretKey, String region, Integer serial, String serviceURL,\n                                  String target, String original, String content) {\n        super(serial, serviceURL, target, original, content);\n        this.region = region;\n        this.secretId = secretId;\n        this.secretKey = secretKey;\n    }\n\n    @Override\n    public TranslateResult call() throws Exception {\n        TimeInterval interval = DateUtil.timer();\n        URL url = URLUtil.url(serviceURL);\n\n        //时间\n        long now = DateUtil.currentSeconds();\n        String timestamp = String.valueOf(now);\n        String date = DateTime.of(now * 1000)\n                .toString(FastDateFormat.getInstance(DatePattern.NORM_DATE_PATTERN, TimeZone.getTimeZone(\"UTC\")));\n\n        //拼接规范请求串\n        String httpRequestMethod = \"POST\";\n        String canonicalUri = \"/\";\n        String canonicalQueryString = \"\";\n        String canonicalHeaders = \"content-type:application/json; charset=utf-8\\n\" + \"host:\" + url.getHost() + \"\\n\";\n        String signedHeaders = \"content-type;host\";\n\n        //整合参数\n        Dict param = Dict.of(\n                \"SourceText\", content,\n                \"Source\", original,\n                \"Target\", target,\n                \"ProjectId\", 0);\n        String payload = JSONUtil.toJsonStr(param);\n        String hashedRequestPayload = TranslateUtil.sha256Hex(payload);\n        String canonicalRequest = httpRequestMethod + \"\\n\" + canonicalUri + \"\\n\" + canonicalQueryString + \"\\n\"\n                + canonicalHeaders + \"\\n\" + signedHeaders + \"\\n\" + hashedRequestPayload;\n\n        //拼接待签名字符串\n        String credentialScope = date + \"/\" + SERVICE + \"/\" + \"tc3_request\";\n        String hashedCanonicalRequest = TranslateUtil.sha256Hex(canonicalRequest);\n        String stringToSign = ALGORITHM + \"\\n\" + timestamp + \"\\n\" + credentialScope + \"\\n\" + hashedCanonicalRequest;\n\n        //计算签名\n        byte[] secretDate = TranslateUtil.hmac256((\"TC3\" + secretKey).getBytes(StandardCharsets.UTF_8), date);\n        byte[] secretService = TranslateUtil.hmac256(secretDate, SERVICE);\n        byte[] secretSigning = TranslateUtil.hmac256(secretService, \"tc3_request\");\n        String signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(secretSigning, stringToSign)).toLowerCase();\n\n        //拼接 Authorization\n        String authorization = ALGORITHM + \" \" + \"Credential=\" + secretId + \"/\" + credentialScope + \", \"\n                + \"SignedHeaders=\" + signedHeaders + \", \" + \"Signature=\" + signature;\n\n        HttpResponse response = HttpUtil.createPost(serviceURL)\n                .header(\"Authorization\", authorization)\n                .header(\"Content-Type\", CT_JSON)\n                .header(\"Host\", url.getHost())\n                .header(\"X-TC-Action\", ACTION)\n                .header(\"X-TC-Timestamp\", timestamp)\n                .header(\"X-TC-Version\", VERSION)\n                .header(\"X-TC-Region\", region)\n                .setFollowRedirects(true)\n                .body(payload)\n                .execute();\n        JSONObject resp = JSONUtil.parseObj(response.body());\n        TranslateResult result = TranslateResult.builder().serial(serial)\n                .success(false).data(\"翻译失败！\").build();\n        if (response.isOk() && resp.containsKey(\"Response\")) {\n            JSONObject respJson = resp.getJSONObject(\"Response\");\n            if (respJson.containsKey(\"TargetText\")) {\n                result.setSuccess(true);\n                result.setData(respJson.getStr(\"TargetText\"));\n            }else {\n                result.setSuccess(false);\n                result.setData(respJson.getJSONObject(\"Error\").getStr(\"Message\"));\n            }\n        }\n        long intervalTime = interval.intervalMs();\n        log.debug(\"序号：{} 请求 {}，耗时：{} ms\", serial,result.isSuccess()? \"成功\":\"失败\", intervalTime);\n//        log.debug(resp.toStringPretty());\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java",
    "content": "package org.fordes.subtitles.view.service.translate.thread;\n\nimport lombok.AllArgsConstructor;\n\n/**\n * 翻译线程抽象\n *\n * @author fordes on 2022/7/27\n */\n@AllArgsConstructor\npublic abstract class TranslateThread {\n\n    /**\n     * 序号，将随结果返回，用于还原顺序\n     */\n    public Integer serial;\n\n    /**\n     * 服务地址，即api调用地址\n     */\n    public String serviceURL;\n\n    /**\n     * 目标语言 通常为代码\n     */\n    public String target;\n\n    /**\n     * 原语言 通常为代码\n     */\n    public String original;\n\n    /**\n     * 待翻译内容\n     */\n    public String content;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java",
    "content": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.UUID;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport net.sf.sevenzipjbinding.ExtractOperationResult;\nimport net.sf.sevenzipjbinding.IInArchive;\nimport net.sf.sevenzipjbinding.SevenZip;\nimport net.sf.sevenzipjbinding.SevenZipException;\nimport net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;\nimport net.sf.sevenzipjbinding.simple.ISimpleInArchive;\nimport net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;\nimport org.fordes.subtitles.view.enums.FileEnum;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.Collections;\n\n/**\n * 文件解压工具类\n *\n * @author fordes on 2022/4/4\n */\n@Slf4j\npublic class ArchiveUtil {\n\n    /**\n     * 解压文件至当前路径下uuid命名路径 并删除原文件\n     * 将排除不受支持的文件\n     *\n     * @param file 压缩文件\n     * @return 文件路径\n     */\n    public static Collection<File> unArchiveToCurrentPath(File file) {\n        Collection<File> result = Collections.emptyList();\n        if (FileUtil.exist(file)) {\n            String outPath = StrUtil.concat(false, file.getParent(), File.separator, UUID.fastUUID().toString());\n            //创建目标文件夹\n            if (!FileUtil.exist(outPath)) {\n                FileUtil.mkdir(outPath);\n            }\n            String suffix = FileUtil.getSuffix(file);\n            if (StrUtil.equalsAnyIgnoreCase(suffix, FileEnum.SUPPORT_SUBTITLE)) {\n                File newFile = FileUtil.file(StrUtil.concat(false, outPath, File.separator, URLUtil.decode(file.getName(), Charset.defaultCharset())));\n                FileUtil.move(file, newFile, true);\n                result = CollUtil.newArrayList(newFile);\n            } else {\n                result = unArchiveFile(file, outPath, FileEnum.SUPPORT_SUBTITLE);\n            }\n            FileUtil.del(file);\n        }\n        return result;\n    }\n\n\n    /**\n     * 解压文件，不保留内部结构\n     *\n     * @param in      压缩文件路径\n     * @param outPath 输出路径\n     * @param filter  指定需要提取的文件后缀 如 ass\n     */\n    public static Collection<File> unArchiveFile(File in, String outPath, String... filter) {\n        Collection<File> result = CollUtil.newArrayList();\n        TimeInterval interval = DateUtil.timer();\n\n        RandomAccessFile randomAccessFile = null;\n        IInArchive inArchive = null;\n        try {\n            randomAccessFile = new RandomAccessFile(in.getPath(), \"r\");\n            inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));\n            ISimpleInArchive archive = inArchive.getSimpleInterface();\n            for (ISimpleInArchiveItem item : archive.getArchiveItems()) {\n                if (!item.isFolder() && StrUtil.equalsAnyIgnoreCase(FileUtil.getSuffix(item.getPath()), filter)) {\n                    File file = FileUtil.file(StrUtil.concat(false, outPath, File.separator, item.getPath()));\n                    ExtractOperationResult operationResult = item.extractSlow(data -> {\n                        FileUtil.writeBytes(data, file);\n                        return data.length;\n                    });\n                    if (operationResult == ExtractOperationResult.OK) {\n                        result.add(file);\n                        log.debug(\"提取成功 => {}\", item.getPath());\n                    } else {\n                        log.error(\"提取失败 => {}\\n{}\", item.getPath(), operationResult);\n                    }\n                }\n            }\n        } catch (FileNotFoundException | SevenZipException e) {\n            log.error(\"解压文件出错！{} => {}\", in.getPath(), outPath);\n            log.error(ExceptionUtil.stacktraceToString(e));\n        } finally {\n            if (inArchive != null) {\n                try {\n                    inArchive.close();\n                } catch (SevenZipException e) {\n                    log.error(ExceptionUtil.stacktraceToString(e));\n                }\n            }\n            if (randomAccessFile != null) {\n                try {\n                    randomAccessFile.close();\n                } catch (IOException e) {\n                    log.error(ExceptionUtil.stacktraceToString(e));\n                }\n            }\n            if (result.isEmpty()) {\n                FileUtil.del(outPath);\n            }\n\n        }\n        log.debug(\"解压文件：{} => {}，耗时：{} ms\", in.getPath(), outPath, interval.intervalMs());\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java",
    "content": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.spring.SpringUtil;\nimport org.fordes.subtitles.view.enums.ServiceProvider;\nimport org.fordes.subtitles.view.enums.ServiceType;\nimport org.fordes.subtitles.view.mapper.InterfaceMapper;\nimport org.fordes.subtitles.view.model.PO.Language;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * 缓存\n *\n * @author fordes on 2022/7/27\n */\npublic class CacheUtil {\n\n    public static final Map<ServiceType, Map<ServiceProvider, List<Language>>> languageMap = MapUtil.newHashMap();\n\n    /**\n     * 初始化语言字典\n     *\n     * @param data 数据\n     */\n    public static void initLanguageDict(List<Dict> data) {\n        data.stream().collect(Collectors.groupingBy(e -> ServiceType.valueOf(e.getStr(Language.COL_TYPE))))\n                .forEach((k, v) -> {\n                    Map<ServiceProvider, List<Language>> providerMap = MapUtil.newHashMap();\n                    Arrays.stream(ServiceProvider.values()).forEach(p -> {\n                        Map<String, List<Language>> idMap = v.stream()\n                                .filter(q -> q.containsKey(p.name().toLowerCase()))\n                                .map(q -> {\n                                    String target = MapUtil.getStr(q, p.name().toLowerCase() + Language.TARGET);\n                                    return new Language()\n                                            .setId(q.getInt(Language.COL_ID))\n                                            .setName(q.getStr(Language.COL_NAME))\n                                            .setCode(q.getStr(p.name().toLowerCase()))\n                                            .setGeneral(q.getBool(Language.COL_GENERAL))\n                                            .set_target(StrUtil.split(target, StrUtil.COMMA, true, true));\n                                })\n                                .collect(Collectors.groupingBy(Language::getCode, Collectors.toList()));\n\n                        List<Language> languageList = CollUtil.newArrayList();\n                        idMap.forEach((x, y) -> {\n                            Language item = y.get(0);\n                            if (item.get_target().isEmpty()) {\n                                item.setTarget(idMap.values().stream().map(e -> e.get(0)).collect(Collectors.toList()));\n                            } else {\n                                item.setTarget(item.get_target().stream()\n                                        .map(e -> idMap.get(e).get(0))\n                                        .collect(Collectors.toList()));\n                            }\n                            languageList.add(item);\n                        });\n                        providerMap.put(p, languageList);\n                    });\n                    languageMap.put(k, providerMap);\n                });\n    }\n\n    /**\n     * 获取语言字典\n     *\n     * @param type     {@link ServiceType}\n     * @param provider {@link ServiceProvider}\n     * @param general  是否只获取常用语言\n     * @return {@link List<Language>}\n     */\n    public static List<Language> getLanguageDict(ServiceType type, ServiceProvider provider, boolean general) {\n        if (languageMap.isEmpty()) {\n            CacheUtil.initLanguageDict(SpringUtil.getBean(InterfaceMapper.class).getLanguageList());\n        }\n        List<Language> result = languageMap.get(type).get(provider);\n        return general ?\n                result.stream().filter(Language::isGeneral).collect(Collectors.toList()) : result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/FileUtils.java",
    "content": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ClassPathResource;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.http.HttpUtil;\nimport javafx.stage.DirectoryChooser;\nimport javafx.stage.FileChooser;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.constant.CommonConstant;\nimport org.fordes.subtitles.view.enums.FileEnum;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.model.DTO.Video;\nimport org.fordes.subtitles.view.model.PO.FileRecord;\nimport org.fordes.subtitles.view.utils.submerge.utils.EncodeUtils;\nimport org.springframework.lang.NonNull;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.RandomAccessFile;\nimport java.nio.channels.FileChannel;\nimport java.nio.channels.FileLock;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.stream.Collectors;\n\nimport static cn.hutool.core.thread.ThreadUtil.sleep;\n\n/**\n * 文件工具类\n *\n * @author fordes on 2022/1/23\n */\n@Slf4j\npublic class FileUtils {\n\n    /**\n     * 根据路径获取文件流，支持http和resource\n     * @param path\n     * @return\n     */\n    public static InputStream getStream(@NonNull String path) {\n        if (ReUtil.isMatch(\"^http[s]?://.*\", path)) {\n            HttpResponse response = HttpUtil.createGet(path, true).execute();\n            if (response.isOk()) {\n                return response.bodyStream();\n            }\n        }else {\n            ClassPathResource resource = new ClassPathResource(path);\n            return resource.getStream();\n        }\n\n        throw new RuntimeException(StrUtil.format(\"resource: {} not found\", path));\n    }\n\n    /**\n     * 选择文件\n     * @param title 选择框标题内容\n     * @param items 选项\n     * @return 返回指定文件选择器\n     */\n    public static FileChooser chooseFile(String title, FileEnum... items) {\n        FileChooser fileChooser = new FileChooser();\n        fileChooser.setTitle(title);\n        fileChooser.setInitialDirectory(new File(CommonConstant.PATH_HOME));\n        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(\"全部文件\", \"*.*\"));\n        if (ArrayUtil.isNotEmpty(items)) {\n            fileChooser.getExtensionFilters().addAll(Arrays.stream(items)\n                    .filter(e -> e.support)\n                    .map(e -> new FileChooser.ExtensionFilter(e.suffix, CommonConstant.PREFIX + e.suffix))\n                    .collect(Collectors.toList()));\n        }\n        return fileChooser;\n    }\n\n\n    /**\n     * 选择路径\n     * @return 文件夹选择器\n     */\n    public static DirectoryChooser choosePath(String path) {\n        DirectoryChooser directoryChooser = new DirectoryChooser();\n        directoryChooser.setTitle(CommonConstant.TITLE_PATH);\n        directoryChooser.setInitialDirectory(FileUtil.file(StrUtil.isNotEmpty(path)? path: CommonConstant.PATH_HOME));\n        return directoryChooser;\n    }\n\n    /**\n     * 读取文件信息\n     * @param file 文件\n     * @return 文件信息实例\n     */\n    public static <T> FileRecord readFileInfo(File file) throws IOException {\n        String suffix = FileUtil.extName(file);\n        FileRecord info;\n        FileEnum type = FileEnum.of(FileUtil.getSuffix(file));\n\n        assert type != null;\n        if (type.media) {\n            info =  new Video().setFormat(type);\n        }else {\n            info = new Subtitle().setCharset(EncodeUtils.guessEncoding(file)).setFormat(type);\n        }\n\n        return info.setFile(file)\n                .setFile_name(file.getName())\n                .setPath(file.getPath())\n                .setSize(FileUtil.readableFileSize(file))\n                .setFile_modify_time(FileUtil.lastModifiedTime(file));\n    }\n\n    /**\n     * 加锁将集合按行写入文件\n     *\n     * @param file    目标文件\n     * @param content 内容集合\n     */\n    public static void write(File file, Collection<String> content, String charset) {\n        write(file,CollUtil.join(content, StrUtil.CRLF), charset);\n    }\n\n    public static void write(File file, String content, String charset) {\n        if (StrUtil.isNotEmpty(content)) {\n            try (RandomAccessFile accessFile = new RandomAccessFile(file, \"rw\");\n                 FileChannel channel = accessFile.getChannel()) {\n                //加锁写入文件，如获取不到锁则休眠\n                FileLock fileLock = null;\n                while (true) {\n                    try {\n                        fileLock = channel.tryLock();\n                        break;\n                    } catch (Exception e) {\n                        sleep(1000);\n                    }\n                }\n                accessFile.seek(accessFile.length());\n                accessFile.write(content.getBytes(charset));\n                accessFile.write(StrUtil.CRLF.getBytes(charset));\n            } catch (IOException ioException) {\n                log.error(\"写入文件出错，{} => {}\", file.getPath(), ioException.getMessage());\n                throw new RuntimeException(\"写入文件出错\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java",
    "content": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport javafx.scene.control.IndexRange;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.enums.FileEnum;\nimport org.fordes.subtitles.view.handler.CallBackHandler;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.utils.submerge.parser.ParserFactory;\nimport org.fordes.subtitles.view.utils.submerge.parser.SubtitleParser;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\nimport org.fxmisc.richtext.StyleClassedTextArea;\n\nimport java.time.LocalTime;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * @author fordes on 2022/7/19\n */\n@Slf4j\npublic class SubtitleUtil {\n\n\n    /**\n     * 纯文本搜索 使用 {@link SearchCache} 单例作为缓存\n     *\n     * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean)\n     */\n    public static void search(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) {\n        search(Singleton.get(SearchCache.class), area, target, isIgnoreCase, isRegular);\n    }\n\n    /**\n     * 文本替换 前置 搜索 使用 {@link ReplaceCache} 单例作为缓存\n     *\n     * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean) (ReplaceCache, StyleClassedTextArea, String, String, boolean, boolean)\n     */\n    public static void find(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) {\n        search(Singleton.get(ReplaceCache.class), area, target, isIgnoreCase, isRegular);\n    }\n\n    /**\n     * 简易文本搜索\n     *\n     * @param area         被搜索文本\n     * @param target       目标关键字\n     * @param isIgnoreCase 忽略大小写\n     * @param isRegular    正则搜索\n     */\n    public static <T extends SearchCache> void search(T cache, StyleClassedTextArea area, String target,\n                                                      boolean isIgnoreCase, boolean isRegular) {\n        int cursor;\n        String text;\n        if (StrUtil.equals(cache.getTarget(), target)) {\n            cursor = cache.getAnchor() + 1;\n            text = area.getText(cursor, area.getLength());\n        } else {\n            cache.reset();\n            cursor = 0;\n            text = area.getText();\n        }\n        int start = 0, end = 0;\n        for (String line : text.split(StrUtil.LF)) {\n            if (isRegular) {\n                Matcher matcher = Pattern.compile(target).matcher(line);\n                if (matcher.find()) {\n                    start = cursor + line.indexOf(matcher.group(0));\n                    end = cursor + line.indexOf(matcher.group(0)) + matcher.group(0).length();\n                    break;\n                }\n            } else {\n                int pos = StrUtil.indexOf(line, target, 0, isIgnoreCase);\n                if (pos >= 0) {\n                    start = cursor + pos;\n                    end = cursor + pos + target.length();\n                    break;\n                }\n            }\n            cursor += line.length() + 1;\n        }\n        if (start != 0 && end != 0) {\n            area.moveTo(end);\n            area.requestFollowCaret();\n            area.selectRange(start, end);\n            cache.setAnchor(start);\n            cache.setTarget(target);\n            if (cache instanceof ReplaceCache) {\n                ((ReplaceCache) cache).setCaretPosition(end);\n            }\n        } else cache.reset();\n    }\n\n\n    /**\n     * 文本替换\n     *\n     * @param area         被处理文本区\n     * @param subtitle     对应字幕文件\n     * @param searchStr    被替换内容\n     * @param replaceStr   替换内容\n     * @param isAll        是否替换全部\n     * @param isIgnoreCase 是否忽略大小写\n     * @param isRegular    （searchStr）是否为正则表达式\n     */\n    public static void replace(StyleClassedTextArea area, Subtitle subtitle, String searchStr, String replaceStr,\n                               boolean isAll, boolean isIgnoreCase, boolean isRegular) throws Exception {\n        if (isAll) {\n            String text = area.getText();\n            if (isRegular) {\n                Matcher matcher = Pattern.compile(searchStr).matcher(text);\n                if (matcher.find()) {\n                    text = matcher.replaceAll(replaceStr);\n                }\n            } else {\n                text = isIgnoreCase ?\n                        StrUtil.replaceIgnoreCase(text, searchStr, replaceStr) :\n                        StrUtil.replace(text, searchStr, replaceStr);\n            }\n            area.clear();\n            area.append(text, StrUtil.EMPTY);\n        } else {\n            ReplaceCache cache = Singleton.get(ReplaceCache.class);\n            if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) {\n                area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY);\n            } else {\n                search(cache, area, searchStr, isIgnoreCase, isRegular);\n                if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) {\n                    area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY);\n                }else return;\n            }\n        }\n\n        TimedTextFile timedTextFile = SubtitleUtil.parse(area.getText(), subtitle.getFormat());\n        subtitle.setTimedTextFile(timedTextFile);\n    }\n\n    /**\n     * 时间轴位移\n     *\n     * @param timedTextFile 字幕\n     * @param begin         开始时间\n     * @param range         位移范围\n     * @param mode          显示模式\n     * @return 时间轴位移后的字幕\n     */\n    public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, IndexRange range, boolean mode) {\n        LocalTime start = CollUtil.getFirst(timedTextFile.getTimedLines()).getTime().getStart();\n        long poor = begin.toNanoOfDay() - start.toNanoOfDay();\n        if (range != null) {\n            long sort = 0;\n            for (TimedLine item : timedTextFile.getTimedLines()) {\n                sort += toStr(item, mode).length();\n                if (sort > range.getEnd()) {\n                    break;\n                } else if (sort >= range.getStart()) {\n                    item.getTime().setStart(LocalTime.ofNanoOfDay(item.getTime().getStart().toNanoOfDay() + poor));\n                    item.getTime().setEnd(LocalTime.ofNanoOfDay(item.getTime().getEnd().toNanoOfDay() + poor));\n                }\n            }\n        } else {\n            for (TimedLine item : timedTextFile.getTimedLines()) {\n                revise(item.getTime(), poor);\n            }\n        }\n        return timedTextFile;\n    }\n\n    /**\n     * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean)\n     */\n    private static void revise(TimedObject timedLine, long poor) {\n        timedLine.setStart(LocalTime.ofNanoOfDay(timedLine.getStart().toNanoOfDay() + poor));\n        timedLine.setEnd(LocalTime.ofNanoOfDay(timedLine.getEnd().toNanoOfDay() + poor));\n    }\n\n    /**\n     * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean)\n     */\n    public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, boolean mode) {\n        return revise(timedTextFile, begin, null, mode);\n    }\n\n    /**\n     * 从文件解析字幕\n     *\n     * @param subtitle 字幕文件\n     * @throws Exception 异常\n     */\n    public static void parse(Subtitle subtitle) throws Exception {\n        TimeInterval timer = DateUtil.timer();\n        SubtitleParser parser = ParserFactory.getParser(subtitle.getFormat().suffix);\n        TimedTextFile content = parser.parse(subtitle.getFile(), subtitle.getCharset());\n        log.debug(\"解析字幕耗时：{} ms\", timer.interval());\n        subtitle.setTimedTextFile(content);\n    }\n\n    /**\n     * 从文本字幕解析字幕\n     *\n     * @param str  字幕文本\n     * @param type 字幕格式\n     * @return 字幕结构\n     * @throws Exception 异常\n     */\n    public static TimedTextFile parse(String str, FileEnum type) throws Exception {\n        return ParserFactory.getParser(type.suffix).parse(str, StrUtil.EMPTY);\n    }\n\n    /**\n     * 字幕结构转换为字符串\n     *\n     * @param mode 解析模式 f-简洁模式 t-完整模式\n     * @return 字符串\n     */\n    public static String toStr(TimedTextFile subtitle, boolean mode) {\n        if (!mode) {\n            StringBuilder content = new StringBuilder();\n            subtitle.getTimedLines().forEach(item\n                    -> content.append(CollUtil.join(item.getTextLines(), StrUtil.CRLF)).append(StrUtil.CRLF));\n            return content.toString();\n        } else {\n            return subtitle.toString();\n        }\n    }\n\n    /**\n     * 字幕结构转换为字符串\n     *\n     * @param mode 解析模式 f-简洁模式 t-完整模式\n     * @return 字符串\n     */\n    public static String toStr(TimedLine timedLine, boolean mode) {\n        return mode ? timedLine.toString() : CollUtil.join(timedLine.getTextLines(), StrUtil.CRLF);\n    }\n\n    /**\n     * 写入字幕结构到源文件\n     *\n     * @param subtitle 字幕\n     * @param handler  回调\n     */\n    public static void write(Subtitle subtitle, CallBackHandler<Boolean> handler) {\n        try {\n            FileUtils.write(subtitle.getFile(), subtitle.getTimedTextFile().toString(), subtitle.getCharset());\n        } catch (RuntimeException e) {\n            handler.handle(false);\n        }\n        handler.handle(true);\n    }\n\n    /**\n     * 搜索操作 上一步结果缓存\n     */\n    @Data\n    static class SearchCache {\n\n        private String target;\n        private int anchor;\n\n        public SearchCache() {\n            reset();\n        }\n\n        public void reset() {\n            this.target = StrUtil.EMPTY;\n            this.anchor = 0;\n        }\n\n    }\n\n    /**\n     * 替换操作 上一步结果缓存\n     */\n    @Data\n    @EqualsAndHashCode(callSuper = false)\n    static class ReplaceCache extends SearchCache {\n        private int caretPosition;\n\n        @Override\n        public void reset() {\n            this.caretPosition = 0;\n            super.reset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java",
    "content": "package org.fordes.subtitles.view.utils;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.model.DTO.Subtitle;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.nio.CharBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 翻译工具\n *\n * @author fordes on 2022/7/26\n */\n@Slf4j\npublic class TranslateUtil {\n\n    private final static String QUESTION_MARK = \"?\";\n\n    public final static String SEPARATIST = \"><\";\n\n    /**\n     * 字符串切分，按照指定的分隔符切分字符串为长度不超过{@code maxLength}的集合\n     * @param content 内容\n     * @param maxLength 最大长度\n     * @return 切分后的集合\n     */\n    public static List<String> segmented(String content, int maxLength) {\n        List<String> result = CollUtil.newArrayList();\n        try (StringReader reader = StrUtil.getReader(content)) {\n            StringBuilder builder = StrUtil.builder();\n            StringBuilder temp = StrUtil.builder();\n            CharBuffer buffer = CharBuffer.allocate(1);\n            while (-1 != reader.read(buffer)) {\n                CharSequence s = buffer.flip();\n                temp.append(s);\n                if (StrUtil.equalsAny(s, StrUtil.COMMA, StrUtil.DOT, StrUtil.LF, QUESTION_MARK)) {\n                    if (builder.length() + temp.length() >= maxLength) {\n                        result.add(builder.toString());\n                        builder.setLength(0);\n                    }\n                    builder.append(temp);\n                    temp.setLength(0);\n                }\n            }\n            result.add(builder.toString());\n        } catch (IOException e) {\n            throw new IORuntimeException(e);\n        }\n        return result;\n    }\n\n\n    /**\n     * @see #segmented(String, int)\n     * 重载方法 多行文本使用\\n分割\n     */\n    public static List<String> segmented(List<String> content, int maxLength) {\n        return segmented(CollUtil.join(content, StrUtil.LF), maxLength);\n    }\n\n    /**\n     * @see #segmented(String, int)\n     * 重载方法 每段字幕使用 {@link #SEPARATIST} 分割\n     */\n    public static List<String> segmented(Subtitle subtitle, int maxLength) {\n        List<String> data = CollUtil.newArrayList();\n        subtitle.getTimedTextFile().getTimedLines().forEach(e\n                -> data.add(CollUtil.join(e.getTextLines(), SEPARATIST)));\n        return segmented(data, maxLength);\n    }\n\n    /**\n     * 将经过 {@link #segmented(Subtitle, int)} 切分后的结构还原至字幕文件中\n     * @param subtitle  字幕\n     * @param data  数据\n     * @param mode  模式 f-覆盖模式，t-追加模式\n     */\n    public static void reduction(Subtitle subtitle, List<String> data, boolean mode) {\n        StringBuilder builder = StrUtil.builder();\n        data.forEach(builder::append);\n\n        List<String> lines = StrUtil.split(builder.toString(), StrUtil.LF);\n        for (int part = 0; part < lines.size(); part++) {\n            TimedLine line = CollUtil.get(subtitle.getTimedTextFile().getTimedLines(), part);\n            List<String> second =  StrUtil.split(lines.get(part), SEPARATIST);\n            if (mode) {\n                List<String> temp = new ArrayList<>(line.getTextLines().size());\n                List<String> first = line.getTextLines();\n                for (int i = 0; i < first.size(); i++) {\n                    temp.add(CollUtil.get(first, i));\n                    temp.add(CollUtil.get(second, i));\n                }\n                line.setTextLines(temp);\n            }else {\n                line.setTextLines(second);\n            }\n        }\n    }\n\n    public static byte[] hmac256(byte[] key, String msg) throws Exception {\n        return hmac256(key, msg.getBytes(StandardCharsets.UTF_8));\n    }\n\n    public static byte[] hmac256(byte[] key, byte[] msg) throws Exception {\n        Mac mac = Mac.getInstance(\"HmacSHA256\");\n        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());\n        mac.init(secretKeySpec);\n        return mac.doFinal(msg);\n    }\n\n    public static byte[] hmac256(String key, String msg) throws Exception {\n        return hmac256(key.getBytes(StandardCharsets.UTF_8), msg.getBytes(StandardCharsets.UTF_8));\n    }\n\n    public static String sha256Hex(String s) throws Exception {\n        MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n        byte[] d = md.digest(s.getBytes(StandardCharsets.UTF_8));\n        return HexUtil.encodeHexStr(d).toLowerCase();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java",
    "content": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.model.search.Selector;\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * HTML解析器\n *\n * @author fordes on 2022/3/29\n */\npublic class HTMLParsing extends Parsing {\n\n    private Document doc;\n\n    public HTMLParsing(Object data) {\n        super(data);\n        this.doc = Jsoup.parse((String) data);\n    }\n\n    @Override\n    public Object parsing(Selector selector) {\n        List<String> fields = getFields(doc, selector);\n        return selector.only ? CollUtil.getFirst(fields): fields;\n    }\n\n    private static List<String> getFields(Document doc, Selector selector) {\n        if (ObjectUtil.isNotEmpty(selector)) {\n            return doc.select(selector.css).stream()\n                    .map(e -> getField(e, selector.attr, selector.regular, selector.format))\n                    .collect(Collectors.toList());\n        }else {\n            return Collections.emptyList();\n        }\n    }\n\n\n    private static String getField(Element element, String attr, String regular, String format) {\n        String attrField = StrUtil.isBlank(attr) ?\n                element.text() : element.attr(attr);\n        String regField = StrUtil.isBlank(regular) ?\n                StrUtil.trim(attrField) : CollUtil.join(ReUtil.findAll(regular, attrField, 1), StrUtil.EMPTY);\n        return StrUtil.isBlank(format) ? regField : StrUtil.format(format, regField);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java",
    "content": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport org.fordes.subtitles.view.model.search.Selector;\n\nimport java.util.List;\n\n/**\n * JSON解析器\n *\n * @author fordes on 2022/3/29\n */\npublic class JSONParsing extends Parsing {\n\n    private JSONObject json;\n\n    public JSONParsing(Object data) {\n        super(data);\n        this.json = JSONUtil.parseObj(data);\n    }\n\n    //jsonKey > regular > foramt\n    //TODO 未测试\n    @Override\n    public Object parsing(Selector selector) {\n        List<String> keys = StrUtil.split(selector.jsonKey, StrUtil.C_DOT);\n        for (int i = 0; i < keys.size(); i++) {\n            if (i == keys.size()-1) {\n                return json.get(keys.get(i));\n            }else {\n                json = json.getJSONObject(keys.get(i));\n            }\n        }\n        return json;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java",
    "content": "package org.fordes.subtitles.view.utils.search;\n\nimport org.fordes.subtitles.view.model.search.Selector;\n\n/**\n * 解析器抽象\n *\n * @author fordes on 2022/3/29\n */\npublic abstract class Parsing {\n\n    public Parsing(Object data) {\n\n    }\n\n    public abstract Object parsing(Selector selector);\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java",
    "content": "package org.fordes.subtitles.view.utils.search;\n\nimport cn.hutool.http.ContentType;\nimport org.fordes.subtitles.view.model.search.Selector;\n\n/**\n * 解析器工厂\n *\n * @author fordes on 2022/3/29\n */\npublic class ParsingFactory {\n\n    private Parsing parsing;\n\n    public ParsingFactory(Object data, ContentType contentType) {\n        parsing = ContentType.JSON.equals(contentType)?\n                new JSONParsing(data): new HTMLParsing(data);\n    }\n\n    public Object getResult(Selector selector) {\n        return parsing.parsing(selector);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java",
    "content": "package org.fordes.subtitles.view.utils.submerge;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSSub;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.Events;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.config.SimpleSubConfig;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTSub;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime;\nimport org.fordes.subtitles.view.utils.submerge.utils.ConvertUtils;\n\nimport java.time.LocalTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Service used to manage subtitles\n */\npublic class SubmergeAPI {\n\n    /**\n     * Change the framerate of a subtitle\n     *\n     * @param timedFile       the subtitle\n     * @param sourceFramerate le source framerate. Ex: 25.000\n     * @param targetFramerate the target framerate. Ex: 23.976\n     */\n    public void convertFramerate(TimedTextFile timedFile, double sourceFramerate, double targetFramerate) {\n\n        double ratio = sourceFramerate / targetFramerate;\n        timedFile.getTimedLines().forEach(timedLine -> {\n            TimedObject time = timedLine.getTime();\n            long s = Math.round(time.getStart().toNanoOfDay() * ratio);\n            long e = Math.round(time.getEnd().toNanoOfDay() * ratio);\n\n            time.setStart(LocalTime.ofNanoOfDay(s));\n            time.setEnd(LocalTime.ofNanoOfDay(e));\n        });\n    }\n\n    /**\n     * TimedTextFile to SRT conversion\n     *\n     * @param timedFile the TimedTextFile\n     * @return the SRTSub object\n     */\n    public SRTSub toSRT(TimedTextFile timedFile) {\n\n        SRTSub srt = new SRTSub();\n\n        int i = 0;\n        for (TimedLine timedLine : timedFile.getTimedLines()) {\n\n            int id = ++i;\n            TimedObject time = timedLine.getTime();\n            SRTTime srtTime = new SRTTime(time.getStart(), time.getEnd());\n\n            List<String> textLines = timedLine.getTextLines();\n            List<String> newLines = new ArrayList<>();\n\n            for (String textLine : textLines) {\n                newLines.add(ConvertUtils.toSRTString(textLine));\n            }\n\n            SRTLine srtLine = new SRTLine(id, srtTime, newLines);\n\n            srt.add(srtLine);\n        }\n\n        return srt;\n    }\n\n    /**\n     * SubInput to ASS conversion\n     *\n     * @param config the configuration object\n     * @return the ASSSub object\n     */\n    public ASSSub toASS(SimpleSubConfig config) {\n\n        return mergeToAss(config);\n    }\n\n    /**\n     * Merge several subtitles into one ASS\n     *\n     * @param configs : configuration object of the subtitles\n     * @return\n     */\n    public ASSSub mergeToAss(SimpleSubConfig... configs) {\n\n        ASSSub ass = new ASSSub();\n        Set<Events> ev = ass.getEvents();\n\n        for (SimpleSubConfig config : configs) {\n            ass.getStyle().add(ConvertUtils.createV4Style(config));\n            TimedTextFile sub = config.getSub();\n            sub.getTimedLines().forEach(line -> ev.add(ConvertUtils.createEvent(line, config.getStyleName())));\n        }\n\n        return ass;\n    }\n\n    /**\n     * Transform all multi-lines subtitles to single-line\n     *\n     * @param timedFile the TimedTextFile\n     */\n    public void mergeTextLines(TimedTextFile timedFile) {\n        timedFile.getTimedLines().forEach(item -> {\n            List<String> textLines = item.getTextLines();\n            if (textLines.size() > 1) {\n                textLines.set(0, String.join(StrUtil.SPACE, textLines));\n                textLines.subList(1, textLines.size()).clear();\n            }\n        });\n    }\n\n    /**\n     * Synchronise the timecodes of a subtitle from another one\n     *\n     * @param fileToAdjust  the subtitle to modify\n     * @param referenceFile the subtitle to take the timecodes from\n     * @param delay         the number of milliseconds allowed to differ\n     */\n    public void adjustTimecodes(TimedTextFile fileToAdjust, TimedTextFile referenceFile, int delay) {\n\n        TimedLinesAPI linesAPI = new TimedLinesAPI();\n        List<? extends TimedLine> timedLines = new ArrayList<>(fileToAdjust.getTimedLines());\n        List<? extends TimedLine> referenceLines = new ArrayList<>(referenceFile.getTimedLines());\n\n        for (TimedLine lineToAdjust : timedLines) {\n\n            TimedObject originalTime = lineToAdjust.getTime();\n            LocalTime originalStart = originalTime.getStart();\n\n            TimedLine referenceLine = linesAPI.closestByStart(referenceLines, originalStart, delay);\n\n            if (referenceLine != null) {\n                LocalTime targetStart = referenceLine.getTime().getStart();\n                LocalTime targetEnd = referenceLine.getTime().getEnd();\n\n                TimedLine fullIntersect = linesAPI.intersected(timedLines, targetStart, targetEnd);\n\n                if (fullIntersect != null && !lineToAdjust.equals(fullIntersect)) {\n                    continue;\n                }\n\n                TimedLine startIntersect = linesAPI.intersected(timedLines, targetStart);\n                TimedLine endIntersect = linesAPI.intersected(timedLines, targetEnd);\n\n                if (startIntersect == null || originalTime.equals(startIntersect.getTime())) {\n                    originalTime.setStart(targetStart);\n                } else {\n                    originalTime.setStart(startIntersect.getTime().getEnd());\n                }\n\n                if (endIntersect == null || originalTime.getStart().equals(endIntersect.getTime().getStart())) {\n                    originalTime.setEnd(targetEnd);\n                } else {\n                    originalTime.setEnd(endIntersect.getTime().getStart());\n                }\n            }\n        }\n\n        expandLongLines(timedLines, referenceLines, 1500);\n    }\n\n    /**\n     * Expand lines in the adjusted file that should be displayed during 2 lines of the\n     * reference file\n     *\n     * @param adjustedLines  the adjusted lines (ascending sort)\n     * @param referenceLines the reference lines (ascending sort)\n     */\n    private static void expandLongLines(List<? extends TimedLine> adjustedLines,\n                                        List<? extends TimedLine> referenceLines, int delay) {\n\n        TimedLinesAPI linesAPI = new TimedLinesAPI();\n        for (int i = 0; i < adjustedLines.size(); i++) {\n\n            TimedObject currentElement = adjustedLines.get(i).getTime();\n\n            int index = linesAPI.findByTime(referenceLines, currentElement);\n            if (index >= 0) {\n\n                int nextReferenceIndex = index + 1;\n                if (nextReferenceIndex < referenceLines.size() && i + 1 < adjustedLines.size()) {\n\n                    TimedObject nextReference = referenceLines.get(nextReferenceIndex).getTime();\n                    TimedObject nextElement = adjustedLines.get(i + 1).getTime();\n\n                    if (linesAPI.isEqualsOrAfter(currentElement, nextReference)\n                            && linesAPI.getDelay(currentElement.getEnd(), nextReference.getStart()) < delay\n                            && linesAPI.isEqualsOrAfter(nextReference, nextElement)) {\n\n                        currentElement.setEnd(nextReference.getEnd());\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java",
    "content": "package org.fordes.subtitles.view.utils.submerge;\n\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject;\n\nimport java.time.LocalTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\n\npublic class TimedLinesAPI {\n\n\t/**\n\t * Search the line that has the closest start time compared to a specified time. If\n\t * the gap beetween the two start times is greater than the toleranceDelay (in ms) the\n\t * line will be ignored.\n\t * \n\t * @param tolerance the maximum gap in millis\n\t * @param lines the lines (ascending sort)\n\t * @param time the target start time\n\t * @return\n\t */\n\tpublic TimedLine closestByStart(List<? extends TimedLine> lines, final LocalTime time, int tolerance) {\n\n\t\t// Binary search will find the first \"random\" match\n\t\tint iAnyMatch = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(time, null)),\n\t\t\t\t(compare, base) -> {\n\n\t\t\t\t\tLocalTime search = base.getTime().getStart();\n\t\t\t\t\tLocalTime start = compare.getTime().getStart();\n\n\t\t\t\t\tif (getDelay(search, start) < tolerance) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn start.compareTo(search);\n\t\t\t\t});\n\n\t\tif (iAnyMatch < 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Search for other matches\n\t\tSet<TimedLine> matches = new TreeSet<>();\n\t\tmatches.add(lines.get(iAnyMatch));\n\n\t\tint i = iAnyMatch;\n\t\twhile (i > 0) {\n\t\t\tTimedLine previous = lines.get(--i);\n\t\t\tif (getDelay(time, previous.getTime().getStart()) >= tolerance) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatches.add(previous);\n\t\t}\n\n\t\ti = iAnyMatch;\n\t\twhile (i < lines.size() -1) {\n\t\t\tTimedLine next = lines.get(++i);\n\t\t\tif (getDelay(time, next.getTime().getStart()) >= tolerance) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatches.add(next);\n\t\t}\n\n\t\t// return the closest match\n\t\treturn matches.stream().min((m1, m2) -> getDelay(m1.getTime().getStart(), time) - getDelay(m2.getTime().getStart(), time)).get();\n\t}\n\n\t/**\n\t * Get the absolute delay beetween 2 times\n\t * \n\t * @return the absolute delay beetween 2 times\n\t */\n\tpublic int getDelay(LocalTime start, LocalTime end) {\n\n\t\treturn (int) Math.abs(ChronoUnit.MILLIS.between(start, end));\n\t}\n\n\t/**\n\t * Check if a timed object appear before or at the same time as an other timed object\n\t * \n\t * @param elementToCompare\n\t * @param comparedElement\n\t * @return\n\t */\n\tpublic boolean isEqualsOrAfter(TimedObject elementToCompare, TimedObject comparedElement) {\n\n\t\treturn comparedElement.getStart().isAfter(elementToCompare.getEnd())\n\t\t\t\t|| comparedElement.getStart().equals(elementToCompare.getEnd());\n\t}\n\n\t/**\n\t * Find the line displayed at <code>targetTime</code>\n\t * \n\t * @param lines the lines (ascending sort)\n\t * @param time the target time\n\t * @return\n\t */\n\tpublic TimedLine intersected(List<? extends TimedLine> lines, LocalTime time) {\n\n\t\tint index = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(time, null)),\n\t\t\t\t(compare, base) -> {\n\n\t\t\t\t\tLocalTime search = base.getTime().getStart();\n\t\t\t\t\tLocalTime start = compare.getTime().getStart();\n\t\t\t\t\tLocalTime end = compare.getTime().getEnd();\n\n\t\t\t\t\tif ((start.isBefore(search) || start.equals(search))\n\t\t\t\t\t\t\t&& (end.isAfter(search) || start.equals(search))) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn start.compareTo(search);\n\t\t\t\t});\n\n\t\treturn index < 0 ? null : lines.get(index);\n\t}\n\n\t/**\n\t * Find a line displayed between 2 times\n\t * \n\t * @param lines the lines (ascending sort)\n\t * @param\n\t * \n\t * @return\n\t */\n\tpublic TimedLine intersected(List<? extends TimedLine> lines, LocalTime start, LocalTime end) {\n\n\t\tint index = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(start, end)),\n\t\t\t\t(compare, base) -> {\n\n\t\t\t\t\tLocalTime searchStart = base.getTime().getStart();\n\t\t\t\t\tLocalTime searchEnd = base.getTime().getEnd();\n\n\t\t\t\t\tLocalTime start1 = compare.getTime().getStart();\n\t\t\t\t\tLocalTime end1 = compare.getTime().getEnd();\n\n\t\t\t\t\tif (searchStart.isBefore(start1) && searchEnd.isAfter(end1)) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn compare.compareTo(base);\n\t\t\t\t});\n\n\t\treturn index < 0 ? null : lines.get(index);\n\t}\n\n\t/**\n\t * Find a sublitle line from it's time\n\t * \n\t * @param lines the subtitle lines\n\t * @param time the timed object\n\t * @return\n\t */\n\tpublic int findByTime(List<? extends TimedLine> lines, TimedObject time) {\n\n\t\treturn Collections.binarySearch(lines, new SubtitleLine<>(time), SubtitleLine.timeComparator);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.constant;\n\n/**\n * Enum all the supported font names of the application\n *\n */\npublic enum FontName {\n\t\n\tArial(\"Arial\"), \n\tCourierNew(\"Courier New\"),\n\tTimes(\"Times\"),\n\tHelvetica(\"Helvetica\"),\n\tDroidSans(\"Droid Sans\"),\n\tCursive(\"cursive\"),\n\tMonospace(\"monospace\"),\n\tSerif(\"serif\"),\n\tSansSerif(\"sans-serif\"),\n\tFantasy(\"fantasy\"),\n\tCourier(\"Courier\"),\n\tGeorgia(\"Georgia\"),\n\tLucidaConsole(\"Lucida Console\"),\n\tPapyrus(\"Papyrus\"),\n\tTahoma(\"Tahoma\"),\n\tTeX(\"TeX\"),\n\tVerdana(\"Verdana\"),\n\tVerona(\"Verona\"),\n\tSimSun(\"SimSun\"),\n\tUbuntu(\"Ubuntu\"),\n\tUbuntuMono(\"Ubuntu Mono\"),\n\tFreeMono(\"FreeMono\"),\n\tLiberationSerif(\"Liberation Serif\"),\n\tPurisa(\"Purisa\"),\n\tTimesNewRoman(\"Times New Roman\");\n\n\tprivate String name;\n\n\tFontName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return the name\n\t */\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.name;\n\t}\t\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidAssSubException;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.*;\nimport org.fordes.subtitles.view.utils.submerge.utils.ColorUtils;\nimport org.springframework.util.StringUtils;\n\nimport java.beans.PropertyDescriptor;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.time.DateTimeException;\nimport java.util.*;\n\n/**\n * Parse SSA/ASS subtitles\n */\npublic class ASSParser extends BaseParser<ASSSub> {\n\n    /**\n     * Comments: lines that start with this character are ignored\n     */\n    private static final String COMMENTS_MARK = \";\";\n\n    @Override\n    protected void parse(BufferedReader br, ASSSub sub) throws IOException, InvalidAssSubException {\n\n        String line = readFirstTextLine(br);\n        if (line != null && !StrUtil.equalsAnyIgnoreCase(\"[script info]\", StrUtil.trim(line))) {\n            throw new InvalidAssSubException(\"The line that says “[Script Info]” must be the first line in the script.\");\n        }\n\n        // [Script Info]\n        sub.setScriptInfo(parseScriptInfo(br));\n        while ((line = readFirstTextLine(br)) != null) {\n            if (line.matches(\"(?i:^\\\\[v.*styles\\\\+?]$)\")) {\n                // [V4+ Styles]\n                sub.setStyle(parseStyle(br));\n            } else if (line.equalsIgnoreCase(\"[events]\")) {\n                // [Events]\n                sub.setEvents(parseEvents(br));\n            }\n        }\n\n        if (sub.getStyle().isEmpty()) {\n            throw new InvalidAssSubException(\"Missing style definition\");\n        }\n\n        if (sub.getEvents().isEmpty()) {\n            throw new InvalidAssSubException(\"No text line found\");\n        }\n    }\n\n    /**\n     * Parse the events section from the reader. <br/>\n     * <p>\n     * Example of events section:\n     *\n     * <pre>\n     * [Events]\n     * Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n     * Dialogue: 0,0:02:30.84,0:02:34.70,StlyeOne,,0000,0000,0000,,A text line\n     * Dialogue: 0,0:02:34.92,0:02:37.54,StyleTwo,,0000,0000,0000,,Another text line\n     * </pre>\n     *\n     * @param br: the buffered reader\n     * @throws IOException\n     * @throws InvalidAssSubException\n     * @throws IOException\n     */\n    private static Set<Events> parseEvents(BufferedReader br) throws IOException, InvalidAssSubException {\n        String[] eventsFormat = findFormat(br, \"events\");\n\n        Set<Events> events = new TreeSet<>();\n        String line = readFirstTextLine(br);\n\n        while (line != null && !StrUtil.startWith(line, StrUtil.C_BRACKET_START)) {\n            if (StrUtil.startWith(line, Events.DIALOGUE)) {\n                String info = findInfo(line, Events.DIALOGUE);\n                String[] dialogLine = StrUtil.splitToArray(info, Events.SEP);\n                //StringUtils.splitByWholeSeparatorPreserveAllTokens(info, Events.SEP);\n\n                int lengthDialog = dialogLine.length;\n                int lengthFormat = eventsFormat.length;\n\n                if (lengthDialog < lengthFormat) {\n                    throw new InvalidAssSubException(\"Incorrect dialog line : \" + info);\n                }\n                if (lengthDialog > lengthFormat) {\n                    // The text field contains commas\n                    StringJoiner joiner = new StringJoiner(Events.SEP);\n                    for (int i = lengthFormat - 1; i < lengthDialog; i++) {\n                        joiner.add(dialogLine[i]);\n                    }\n                    dialogLine[lengthFormat - 1] = joiner.toString();\n                    dialogLine = Arrays.copyOfRange(dialogLine, 0, lengthFormat);\n                }\n                events.add(parseDialog(eventsFormat, dialogLine));\n            }\n\n            line = markAndRead(br);\n\n        }\n\n        reset(br, line);\n        return events;\n    }\n\n    /**\n     * Parse the style section from the reader. <br/>\n     * <p>\n     * Example of style section:\n     *\n     * <pre>\n     * [V4+ Styles]\n     * Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour\n     * Style: StyleOne,Arial,16,64250,16777215,0\n     * Style: StyleTwo,Arial,16,16383999,16777215,0\n     * </pre>\n     *\n     * @param br: the buffered reader\n     * @throws IOException\n     * @throws InvalidAssSubException\n     */\n    private static List<V4Style> parseStyle(BufferedReader br) throws IOException, InvalidAssSubException {\n        String[] styleFormat = findFormat(br, \"styles\");\n\n        List<V4Style> styles = new ArrayList<>();\n        String line = readFirstTextLine(br);\n        int index = 1;\n        while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) {\n            if (line.startsWith(V4Style.STYLE) && !line.startsWith(COMMENTS_MARK)) {\n                String[] textLine = line.split(StrUtil.COLON);\n                if (textLine.length > 1) {\n                    String[] styleLine = textLine[1].split(V4Style.SEP);\n                    styles.add(parseV4Style(styleFormat, styleLine, index));\n                    index++;\n                }\n            }\n            line = markAndRead(br);\n        }\n\n        while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) {\n            if (StrUtil.startWith(line, V4Style.STYLE)) {\n                List<String> textLine = StrUtil.split(line, StrUtil.COLON);\n                if (!textLine.isEmpty()) {\n                    String[] styleLine = StrUtil.splitToArray(textLine.get(1), V4Style.SEP);\n                    styles.add(parseV4Style(styleFormat, styleLine, index));\n                    index++;\n                }\n            }\n        }\n\n        reset(br, line);\n        return styles;\n    }\n\n    /**\n     * Return the Events object from text dialog line\n     *\n     * @param eventsFormat: the format definition\n     * @param dialogLine:   the dialog line\n     * @return the Events object\n     * @throws InvalidAssSubException\n     */\n    private static Events parseDialog(String[] eventsFormat, String[] dialogLine) throws InvalidAssSubException {\n\n        Events events = new Events();\n\n        for (int i = 0; i < eventsFormat.length; i++) {\n            String property = StringUtils.uncapitalize(eventsFormat[i].trim());\n            String value = dialogLine[i].trim();\n\n            try {\n                switch (property) {\n                    case \"start\":\n                        events.getTime().setStart(ASSTime.fromString(value));\n                        break;\n                    case \"end\":\n                        events.getTime().setEnd(ASSTime.fromString(value));\n                        break;\n                    case \"text\":\n                        List<String> textLines = Arrays.asList(value.split(\"\\\\\\\\N\"));\n                        events.setTextLines(new ArrayList<>(textLines));\n                        break;\n                    default:\n                        String error = callProperty(events, property, value);\n                        if (error != null) {\n                            throw new InvalidAssSubException(StrUtil.format(\"Invalid property ({}) {}\", property, value));\n                        }\n                        break;\n                }\n            } catch (DateTimeException e) {\n                throw new InvalidAssSubException(StrUtil.format(\"Invalid property ({}) {}\", property, value));\n            }\n\n        }\n        return events;\n    }\n\n    /**\n     * Return the V4Style object from text style line\n     *\n     * @param styleFormat: format line\n     * @param styleLine:   the style line\n     * @param lineIndex:   the line index\n     * @return the style object\n     * @throws InvalidAssSubException\n     */\n    private static V4Style parseV4Style(String[] styleFormat, String[] styleLine, int lineIndex)\n            throws InvalidAssSubException {\n\n        String message = \"Style at index \" + lineIndex + \": \";\n\n        if (styleFormat.length != styleLine.length) {\n            throw new InvalidAssSubException(message + \"does not match style definition\");\n        }\n\n        V4Style style = new V4Style();\n        for (int i = 0; i < styleFormat.length; i++) {\n            String property = StringUtils.uncapitalize(styleFormat[i].trim());\n            String value = styleLine[i].trim();\n\n            if (StrUtil.containsIgnoreCase(property, \"colour\")) {\n                try {\n                    Integer.parseInt(value);\n                } catch (NumberFormatException e) {\n                    int bgr = getBGR(value);\n                    if (bgr != -1) {\n                        value = Integer.toString(bgr);\n                    }\n                }\n            }\n\n            String error = callProperty(style, property, value);\n\n            if (error != null) {\n                throw new InvalidAssSubException(message + error);\n            }\n        }\n\n        if (StrUtil.isEmpty(style.getName())) {\n            throw new InvalidAssSubException(message + \" missing name\");\n        }\n\n        return style;\n    }\n\n    /**\n     * Get the BGR code from the &HBBGGRR or &HAABBGGRR pattern\n     *\n     * @param value: the value to convert\n     * @return the bgr code\n     */\n    private static int getBGR(String value) {\n\n        int length = value.length();\n        int bgr = -1;\n        if (length == 10) {\n            // From ASS\n            bgr = ColorUtils.HAABBGGRRToBGR(value);\n        } else if (length == 8) {\n            // From SSA\n            bgr = ColorUtils.HBBGGRRToBGR(value);\n        }\n        return bgr;\n    }\n\n    /**\n     * Parse the script info section from the reader. <br/>\n     * <p>\n     * Example of script info section:\n     *\n     * <pre>\n     * [Script Info]\n     * ScriptType: v4.00+\n     * Collisions: Normal\n     * Timer: 100,0000\n     * Title: My movie title\n     * </pre>\n     *\n     * @param br: the buffered reader\n     * @throws IOException\n     * @throws InvalidAssSubException\n     */\n    private static ScriptInfo parseScriptInfo(BufferedReader br) throws IOException, InvalidAssSubException {\n\n        ScriptInfo scriptInfo = new ScriptInfo();\n        String line = readFirstTextLine(br);\n\n        while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) {\n\n            if (!line.startsWith(COMMENTS_MARK)) {\n\n                String[] split = line.split(ScriptInfo.SEP);\n                if (split.length > 1) {\n                    String property = StrUtil.lowerFirst(StrUtil.cleanBlank(split[0]));\n\n                    StringJoiner joiner = new StringJoiner(ScriptInfo.SEP);\n                    for (int i = 1; i < split.length; i++) {\n                        joiner.add(split[i]);\n                    }\n                    String value = joiner.toString().trim();\n\n                    String error = callProperty(scriptInfo, property, value);\n\n                    if (error != null) {\n                        throw new InvalidAssSubException(\"Script info : \" + error);\n                    }\n                }\n            }\n            line = markAndRead(br);\n        }\n\n        reset(br, line);\n        return scriptInfo;\n    }\n\n    /**\n     * Call a specific property of an object with reflection\n     *\n     * @param object:   the object to set a property\n     * @param property: the property to define\n     * @param value:    the value to set\n     * @return the error message if an error has occured, null otherwise\n     */\n    private static String callProperty(Object object, String property, String value) {\n\n        String error = null;\n\n\n        PropertyDescriptor descriptor = BeanUtil.getPropertyDescriptor(object.getClass(), property);\n\n        if (descriptor != null) {\n            String type = descriptor.getPropertyType().getSimpleName();\n            switch (type) {\n                case \"String\":\n                    BeanUtil.setProperty(object, property, value);\n                    break;\n                case \"int\":\n                    BeanUtil.setProperty(object, property, Convert.toInt(value));\n                    break;\n                case \"boolean\":\n                    BeanUtil.setProperty(object, property, Convert.toBool(value));\n                    break;\n                case \"double\":\n                    BeanUtil.setProperty(object, property, Convert.toDouble(StrUtil.replace(value, StrUtil.COMMA, StrUtil.DOT)));\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        return error;\n    }\n\n    /**\n     * Get the format string definition\n     *\n     * @param br:          the buffered reader\n     * @param sectionName: the name of the section to parse\n     * @return the format string definition\n     * @throws IOException\n     * @throws InvalidAssSubException\n     */\n    private static String[] findFormat(BufferedReader br, String sectionName) throws IOException,\n            InvalidAssSubException {\n\n        String line = readFirstTextLine(br);\n        if (StrUtil.isEmpty(line)) {\n            throw new InvalidAssSubException(\"Missing format definition in \" + sectionName + \" section\");\n        }\n        if (!StrUtil.startWith(line.trim(), ASSSub.FORMAT)) {\n            throw new InvalidAssSubException(StrUtil.upperFirst(sectionName) + \" definition must start with 'Format' line\");\n        }\n        return StrUtil.splitToArray(findInfo(line, ASSSub.FORMAT), V4Style.SEP);\n    }\n\n    /**\n     * Find the information after \":\" in a text line\n     *\n     * @param line:   the line\n     * @param search: the information to search\n     * @return info or null if the info is empty / not found\n     */\n    private static String findInfo(String line, String search) {\n        if (StrUtil.startWithIgnoreCase(line.trim(), search) && line.indexOf(StrUtil.COLON) > 0) {\n            return line.substring(line.indexOf(StrUtil.COLON) + 1).trim();\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidFileException;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\nimport org.fordes.subtitles.view.utils.submerge.utils.EncodeUtils;\n\nimport java.io.*;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\n\n\npublic abstract class BaseParser<T extends TimedTextFile> implements SubtitleParser {\n\n    /**\n     * UTF-8 BOM Marker\n     */\n    private static final char BOM_MARKER = '\\ufeff';\n\n    @Override\n    public T parse(File file) {\n        try {\n            return parse(file, EncodeUtils.guessEncoding(file));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public T parse(File file, String charset) {\n\n        if (!file.isFile()) {\n            throw new InvalidFileException(\"File \" + file.getName() + \" is invalid\");\n        }\n\n        try (FileInputStream fis = new FileInputStream(file)) {\n            return parse(fis, file.getName(), charset);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public T parse(InputStream is, String fileName) {\n        try {\n            return parse(is, fileName, EncodeUtils.guessEncoding(is));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public T parse(InputStream is, String fileName, String charset) {\n        try {\n            Type type = TypeUtil.getTypeArgument(this.getClass().getGenericSuperclass());\n            T sub = ReflectUtil.newInstance((Class<T>) type);\n            sub.setFileName(fileName);\n\n            try (BufferedReader br = IoUtil.getReader(is, Charset.forName(charset))) {\n                skipBom(br);\n                parse(br, sub);\n            }\n            return sub;\n        } catch (IOException e) {\n            throw new InvalidFileException(e);\n        }\n    }\n\n    @Override\n    public T parse(String str, String fileName) {\n        try {\n            Type type = TypeUtil.getTypeArgument(this.getClass().getGenericSuperclass());\n            T sub = ReflectUtil.newInstance((Class<T>) type);\n            sub.setFileName(fileName);\n\n            try (BufferedReader br = IoUtil.getReader(StrUtil.getReader(str))) {\n                skipBom(br);\n                parse(br, sub);\n            }\n            return sub;\n        } catch (IOException e) {\n            throw new InvalidFileException(e);\n        }\n    }\n\n    /**\n     * Parse the subtitle file into a <code>ParsableSubtitle</code> object\n     *\n     * @param br: the buffered reader\n     * @param sub : the subtitle object to fill\n     * @throws IOException\n     * @throws InvalidSubException if an error has occured when parsing the subtitle file\n     */\n    protected abstract void parse(BufferedReader br, T sub) throws IOException;\n\n    /**\n     * Ignore blank spaces and return the first text line\n     *\n     * @param br: the buffered reader\n     * @throws IOException\n     */\n    protected static String readFirstTextLine(BufferedReader br) throws IOException {\n\n        String line = null;\n        while ((line = br.readLine()) != null) {\n            if (!StrUtil.isEmpty(line.trim())) {\n                break;\n            }\n        }\n        return line;\n    }\n\n    /**\n     * Remove the byte order mark if exists\n     *\n     * @param br: the buffered reader\n     * @throws IOException\n     */\n    private static void skipBom(BufferedReader br) throws IOException {\n\n        br.mark(4);\n        if (BOM_MARKER != br.read()) {\n            br.reset();\n        }\n    }\n\n    /**\n     * Reset the reader at the previous mark if the current line is a new section\n     *\n     * @param br:   the reader\n     * @param line: the current line\n     * @throws IOException\n     */\n    protected static void reset(BufferedReader br, String line) throws IOException {\n        if (StrUtil.startWith(line, StrUtil.C_BRACKET_START)) {\n            br.reset();\n        }\n    }\n\n    /**\n     * Mark the position in the reader and read the next text line\n     *\n     * @param br: the buffered reader\n     * @return the next text line\n     * @throws IOException\n     */\n    protected static String markAndRead(BufferedReader br) throws IOException {\n\n        br.mark(32);\n        return readFirstTextLine(br);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCSub;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCTime;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * @author fordes on 2022/7/21\n */\n@Slf4j\npublic final class LRCParser extends BaseParser<LRCSub> {\n    @Override\n    protected void parse(BufferedReader br, LRCSub sub) throws IOException {\n\n        boolean found = true;\n        String lineStr = readFirstTimeLine(br);\n\n        while (found) {\n\n            String timeStr = StrUtil.subBetween(lineStr, StrUtil.BRACKET_START, StrUtil.BRACKET_END);\n            LRCTime time = LRCTime.fromString(timeStr);\n\n            List<String> texts = CollUtil.newArrayList(time == null ?\n                    lineStr : lineStr.substring(10));\n            LRCLine line = new LRCLine(time, texts);\n\n            try {\n                lineStr = br.readLine();\n                while (lineStr != null && !StrUtil.startWith(lineStr, StrUtil.C_BRACKET_START)) {\n                    texts.add(lineStr);\n                    lineStr = br.readLine();\n                }\n                sub.add(line);\n                found = (lineStr != null);\n            } catch (Exception e) {\n                log.error(ExceptionUtil.stacktraceToString(e));\n                found = false;\n            }\n\n        }\n    }\n\n\n    /**\n     * 获得首个有效行，即第一个形如：[00:00:00.000]的行\n     *\n     * @param br\n     * @return\n     * @throws IOException\n     */\n    private String readFirstTimeLine(BufferedReader br) throws IOException {\n        String lineStr = br.readLine();\n        while (lineStr != null && !StrUtil.startWith(lineStr, StrUtil.C_BRACKET_START)) {\n            lineStr = br.readLine();\n        }\n        return lineStr;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.util.StrUtil;\n\npublic final class ParserFactory {\n\n    /**\n     * Return the subtitle parser for the subtitle format matching the extension\n     *\n     * @param extension the subtitle extention\n     * @return the subtitle parser, null if no matching parser\n     */\n    public static SubtitleParser getParser(String extension) throws Exception {\n\n        SubtitleParser parser = null;\n        if (StrUtil.equalsAnyIgnoreCase(extension, \"ass\", \"ssa\")) {\n            return new ASSParser();\n        } else if (StrUtil.equalsIgnoreCase(extension, \"srt\")) {\n            return new SRTParser();\n        } else if (StrUtil.equalsIgnoreCase(extension, \"lrc\")) {\n            return new LRCParser();\n        }\n        throw new Exception(extension + \" format not supported\");\n    }\n\n    /**\n     * Private constructor\n     */\n    private ParserFactory() {\n\n        throw new AssertionError();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSRTSubException;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTSub;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.time.LocalTime;\nimport java.time.format.DateTimeParseException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n\n\n/**\n * Parse SRT subtitles\n */\npublic final class SRTParser extends BaseParser<SRTSub> {\n\n\t@Override\n\tprotected void parse(BufferedReader br, SRTSub sub) throws IOException, InvalidSubException {\n\n\t\tboolean found = true;\n\t\twhile (found) {\n\t\t\tSRTLine line = firstIn(br);\n\t\t\tif (found = (line != null)) {\n\t\t\t\tsub.add(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Extract the firt SRTLine found in a buffered reader. <br/>\n\t * \n\t * Example of SRT line:\n\t * \n\t * <pre>\n\t * 1\n\t * 00:02:46,813 --> 00:02:50,063\n\t * A text line\n\t * </pre>\n\t * \n\t * @param br\n\t * @return SRTLine the line extracted, null if no SRTLine found\n\t * @throws IOException\n\t * @throws InvalidSRTSubException\n\t */\n\tprivate static SRTLine firstIn(BufferedReader br) throws IOException, InvalidSRTSubException {\n\n\t\tString idLine = readFirstTextLine(br);\n\t\tString timeLine = br.readLine();\n\n\t\tif (idLine == null || timeLine == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tint id = parseId(idLine);\n\t\tSRTTime time = parseTime(timeLine);\n\n\t\tList<String> textLines = new ArrayList<>();\n\t\tString testLine;\n\t\twhile ((testLine = br.readLine()) != null) {\n\t\t\tif (StrUtil.isEmpty(testLine.trim())) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttextLines.add(testLine);\n\t\t}\n\n\t\treturn new SRTLine(id, time, textLines);\n\t}\n\n\t/**\n\t * Extract a subtitle id from string\n\t * \n\t * @param textLine ex 1\n\t * @return the id extracted\n\t * @throws InvalidSRTSubException\n\t */\n\tprivate static int parseId(String textLine) throws InvalidSRTSubException {\n\n\t\tint idSRTLine;\n\t\ttry {\n\t\t\tidSRTLine = Integer.parseInt(textLine.trim());\n\t\t} catch (NumberFormatException e) {\n\t\t\tthrow new InvalidSRTSubException(\"Expected id not found -> \" + textLine);\n\t\t}\n\n\t\treturn idSRTLine;\n\t}\n\n\t/**\n\t * Extract a subtitle time from string\n\t * \n\t * @param timeLine: ex 00:02:08,822 --> 00:02:11,574\n\t * @return the SRTTime object\n\t * @throws InvalidSRTSubException\n\t */\n\tpublic static SRTTime parseTime(String timeLine) throws InvalidSRTSubException {\n\n\t\tSRTTime time = null;\n\t\tString times[] = timeLine.split(SRTTime.DELIMITER.trim());\n\n\t\tif (times.length != 2) {\n\t\t\tthrow new InvalidSRTSubException(\"Subtitle \" + timeLine + \" - invalid times : \" + timeLine);\n\t\t}\n\n\t\ttry {\n\t\t\tLocalTime start = SRTTime.fromString(times[0]);\n\t\t\tLocalTime end = SRTTime.fromString(times[1]);\n\t\t\ttime = new SRTTime(start, end);\n\t\t} catch (DateTimeParseException e) {\n\t\t\tthrow new InvalidSRTSubException(\"Invalid time string : \" + timeLine, e);\n\t\t}\n\n\t\treturn time;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser;\n\n\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidFileException;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\nimport java.io.File;\nimport java.io.InputStream;\n\n\n\npublic interface SubtitleParser {\n\n\t/**\n\t * Parse a subtitle file and return the corresponding subtitle object\n\t * \n\t * @param file the subtitle file\n\t * @return the subtitle object\n\t * @throws InvalidSubException if the subtitle is not valid\n\t * @throws InvalidFileException if the file is not valid\n\t */\n\tTimedTextFile parse(File file);\n\n\t/**\n\t * Parse a subtitle file from an inputstream and return the corresponding subtitle\n\t * object\n\t * \n\t * @param is the input stream\n\t * @param fileName the fileName\n\t * @return the subtitle object\n\t * @throws InvalidSubException if the subtitle is not valid\n\t * @throws InvalidFileException if the file is not valid\n\t */\n\tTimedTextFile parse(InputStream is, String fileName);\n\n\t/**\n\t * Parse a subtitle file and return the corresponding subtitle object\n\t *\n\t * @param file the file\n\t * @param charset the file charset\n\t * @return the subtitle object\n\t * @throws InvalidSubException if the subtitle is not valid\n\t * @throws InvalidFileException if the file is not valid\n\t */\n\tTimedTextFile parse(File file, String charset);\n\n\t/**\n\t * Parse a subtitle file from an string and return the corresponding subtitle\n\t * object\n\t *\n\t * @param is the input stream\n\t * @param fileName the fileName\n\t * @parse charset the file charset\n\t * @return the subtitle object\n\t * @throws InvalidSubException if the subtitle is not valid\n\t * @throws InvalidFileException if the file is not valid\n\t */\n\tTimedTextFile parse(InputStream is, String fileName, String charset);\n\n\t/**\n\t * Parse a subtitle file from an string and return the corresponding subtitle object\n\t *\n\t * @param str the subtitle string\n\t * @param fileName the fileName\n\t * @return the subtitle object\n\t */\n\tTimedTextFile parse(String str, String fileName);\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\n\npublic class InvalidAssSubException extends InvalidSubException {\n\n\tprivate static final long serialVersionUID = 8942033846085284666L;\n\n\tpublic InvalidAssSubException() {\n\t}\n\n\tpublic InvalidAssSubException(String arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidAssSubException(Throwable arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidAssSubException(String arg0, Throwable arg1) {\n\t\tsuper(arg0, arg1);\n\t}\n\n\tpublic InvalidAssSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {\n\t\tsuper(arg0, arg1, arg2, arg3);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidColorCode extends RuntimeException {\n\n\tprivate static final long serialVersionUID = -4904697807940273825L;\n\n\tpublic InvalidColorCode() {\n\t}\n\n\tpublic InvalidColorCode(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic InvalidColorCode(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\tpublic InvalidColorCode(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic InvalidColorCode(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, cause, enableSuppression, writableStackTrace);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidFileException extends RuntimeException {\n\n\tprivate static final long serialVersionUID = -943455563476464982L;\n\n\tpublic InvalidFileException() {\n\t}\n\n\tpublic InvalidFileException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic InvalidFileException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\tpublic InvalidFileException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic InvalidFileException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, cause, enableSuppression, writableStackTrace);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\n\npublic class InvalidSRTSubException extends InvalidSubException {\n\n\tprivate static final long serialVersionUID = -8672533341983848962L;\n\n\tpublic InvalidSRTSubException() {\n\t}\n\n\tpublic InvalidSRTSubException(String arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidSRTSubException(Throwable arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidSRTSubException(String arg0, Throwable arg1) {\n\t\tsuper(arg0, arg1);\n\t}\n\n\tpublic InvalidSRTSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {\n\t\tsuper(arg0, arg1, arg2, arg3);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.parser.exception;\n\npublic class InvalidSubException extends RuntimeException {\n\n\tprivate static final long serialVersionUID = -8431409375872882596L;\n\n\tpublic InvalidSubException() {\n\t}\n\n\tpublic InvalidSubException(String arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidSubException(Throwable arg0) {\n\t\tsuper(arg0);\n\t}\n\n\tpublic InvalidSubException(String arg0, Throwable arg1) {\n\t\tsuper(arg0, arg1);\n\t}\n\n\tpublic InvalidSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {\n\t\tsuper(arg0, arg1, arg2, arg3);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\n\n/**\n * The class <code>ASSSub</code> represents a SubStation Alpha subtitle\n *\n */\n@Data\npublic class ASSSub implements TimedTextFile, Serializable {\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = 8812933867812351549L;\n\n\t\n\t/**\n\t * Format\n\t */\n\tpublic static final String FORMAT = \"Format\";\n\n\t/**\n\t * Events section\n\t */\n\tprivate static final String EVENTS = \"[Events]\";\n\n\t/**\n\t * Styles section\n\t */\n\tprivate static final String V4_STYLES = \"[V4+ Styles]\";\n\n\t/**\n\t * Script info section\n\t */\n\tprivate static final String SCRIPT_INFO = \"[Script Info]\";\n\n\t/**\n\t * Line separator\n\t */\n\tprivate static final String NEW_LINE = \"\\n\";\n\n\t/**\n\t * Key / Value info separator. Ex : \"Color: red\"\n\t */\n\tpublic static final String SEP = \": \";\n\n\t/**\n\t * Subtitle name\n\t */\n\tprivate String filename;\n\n\t/**\n\t * Headers and general information about the script\n\t */\n\tprivate ScriptInfo scriptInfo = new ScriptInfo();\n\n\t/**\n\t * Style definitions required by the script\n\t */\n\tprivate List<V4Style> style = new ArrayList<>();\n\n\t/**\n\t * Events for the script - all the subtitles, comments, pictures, sounds, movies and\n\t * commands\n\t */\n\tprivate Set<Events> events = new TreeSet<>();\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\n\t\t// [Script Info]\n\t\tsb.append(SCRIPT_INFO).append(NEW_LINE).append(this.scriptInfo.toString());\n\t\tsb.append(NEW_LINE).append(NEW_LINE);\n\n\t\t// [V4 Styles]\n\t\tsb.append(V4_STYLES).append(NEW_LINE);\n\t\tsb.append(FORMAT).append(SEP).append(V4Style.FORMAT_STRING).append(NEW_LINE);\n\t\tthis.style.forEach(s -> sb.append(s.toString()).append(NEW_LINE));\n\t\tsb.append(NEW_LINE);\n\n\t\t// [Events]\n\t\tsb.append(EVENTS).append(NEW_LINE);\n\t\tsb.append(FORMAT).append(SEP).append(Events.FORMAT_STRING).append(NEW_LINE);\n\t\tthis.events.forEach(e -> sb.append(e.toString()).append(NEW_LINE));\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * Get the ass file as an input stream\n\t * \n\t * @return the file\n\t */\n\tpublic InputStream toInputStream() {\n\t\treturn new ByteArrayInputStream(toString().getBytes());\n\t}\n\n\n\t@Override\n\tpublic void setFileName(String fileName) {\n\t\tthis.filename = fileName;\n\t}\n\n\t@Override\n\tpublic String getFileName() {\n\t\treturn this.filename;\n\t}\n\n\t@Override\n\tpublic Set<? extends TimedLine> getTimedLines() {\n\t\treturn this.events;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime;\n\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\n\n/**\n * The class <code>ASSTime</code> represents a SubStation Alpha time : meaning the time at\n * which the text will appear and disappear onscreen\n *\n */\npublic class ASSTime extends SubtitleTime {\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = -8393452818120120069L;\n\n\t/**\n\t * The time pattern\n\t */\n\tpublic static final String TIME_PATTERN = \"H:mm:ss.SS\";\n\n\t/**\n\t * The time pattern formatter\n\t */\n\tpublic static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);\n\n\t/**\n\t * Constructor\n\t */\n\tpublic ASSTime(LocalTime start, LocalTime end) {\n\t\tsuper(start, end);\n\t}\n\n\t/**\n\t * Constructor\n\t */\n\tpublic ASSTime() {\n\t\tsuper();\n\t}\n\n\t/**\n\t * Convert a <code>LocalTime</code> to string\n\t * \n\t * @param time: the time to format\n\t * @return the formatted time\n\t */\n\tpublic static String format(LocalTime time) {\n\n\t\treturn time.format(FORMATTER);\n\t}\n\n\t/**\n\t * Convert a string pattern to a Local time\n\t * \n\t * @param time\n\t * @return\n\t * @throws DateTimeParseException\n\t */\n\tpublic static LocalTime fromString(String time) {\n\n\t\treturn LocalTime.parse(time.replace(',', '.'), FORMATTER);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * Contain the subtitle text, their timings, and how it should be displayed. The fields\n * which appear in each Dialogue line are defined by a Format: line, which must appear\n * before any events in the section. The format line specifies how SSA will interpret all\n * following Event lines.\n * \n * The field names must be spelled correctly, and are as follows:\n * \n * Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n * \n * The last field will always be the Text field, so that it can contain commas. The format\n * line allows new fields to be added to the script format in future, and yet allow old\n * versions of the software to read the fields it recognises - even if the field order is\n * changed.\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\npublic class Events extends SubtitleLine<ASSTime> implements Serializable{\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = -6706119890451628726L;\n\n\t/**\n\t * Format declaration\n\t */\n\tpublic static final String FORMAT_STRING = \"Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\";\n\n\t/**\n\t * New line separator\n\t */\n\tprivate static final String ESCAPED_RETURN = \"\\\\N\";\n\n\t/**\n\t * Dialog\n\t */\n\tpublic static final String DIALOGUE = \"Dialogue: \";\n\n\t/**\n\t * Separator\n\t */\n\tpublic static final String SEP = \",\";\n\n\t/**\n\t * Subtitles having different layer number will be ignored during the collusion\n\t * detection.\n\t * \n\t * Higher numbered layers will be drawn over the lower numbered.\n\t */\n\tprivate int layer;\n\n\t/**\n\t * Style name. If it is \"Default\", then your own *Default style will be subtituted.\n\t * \n\t * However, the Default style used by the script author IS stored in the script even\n\t * though SSA ignores it - so if you want to use it, the information is there - you\n\t * could even change the Name in the Style definition line, so that it will appear in\n\t * the list of \"script\" styles.\n\t */\n\tprivate String style;\n\n\t/**\n\t * Character name. This is the name of the character who speaks the dialogue. It is\n\t * for information only, to make the script is easier to follow when editing/timing.\n\t */\n\tprivate String name = StrUtil.EMPTY;\n\n\t/**\n\t * 4-figure Left Margin override. The values are in pixels. All zeroes means the\n\t * default margins defined by the style are used.\n\t */\n\tprivate String marginL = \"0000\";\n\n\t/**\n\t * 4-figure Right Margin override. The values are in pixels. All zeroes means the\n\t * default margins defined by the style are used.\n\t */\n\tprivate String marginR = \"0000\";\n\n\t/**\n\t * 4-figure Bottom Margin override. The values are in pixels. All zeroes means the\n\t * default margins defined by the style are used.\n\t */\n\tprivate String marginV = \"0000\";\n\n\t/**\n\t * Transition Effect. This is either empty, or contains information for one of the\n\t * three transition effects implemented in SSA v4.x\n\t * \n\t * The effect names are case sensitive and must appear exactly as shown. The effect\n\t * names do not have quote marks around them.\n\t * \n\t * \"Scroll up;y1;y2;delay[;fadeawayheight]\"means that the text/picture will scroll up\n\t * the screen. The parameters after the words \"Scroll up\" are separated by semicolons.\n\t * \n\t * “Banner;delay” means that text will be forced into a single line, regardless of\n\t * length, and scrolled from right to left accross the screen.\n\t */\n\tprivate String effect = StrUtil.EMPTY;\n\n\t/**\n\t * Constructor\n\t * \n\t * @param style style name to apply\n\t * @param time Start Time of the Event\n\t * @param textLines End Time of the Event\n\t */\n\tpublic Events(String style, ASSTime time, List<String> textLines) {\n\t\tthis.style = style;\n\t\tthis.time = time;\n\t\tthis.textLines = textLines;\n\t}\n\n\t/**\n\t * Constructor\n\t * \n\t */\n\tpublic Events() {\n\t\tsuper();\n\t\tthis.style = StrUtil.EMPTY;\n\t\tthis.time = new ASSTime();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(DIALOGUE);\n\n\t\tsb.append(this.layer).append(SEP);\n\t\tsb.append(ASSTime.format(this.time.getStart())).append(SEP);\n\t\tsb.append(ASSTime.format(this.time.getEnd())).append(SEP);\n\t\tsb.append(this.style).append(SEP);\n\t\tsb.append(this.name).append(SEP);\n\t\tsb.append(this.marginL).append(SEP);\n\t\tsb.append(this.marginR).append(SEP);\n\t\tsb.append(this.marginV).append(SEP);\n\t\tsb.append(this.effect).append(SEP);\n\t\tthis.textLines.forEach(tl -> sb.append(tl.toString()).append(ESCAPED_RETURN));\n\n\t\treturn StrUtil.removeSuffix(sb.toString(), ESCAPED_RETURN);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.text.DecimalFormat;\n\n/**\n * The <code>ScriptInfo</code> section contains headers and general information about the\n * script\n */\n@Data\npublic class ScriptInfo implements Serializable {\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = -6613873382621648995L;\n\n\t/**\n\t * Timer declaration\n\t */\n\tprivate static final String TIMER = \"Timer\";\n\n\t/**\n\t * PlayDepth declaration\n\t */\n\tprivate static final String PLAY_DEPTH = \"PlayDepth\";\n\n\t/**\n\t * PlayResX declaration\n\t */\n\tprivate static final String PLAY_RES_X = \"PlayResX\";\n\n\t/**\n\t * PlayResY declaration\n\t */\n\tprivate static final String PLAY_RES_Y = \"PlayResY\";\n\n\t/**\n\t * Collisions declaration\n\t */\n\tprivate static final String COLLISIONS = \"Collisions\";\n\n\t/**\n\t * Script Type declaration\n\t */\n\tprivate static final String SCRIPT_TYPE = \"ScriptType\";\n\n\t/**\n\t * Update Details declaration\n\t */\n\tprivate static final String UPDATE_DETAILS = \"Update Details\";\n\n\t/**\n\t * Script Updated By declaration\n\t */\n\tprivate static final String SCRIPT_UPDATED_BY = \"Script Updated By\";\n\n\t/**\n\t * Synch Point declaration\n\t */\n\tprivate static final String SYNCH_POINT = \"Synch Point\";\n\n\t/**\n\t * Original Timing declaration\n\t */\n\tprivate static final String ORIGINAL_TIMING = \"Original Timing\";\n\n\t/**\n\t * Original Editing declaration\n\t */\n\tprivate static final String ORIGINAL_EDITING = \"Original Editing\";\n\n\t/**\n\t * Original Translation declaration\n\t */\n\tprivate static final String ORIGINAL_TRANSLATION = \"Original Translation\";\n\n\t/**\n\t * Original Script declaration\n\t */\n\tprivate static final String ORIGINAL_SCRIPT = \"Original Script\";\n\n\t/**\n\t * Title declaration\n\t */\n\tprivate static final String TITLE = \"Title\";\n\n\t/**\n\t * Separator\n\t */\n\tpublic static final String SEP = \": \";\n\n\t/**\n\t * New line separator\n\t */\n\tprivate static final String NEW_LINE = \"\\n\";\n\n\t/**\n\t * Decimal time formater\n\t */\n\tprivate static final DecimalFormat timeFormatter = new DecimalFormat(\"#.0000\");\n\n\tpublic enum Collision {\n\n\t\t/**\n\t\t * position subtitles in the position specified by the \"margins\"\n\t\t */\n\t\tNORMAL(\"Normal\"),\n\n\t\t/**\n\t\t * subtitles will be shifted upwards to make room for subsequent overlapping\n\t\t * subtitles\n\t\t */\n\t\tREVERSE(\"Reverse\");\n\n\t\tprivate String type;\n\n\t\tCollision(String type) {\n\t\t\tthis.type = type;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn this.type;\n\t\t}\n\n\t}\n\n\t/**\n\t * This is a description of the script. If the original author(s) did not provide this\n\t * information then <untitled> is automatically substituted.\n\t */\n\tprivate String title;\n\n\t/**\n\t * The original author(s) of the script. If the original author(s) did not provide\n\t * this information then <unknown> is automatically substituted.\n\t */\n\tprivate String originalScript;\n\n\t/**\n\t * (optional) The original translator of the dialogue. This entry does not appear if\n\t * no information was entered by the author.\n\t */\n\tprivate String originalTranslation;\n\n\t/**\n\t * (optional) The original script editor(s), typically whoever took the raw\n\t * translation and turned it into idiomatic english and reworded for readability. This\n\t * entry does not appear if no information was entered by the author.\n\t */\n\tprivate String originalEditing;\n\n\t/**\n\t * (optional) Whoever timed the original script. This entry does not appear if no\n\t * information was entered by the author.\n\t */\n\tprivate String originalTiming;\n\n\t/**\n\t * (optional) Description of where in the video the script should begin playback.\n\t */\n\tprivate String synchPoint;\n\t/**\n\t * (optional) The original script editor(s), typically whoever took the raw\n\t * translation and turned it into idiomatic english and reworded for readability. This\n\t * entry does not appear if no information was entered by the author.\n\t */\n\tprivate String originalScriptChecking;\n\n\t/**\n\t * (optional) Names of any other subtitling groups who edited the original script.\n\t */\n\tprivate String scriptUpdatedBy;\n\n\t/**\n\t * The details of any updates to the original script made by other subtilting groups.\n\t */\n\tprivate String userDetails;\n\n\t/**\n\t * This is the SSA script format version eg. \"V4.00\". It is used by SSA to give a\n\t * warning if you are using a version of SSA older than the version that created the\n\t * script.\n\t */\n\tprivate String scriptType = \"v4.00+\";\n\n\t/**\n\t * This determines how subtitles are moved, when automatically preventing onscreen\n\t * collisions.\n\t * \n\t * If the entry says \"Normal\" then SSA will attempt to position subtitles in the\n\t * position specified by the \"margins\". However, subtitles can be shifted vertically\n\t * to prevent onscreen collisions. With \"normal\" collision prevention, the subtitles\n\t * will \"stack up\" one above the other - but they will always be positioned as close\n\t * the vertical (bottom) margin as possible - filling in \"gaps\" in other subtitles if\n\t * one large enough is available.\n\t * \n\t * If the entry says \"Reverse\" then subtitles will be shifted upwards to make room for\n\t * subsequent overlapping subtitles. This means the subtitles can nearly always be\n\t * read top-down - but it also means that the first subtitle can appear half way up\n\t * the screen before the subsequent overlapping subtitles appear. It can use a lot of\n\t * screen area.\n\t */\n\tprivate Collision collisions = Collision.NORMAL;\n\n\t/**\n\t * This is the height of the screen used by the script's author(s) when playing the\n\t * script. SSA v4 will automatically select the nearest enabled setting, if you are\n\t * using Directdraw playback.\n\t */\n\tprivate int playResY;\n\n\t/**\n\t * This is the width of the screen used by the script's author(s) when playing the\n\t * script. SSA will automatically select the nearest enabled, setting if you are using\n\t * Directdraw playback.\n\t */\n\tprivate int playResX;\n\n\t/**\n\t * This is the colour depth used by the script's author(s) when playing the script.\n\t * SSA will automatically select the nearest enabled setting if you are using\n\t * Directdraw playback.\n\t */\n\tprivate int playDepth;\n\n\t/**\n\t * This is the Timer Speed for the script, as a percentage.\n\t */\n\tprivate double timer = 100.0000;\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tappendNotNull(sb, TITLE, this.title);\n\t\tappendNotNull(sb, ORIGINAL_SCRIPT, this.originalScript);\n\t\tappendNotNull(sb, ORIGINAL_TRANSLATION, this.originalTranslation);\n\t\tappendNotNull(sb, ORIGINAL_EDITING, this.originalEditing);\n\t\tappendNotNull(sb, ORIGINAL_TIMING, this.originalTiming);\n\t\tappendNotNull(sb, SYNCH_POINT, this.synchPoint);\n\t\tappendNotNull(sb, SCRIPT_UPDATED_BY, this.scriptUpdatedBy);\n\t\tappendNotNull(sb, UPDATE_DETAILS, this.userDetails);\n\t\tappendNotNull(sb, SCRIPT_TYPE, this.scriptType);\n\t\tappendNotNull(sb, COLLISIONS, this.collisions.toString());\n\t\tappendPositive(sb, PLAY_RES_Y, this.playResY);\n\t\tappendPositive(sb, PLAY_RES_X, this.playResX);\n\t\tappendPositive(sb, PLAY_DEPTH, this.playDepth);\n\t\tsb.append(TIMER).append(SEP).append(timeFormatter.format(this.timer));\n\t\treturn sb.toString();\n\t}\n\n\t// ======================= private methods =======================\n\n\t/**\n\t * Append a value in a <code>StringBuilder</code> if the value is not null\n\t * \n\t * @param sb: the string builder\n\t * @param desc: the description\n\t * @param val: the value\n\t */\n\tprivate static void appendNotNull(StringBuilder sb, String desc, String val) {\n\t\tif (val != null) {\n\t\t\tsb.append(desc).append(SEP).append(val).append(NEW_LINE);\n\t\t}\n\t}\n\n\t/**\n\t * Append a value in a <code>StringBuilder</code> if the value is positive\n\t * \n\t * @param sb: the string builder\n\t * @param desc: the description\n\t * @param val: the value\n\t */\n\tprivate static void appendPositive(StringBuilder sb, String desc, int val) {\n\t\tif (val > 0) {\n\t\t\tsb.append(desc).append(SEP).append(val).append(NEW_LINE);\n\t\t}\n\t}\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.ass;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * Styles define the appearance and position of subtitles. All styles used by the script\n * are are defined by a Style line in the script.\n * \n * Any of the the settings in the Style, (except shadow/outline type and depth) can\n * overridden by control codes in the subtitle text.\n * \n * The fields which appear in each Style definition line are named in a special line with\n * the line type “Format:”. The Format line must appear before any Styles - because it\n * defines how SSA will interpret the Style definition lines. The field names listed in\n * the format line must be correctly spelled!\n * \n * The fields are as follows:\n * \n * Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour,\n * Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle,\n * Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n * \n * The format line allows new fields to be added to the script format in future, and yet\n * allow old versions of the software to read the fields it recognises - even if the field\n * order is changed.\n */\n@Data\npublic class V4Style implements Serializable {\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = -4910432063071707768L;\n\n\t/**\n\t * Style declaration\n\t */\n\tpublic static final String STYLE = \"Style: \";\n\n\t/**\n\t * Format declaration\n\t */\n\tpublic static final String FORMAT_STRING = \"Name,Fontname,Fontsize,PrimaryColour,\"\n\t\t\t+ \"SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,\"\n\t\t\t+ \"StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,\"\n\t\t\t+ \"Alignment,MarginL,MarginR,MarginV,Encoding\";\n\n\t/**\n\t * Separator\n\t */\n\tpublic static final String SEP = \",\";\n\n\t/**\n\t * The name of the Style. Case sensitive. Cannot include commas.\n\t */\n\tprivate String name;\n\n\t/**\n\t * The fontname as used by Windows. Case-sensitive.\n\t */\n\tprivate String fontname = \"Arial\";\n\n\t/**\n\t * The font size\n\t */\n\tprivate int fontsize;\n\n\t/**\n\t * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal\n\t * equivelent of this number is BBGGRR\n\t * \n\t * The color format contains the alpha channel, too. (AABBGGRR)\n\t */\n\tprivate int primaryColour;\n\n\t/**\n\t * long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal\n\t * equivelent of this number is BBGGRR\n\t * \n\t * This colour may be used instead of the Primary colour when a subtitle is\n\t * automatically shifted to prevent an onscreen collsion, to distinguish the different\n\t * subtitles.\n\t * \n\t * The color format contains the alpha channel, too. (AABBGGRR)\n\t */\n\tprivate int secondaryColour = 16777215; // #FFFFFF (white)\n\n\t/**\n\t * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal\n\t * equivelent of this number is BBGGRR\n\t * \n\t * This colour may be used instead of the Primary or Secondary colour when a subtitle\n\t * is automatically shifted to prevent an onscreen collsion, to distinguish the\n\t * different subtitles.\n\t * \n\t * The color format contains the alpha channel, too. (AABBGGRR)\n\t */\n\tprivate int outlineColour;\n\n\t/**\n\t * This is the colour of the subtitle outline or shadow, if these are used. A long\n\t * integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal\n\t * equivelent of this number is BBGGRR.\n\t * \n\t * The color format contains the alpha channel, too. (AABBGGRR)\n\t */\n\tprivate int backColour;\n\n\t/**\n\t * This defines whether text is bold (true) or not (false). -1 is True, 0 is False.\n\t * This is independant of the Italic attribute - you can have have text which is both\n\t * bold and italic.\n\t */\n\tprivate boolean bold;\n\n\t/**\n\t * This defines whether text is italic (true) or not (false). -1 is True, 0 is False.\n\t * This is independant of the bold attribute - you can have have text which is both\n\t * bold and italic.\n\t */\n\tprivate boolean italic;\n\n\t/**\n\t * -1 is True, 0 is False\n\t */\n\tprivate boolean underline;\n\n\t/**\n\t * -1 is True, 0 is False\n\t */\n\tprivate boolean strikeOut;\n\n\t/**\n\t * Modifies the width of the font. [percent]\n\t */\n\tprivate int scaleX = 100;\n\n\t/**\n\t * Modifies the height of the font. [percent]\n\t */\n\tprivate int scaleY = 100;\n\n\t/**\n\t * Extra space between characters. [pixels]\n\t */\n\tprivate int spacing;\n\n\t/**\n\t * The origin of the rotation is defined by the alignment. Can be a floating point\n\t * number. [degrees]\n\t */\n\tprivate double angle;\n\n\t/**\n\t * 1=Outline + drop shadow, 3=Opaque box\n\t */\n\tprivate int borderStyle = 1;\n\n\t/**\n\t * If BorderStyle is 1, then this specifies the width of the outline around the text,\n\t * in pixels. Values may be 0, 1, 2, 3 or 4.\n\t */\n\tprivate int outline = 2;\n\n\t/**\n\t * If BorderStyle is 1, then this specifies the depth of the drop shadow behind the\n\t * text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in\n\t * addition to an outline - SSA will force an outline of 1 pixel if no outline width\n\t * is given.\n\t */\n\tprivate int shadow;\n\n\t/**\n\t * This sets how text is \"justified\" within the Left/Right onscreen margins, and also\n\t * the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value\n\t * for a \"Toptitle\". Add 8 to the value for a \"Midtitle\". eg. 5 = left-justified\n\t * toptitle\n\t */\n\tprivate int alignment = 2;\n\n\t/**\n\t * This defines the Left Margin in pixels. It is the distance from the left-hand edge\n\t * of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas\n\t * in which the subtitle text will be displayed.\n\t */\n\tprivate int marginL = 10;\n\n\t/**\n\t * This defines the Right Margin in pixels. It is the distance from the right-hand\n\t * edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define\n\t * areas in which the subtitle text will be displayed.\n\t */\n\tprivate int marginR = 10;\n\n\t/**\n\t * This defines the vertical Left Margin in pixels. For a subtitle, it is the distance\n\t * from the bottom of the screen. For a toptitle, it is the distance from the top of\n\t * the screen. For a midtitle, the value is ignored - the text will be vertically\n\t * centred\n\t */\n\tprivate int marginV = 10;\n\n\t/**\n\t * This specifies the font character set or encoding and on multi-lingual Windows\n\t * installations it provides access to characters used in multiple than one languages.\n\t * It is usually 0 (zero) for English (Western, ANSI) Windows.\n\t * \n\t * When the file is Unicode, this field is useful during file format conversions.\n\t */\n\tprivate int encoding;\n\n\t/**\n\t * Default constructor\n\t */\n\tpublic V4Style() {\n\t}\n\n\t/**\n\t * Constructor\n\t * \n\t * @param name: the style name\n\t */\n\tpublic V4Style(String name) {\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(STYLE);\n\t\tsb.append(this.name).append(SEP);\n\t\tsb.append(this.fontname).append(SEP);\n\t\tsb.append(this.fontsize).append(SEP);\n\t\tsb.append(this.primaryColour).append(SEP);\n\t\tsb.append(this.secondaryColour).append(SEP);\n\t\tsb.append(this.outlineColour).append(SEP);\n\t\tsb.append(this.backColour).append(SEP);\n\t\tsb.append(this.bold ? -1 : 0).append(SEP);\n\t\tsb.append(this.italic ? -1 : 0).append(SEP);\n\t\tsb.append(this.underline ? -1 : 0).append(SEP);\n\t\tsb.append(this.strikeOut ? -1 : 0).append(SEP);\n\t\tsb.append(this.scaleX).append(SEP);\n\t\tsb.append(this.scaleY).append(SEP);\n\t\tsb.append(this.spacing).append(SEP);\n\t\tsb.append(this.angle).append(SEP);\n\t\tsb.append(this.borderStyle).append(SEP);\n\t\tsb.append(this.outline).append(SEP);\n\t\tsb.append(this.shadow).append(SEP);\n\t\tsb.append(this.alignment).append(SEP);\n\t\tsb.append(this.marginL).append(SEP);\n\t\tsb.append(this.marginR).append(SEP);\n\t\tsb.append(this.marginV).append(SEP);\n\t\tsb.append(this.encoding);\n\t\treturn sb.toString();\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\npublic class SubtitleLine<T extends TimedObject> implements TimedLine, Serializable {\n\n    /**\n     * Serial Id\n     */\n    private static final long serialVersionUID = 288560648398584309L;\n\n    /**\n     * Subtitle Text. This is the actual text which will be displayed as a subtitle\n     * onscreen.\n     */\n    protected List<String> textLines = new ArrayList<>();\n\n    /**\n     * Timecodes\n     */\n    protected T time;\n\n    /**\n     * Comparator that only compare timings\n     *\n     * @return the comparator\n     */\n    public static Comparator<TimedLine> timeComparator = Comparator.comparing(TimedLine::getTime);\n\n    /**\n     * Constructor\n     */\n    public SubtitleLine() {\n        super();\n    }\n\n    /**\n     * Constructor\n     */\n    public SubtitleLine(T time) {\n\n        super();\n        this.time = time;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null || getClass() != obj.getClass()) {\n            return false;\n        }\n\n        TimedLine other = (TimedLine) obj;\n        return compareTo(other) == 0;\n    }\n\n    @Override\n    public int compare(TimedLine o1, TimedLine o2) {\n\n        return o1.compareTo(o2);\n    }\n\n    @Override\n    public int compareTo(TimedLine o) {\n\n        if (o.getTime() == null) {\n            return 1;\n        }\n        int compare = this.time.compareTo(o.getTime());\n        if (compare == 0) {\n            String thisText = String.join(\",\", this.textLines);\n            String otherText = String.join(\",\", o.getTextLines());\n            compare = thisText.compareTo(otherText);\n        }\n\n        return compare;\n    }\n\n    // ===================== getter and setter start =====================\n\n    @Override\n    public T getTime() {\n        return this.time;\n    }\n\n    public void setTime(T time) {\n        this.time = time;\n    }\n\n    @Override\n    public List<String> getTextLines() {\n        return this.textLines;\n    }\n\n    @Override\n    public void setTextLines(List<String> textLines) {\n        this.textLines = textLines;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.time.LocalTime;\n\npublic class SubtitleTime implements TimedObject, Serializable {\n\n    private static final long serialVersionUID = -2283115927128309201L;\n\n    /**\n     * Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is\n     * the time elapsed during script playback at which the text will appear onscreen.\n     */\n    protected LocalTime start;\n\n    /**\n     * End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is\n     * the time elapsed during script playback at which the text will disappear offscreen.\n     */\n    protected LocalTime end;\n\n    public SubtitleTime() {\n    }\n\n    public SubtitleTime(LocalTime start, LocalTime end) {\n\n        super();\n        this.start = start;\n        this.end = end;\n    }\n\n    @Override\n    public int compare(TimedObject o1, TimedObject o2) {\n\n        return o1.compareTo(o2);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null || getClass() != obj.getClass()) {\n            return false;\n        }\n\n        TimedObject other = (TimedObject) obj;\n        return compareTo(other) == 0;\n    }\n\n    @Override\n    public int compareTo(TimedObject other) {\n\n        int compare = this.start.compareTo(other.getStart());\n        if (compare == 0) {\n            compare = this.end.compareTo(other.getEnd());\n        }\n        return compare;\n    }\n\n    // ===================== getter and setter start =====================\n\n    @Override\n    public LocalTime getStart() {\n        return this.start;\n    }\n\n    @Override\n    public void setStart(LocalTime start) {\n        this.start = start;\n    }\n\n    @Override\n    public LocalTime getEnd() {\n        return this.end;\n    }\n\n    @Override\n    public void setEnd(LocalTime end) {\n        this.end = end;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.Comparator;\nimport java.util.List;\n\n/**\n * Simple object that contains a text line with a time\n */\npublic interface TimedLine extends Serializable, Comparable<TimedLine>, Comparator<TimedLine> {\n\n    /**\n     * Get the text lines\n     *\n     * @return textLines\n     */\n    List<String> getTextLines();\n\n    /**\n     * Set the text lines\n     *\n     */\n    void setTextLines(List<String> textLines);\n\n    /**\n     * Get the timed object\n     *\n     * @return the time\n     */\n    TimedObject getTime();\n\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\nimport java.io.Serializable;\nimport java.time.LocalTime;\nimport java.util.Comparator;\n\n/**\n * Simple object that contains timed start ant end\n */\npublic interface TimedObject extends Serializable, Comparable<TimedObject>, Comparator<TimedObject> {\n\n    /**\n     * Return the time elapsed during script playback at which the text will appear\n     * onscreen.\n     *\n     * @return start time\n     */\n    LocalTime getStart();\n\n    /**\n     * Return the time elapsed during script playback at which the text will disappear\n     * offscreen.\n     *\n     * @return end time\n     */\n    LocalTime getEnd();\n\n    /**\n     * Set the time elapsed during script playback at which the text will appear onscreen.\n     *\n     * @param start time\n     */\n    void setStart(LocalTime start);\n\n    /**\n     * Set the time elapsed during script playback at which the text will disappear\n     * offscreen.\n     *\n     * @param end time\n     */\n    void setEnd(LocalTime end);\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.common;\n\n\nimport java.io.Serializable;\nimport java.util.Set;\n\n/**\n * Object that represents a text file containing timed lines\n */\npublic interface TimedTextFile extends Serializable {\n\n    /**\n     * Get the filename\n     *\n     * @return the filename\n     */\n    String getFileName();\n\n    /**\n     * Set the filename\n     *\n     * @param fileName: the filename\n     */\n    void setFileName(String fileName);\n\n    /**\n     * Get the timed lines\n     *\n     * @return lines\n     */\n    Set<? extends TimedLine> getTimedLines();\n\n}"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.config;\n\n\nimport lombok.Data;\nimport org.fordes.subtitles.view.utils.submerge.constant.FontName;\n\nimport java.io.Serializable;\n\n@Data\npublic class Font implements Serializable {\n\n\t/**\n\t * Serial\n\t */\n\tprivate static final long serialVersionUID = -3711480706383195193L;\n\n\t/**\n\t * Font name\n\t */\n\tprivate String name = FontName.Arial.toString();\n\n\t/**\n\t * Font size\n\t */\n\tprivate int size = 16;\n\n\t/**\n\t * Font color\n\t */\n\tprivate String color = \"#fffff9\";\n\n\t/**\n\t * Outline color\n\t */\n\tprivate String outlineColor = \"#000000\";\n\n\t/**\n\t * Outline width\n\t */\n\tprivate int outlineWidth = 2;\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.config;\n\n\nimport lombok.Data;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\nimport java.io.Serializable;\n\n\n@Data\npublic class SimpleSubConfig implements Serializable {\n\n\tprivate static final long serialVersionUID = -485125721913729063L;\n\n\tprivate String styleName;\n\tprivate TimedTextFile sub;\n\tprivate Font fontconfig = new Font();\n\tprivate int alignment;\n\tprivate int verticalMargin = 10;\n\n\tpublic SimpleSubConfig() {\n\t}\n\n\tpublic SimpleSubConfig(TimedTextFile sub, Font fontConfig) {\n\t\tthis.sub = sub;\n\t\tthis.fontconfig = fontConfig;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.NoArgsConstructor;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine;\n\nimport java.util.List;\n\n/**\n * @author fordes on 2022/7/21\n */\n@NoArgsConstructor\npublic class LRCLine extends SubtitleLine<LRCTime> {\n\n    private static final long serialVersionUID = -5787808773967579723L;\n\n\n    public LRCLine(LRCTime time, List<String> textLines) {\n        this.time = time;\n        this.textLines = textLines;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(this.time == null ? StrUtil.EMPTY: this.time);\n        textLines.forEach(line -> sb.append(line).append(StrUtil.CR));\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\nimport java.io.Serializable;\nimport java.util.Set;\nimport java.util.TreeSet;\n\n/**\n * @author fordes on 2022/7/21\n */\n@Data\n@NoArgsConstructor\npublic class LRCSub implements TimedTextFile, Serializable {\n\n    private static final long serialVersionUID = -2909833789376537734L;\n\n    private String fileName;\n    private Set<LRCLine> lines = new TreeSet<>();\n\n    public void add(LRCLine line) {\n        this.lines.add(line);\n    }\n\n    public void remove(TimedLine line) {\n        this.lines.remove((LRCLine) line);\n    }\n\n    public String toString() {\n        return CollUtil.join(lines, StrUtil.EMPTY);\n    }\n\n    @Override\n    public Set<? extends TimedLine> getTimedLines() {\n        return this.lines;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.lrc;\n\nimport cn.hutool.core.date.LocalDateTimeUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.NoArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime;\n\nimport java.io.Serializable;\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoField;\n\n/**\n * @author fordes on 2022/7/21\n */\n@Slf4j\n@NoArgsConstructor\npublic class LRCTime extends SubtitleTime implements Serializable {\n\n    private static final long serialVersionUID = -5787808223967579723L;\n\n    public static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(SRTTime.PATTERN);\n\n    public static final String PATTERN = \"mm:ss.SS\";\n\n    private static final String TS_PATTERN = \"%02d:%02d.%02d\";\n\n    public LRCTime(LocalTime start) {\n        this.start = start;\n    }\n\n    @Override\n    public String toString() {\n        return StrUtil.format(\"[{}]\", format(start));\n    }\n\n    public static String format(LocalTime time) {\n        int min = time.get(ChronoField.MINUTE_OF_HOUR);\n        int sec = time.get(ChronoField.SECOND_OF_MINUTE);\n        int ms = time.get(ChronoField.MILLI_OF_SECOND);\n\n        return String.format(TS_PATTERN, min, sec, ms);\n    }\n\n    public static LRCTime fromString(String times) {\n        try {\n            LocalTime time = LocalDateTimeUtil.parse(times, PATTERN).toLocalTime();\n            return new LRCTime(time);\n        }catch (Exception e) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine;\n\nimport java.util.List;\n\n\n/**\n * Class <SRTLine> represents an abstract line of SRT, meaning text, timecodes and index\n *\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\npublic class SRTLine extends SubtitleLine<SRTTime> {\n\n\tprivate static final long serialVersionUID = -1220593401999895814L;\n\n\tprivate static final String NEW_LINE = \"\\n\";\n\n\tprivate int id;\n\n\tpublic SRTLine(int id, SRTTime time, List<String> textLines) {\n\t\t\n\t\tthis.id = id;\n\t\tthis.time = time;\n\t\tthis.textLines = textLines;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\t\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(this.id).append(NEW_LINE);\n\t\tsb.append(this.time).append(NEW_LINE);\n\t\tthis.textLines.forEach(textLine -> sb.append(textLine).append(NEW_LINE));\n\t\treturn sb.append(NEW_LINE).toString();\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile;\n\nimport java.io.Serializable;\nimport java.util.Set;\nimport java.util.TreeSet;\n\n\n/**\n * Class <SRTLine> represents an SRT file, meandin a complete set of subtitle lines\n *\n */\npublic class SRTSub implements TimedTextFile, Serializable {\n\n\tprivate static final long serialVersionUID = -2909833999376537734L;\n\n\tprivate String fileName;\n\tprivate Set<SRTLine> lines = new TreeSet<>();\n\n\t// ======================== Public methods ==========================\n\n\tpublic void add(SRTLine line) {\n\n\t\tthis.lines.add(line);\n\t}\n\n\tpublic void remove(TimedLine line) {\n\n\t\tthis.lines.remove(line);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\t\n\t\tStringBuilder sb = new StringBuilder();\n\t\tthis.lines.forEach(srtLine -> sb.append(srtLine));\n\t\treturn sb.toString();\n\t}\n\n\t// ===================== getter and setter start =====================\n\n\tpublic Set<SRTLine> getLines() {\n\t\treturn this.lines;\n\t}\n\n\t@Override\n\tpublic Set<? extends TimedLine> getTimedLines() {\n\t\treturn this.lines;\n\t}\n\n\tpublic void setLines(Set<SRTLine> lines) {\n\t\tthis.lines = lines;\n\t}\n\n\t@Override\n\tpublic String getFileName() {\n\t\treturn this.fileName;\n\t}\n\n\t@Override\n\tpublic void setFileName(String fileName) {\n\t\tthis.fileName = fileName;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.subtitle.srt;\n\n\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime;\n\nimport java.io.Serializable;\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.time.temporal.ChronoField;\n\n\npublic class SRTTime extends SubtitleTime implements Serializable {\n\n\tprivate static final long serialVersionUID = -5784108223967579723L;\n\n\tpublic static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(SRTTime.PATTERN);\n\tpublic static final String PATTERN = \"HH:mm:ss,SSS\";\n\tprivate static final String TS_PATTERN = \"%02d:%02d:%02d,%03d\";\n\tpublic static final String DELIMITER = \" --> \";\n\n\tpublic SRTTime() {\n\t\tsuper();\n\t}\n\n\tpublic SRTTime(LocalTime start, LocalTime end) {\n\n\t\tsuper(start, end);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(format(this.start));\n\t\tsb.append(DELIMITER);\n\t\tsb.append(format(this.end));\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * Convert a <code>LocalTime</code> to string\n\t * \n\t * @param time: the time to format\n\t * @return the formatted time\n\t */\n\tpublic static String format(LocalTime time) {\n\n\t\tint hr = time.get(ChronoField.HOUR_OF_DAY);\n\t\tint min = time.get(ChronoField.MINUTE_OF_HOUR);\n\t\tint sec = time.get(ChronoField.SECOND_OF_MINUTE);\n\t\tint ms = time.get(ChronoField.MILLI_OF_SECOND);\n\n\t\treturn String.format(TS_PATTERN, hr, min, sec, ms);\n\t}\n\n\t/**\n\t * Convert a string pattern to a Local time\n\t * \n\t * @param times\n\t * @see SRTTime.PATTERN\n\t * @return\n\t * @throws DateTimeParseException\n\t */\n\tpublic static LocalTime fromString(String times) {\n\n\t\treturn LocalTime.parse(times.replace('.', ',').trim(), FORMATTER);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.utils;\n\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidColorCode;\n\nimport java.awt.*;\n\n\npublic final class ColorUtils {\n\n    /**\n     * Convert the hexadecimal color code to BGR code\n     *\n     * @param hex\n     * @return rgb\n     */\n    public static int hexToBGR(String hex) {\n        Color color = Color.decode(hex);\n        int in = Integer.decode(Integer.toString(color.getRGB()));\n        int red = (in >> 16) & 0xFF;\n        int green = (in >> 8) & 0xFF;\n        int blue = (in) & 0xFF;\n        return (blue << 16) | (green << 8) | (red);\n    }\n\n    /**\n     * Convert a &HAABBGGRR to hexadecimal\n     *\n     * @param haabbggrr: the color code\n     * @return the hexadecimal code\n     * @throws InvalidColorCode\n     */\n    public static String HAABBGGRRToHex(String haabbggrr) {\n        if (haabbggrr.length() != 10) {\n            throw new InvalidColorCode(\"Invalid pattern, must be &HAABBGGRR\");\n        }\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"#\");\n        sb.append(haabbggrr.substring(8));\n        sb.append(haabbggrr.charAt(6));\n        sb.append(haabbggrr.charAt(4));\n        sb.append(haabbggrr.charAt(2));\n        return sb.toString().toLowerCase();\n    }\n\n    /**\n     * Convert a &HBBGGRR to hexadecimal\n     *\n     * @param hbbggrr: the color code\n     * @return the hexadecimal code\n     */\n    public static String HBBGGRRToHex(String hbbggrr) {\n        if (hbbggrr.length() != 8) {\n            throw new InvalidColorCode(\"Invalid pattern, must be &HBBGGRR\");\n        }\n        return StrUtil.concat(false, \"#\", hbbggrr.substring(6),\n\t\t\t\thbbggrr.substring(4, 5), hbbggrr.substring(2, 3)).toLowerCase();\n    }\n\n    /**\n     * Convert a &HAABBGGRR to BGR\n     *\n     * @param haabbggrr: the color code\n     * @return the BGR code\n     * @throws InvalidColorCode\n     */\n    public static int HAABBGGRRToBGR(String haabbggrr) {\n        return hexToBGR(HAABBGGRRToHex(haabbggrr));\n    }\n\n    /**\n     * Convert a &HBBGGRR to BGR\n     *\n     * @param hbbggrr: the color code\n     * @return the BGR code\n     * @throws InvalidColorCode\n     */\n    public static int HBBGGRRToBGR(String hbbggrr) {\n        return hexToBGR(HBBGGRRToHex(hbbggrr));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.utils;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSTime;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.Events;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.ass.V4Style;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.config.Font;\nimport org.fordes.subtitles.view.utils.submerge.subtitle.config.SimpleSubConfig;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n\npublic class ConvertUtils {\n\n\tprivate static final String RGX_XML_TAG = \"<[^>]+>\";\n\tprivate static final String RGX_ASS_FORMATTING = \"\\\\{[^\\\\}]*\\\\}\";\n\tprivate static final String SRT_ITALIC_CLOSE = \"\\\\</i\\\\>\";\n\tprivate static final String SRT_ITALIC_OPEN = \"\\\\<i\\\\>\";\n\tprivate static final String ASS_ITALIC_CLOSE = \"\\\\{\\\\\\\\i0\\\\}\";\n\tprivate static final String ASS_ITALIC_OPEN = \"\\\\{\\\\\\\\i1\\\\}\";\n\n\t/**\n\t * Create an <code>Events</code> object from a timed line\n\t * \n\t * @param line: a timed line\n\t * @param style: the style name\n\t * @return the corresponding <code>Events</code>\n\t */\n\tpublic static Events createEvent(TimedLine line, String style) {\n\n\t\tList<String> newLine = line.getTextLines().stream()\n\t\t\t\t.map(ConvertUtils::toASSString).collect(Collectors.toList());\n\n\t\tTimedObject timeLine = line.getTime();\n\t\tASSTime time = new ASSTime(timeLine.getStart(), timeLine.getEnd());\n\n\t\treturn new Events(style, time, newLine);\n\t}\n\n\t/**\n\t * Create a <code>V4Style</code> object from <code>SubInput</code>\n\t * \n\t * @param config: the configuration object\n\t * @return the corresponding style\n\t */\n\tpublic static V4Style createV4Style(SimpleSubConfig config) {\n\n\t\tV4Style style = new V4Style(config.getStyleName());\n\t\tFont font = config.getFontconfig();\n\t\tstyle.setFontname(font.getName());\n\t\tstyle.setFontsize(font.getSize());\n\t\tstyle.setAlignment(config.getAlignment());\n\t\tstyle.setPrimaryColour(ColorUtils.hexToBGR(font.getColor()));\n\t\tstyle.setOutlineColour(ColorUtils.hexToBGR(font.getOutlineColor()));\n\t\tstyle.setOutline(font.getOutlineWidth());\n\t\tstyle.setMarginV(config.getVerticalMargin());\n\t\treturn style;\n\t}\n\n\t/**\n\t * Format a text line to be srt compliant\n\t * \n\t * @param textLine the text line\n\t * @return the formatted text line\n\t */\n\tpublic static String toSRTString(String textLine) {\n\n\t\tString formatted = textLine.replaceAll(ASS_ITALIC_OPEN, SRT_ITALIC_OPEN);\n\t\tformatted = formatted.replaceAll(ASS_ITALIC_CLOSE, SRT_ITALIC_CLOSE);\n\t\tformatted = formatted.replaceAll(RGX_ASS_FORMATTING, StrUtil.EMPTY);\n\n\t\treturn formatted;\n\t}\n\n\t/**\n\t * Format a text line to be ass compliant\n\t * \n\t * @param textLine the text line\n\t * @return\n\t */\n\tpublic static String toASSString(String textLine) {\n\n\t\tString formatted = textLine.replaceAll(SRT_ITALIC_OPEN, ASS_ITALIC_OPEN);\n\t\tformatted = formatted.replaceAll(SRT_ITALIC_CLOSE, ASS_ITALIC_CLOSE);\n\n\t\treturn formatted.replaceAll(RGX_XML_TAG, StrUtil.EMPTY);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java",
    "content": "package org.fordes.subtitles.view.utils.submerge.utils;\n\nimport cn.hutool.core.io.CharsetDetector;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.mozilla.universalchardet.UniversalDetector;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n\n@Slf4j\npublic class EncodeUtils {\n\n\t/**\n\t * Detect charset encoding of a file\n\t * \n\t * @param file: the file to detect encoding from\n\t * @return the charset encoding\n\t * @throws IOException\n\t */\n\tpublic static String guessEncoding(File file) throws IOException {\n\t\ttry (FileInputStream is = new FileInputStream(file)) {\n\t\t\treturn guessEncoding(is);\n\t\t}\n\t}\n\n\t/**\n\t * Detect charset encoding of an input stream\n\t * \n\t * @param is: the InputStream to detect encoding from\n\t * @return the charset encoding\n\t * @throws IOException\n\t */\n\tpublic static String guessEncoding(InputStream is) throws IOException {\n\t\t//先使用juniversalchardet检测\n\t\tString code =  guessEncoding(IoUtil.readBytes(is));\n\t\tif (code != null) {\n\t\t\treturn code;\n\t\t}\n\t\t//使用hutool的charset检测\n\t\tCharset charset = CharsetDetector.detect(is);\n\t\tif (charset != null) {\n\t\t\treturn charset.name();\n\t\t}\n\t\t//默认使用UTF-8\n\t\tlog.debug(\"文件编码检测失败，使用默认编码UTF-8\");\n\t\treturn CharsetUtil.UTF_8;\n\t}\n\n\t/**\n\t * Detect charset encoding of a byte array\n\t * \n\t * @param bytes: the byte array to detect encoding from\n\t * @return the charset encoding\n\t */\n\tpublic static String guessEncoding(byte[] bytes) {\n\t\tUniversalDetector detector = new UniversalDetector(null);\n\t\tdetector.handleData(bytes, 0, bytes.length);\n\t\tdetector.dataEnd();\n\t\tString encoding = detector.getDetectedCharset();\n\t\tdetector.reset();\n\t\treturn encoding;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "logging:\n  config: classpath:logback/logback-spring.xml\n  file:\n    path: ./logs\n\n\nspring:\n  application:\n    name: subtitles-view\n  profiles:\n    active: dev\n  datasource:\n    url: jdbc:sqlite::resource:db/subtitles-view.sqlite\n    driver-class-name: org.sqlite.JDBC\n\n\nservice:\n  translate:\n    tencent:\n      region: ap-shanghai #接口服务，详见：https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8\n    huoshan:\n      region: cn-north-1 #https://www.volcengine.com/docs/6369/67269\n      version-date: 2020-06-01\n\n\nmybatis-plus:\n  mapper-locations: classpath*:mapper/**/*.xml\n  type-aliases-package: org.fordes.subtitles.view.mode.PO\n  configuration:\n    map-underscore-to-camel-case: false\n    cache-enabled: true\n  global-config:\n    banner: off\n    db-config:\n      update-strategy: not_null\n\nconfig:\n  editMode: false\n  exitMode: false\n  languageListMode: true\n  fontSize: 18\n  currentTheme: false"
  },
  {
    "path": "src/main/resources/banner.txt",
    "content": "\n${AnsiColor.BRIGHT_GREEN}  $$$$$$\\            $$\\        $$\\     $$\\   $$\\     $$\\                           $$\\    $$\\ $$\\\n${AnsiColor.BRIGHT_GREEN} $$  __$$\\           $$ |       $$ |    \\__|  $$ |    $$ |                          $$ |   $$ |\\__|\n${AnsiColor.BRIGHT_GREEN} $$ /  \\__|$$\\   $$\\ $$$$$$$\\ $$$$$$\\   $$\\ $$$$$$\\   $$ | $$$$$$\\   $$$$$$$\\       $$ |   $$ |$$\\  $$$$$$\\  $$\\  $$\\  $$\\\n${AnsiColor.BRIGHT_GREEN} \\$$$$$$\\  $$ |  $$ |$$  __$$\\\\_$$  _|  $$ |\\_$$  _|  $$ |$$  __$$\\ $$  _____|      \\$$\\  $$  |$$ |$$  __$$\\ $$ | $$ | $$ |\n${AnsiColor.BRIGHT_GREEN}  \\____$$\\ $$ |  $$ |$$ |  $$ | $$ |    $$ |  $$ |    $$ |$$$$$$$$ |\\$$$$$$\\         \\$$\\$$  / $$ |$$$$$$$$ |$$ | $$ | $$ |\n${AnsiColor.BRIGHT_GREEN} $$\\   $$ |$$ |  $$ |$$ |  $$ | $$ |$$\\ $$ |  $$ |$$\\ $$ |$$   ____| \\____$$\\         \\$$$  /  $$ |$$   ____|$$ | $$ | $$ |\n${AnsiColor.BRIGHT_GREEN} \\$$$$$$  |\\$$$$$$  |$$$$$$$  | \\$$$$  |$$ |  \\$$$$  |$$ |\\$$$$$$$\\ $$$$$$$  |         \\$  /   $$ |\\$$$$$$$\\ \\$$$$$\\$$$$  |\n${AnsiColor.BRIGHT_GREEN}  \\______/  \\______/ \\_______/   \\____/ \\__|   \\____/ \\__| \\_______|\\_______/           \\_/    \\__| \\_______| \\_____\\____/\n\n${AnsiColor.BRIGHT_CYAN} :: Application :: ${AnsiColor.BRIGHT_RED}${spring.application.name}\n${AnsiColor.BRIGHT_CYAN} :: Developers :: ${AnsiColor.BRIGHT_RED}fordes\n${AnsiColor.BRIGHT_CYAN} :: Github :: ${AnsiColor.BRIGHT_RED}https://github.com/fordes123/Subtitles-View${AnsiColor.BRIGHT_WHITE}\n"
  },
  {
    "path": "src/main/resources/css/edit-tool.css",
    "content": ".toolPanel {\n    -fx-font-size: 15;\n    -fx-text-fill: -fx-dark-0;\n    -fx-background-radius: 5;\n    -fx-border-radius: 5;\n    -fx-background-color: -fx-white-0;\n    -fx-effect: dropshadow(GAUSSIAN, -fx-dark-3, 7, 0, 0, 0);\n}\n\n.dark .toolPanel {\n    -fx-text-fill: -fx-white-0;\n    -fx-background-color: -fx-dark-3;\n    -fx-effect: dropshadow(GAUSSIAN, -fx-dark-0, 7, 0, 0, 0);\n}\n\n.toolPanel .text-field,\n.toolPanel .label,\n.toolPanel .button {\n    -fx-text-fill: -fx-dark-0;\n}\n\n.dark .toolPanel .text-field,\n.dark .toolPanel .label,\n.dark .toolPanel .button {\n    -fx-text-fill: -fx-white-0\n}\n\n.menu-bar:hover,\n.menu:hover,\n.menu-bar:focused,\n.menu:focused,\n.label:hover,\n.button:hover {\n    -fx-text-fill: -fx-focus-0 !important;\n    -fx-background-color: transparent;\n}\n\n.font-icon {\n    -fx-font-size: 20 !important;\n}\n\n\n.menu-bar,.menu {\n    -fx-pref-height: 50;\n    -fx-pref-width: 50;\n    -fx-min-height: 50;\n    -fx-min-width: 50;\n    -fx-alignment: center;\n}\n\n.menu, .menu-bar, .check-menu-item {\n    -fx-graphic: true;\n    -fx-background-color: transparent;\n}\n\n.context-menu {\n    -fx-background-color: -fx-white-1;\n}\n\n.dark .context-menu {\n    -fx-background-color: -fx-dark-4;\n}\n\n.left-item {\n    -fx-background-radius: 5 0 0 5;\n}\n\n.right-item {\n    -fx-background-radius: 0 5 5 0;\n}\n\n.left-top-item {\n    -fx-background-radius: 5 0 0 0;\n}\n\n.left-bottom-item {\n    -fx-background-radius: 0 0 0 5;\n}\n\n.right-top-item {\n    -fx-background-radius: 0 5 0 0;\n}\n\n.right-bottom-item {\n    -fx-background-radius: 0 0 5 0;\n}\n\n.error {\n    -fx-text-fill: -fx-error-0 !important;\n}\n\n.dark .error {\n    -fx-text-fill: -fx-error-1 !important;\n}\n\n.jfx-combo-box,\n.jfx-combo-box > .list-cell {\n    -fx-font-size: 15;\n    -fx-alignment: center;\n}\n\n\n.input-line,\n.input-focused-line {\n    -fx-background-color: transparent !important;\n    -fx-pref-height: 0px !important;\n    -fx-translate-y: 0px !important;\n}\n"
  },
  {
    "path": "src/main/resources/css/font.css",
    "content": "@font-face {\n    font-family: \"iconfont\";\n    src: url('/font/iconfont.ttf') format('truetype');\n}\n\n@font-face {\n    font-family: \"butter sans Rounded\";\n    src: url('/font/buttersans-Rounded.otf') format('truetype');\n}"
  },
  {
    "path": "src/main/resources/css/main-editor.css",
    "content": ".bar .item {\n    -fx-font-family: iconfont;\n    -fx-text-fill: -fx-white-5;\n    -fx-text-alignment: center;\n    -fx-padding: 0;\n    -fx-font-size: 20;\n}\n\n.bar .item:selected, .bar .item:hover {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .bar .item:selected, .dark .bar .item:hover {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .bar .item {\n    -fx-text-fill: -fx-white-4;\n}\n\n.text-area {\n    -fx-background-color: -fx-white-0;\n    -fx-text-fill: -fx-dark-0;\n}\n\n.dark .text-area {\n    -fx-background-color: -fx-dark-3;\n    -fx-text-fill: -fx-white-0;\n}\n\n.text-area .scroll-pane .content {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n    -fx-border-insets: 0;\n    -fx-background-insets: 0;\n}\n\n\n.text-area .scroll-bar,\n.text-area .track {\n    -fx-max-width: 15;\n    -fx-min-width: 15;\n\n}\n\n.bottom .toggle-button,\n.bottom .label {\n    -fx-text-fill: -fx-dark-1;\n    -fx-font-size: 15;\n}\n\n.dark .bottom .label {\n    -fx-text-fill: -fx-white-1;\n}\n\n.bottom .toggle-button .label {\n    -fx-padding: 0;\n    -fx-font-family: iconfont;\n    -fx-font-size: 26;\n    -fx-text-fill: -fx-dark-1;\n}\n\n.bottom .toggle-button:selected .label {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .bottom .toggle-button:selected .label {\n    -fx-text-fill: -fx-focus-1;\n}\n\n.dark #editMode,\n.dark #editModeIcon{\n    -fx-text-fill: -fx-white-1;\n}\n\n.styled-text-area {\n    -fx-font-color: -fx-dark-1 !important;\n    -fx-text-fill: -fx-dark-0 !important;\n    -fx-fill: -fx-dark-0 !important;\n}\n\n.dark .styled-text-area {\n    -fx-font-color: -fx-white-0 !important;\n    -fx-text-fill: -fx-white-0 !important;\n    -fx-fill: -fx-white-0 !important;\n}\n"
  },
  {
    "path": "src/main/resources/css/quick-start.css",
    "content": "#root {\n    -fx-background-color: transparent;\n    -fx-border-style: dashed;\n    -fx-border-radius: 10;\n    -fx-border-width: 4;\n    -fx-border-color: -fx-white-4;\n}\n\n.error {\n    -fx-border-color: -fx-error-0 !important;\n}\n\n.error #clues, .error .button .label {\n    -fx-text-fill: -fx-error-0c !important;\n}\n\n.dark .error {\n    -fx-border-color: -fx-error-1 !important;\n}\n\n.dark .error #clues, .dark .error .button .label {\n    -fx-text-fill: -fx-error-1 !important;\n}\n\n.warning {\n    -fx-border-color: -fx-wran-0 !important;\n}\n\n.warning #clues, .warning .button .label {\n    -fx-text-fill: -fx-wran-0 !important;\n}\n\n.dark .warning {\n    -fx-border-color: -fx-wran-1 !important;\n}\n\n.dark .warning #clues, .dark .warning .button .label {\n    -fx-text-fill: -fx-wran-1 !important;\n}\n\n.success {\n    -fx-border-color: -fx-success-0 !important;\n}\n\n.success #clues, .success .button .label {\n    -fx-text-fill: -fx-success-0 !important;\n}\n\n.dark .success {\n    -fx-border-color: -fx-success-1 !important;\n}\n\n.dark .success #clues, .dark .success .button .label {\n    -fx-text-fill: -fx-success-1 !important;\n}\n\n.button {\n    -fx-background-color: transparent !important;\n}\n\n.button .label {\n    -fx-font-family: iconfont;\n    -fx-font-size: 80;\n    -fx-text-alignment: center;\n}\n\n.label {\n    -fx-text-fill: -fx-white-4 !important;\n}\n\n.button:hover .label {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .button:hover .label {\n    -fx-text-fill: -fx-focus-1;\n}\n\n.label {\n    -fx-font-size: 22;\n    -fx-text-fill: -fx-white-5;\n}\n\n"
  },
  {
    "path": "src/main/resources/css/setting.css",
    "content": ".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-0;\n}\n\n.dark .item, .dark .sub-title {\n    -fx-text-fill: -fx-white-0;\n}\n\n.text-field {\n    -fx-font-size: 15;\n    -fx-text-fill: -fx-dark-0;\n    -fx-background-position: 5;\n    -fx-border-radius: 5;\n    -fx-background-color: -fx-white-2;\n    -fx-pref-width: 300;\n    -fx-pref-height: 40;\n    -fx-min-width: -fx-pref-width;\n    -fx-min-height: -fx-pref-height;\n    -fx-max-width: -fx-pref-width;\n    -fx-max-height: -fx-pref-height;\n}\n\n.dark .text-field {\n    -fx-text-fill: -fx-white-0;\n    -fx-background-color: -fx-dark-5;\n}\n\n.text-field :focused {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .text-field :focused {\n    -fx-text-fill: -fx-focus-1;\n}\n\n.popup-text {\n    -fx-font-size: 14;\n    -fx-text-fill: -fx-dark-4 !important;\n}\n\n.popup-text:hover {\n    -fx-text-fill: -fx-focus-0 !important;\n}\n\n.dark .popup-text {\n    -fx-text-fill: -fx-white-4 !important;\n}\n\n.dark .popup-text:hover {\n    -fx-text-fill: -fx-focus-1 !important;\n}\n\n.tips {\n    -fx-border-width: 0;\n    -fx-border-insets: 0;\n    -fx-background-insets: 0;\n    -fx-background-radius: 8;\n    -fx-border-radius: 8;\n    -fx-background-color: -fx-white-2;\n    -fx-line-spacing: 8px;\n    -fx-font-size: 14;\n}\n\n .tips .text {\n     -fx-fill: -fx-focus-0;\n }\n\n .dark .tips .text {\n     -fx-fill: -fx-focus-1;\n }\n\n.dark .tips {\n    -fx-background-color: -fx-dark-5;\n}\n\n.icon {\n    -fx-font-family: iconfont;\n    -fx-font-size: 26\n}\n\n"
  },
  {
    "path": "src/main/resources/css/speech-conversion.css",
    "content": ""
  },
  {
    "path": "src/main/resources/css/styles.css",
    "content": "* {\n    -fx-white-0: #ffffff;\n    -fx-white-1: #f5f5f5;\n    -fx-white-2: #ebebed;\n    -fx-white-3: #e8e8e8;\n    -fx-white-4: #aaaaaa;\n    -fx-white-5: #4d4d4d;\n\n    -fx-dark-0: #000000;\n    -fx-dark-1: #101010;\n    -fx-dark-2: #1a1a1a;\n    -fx-dark-3: #212121;\n    -fx-dark-4: #282828;\n    -fx-dark-5: #303030;\n\n    -fx-focus-0: #5b5bfa;\n    -fx-focus-1: #5b5bfa;\n\n    -fx-error-0: #e74c3c;\n    -fx-error-1: #c0392b;\n    -fx-wran-0: #f1c40f;\n    -fx-wran-1: #f39c12;\n    -fx-success-0: #2ecc71;\n    -fx-success-1: #27ae60;\n}\n\n.screen {\n    -fx-background-color: transparent;\n    -fx-cursor: hand;\n}\n\n.dark .screen {\n    -fx-effect: dropshadow(gaussian, -fx-dark-4, 8, 0, 0, 0);\n}\n\n.full-screen {\n    -fx-padding: 0;\n    -fx-border-insets: 0;\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n    -fx-effect: dropshadow(gaussian, -fx-dark-0, 0, 0, 0, 0);\n}\n\n.normal-screen {\n    -fx-padding: 20 20 20 20;\n    -fx-border-insets: 0.5;\n    -fx-border-radius: 8;\n    -fx-background-radius: 8;\n    -fx-effect: dropshadow(gaussian, -fx-dark-0, 8, 0, 0, 0);\n}\n\n.content {\n    -fx-background-color: -fx-white-0;\n    -fx-background-repeat: repeat;\n    -fx-border-radius: 0 0 8 0;\n    -fx-background-radius: 0 0 8 0;\n}\n\n.content-exclusive {\n    -fx-border-radius: 0 0 8 8;\n    -fx-background-radius: 0 0 8 8;\n}\n\n.dark .content {\n    -fx-background-color: -fx-dark-3;\n}\n\n.sidebar {\n    -fx-padding: 10 0 0 0;\n    -fx-background-color: -fx-white-1;\n    -fx-background-repeat: repeat;\n    -fx-border-radius: 0 0 0 8;\n    -fx-background-radius: 0 0 0 8;\n}\n\n.dark .sidebar {\n    -fx-background-color: -fx-dark-4;\n}\n\n.sidebar-item {\n    -fx-text-fill: -fx-dark-4;\n    -fx-font-size: 14;\n    -fx-end-margin: 6 10 6 10;\n    -fx-start-margin: 6 10 6 10;\n    -fx-background-radius: 10;\n    -fx-background-color: transparent;\n}\n\n.dark .sidebar-item,\n.dark .sidebar-item:selected,\n.dark .sidebar-item:hover,\n.normal-button,\n.dark .tooltip {\n    -fx-text-fill: -fx-white-0;\n}\n\n\n.sidebar-item:selected,\n.sidebar-item:hover {\n    -fx-background-color: -fx-white-3;\n    /*-fx-text-fill: -fx-focus-0;*/\n}\n\n.dark .sidebar-item:selected,\n.dark .sidebar-item:hover {\n    -fx-background-color: -fx-dark-5;\n    /*-fx-text-fill: -fx-focus-1;*/\n}\n\n\n.sidebar .app-name,\n.sidebar .logo,\n.sidebar .setting:hover {\n    -fx-text-fill: -fx-focus-0 !important;\n}\n\n\n.dark .sidebar .app-name,\n.dark .sidebar .logo,\n.dark .sidebar .setting:hover {\n    -fx-text-fill: -fx-focus-1 !important;\n}\n\n\n.sidebar-icon,\n.sidebar .setting {\n    -fx-text-alignment: center;\n    -fx-font-family: iconfont !important;\n    -fx-text-fill: -fx-white-5 !important;\n}\n\n.sidebar .logo {\n    -fx-text-alignment: center;\n    -fx-font-family: iconfont !important;\n}\n\n.dark .sidebar-icon,\n.dark .sidebar .setting {\n    -fx-text-fill: -fx-white-4 !important;\n}\n\n.sidebar-icon {\n    -fx-padding: 0 10 0 0;\n    -fx-font-size: 24 !important;\n}\n\n.sidebar .logo {\n    -fx-font-size: 36 !important;\n}\n\n.sidebar .app-name {\n    -fx-padding: 0 0 0 5;\n    -fx-font-size: 20;\n    -fx-font-family: \"butter sans Rounded\";\n}\n\n\n.sidebar .setting {\n    -fx-background-color: transparent;\n    -fx-font-size: 20;\n}\n\n.normal-button,.dark .normal-button:hover {\n    -jfx-button-type: FLAT;\n    -fx-background-color: -fx-focus-0;\n}\n\n.dark .normal-button, .normal-button:hover {\n    -fx-background-color: -fx-focus-1;\n}\n\n.font-icon {\n    -fx-font-family: iconfont;\n}\n\n.tooltip {\n    -fx-background-color: -fx-white-1;\n    -fx-text-fill: -fx-dark-0;\n    -fx-font-size: 14;\n}\n\n.dark .tooltip {\n    -fx-background-color: -fx-dark-4;\n}\n\n.separator *.line {\n    -fx-border-style: solid;\n    -fx-border-width: 0 0 2 0; /* 宽度 */\n    -fx-background-color: #E6E6E6;\n    -fx-border-color: #E6E6E6;\n}\n\n.dark .separator *.line {\n    -fx-background-color: #494949;\n    -fx-border-color: #494949\n}\n\n/*滚动条背景色*/\n.scroll-bar,\n.track {\n    -fx-background-color: transparent;\n    -fx-pref-width: 15;\n}\n\n/*滚动条颜色*/\n.thumb {\n    -fx-background-radius: 2;\n    -fx-border-radius: 0;\n    -fx-background-color: -fx-white-3;\n}\n\n.thumb:pressed, .thumb:hover {\n    -fx-background-color: -fx-white-4;\n}\n\n.dark .thumb {\n    -fx-background-color: -fx-dark-4;\n}\n\n.dark .thumb:pressed, .dark .thumb:hover {\n    -fx-background-color: -fx-dark-5;\n}\n\n.separator *.line {\n    -fx-border-style: solid;\n    -fx-border-width: 0 0 2 0;\n    -fx-background-color: -fx-white-3;\n    -fx-border-color: -fx-white-3;\n}\n\n.dark .separator *.line {\n    -fx-background-color: -fx-dark-3;\n    -fx-border-color: -fx-dark-3;\n}\n\n.drawer {\n    -fx-border-insets: 0;\n    -fx-background-insets: 0;\n    -fx-background-radius: 0 5 5 0;\n    -fx-background-color: transparent;\n    -fx-text-fill: transparent;\n    -fx-font-family: iconfont;\n    -fx-font-size: 42;\n    -fx-text-alignment: left;\n}\n\n.drawer:hover {\n    -fx-text-fill: -fx-white-3 !important;\n}\n\n.dark .drawer:hover {\n    -fx-text-fill: -fx-white-5 !important;\n}\n\n.no-border {\n    -fx-background-insets: 0;\n    -fx-border-insets: 0;\n    -fx-border-width: 0;\n}\n\n.transparent {\n    -fx-background-color: transparent;\n}\n\n.scroll-pane .viewport {\n    -fx-background-color: -fx-white-0;\n}\n\n.dark .scroll-pane .viewport {\n    -fx-background-color: -fx-dark-3;\n}\n\n/*ListView*/\n.jfx-list-cell-container {\n    -fx-alignment: center-left;\n}\n\n.dark .jfx-list-view,\n.jfx-list-cell {\n    -fx-background-color: transparent;\n}\n\n.jfx-list-cell,\n.jfx-list-cell > .jfx-rippler > StackPane {\n    -fx-background-radius: 8;\n}\n\n.jfx-list-cell:selected > .jfx-rippler > StackPane {\n    -fx-background-color: -fx-white-1;\n}\n\n.dark .jfx-list-cell:selected > .jfx-rippler > StackPane {\n    -fx-background-color: -fx-dark-5;\n}\n\n.jfx-list-cell {\n    -fx-background-insets: 0.0;\n}\n\n.jfx-list-cell .jfx-rippler {\n    -jfx-rippler-fill: -fx-white-1;\n    -fx-padding: 0 5 0 0;\n}\n\n.dark .jfx-list-cell .jfx-rippler {\n    -jfx-rippler-fill: -fx-white-5;\n}\n\n.jfx-list-view {\n    -fx-background-insets: 0;\n    -jfx-cell-horizontal-margin: 0.0;\n    -jfx-cell-vertical-margin: 5.0;\n    -jfx-vertical-gap: 10;\n    -jfx-expanded: false;\n    /*-fx-pref-width: 200;*/\n}\n\n    /*RadioButton*/\n.jfx-radio-button {\n    -fx-font-size: 15;\n    -fx-text-fill: -fx-dark-0;\n    -jfx-selected-color: -fx-focus-0;\n    -jfx-unselected-color: -fx-white-4;\n}\n\n.dark .jfx-radio-button {\n    -fx-text-fill: -fx-white-0;\n    -jfx-selected-color: -fx-focus-1;\n    -jfx-unselected-color: -fx-white-3;\n}\n\n\n/*ComboBox*/\n.jfx-combo-box {\n    -fx-font-size: 15;\n    -jfx-focus-color: -fx-focus-0;\n    -jfx-unfocus-color: -fx-white-4;\n    -jfx-label-float: false;\n    -fx-text-fill: -fx-dark-0;\n}\n\n.dark .jfx-combo-box {\n    -jfx-focus-color: -fx-focus-1;\n    -jfx-unfocus-color: -fx-white-3;\n    -jfx-label-float: false;\n    -fx-text-fill: -fx-white-0;\n\n}\n\n.dark .jfx-combo-box .list-cell {\n    -fx-text-fill: -fx-white-0;\n}\n\n.jfx-combo-box .list-view {\n    -fx-background-color: -fx-white-1;\n    -fx-background-radius: 0 0 5 5;\n}\n\n.dark .jfx-combo-box .list-view {\n    -fx-background-color: -fx-dark-4;\n}\n\n.dark .jfx-combo-box .list-view .list-cell {\n\n}\n\n.jfx-combo-box .list-view .list-cell:filled:selected,\n.jfx-combo-box .list-view .list-cell:filled:selected:hover {\n    -fx-background-color: -fx-white-1;\n    -fx-text-fill: -fx-focus-0; /*下拉列表字体色*/\n}\n\n.dark .jfx-combo-box .list-view .list-cell:filled:selected,\n.dark .jfx-combo-box .list-view .list-cell:filled:selected:hover {\n    -fx-background-color: -fx-dark-4;\n    -fx-text-fill: -fx-focus-1; /*下拉列表字体色*/\n}\n\n.jfx-combo-box .list-view .list-cell:filled:hover {\n    -fx-background-color: -fx-white-2;\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .jfx-combo-box .list-view .list-cell:filled:hover {\n    -fx-background-color: -fx-dark-5;\n    -fx-text-fill: -fx-focus-1;\n}\n\n.dark .jfx-combo-box .jfx-list-cell:selected > .jfx-rippler > StackPane {\n    -fx-background-color: -fx-dark-4;\n}\n\n\n\n.jfx-spinner .arc {\n    -fx-stroke-width: 8.0;\n}\n\n\n"
  },
  {
    "path": "src/main/resources/css/subtitle-search.css",
    "content": ".engine {\n    -fx-background-radius: 50px;\n    -fx-pref-height: 50px;\n    -fx-pref-width: 50px;\n    -fx-min-width: -fx-pref-width;\n    -fx-max-width: -fx-pref-width;\n    -fx-min-height: -fx-pref-height;\n    -fx-max-height: -fx-pref-height;\n    -fx-background-color: -fx-white-1;\n    -fx-font-family: iconfont;\n    -fx-text-fill: -fx-white-5;\n    -fx-font-size: 20;\n    -fx-text-alignment: center;\n    -fx-effect: dropshadow(gaussian, -fx-white-4, 10, 0, 0, 0);\n}\n\n.engine:selected,\n.engine:hover {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .engine:selected,\n.dark .engine:hover {\n    -fx-text-fill: -fx-focus-1;\n}\n\n.dark .engine {\n    -fx-background-color: -fx-dark-5;\n    -fx-text-fill: -fx-white-4;\n    -fx-effect: dropshadow(gaussian, -fx-dark-2, 10, 0, 0, 0);\n}\n\n.jfx-button {\n    -jfx-button-type: RAISED;\n}\n\n.list-cell .label {\n    -fx-text-fill: -fx-dark-0;\n}\n\n.dark .list-cell .label, .dark #searchField {\n    -fx-text-fill: -fx-white-0;\n}\n\n.list-cell:selected .label {\n    -fx-text-fill: -fx-focus-0;\n}\n\n.dark .list-cell:selected .label {\n    -fx-text-fill: -fx-focus-1;\n}\n\n.list-cell .caption {\n    -fx-font-size: 14;\n}\n\n.search-item {\n    -fx-pref-width: 200;\n    -fx-pref-height: 40;\n    -fx-min-width: -fx-pref-width;\n    -fx-min-height: -fx-pref-height;\n}\n\n#searchField {\n    -fx-font-size: 14;\n    -fx-prompt-text-fill: -fx-white-5;\n}"
  },
  {
    "path": "src/main/resources/css/title-bar.css",
    "content": "#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 #root {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.normal-screen #root {\n    -fx-border-radius: 8 8 0 0;\n    -fx-background-radius: 8 8 0 0;\n}\n\n#closed:hover {\n    -fx-background-color: red;\n    -fx-text-fill: -fx-white-3;\n}\n\n.dark #closed:hover {\n    -fx-background-color: red;\n}\n\n.normal-screen #closed {\n    -fx-background-radius: 0 8 0 0;\n}\n\n.full-screen #closed {\n    -fx-background-radius: 0 0 0 0;\n}\n\n#minimize:hover,\n#maximize:hover {\n    -fx-background-color: -fx-white-3;\n    -fx-background-radius: 0;\n}\n\n.dark #minimize:hover,\n.dark #maximize:hover {\n    -fx-background-color: -fx-dark-3;\n    -fx-background-radius: 0;\n}\n\n.title-button {\n    -fx-pref-height: 30;\n    -fx-pref-width: 50;\n    -fx-background-color: transparent;\n    -fx-font-family: iconfont;\n    -fx-text-fill: -fx-white-5;\n    -fx-font-size: 20;\n    -fx-text-alignment: center;\n}\n"
  },
  {
    "path": "src/main/resources/css/toast.css",
    "content": ".toast {\n    -fx-cursor: hand;\n    -fx-background-color: -fx-white-0;\n    -fx-border-insets: 0;\n    -fx-background-radius: 10;\n    -fx-padding: 10 10 10 10;\n    -fx-effect: dropshadow(gaussian, -fx-white-4, 10, 0, 0, 0);\n}\n\n.dark .toast {\n    -fx-background-color: -fx-dark-3;\n    -fx-effect: dropshadow(gaussian, -fx-dark-1, 10, 0, 0, 0);\n}\n\n#perform {\n    -fx-pref-height: 30;\n    -fx-pref-width: 70;\n    -fx-background-radius: 5;\n    -fx-font-size: 12;\n}\n\n.choose {\n    -fx-pref-height: 20;\n    -fx-pref-width: 40;\n    -fx-background-radius: 5;\n    -fx-font-size: 10;\n}\n\n#perform:hover {\n    -fx-background-color: -fx-focus-0;\n    -fx-effect: dropshadow(gaussian, -fx-white-4, 5, 0, 0, 0);\n}\n\n.dark #perform {\n    -fx-background-color: -fx-focus-1;\n}\n.dark #perform:hover {\n    -fx-background-color: -fx-focus-1;\n    -fx-effect: dropshadow(gaussian, -fx-dark-2, 5, 0, 0, 0);\n}\n\n\n\n#_caption {\n    -fx-font-size: 14;\n    -fx-text-fill: -fx-dark-0 !important;\n}\n\n.dark #_caption {\n    -fx-text-fill: -fx-white-1 !important;\n}\n\n#_text {\n    -fx-font-size: 13;\n    -fx-text-fill: -fx-white-5 !important;\n}\n\n.dark #_text {\n    -fx-text-fill: -fx-white-4 !important;\n}"
  },
  {
    "path": "src/main/resources/css/tool-box.css",
    "content": ""
  },
  {
    "path": "src/main/resources/fxml/edit-tool.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.JFXComboBox?>\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.*?>\n<?import java.lang.*?>\n<GridPane fx:id=\"root\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\" prefHeight=\"100.0\" prefWidth=\"300.0\"\n          styleClass=\"transparent\" stylesheets=\"/css/edit-tool.css\" xmlns=\"http://javafx.com/javafx/11\"\n          xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"org.fordes.subtitles.view.controller.EditTool\">\n    <rowConstraints>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"ALWAYS\"/>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"ALWAYS\"/>\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"ALWAYS\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n    </columnConstraints>\n    <!--查找面板-->\n    <GridPane maxHeight=\"50\" maxWidth=\"350\" prefHeight=\"50.0\" prefWidth=\"350.0\" styleClass=\"toolPanel\" userData=\"SEARCH\"\n              visible=\"false\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"NEVER\" maxWidth=\"200.0\" minWidth=\"200.0\" prefWidth=\"200.0\"/>\n            <ColumnConstraints hgrow=\"NEVER\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"NEVER\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"NEVER\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <TextField fx:id=\"search_input\" prefHeight=\"50\" prefWidth=\"200\" promptText=\"查找\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </TextField>\n        <Button onAction=\"#applySearch\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"1\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_SEARCH\"/>\n            <tooltip>\n                <Tooltip text=\"查找下一个\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" onAction=\"#onClose\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <MenuBar maxHeight=\"-Infinity\" maxWidth=\"-Infinity\" minHeight=\"50.0\" minWidth=\"50.0\"\n                 prefHeight=\"50.0\" prefWidth=\"50.0\" GridPane.columnIndex=\"2\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"right-item\"/>\n            </styleClass>\n            <Menu mnemonicParsing=\"false\">\n                <styleClass>\n                    <String fx:value=\"no-border\"/>\n                    <String fx:value=\"transparent\"/>\n                </styleClass>\n                <graphic>\n                    <Label styleClass=\"font-icon\">\n                        <FontIcon fx:constant=\"EDIT_BAR_OPTION\"/>\n                    </Label>\n                </graphic>\n                <CheckMenuItem fx:id=\"search_case\" mnemonicParsing=\"false\" selected=\"false\" text=\"区分大小写\"/>\n                <CheckMenuItem fx:id=\"search_regex\" mnemonicParsing=\"false\" text=\"正则表达式\"/>\n            </Menu>\n        </MenuBar>\n    </GridPane>\n    <!--替换面板-->\n    <GridPane maxHeight=\"100\" maxWidth=\"350\" prefHeight=\"100.0\" prefWidth=\"350.0\" styleClass=\"toolPanel\"\n              userData=\"REPLACE\" visible=\"false\" GridPane.rowSpan=\"2\" GridPane.halignment=\"RIGHT\"\n              GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"200.0\" minWidth=\"200.0\" prefWidth=\"200.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <TextField fx:id=\"replace_find_input\" prefHeight=\"50\" prefWidth=\"200\" promptText=\"查找\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-top-item\"/>\n            </styleClass>\n        </TextField>\n        <TextField fx:id=\"replace_input\" prefHeight=\"50\" prefWidth=\"200\" promptText=\"替换\" GridPane.columnSpan=\"2\" GridPane.rowIndex=\"1\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-bottom-item\"/>\n            </styleClass>\n        </TextField>\n        <Button onAction=\"#applyReplaceFind\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"查找下一个\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_SEARCH\"/>\n        </Button>\n        <MenuBar maxHeight=\"-Infinity\" maxWidth=\"-Infinity\" minHeight=\"50.0\" minWidth=\"50.0\"\n                 prefHeight=\"50.0\" prefWidth=\"50.0\" GridPane.columnIndex=\"2\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n            </styleClass>\n            <Menu mnemonicParsing=\"false\">\n                <styleClass>\n                    <String fx:value=\"no-border\"/>\n                    <String fx:value=\"transparent\"/>\n                </styleClass>\n                <graphic>\n                    <Label styleClass=\"font-icon\">\n                        <FontIcon fx:constant=\"EDIT_BAR_OPTION\"/>\n                    </Label>\n                </graphic>\n                <CheckMenuItem fx:id=\"replace_case\" mnemonicParsing=\"false\" selected=\"true\" text=\"区分大小写\"/>\n                <CheckMenuItem fx:id=\"replace_regex\" mnemonicParsing=\"false\" text=\"正则表达式\"/>\n            </Menu>\n        </MenuBar>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-top-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <Button onAction=\"#applyReplaceNext\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"2\" GridPane.rowIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"替换\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ITEM\"/>\n        </Button>\n        <Button onAction=\"#applyReplaceAll\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" GridPane.rowIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"全部替换\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-bottom-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ALL\"/>\n        </Button>\n    </GridPane>\n    <!--跳转面板-->\n    <GridPane maxHeight=\"50\" maxWidth=\"250\" prefHeight=\"50.0\" prefWidth=\"250.0\" styleClass=\"toolPanel\" userData=\"JUMP\"\n              visible=\"false\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"150\" minWidth=\"150\" prefWidth=\"150\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <TextField fx:id=\"jump_input\" prefHeight=\"50\" prefWidth=\"150\" promptText=\"跳转至\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </TextField>\n        <Button onAction=\"#applyJump\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"跳转至(指定行)\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ITEM\"/>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"2\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n    </GridPane>\n    <!--编码面板-->\n    <GridPane maxHeight=\"50\" maxWidth=\"250\" prefHeight=\"50.0\" prefWidth=\"250.0\" styleClass=\"toolPanel\" userData=\"CODE\"\n              visible=\"false\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"150\" minWidth=\"150\" prefWidth=\"150\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <Button onAction=\"#applyCode\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"确认以生效\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ITEM\"/>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"2\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <JFXComboBox fx:id=\"code_choice\" prefHeight=\"50\" prefWidth=\"150\" GridPane.halignment=\"CENTER\" GridPane.valignment=\"CENTER\">\n            <tooltip>\n                <Tooltip text=\"如无乱码请勿更改\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </JFXComboBox>\n    </GridPane>\n    <!--样式面板-->\n    <GridPane maxHeight=\"50\" maxWidth=\"340\" prefHeight=\"50.0\" prefWidth=\"340.0\" styleClass=\"toolPanel\" userData=\"FONT\"\n              visible=\"false\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"160.0\" minWidth=\"160.0\" prefWidth=\"160.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"80.0\" minWidth=\"80.0\" prefWidth=\"80.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n            <RowConstraints/>\n        </rowConstraints>\n        <JFXComboBox fx:id=\"font_family\" prefHeight=\"50\" prefWidth=\"160\">\n            <tooltip>\n                <Tooltip text=\"更改字体\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </JFXComboBox>\n        <JFXComboBox fx:id=\"font_size\" editable=\"true\" prefHeight=\"50\" maxHeight=\"50\" prefWidth=\"80\" GridPane.columnIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"字体显示大小\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n            </styleClass>\n        </JFXComboBox>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <Button onAction=\"#applyFont\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"2\">\n            <tooltip>\n                <Tooltip text=\"恢复默认\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REF\"/>\n        </Button>\n    </GridPane>\n    <!--时间轴面板-->\n    <GridPane maxHeight=\"50\" maxWidth=\"340\" prefHeight=\"50.0\" prefWidth=\"340.0\" styleClass=\"toolPanel\" userData=\"TIMELINE\"\n              visible=\"false\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"160.0\" minWidth=\"160.0\" prefWidth=\"160.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"80.0\" minWidth=\"80.0\" prefWidth=\"80.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n            <RowConstraints/>\n        </rowConstraints>\n        <TextField fx:id=\"timeline_input\" prefHeight=\"50\" prefWidth=\"160\" promptText=\"时间或偏移量(秒)\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"更改时间轴起点，可输入：xx:xx:xx:xx格式；同时支持+90、-100形式，单位为秒\"/>\n            </tooltip>\n        </TextField>\n        <ChoiceBox fx:id=\"timeline_option\" prefHeight=\"50\" prefWidth=\"80\" GridPane.columnIndex=\"1\">\n            <tooltip>\n                <Tooltip text=\"处理范围\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n            </styleClass>\n        </ChoiceBox>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <Button onAction=\"#applyTimeline\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"2\">\n            <tooltip>\n                <Tooltip text=\"执行\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ITEM\"/>\n        </Button>\n    </GridPane>\n    <!--翻译面板-->\n    <GridPane maxHeight=\"100\" maxWidth=\"400\" prefHeight=\"100.0\" prefWidth=\"400.0\" styleClass=\"toolPanel\"\n              userData=\"TRANSLATE\" visible=\"false\" GridPane.rowSpan=\"2\" GridPane.halignment=\"RIGHT\"\n              GridPane.valignment=\"TOP\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"125.0\" minWidth=\"125.0\" prefWidth=\"125.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"125.0\" minWidth=\"125.0\" prefWidth=\"125.0\"/>\n            <ColumnConstraints hgrow=\"SOMETIMES\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <ChoiceBox fx:id=\"translate_source\" accessibleText=\"翻译源\" cache=\"true\" prefHeight=\"50\" prefWidth=\"175\" GridPane.columnSpan=\"2\" GridPane.rowIndex=\"0\" GridPane.columnIndex=\"0\">\n            <tooltip>\n                <Tooltip text=\"翻译源\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n            </styleClass>\n        </ChoiceBox>\n        <ChoiceBox fx:id=\"translate_mode\"  prefHeight=\"50\" prefWidth=\"125\" GridPane.rowIndex=\"0\" GridPane.columnIndex=\"2\">\n            <tooltip>\n                <Tooltip text=\"翻译源\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n            </styleClass>\n        </ChoiceBox>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\" GridPane.columnIndex=\"3\" onAction=\"#onClose\">\n            <tooltip>\n                <Tooltip text=\"关闭\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"right-top-item\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n        </Button>\n        <JFXComboBox fx:id=\"translate_original\" prefHeight=\"50\" prefWidth=\"125\" GridPane.rowIndex=\"1\" GridPane.columnIndex=\"0\">\n            <tooltip>\n                <Tooltip text=\"源语言\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </JFXComboBox>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\"  GridPane.rowIndex=\"1\" GridPane.columnIndex=\"1\">\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REF\"/>\n        </Button>\n        <JFXComboBox fx:id=\"translate_target\" prefHeight=\"50\" prefWidth=\"125\" GridPane.rowIndex=\"1\" GridPane.columnIndex=\"2\">\n            <tooltip>\n                <Tooltip text=\"目标语言\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"left-item\"/>\n            </styleClass>\n        </JFXComboBox>\n        <Button onAction=\"#applyTranslate\" mnemonicParsing=\"false\" prefHeight=\"50\" prefWidth=\"50\"  GridPane.rowIndex=\"1\" GridPane.columnIndex=\"3\">\n            <tooltip>\n                <Tooltip text=\"执行\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE_ITEM\"/>\n        </Button>\n    </GridPane>\n    <GridPane.margin>\n        <Insets right=\"30.0\" top=\"30.0\"/>\n    </GridPane.margin>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/export.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<GridPane fx:id=\"root\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n          GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" xmlns=\"http://javafx.com/javafx/11.0.14-internal\"\n          xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"org.fordes.subtitles.view.controller.Export\">\n    <children>\n        <Label text=\"构建导出\">\n            <font>\n                <Font size=\"20.0\"/>\n            </font>\n        </Label>\n    </children>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/main-editor.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.*?>\n<?import org.fxmisc.flowless.VirtualizedScrollPane?>\n<?import org.fxmisc.richtext.StyleClassedTextArea?>\n<?import java.lang.*?>\n<GridPane fx:id=\"root\" prefHeight=\"1000.0\" prefWidth=\"800.0\" stylesheets=\"/css/main-editor.css\"\n          GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\"\n          xmlns=\"http://javafx.com/javafx/11\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.MainEditor\" visible=\"false\">\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"ALWAYS\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n    </columnConstraints>\n    <rowConstraints>\n        <RowConstraints fx:id=\"toolbarRow\" maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" valignment=\"TOP\"\n                        vgrow=\"NEVER\"/>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" valignment=\"BOTTOM\" vgrow=\"ALWAYS\"/>\n    </rowConstraints>\n    <HBox fx:id=\"toolbarPanel\" maxHeight=\"40.0\" prefHeight=\"40.0\" prefWidth=\"200.0\" styleClass=\"bar\"\n          GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\" userData=\"SEARCH\"\n                HBox.hgrow=\"NEVER\">\n            <HBox.margin>\n                <Insets left=\"30.0\"/>\n            </HBox.margin>\n            <FontIcon fx:constant=\"EDIT_BAR_SEARCH\"/>\n            <tooltip>\n                <Tooltip text=\"查找关键字\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"REPLACE\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_REPLACE\"/>\n            <tooltip>\n                <Tooltip text=\"替换关键字\"/>\n            </tooltip>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"JUMP\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_JUMP\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"跳转至指定行\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"CODE\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_CODE\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"更改文件读写编码（适用于显示乱码）\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"FONT\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_FONT\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"更改编辑器字体样式和大小\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"TIMELINE\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_TIMELINE\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"新建时间轴行\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"TRANSLATE\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_TRANSLATE\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"对字幕内容进行翻译\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                userData=\"REF\" HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_REF\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"重新读取文件，未保存的修改将丢失\"/>\n            </tooltip>\n        </Button>\n        <Button mnemonicParsing=\"false\" onAction=\"#hideToolbar\" prefHeight=\"50.0\" prefWidth=\"50.0\"\n                HBox.hgrow=\"NEVER\">\n            <FontIcon fx:constant=\"EDIT_BAR_HIDE\"/>\n            <styleClass>\n                <String fx:value=\"no-border\"/>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"font-icon\"/>\n                <String fx:value=\"item\"/>\n            </styleClass>\n            <tooltip>\n                <Tooltip text=\"收起工具栏\"/>\n            </tooltip>\n        </Button>\n    </HBox>\n    <Separator prefWidth=\"10.0\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"BOTTOM\" GridPane.vgrow=\"ALWAYS\">\n        <GridPane.margin>\n            <Insets left=\"30.0\" right=\"30.0\"/>\n        </GridPane.margin>\n    </Separator>\n    <VirtualizedScrollPane GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.rowIndex=\"1\"\n                           GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <content>\n            <StyleClassedTextArea fx:id=\"editor\" prefWidth=\"800\" wrapText=\"true\">\n                <styleClass>\n                    <String fx:value=\"no-border\"/>\n                </styleClass>\n            </StyleClassedTextArea>\n        </content>\n        <GridPane.margin>\n            <Insets bottom=\"45.0\" left=\"20.0\" right=\"10.0\" top=\"10.0\"/>\n        </GridPane.margin>\n    </VirtualizedScrollPane>\n    <HBox styleClass=\"bottom\" minHeight=\"40.0\" maxHeight=\"40\" minWidth=\"400.0\" maxWidth=\"400.0\"\n          GridPane.halignment=\"RIGHT\" GridPane.rowIndex=\"1\" GridPane.valignment=\"BOTTOM\">\n        <Label fx:id=\"indicator\" textAlignment=\"RIGHT\" alignment=\"CENTER_RIGHT\" text=\"-\" userData=\"第 {}行, 第 {}列\"\n               minHeight=\"40\" maxHeight=\"40\" minWidth=\"280\" maxWidth=\"280\" onMouseClicked=\"#onIndicatorClicked\"\n               GridPane.rowIndex=\"1\" GridPane.halignment=\"RIGHT\" GridPane.valignment=\"BOTTOM\"/>\n        <ToggleButton text=\"简洁模式\" fx:id=\"editMode\" minHeight=\"40.0\" maxHeight=\"40\" minWidth=\"120.0\" maxWidth=\"120.0\"\n                      mnemonicParsing=\"false\" GridPane.halignment=\"RIGHT\" GridPane.rowIndex=\"1\" userData=\"完整模式\"\n                      GridPane.valignment=\"BOTTOM\" onAction=\"#changeEditMode\">\n            <styleClass>\n                <String fx:value=\"transparent\"/>\n                <String fx:value=\"no-border\"/>\n            </styleClass>\n            <graphic>\n                <Label fx:id=\"editModeIcon\">\n                    <styleClass>\n                        <String fx:value=\"font-icon\"/>\n                    </styleClass>\n                    <FontIcon fx:constant=\"SWITCH_OFF_DARK\"/>\n                </Label>\n            </graphic>\n        </ToggleButton>\n        <GridPane.margin>\n            <Insets bottom=\"5.0\"/>\n        </GridPane.margin>\n    </HBox>\n    <fx:include fx:id=\"editTool\" source=\"edit-tool.fxml\" visible=\"false\" GridPane.rowIndex=\"1\"\n                GridPane.halignment=\"RIGHT\" GridPane.valignment=\"TOP\"/>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/main-view.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<?import java.lang.String?>\n<?import com.jfoenix.controls.JFXSpinner?>\n<GridPane fx:id=\"root\" onMouseDragged=\"#mouseDraggedHandle\" onMouseMoved=\"#mouseMoveHandle\"\n          onMousePressed=\"#mousePressedHandle\" prefHeight=\"800.0\" prefWidth=\"1200.0\"\n          xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.MainController\">\n    <styleClass>\n        <String fx:value=\"screen\"/>\n        <String fx:value=\"normal-screen\"/>\n    </styleClass>\n    <rowConstraints>\n        <RowConstraints maxHeight=\"30.0\" minHeight=\"30.0\" prefHeight=\"30.0\"/>\n        <RowConstraints/>\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints fx:id=\"sidebarColumn\" hgrow=\"NEVER\" prefWidth=\"250.0\"/>\n        <ColumnConstraints/>\n    </columnConstraints>\n    <fx:include fx:id=\"titleBar\" source=\"title-bar.fxml\" onMouseDragged=\"#titleBarDraggedHandle\"\n                GridPane.columnSpan=\"2\"/>\n    <GridPane styleClass=\"sidebar\" GridPane.rowIndex=\"1\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"ALWAYS\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints vgrow=\"ALWAYS\"/>\n            <RowConstraints maxHeight=\"80.0\" minHeight=\"80.0\" prefHeight=\"80.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <fx:include fx:id=\"sidebarBefore\" source=\"sidebar-before.fxml\"/>\n        <fx:include fx:id=\"sidebarAfter\" source=\"sidebar-after.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"sidebarBottom\" source=\"sidebar-bottom.fxml\" GridPane.rowIndex=\"1\"/>\n    </GridPane>\n\n    <GridPane fx:id=\"content\" styleClass=\"content\" GridPane.columnIndex=\"1\" GridPane.halignment=\"CENTER\"\n              GridPane.hgrow=\"ALWAYS\"\n              GridPane.rowIndex=\"1\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n        <fx:include fx:id=\"quickStart\" source=\"quick-start.fxml\"/>\n        <fx:include fx:id=\"subtitleSearch\" source=\"subtitle-search.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"toolBox\" source=\"tool-box.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"setting\" source=\"setting.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"export\" source=\"export.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"mainEditor\" source=\"main-editor.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"syncEditor\" source=\"sync-editor.fxml\" visible=\"false\"/>\n        <fx:include fx:id=\"voiceConvert\" source=\"voice-convert.fxml\" visible=\"false\"/>\n    </GridPane>\n    <Label fx:id=\"drawer\" styleClass=\"drawer\" onMouseClicked=\"#onDrawer\" GridPane.columnIndex=\"1\" GridPane.rowIndex=\"1\"\n           GridPane.halignment=\"LEFT\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <FontIcon fx:constant=\"PLACE_THE_LEFT\"/>\n    </Label>\n    <fx:include source=\"toast.fxml\" GridPane.columnIndex=\"1\" GridPane.rowIndex=\"1\"\n                GridPane.halignment=\"RIGHT\" GridPane.valignment=\"BOTTOM\"\n                GridPane.hgrow=\"NEVER\" GridPane.vgrow=\"NEVER\"/>\n    <!--全局loading-->\n    <StackPane fx:id=\"loading\" prefWidth=\"1200\" prefHeight=\"800\" visible=\"false\" GridPane.rowIndex=\"1\" styleClass=\"transparent\"\n               GridPane.columnSpan=\"2\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <JFXSpinner StackPane.alignment=\"CENTER\" maxWidth=\"150\" maxHeight=\"150\" prefHeight=\"150\" prefWidth=\"150\"/>\n    </StackPane>\n\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/quick-start.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.Button?>\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.control.Tooltip?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<GridPane xmlns=\"http://javafx.com/javafx\" xmlns:fx=\"http://javafx.com/fxml\" stylesheets=\"/css/quick-start.css\"\n          fx:id=\"root\" fx:controller=\"org.fordes.subtitles.view.controller.QuickStart\"\n          onDragOver=\"#onDragOver\" onDragExited=\"#onDragExited\" onDragDropped=\"#onDragDropped\"\n          GridPane.halignment=\"CENTER\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" GridPane.hgrow=\"ALWAYS\">\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"SOMETIMES\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n    </columnConstraints>\n    <rowConstraints>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n    </rowConstraints>\n    <GridPane.margin>\n        <Insets bottom=\"80.0\" left=\"80.0\" right=\"80.0\" top=\"90.0\"/>\n    </GridPane.margin>\n    <Button maxHeight=\"-Infinity\"\n            maxWidth=\"-Infinity\" minHeight=\"-Infinity\" minWidth=\"-Infinity\"\n            onAction=\"#chooseFile\" prefHeight=\"128.0\" prefWidth=\"128.0\"\n            GridPane.halignment=\"CENTER\" GridPane.rowIndex=\"1\"\n            GridPane.valignment=\"CENTER\">\n        <graphic>\n            <Label>\n                <FontIcon fx:constant=\"CHOOSE_FILE\"/>\n            </Label>\n        </graphic>\n        <tooltip>\n            <Tooltip text=\"可以选择视频或者字幕文件~\" />\n        </tooltip>\n    </Button>\n    <Label fx:id=\"clues\" text=\"拖放或选择文件以继续\"\n           GridPane.halignment=\"CENTER\" GridPane.rowIndex=\"1\" GridPane.valignment=\"BOTTOM\"/>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/setting.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.*?>\n<?import java.lang.*?>\n<?import javafx.geometry.*?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.*?>\n\n<StackPane fx:id=\"root\" stylesheets=\"/css/setting.css\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n           GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" xmlns=\"http://javafx.com/javafx/11\"\n           xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"org.fordes.subtitles.view.controller.Setting\">\n    <ScrollPane>\n        <styleClass>\n            <String fx:value=\"no-border\"/>\n            <String fx:value=\"transparent\"/>\n        </styleClass>\n        <StackPane.margin>\n            <Insets bottom=\"10.0\" left=\"40.0\" top=\"10.0\"/>\n        </StackPane.margin>\n        <VBox minWidth=\"150\" prefWidth=\"150\">\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label styleClass=\"sub-title\" text=\"首选项\" prefHeight=\"90\" alignment=\"CENTER\">\n                    <font>\n                        <Font size=\"15.0\"/>\n                    </font>\n                </Label>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"外观\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"主题颜色\"/>\n                    </tooltip>\n                </Label>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"140\" mnemonicParsing=\"false\" selected=\"true\" text=\"跟随系统\"\n                                HBox.hgrow=\"ALWAYS\">\n                    <toggleGroup>\n                        <ToggleGroup fx:id=\"themeGroup\"/>\n                    </toggleGroup>\n                </JFXRadioButton>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"140\" mnemonicParsing=\"false\" text=\"浅色\"\n                                toggleGroup=\"$themeGroup\" userData=\"false\"/>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"140\" mnemonicParsing=\"false\" text=\"深色\"\n                                toggleGroup=\"$themeGroup\" userData=\"true\">\n                </JFXRadioButton>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"显示样式\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"编辑器显示样式\"/>\n                    </tooltip>\n                </Label>\n                <JFXComboBox minHeight=\"40\" minWidth=\"180\" fx:id=\"fontFace\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                             promptText=\"暂无字体\"/>\n                <JFXComboBox minHeight=\"40\" minWidth=\"80\" fx:id=\"fontSize\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                             promptText=\"暂无\">\n                    <HBox.margin>\n                        <Insets left=\"30\"/>\n                    </HBox.margin>\n                </JFXComboBox>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"编辑模式\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"编辑器显示模式\"/>\n                    </tooltip>\n                </Label>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"140\" mnemonicParsing=\"false\" text=\"简洁模式\" userData=\"false\">\n                    <toggleGroup>\n                        <ToggleGroup fx:id=\"editorModeGroup\"/>\n                    </toggleGroup>\n                </JFXRadioButton>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"140\" mnemonicParsing=\"false\" text=\"完整模式\"\n                                toggleGroup=\"$editorModeGroup\" userData=\"true\"/>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"退出选项\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"在线翻译服务首选接口\"/>\n                    </tooltip>\n                </Label>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"120\" mnemonicParsing=\"false\" text=\"直接退出\" userData=\"true\">\n                    <toggleGroup>\n                        <ToggleGroup fx:id=\"exitModeGroup\"/>\n                    </toggleGroup>\n                </JFXRadioButton>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"120\" mnemonicParsing=\"false\" text=\"最小化至托盘\"\n                                toggleGroup=\"$exitModeGroup\" userData=\"false\"/>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"文件输出位置\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"默认导出/下载等操作文件输出路径\"/>\n                    </tooltip>\n                </Label>\n                <TextField minHeight=\"40\" minWidth=\"140\" fx:id=\"outPath\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                           prefHeight=\"40.0\" prefWidth=\"300.0\"/>\n                <Label minHeight=\"90\" minWidth=\"50\" onMouseClicked=\"#onChooseOutPath\" styleClass=\"popup-text\"\n                       text=\"更改\">\n                    <HBox.margin>\n                        <Insets left=\"20.0\"/>\n                    </HBox.margin>\n                </Label>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"90\" styleClass=\"sub-title\" text=\"接口设置\">\n                    <font>\n                        <Font size=\"15.0\"/>\n                    </font>\n                </Label>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"服务商\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"接口提供者\"/>\n                    </tooltip>\n                </Label>\n                <JFXComboBox minHeight=\"40\" minWidth=\"140\" fx:id=\"provider\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                             promptText=\"暂无数据\"/>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"接口类型\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"接口用途，一般为语音转换或文字翻译\"/>\n                    </tooltip>\n                </Label>\n                <JFXComboBox minHeight=\"40\" minWidth=\"140\" fx:id=\"type\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                             promptText=\"暂无数据\"/>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"套餐类型\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"套餐类型，付费用户拥有更好的性能\"/>\n                    </tooltip>\n                </Label>\n                <JFXComboBox minHeight=\"40\" minWidth=\"140\" fx:id=\"version\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\"\n                             promptText=\"暂无数据\"/>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"180\" >\n                <StackPane alignment=\"TOP_LEFT\">\n                    <VBox alignment=\"CENTER_LEFT\" minWidth=\"150\" fx:id=\"infoPanel\" visible=\"false\"/>\n                    <TextFlow fx:id=\"tips\" minHeight=\"160.0\" maxHeight=\"160\" minWidth=\"440.0\" styleClass=\"tips\">\n                        <Text styleClass=\"text\" text=\"◾  视频生成字幕以及翻译为在线服务，需全程保持网络连接.&#10;\"/>\n                        <Text styleClass=\"text\" text=\"◾  选择套餐后悬停可以查看套餐说明，输入密钥前记得核对套餐是否正确.&#10;\"/>\n                        <Text styleClass=\"text\" text=\"◾  本软件开源且免费，所有在线服务系第三方提供，任何收费行为与本软件无关.&#10;\"/>\n                        <padding>\n                            <Insets bottom=\"20.0\" left=\"15.0\" right=\"20.0\" top=\"15.0\"/>\n                        </padding>\n                    </TextFlow>\n                </StackPane>\n                <HBox.margin>\n                    <Insets left=\"40\" top=\"20\"/>\n                </HBox.margin>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label styleClass=\"sub-title\" text=\"附加选项\" prefHeight=\"90\" alignment=\"CENTER\">\n                    <font>\n                        <Font size=\"15.0\"/>\n                    </font>\n                </Label>\n            </HBox>\n            <HBox alignment=\"CENTER_LEFT\" minHeight=\"90\" prefHeight=\"90\">\n                <Label minHeight=\"90\" minWidth=\"120\" styleClass=\"item\" text=\"语种列表\">\n                    <HBox.margin>\n                        <Insets left=\"30.0\"/>\n                    </HBox.margin>\n                    <tooltip>\n                        <Tooltip text=\"在线翻译和语音转换中只展示常见语种\"/>\n                    </tooltip>\n                </Label>\n                <JFXRadioButton selected=\"true\" minHeight=\"30\" minWidth=\"120\" mnemonicParsing=\"false\" text=\"常见语种\" userData=\"true\">\n                    <toggleGroup>\n                        <ToggleGroup fx:id=\"languageListGroup\"/>\n                    </toggleGroup>\n                </JFXRadioButton>\n                <JFXRadioButton minHeight=\"30\" minWidth=\"120\" mnemonicParsing=\"false\" text=\"全部语种\"\n                                toggleGroup=\"$languageListGroup\" userData=\"false\"/>\n            </HBox>\n        </VBox>\n    </ScrollPane>\n</StackPane>\n"
  },
  {
    "path": "src/main/resources/fxml/sidebar-after.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<?import java.lang.String?>\n<GridPane fx:id=\"root\" xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.SidebarAfter\">\n    <rowConstraints>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints prefHeight=\"100\" vgrow=\"ALWAYS\"/>\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints minWidth=\"10.0\" prefWidth=\"100.0\"/>\n    </columnConstraints>\n    <ToggleButton fx:id=\"mainEditor\" text=\"主编辑器\" selected=\"true\" styleClass=\"sidebar-item\"\n                  alignment=\"CENTER_LEFT\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\"\n                  GridPane.vgrow=\"ALWAYS\">\n        <toggleGroup>\n            <ToggleGroup fx:id=\"itemGroup\"/>\n        </toggleGroup>\n        <graphic>\n            <Label>\n                <styleClass>\n                    <String fx:value=\"sidebar-icon\"/>\n                    <String fx:value=\"font-icon\"/>\n                </styleClass>\n                <FontIcon fx:constant=\"ITEM_START\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n    <ToggleButton fx:id=\"syncEditor\" text=\"同步编辑\" styleClass=\"sidebar-item\" toggleGroup=\"$itemGroup\"\n                  alignment=\"CENTER_LEFT\" GridPane.rowIndex=\"1\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n                  GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <graphic>\n            <Label>\n                <styleClass>\n                    <String fx:value=\"sidebar-icon\"/>\n                    <String fx:value=\"font-icon\"/>\n                </styleClass>\n                <FontIcon fx:constant=\"ITEM_SEARCH\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n    <ToggleButton fx:id=\"export\" text=\"构建导出\" styleClass=\"sidebar-item\" toggleGroup=\"$itemGroup\"\n                  alignment=\"CENTER_LEFT\" GridPane.rowIndex=\"2\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\"\n                  GridPane.vgrow=\"ALWAYS\">\n        <graphic>\n            <Label>\n                <styleClass>\n                    <String fx:value=\"sidebar-icon\"/>\n                    <String fx:value=\"font-icon\"/>\n                </styleClass>\n                <FontIcon fx:constant=\"ITEM_TOOL\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/sidebar-before.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<GridPane fx:id=\"root\" xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.SidebarBefore\">\n    <rowConstraints>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints maxHeight=\"60.0\" minHeight=\"60.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n        <RowConstraints prefHeight=\"100\" vgrow=\"ALWAYS\"/>\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints minWidth=\"10.0\" prefWidth=\"100.0\"/>\n    </columnConstraints>\n    <ToggleButton fx:id=\"quickStart\" text=\"快速开始\" selected=\"true\" styleClass=\"sidebar-item\"\n                  alignment=\"CENTER_LEFT\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\"\n                  GridPane.vgrow=\"ALWAYS\">\n        <toggleGroup>\n            <ToggleGroup fx:id=\"itemGroup\"/>\n        </toggleGroup>\n        <graphic>\n            <Label styleClass=\"sidebar-icon\">\n                <FontIcon fx:constant=\"ITEM_START\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n    <ToggleButton fx:id=\"subtitleSearch\" text=\"字幕搜索\" styleClass=\"sidebar-item\" toggleGroup=\"$itemGroup\"\n                  alignment=\"CENTER_LEFT\" GridPane.rowIndex=\"1\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n                  GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <graphic>\n            <Label styleClass=\"sidebar-icon\">\n                <FontIcon fx:constant=\"ITEM_SEARCH\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n    <ToggleButton fx:id=\"toolBox\" text=\"工具箱\" styleClass=\"sidebar-item\" toggleGroup=\"$itemGroup\"\n                  alignment=\"CENTER_LEFT\" GridPane.rowIndex=\"2\" prefWidth=\"250\" prefHeight=\"60.0\"\n                  GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\"\n                  GridPane.vgrow=\"ALWAYS\">\n        <graphic>\n            <Label styleClass=\"sidebar-icon\">\n                <FontIcon fx:constant=\"ITEM_TOOL\"/>\n            </Label>\n        </graphic>\n        <GridPane.margin>\n            <Insets bottom=\"8.0\" left=\"13.0\" right=\"13.0\" top=\"8.0\"/>\n        </GridPane.margin>\n    </ToggleButton>\n\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/sidebar-bottom.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Button?>\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.control.Separator?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<?import java.lang.String?>\n<GridPane xmlns=\"http://javafx.com/javafx\"\n          xmlns:fx=\"http://javafx.com/fxml\"\n          fx:controller=\"org.fordes.subtitles.view.controller.SidebarBottom\">\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"NEVER\" maxWidth=\"50.0\" minWidth=\"50.0\" prefWidth=\"50.0\"/>\n        <ColumnConstraints hgrow=\"ALWAYS\"/>\n        <ColumnConstraints hgrow=\"NEVER\" minWidth=\"60.0\" prefWidth=\"60.0\"/>\n    </columnConstraints>\n    <rowConstraints>\n        <RowConstraints minHeight=\"30.0\" prefHeight=\"60.0\" vgrow=\"SOMETIMES\"/>\n    </rowConstraints>\n    <Separator prefWidth=\"200.0\" GridPane.columnSpan=\"3\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n               GridPane.valignment=\"TOP\" GridPane.vgrow=\"ALWAYS\"/>\n    <Label GridPane.halignment=\"RIGHT\" GridPane.valignment=\"CENTER\">\n        <FontIcon fx:constant=\"LOGO\"/>\n        <styleClass>\n            <String fx:value=\"logo\"/>\n        </styleClass>\n    </Label>\n    <Label styleClass=\"app-name\" text=\"SubView\" GridPane.columnIndex=\"1\" GridPane.halignment=\"LEFT\"\n           GridPane.valignment=\"CENTER\"/>\n    <Button fx:id=\"setting\" mnemonicParsing=\"false\" GridPane.columnIndex=\"2\" GridPane.halignment=\"CENTER\"\n            GridPane.valignment=\"CENTER\">\n        <styleClass>\n            <String fx:value=\"setting\"/>\n        </styleClass>\n        <FontIcon fx:constant=\"SETTING\"/>\n    </Button>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/speech-conversion.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<GridPane xmlns=\"http://javafx.com/javafx\"\n          xmlns:fx=\"http://javafx.com/fxml\"\n          stylesheets=\"/css/quick-start.css\"\n          fx:id=\"root\" fx:controller=\"org.fordes.subtitles.view.controller.SpeechConversion\"\n          GridPane.halignment=\"CENTER\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" GridPane.hgrow=\"ALWAYS\">\n    <children>\n        <Label text=\"语音转写\">\n            <font>\n                <Font size=\"20.0\"/>\n            </font>\n        </Label>\n    </children>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/subtitle-search.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.*?>\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.Tooltip?>\n<?import javafx.scene.layout.*?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<?import java.lang.String?>\n<GridPane fx:id=\"root\" prefHeight=\"400.0\" prefWidth=\"600.0\" stylesheets=\"/css/subtitle-search.css\"\n          GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\"\n          xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.SubtitleSearch\">\n    <rowConstraints>\n        <RowConstraints maxHeight=\"100\" minHeight=\"100\" prefHeight=\"100\" valignment=\"CENTER\" vgrow=\"NEVER\"/>\n        <RowConstraints vgrow=\"ALWAYS\"/>\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"ALWAYS\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n        <ColumnConstraints/>\n    </columnConstraints>\n    <GridPane GridPane.rowIndex=\"1\">\n        <columnConstraints>\n            <ColumnConstraints hgrow=\"SOMETIMES\" minWidth=\"10.0\" prefWidth=\"100.0\"/>\n        </columnConstraints>\n        <rowConstraints>\n            <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"SOMETIMES\"/>\n        </rowConstraints>\n\n        <JFXListView fx:id=\"listView\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n                     GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        </JFXListView>\n        <GridPane.margin>\n            <Insets bottom=\"30.0\" left=\"40.0\" right=\"30.0\"/>\n        </GridPane.margin>\n    </GridPane>\n    <JFXNodesList fx:id=\"nodesList\" rotate=\"180.0\" spacing=\"15\" GridPane.halignment=\"RIGHT\" GridPane.hgrow=\"ALWAYS\"\n                  GridPane.rowIndex=\"1\" GridPane.valignment=\"BOTTOM\" GridPane.vgrow=\"ALWAYS\">\n        <GridPane.margin>\n            <Insets bottom=\"50.0\" right=\"50.0\"/>\n        </GridPane.margin>\n        <JFXButton>\n            <styleClass>\n                <String fx:value=\"engine\"/>\n            </styleClass>\n            <FontIcon fx:constant=\"ENGINE\"/>\n            <tooltip>\n                <Tooltip text=\"切换搜索字幕引擎\"/>\n            </tooltip>\n        </JFXButton>\n    </JFXNodesList>\n    <JFXTextField fx:id=\"searchField\"\n                  maxHeight=\"-Infinity\" maxWidth=\"-Infinity\" prefHeight=\"40.0\" prefWidth=\"400.0\"\n                  onAction=\"#searchBeginHandle\"\n                  GridPane.halignment=\"LEFT\" GridPane.hgrow=\"ALWAYS\"\n                  GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <GridPane.margin>\n            <Insets left=\"40.0\"/>\n        </GridPane.margin>\n    </JFXTextField>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/sync-editor.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<GridPane xmlns=\"http://javafx.com/javafx\"\n          xmlns:fx=\"http://javafx.com/fxml\"\n          fx:id=\"root\" fx:controller=\"org.fordes.subtitles.view.controller.SyncEditor\"\n          GridPane.halignment=\"CENTER\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" GridPane.hgrow=\"ALWAYS\">\n    <children>\n        <Label text=\"同步编辑器\">\n            <font>\n                <Font size=\"20.0\"/>\n            </font>\n        </Label>\n    </children>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/title-bar.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Button?>\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<?import org.fordes.subtitles.view.enums.FontIcon?>\n<?import java.lang.String?>\n<VBox fx:id=\"root\" stylesheets=\"/css/title-bar.css\"\n      xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n      GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n      GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\"\n      fx:controller=\"org.fordes.subtitles.view.controller.TitleBar\">\n    <AnchorPane prefHeight=\"30.0\">\n        <Button fx:id=\"closed\" contentDisplay=\"CENTER\" mnemonicParsing=\"false\" AnchorPane.rightAnchor=\"0.0\" onAction=\"#closed\">\n            <FontIcon fx:constant=\"SCENE_CLOSE\"/>\n            <styleClass>\n                <String fx:value=\"title-button\"/>\n            </styleClass>\n        </Button>\n        <Button fx:id=\"maximize\" contentDisplay=\"CENTER\" mnemonicParsing=\"false\" AnchorPane.rightAnchor=\"50.0\" onAction=\"#maximize\">\n            <FontIcon fx:constant=\"FULL_SCREEN\"/>\n            <styleClass>\n                <String fx:value=\"title-button\"/>\n            </styleClass>\n        </Button>\n        <Button fx:id=\"minimize\" contentDisplay=\"CENTER\" mnemonicParsing=\"false\" AnchorPane.rightAnchor=\"100.0\" onAction=\"#minimize\">\n            <FontIcon fx:constant=\"SCENE_MINIMIZE\"/>\n            <styleClass>\n                <String fx:value=\"title-button\"/>\n            </styleClass>\n        </Button>\n        <Label fx:id=\"title\" prefHeight=\"30.0\" styleClass=\"\" AnchorPane.leftAnchor=\"20\">\n            <font>\n                <Font name=\"Microsoft YaHei\" size=\"15.0\"/>\n            </font>\n        </Label>\n    </AnchorPane>\n</VBox>\n"
  },
  {
    "path": "src/main/resources/fxml/toast.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import com.jfoenix.controls.JFXButton?>\n<?import javafx.geometry.Insets?>\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import java.lang.String?>\n<GridPane fx:id=\"root\" maxHeight=\"-Infinity\" maxWidth=\"-Infinity\" minHeight=\"-Infinity\" minWidth=\"-Infinity\"\n          visible=\"false\"\n          prefHeight=\"68\" prefWidth=\"260.0\" styleClass=\"toast\" stylesheets=\"/css/toast.css\"\n          xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\"\n          fx:controller=\"org.fordes.subtitles.view.controller.Toast\">\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"NEVER\" prefWidth=\"150\"/>\n        <ColumnConstraints hgrow=\"NEVER\" prefWidth=\"90\"/>\n    </columnConstraints>\n    <rowConstraints>\n        <RowConstraints prefHeight=\"24\" vgrow=\"NEVER\"/>\n        <RowConstraints prefHeight=\"24\" vgrow=\"NEVER\"/>\n    </rowConstraints>\n    <GridPane.margin>\n        <Insets bottom=\"20.0\" right=\"20.0\"/>\n    </GridPane.margin>\n    <JFXButton fx:id=\"_perform\" mnemonicParsing=\"false\" styleClass=\"normal-button\" text=\"确定\"\n               GridPane.columnIndex=\"1\" GridPane.halignment=\"CENTER\" GridPane.rowSpan=\"2\"\n               GridPane.valignment=\"CENTER\"/>\n    <JFXButton fx:id=\"_choose1\" mnemonicParsing=\"false\" text=\"确定\" GridPane.columnIndex=\"1\" visible=\"false\"\n               GridPane.halignment=\"LEFT\" GridPane.rowSpan=\"2\" GridPane.valignment=\"CENTER\">\n        <styleClass>\n            <String fx:value=\"normal-button\"/>\n            <String fx:value=\"choose\"/>\n        </styleClass>\n    </JFXButton>\n    <JFXButton fx:id=\"_choose2\" mnemonicParsing=\"false\" text=\"取消\" GridPane.columnIndex=\"1\" visible=\"false\"\n               GridPane.halignment=\"RIGHT\" GridPane.rowSpan=\"2\" GridPane.valignment=\"CENTER\">\n        <styleClass>\n            <String fx:value=\"normal-button\"/>\n            <String fx:value=\"choose\"/>\n        </styleClass>\n    </JFXButton>\n    <Label fx:id=\"_caption\">\n        <GridPane.margin>\n            <Insets left=\"15.0\"/>\n        </GridPane.margin>\n    </Label>\n    <Label fx:id=\"_text\" GridPane.rowIndex=\"1\">\n        <GridPane.margin>\n            <Insets left=\"15.0\"/>\n        </GridPane.margin>\n    </Label>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/tool-box.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<GridPane fx:id=\"root\" prefHeight=\"400.0\" prefWidth=\"600.0\" stylesheets=\"/css/tool-box.css\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" xmlns=\"http://javafx.com/javafx/11.0.2\" xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"org.fordes.subtitles.view.controller.ToolBox\">\n    <rowConstraints>\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"ALWAYS\" />\n        <RowConstraints minHeight=\"10.0\" prefHeight=\"30.0\" vgrow=\"ALWAYS\" />\n    </rowConstraints>\n    <columnConstraints>\n        <ColumnConstraints hgrow=\"ALWAYS\" minWidth=\"10.0\" prefWidth=\"100.0\" />\n    </columnConstraints>\n    <Label text=\"To be continue\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\" GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\">\n        <font>\n            <Font size=\"20.0\" />\n        </font>\n    </Label>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/fxml/voice-convert.fxml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\n<?import javafx.scene.control.Label?>\n<?import javafx.scene.layout.*?>\n<?import javafx.scene.text.Font?>\n<GridPane fx:id=\"root\" GridPane.halignment=\"CENTER\" GridPane.hgrow=\"ALWAYS\"\n          GridPane.valignment=\"CENTER\" GridPane.vgrow=\"ALWAYS\" xmlns=\"http://javafx.com/javafx/11.0.14-internal\"\n          xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"org.fordes.subtitles.view.controller.VoiceConvert\">\n    <children>\n        <Label text=\"语音转换\">\n            <font>\n                <Font size=\"20.0\"/>\n            </font>\n        </Label>\n    </children>\n</GridPane>\n"
  },
  {
    "path": "src/main/resources/logback/logback-spring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--\n    scan：当此属性设置为true时，配置文件如果发生改变，将会被重新加载，默认值为true。\n    scanPeriod：设置监测配置文件是否有修改的时间间隔，如果没有给出时间单位，默认单位是毫秒当scan为true时，此属性生效。默认的时间间隔为1分钟。\n    debug：当此属性设置为true时，将打印出logback内部日志信息，实时查看logback运行状态。默认值为false。\n-->\n<configuration scan=\"false\" scanPeriod=\"60 seconds\" debug=\"false\">\n\n    <conversionRule conversionWord=\"clr\" converterClass=\"org.springframework.boot.logging.logback.ColorConverter\"/>\n    <conversionRule conversionWord=\"wex\" converterClass=\"org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter\"/>\n    <conversionRule conversionWord=\"wEx\" converterClass=\"org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter\"/>\n\n    <!-- 定义日志的根目录 -->\n    <property resource=\"application.yml\"/>\n    <springProperty scope=\"context\" name=\"LOG_HOME\" source=\"logging.file.path\"/>\n\n    <!-- 定义日志文件名称 -->\n    <springProperty scope=\"context\" name=\"appName\" source=\"spring.application.name\"/>\n\n    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->\n    <appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <springProfile name=\"prod\">\n            <filter class=\"ch.qos.logback.classic.filter.ThresholdFilter\">\n                <level>INFO</level>\n            </filter>\n        </springProfile>\n\n        <layout class=\"ch.qos.logback.classic.PatternLayout\">\n            <pattern>%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%-6L){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>\n        </layout>\n    </appender>\n\n    <!-- 滚动记录文件，先将日志记录到指定文件，当符合某个条件时，将日志记录到其他文件 -->\n    <appender name=\"appLogAppender\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <!-- 指定日志文件的名称 -->\n        <file>${LOG_HOME}/${appName}.log</file>\n\n        <!--\n            当发生滚动时，决定 RollingFileAppender 的行为，涉及文件移动和重命名\n            TimeBasedRollingPolicy： 最常用的滚动策略，它根据时间来制定滚动策略，既负责滚动也负责出发滚动。\n        -->\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <!--\n            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}：按天进行日志滚动\n            %i：当文件大小超过maxFileSize时，按照i进行文件滚动\n            -->\n            <fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>\n            <!--\n            可选节点，控制保留的归档文件的最大数量，超出数量就删除旧文件。假设设置每天滚动，\n            且maxHistory是365，则只保存最近365天的文件，删除之前的旧文件。注意，删除旧文件是，\n            那些为了归档而创建的目录也会被删除。\n            -->\n            <MaxHistory>365</MaxHistory>\n            <!--\n            当日志文件超过maxFileSize指定的大小是，根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的，必须配置timeBasedFileNamingAndTriggeringPolicy\n            -->\n            <timeBasedFileNamingAndTriggeringPolicy class=\"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP\">\n                <maxFileSize>5MB</maxFileSize>\n            </timeBasedFileNamingAndTriggeringPolicy>\n        </rollingPolicy>\n        <!-- 日志输出格式： -->\n        <layout class=\"ch.qos.logback.classic.PatternLayout\">\n            <pattern>%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>\n        </layout>\n    </appender>\n\n    <!--\n\t\tlogger主要用于存放日志对象，也可以定义日志类型、级别\n\t\tname：表示匹配的logger类型前缀，也就是包的前半部分\n\t\tlevel：要记录的日志级别，包括 TRACE < DEBUG < INFO < WARN < ERROR\n\t\tadditivity：作用在于children-logger是否使用 rootLogger配置的appender进行输出，\n\t\tfalse：表示只用当前logger的appender-ref，\n\t\ttrue： 表示当前logger的appender-ref和rootLogger的appender-ref都有效\n    -->\n\n    <!-- mybatis logger -->\n    <logger name=\"org.mybatis\" additivity=\"false\" level=\"debug\"/>\n\n    <!-- springframework logger -->\n    <logger name=\"org.springframework\" additivity=\"false\" level=\"debug\"/>\n\n    <!-- HikariDataSource logger -->\n    <logger name=\"com.zaxxer.hikari\" additivity=\"false\" level=\"error\"/>\n\n    <springProfile name=\"dev\">\n        <logger name=\"org.fordes\" additivity=\"true\" level=\"debug\"/>\n    </springProfile>\n\n    <springProfile name=\"prod\">\n        <logger name=\"org.fordes\" additivity=\"true\" level=\"info\"/>\n    </springProfile>\n\n    <!--\n        root与logger是父子关系，没有特别定义则默认为root，任何一个类只会和一个logger对应，\n        要么是定义的logger，要么是root，判断的关键在于找到这个logger，然后判断这个logger的appender和level。\n    -->\n    <root level=\"debug\">\n        <appender-ref ref=\"stdout\"/>\n        <appender-ref ref=\"appLogAppender\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "src/main/resources/mapper/InterfaceMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"org.fordes.subtitles.view.mapper.InterfaceMapper\">\n    <resultMap id=\"availableMap\" type=\"org.fordes.subtitles.view.model.DTO.AvailableServiceInfo\">\n        <result column=\"auth\" property=\"auth\"/>\n        <result column=\"type\" property=\"type\"/>\n        <result column=\"provider\" property=\"provider\"/>\n        <association property=\"versionInfo\" javaType=\"org.fordes.subtitles.view.model.PO.Version\">\n            <result column=\"concurrent\" property=\"concurrent\"/>\n            <result column=\"carrying\" property=\"carrying\"/>\n            <result column=\"name\" property=\"name\"/>\n            <result column=\"remark\" property=\"remark\"/>\n            <result column=\"server_url\" property=\"serverUrl\"/>\n        </association>\n    </resultMap>\n\n    <select id=\"serviceInfo\" resultMap=\"availableMap\">\n        SELECT i.auth, i.type, i.provider, v.carrying, v.concurrent, v.name, v.server_url, v.remark\n        FROM \"interface\" i\n                 LEFT JOIN \"version\" v ON i.id = v.interface_id\n        WHERE \"type\" = #{type}\n          AND \"auth\" IS NOT NULL\n    </select>\n\n\n    <select id=\"getVersions\" resultType=\"org.fordes.subtitles.view.model.PO.Version\">\n        SELECT v.*\n        FROM \"version\" v\n                 LEFT JOIN \"interface\" i ON v.interface_id = i.id\n        WHERE i.\"provider\" = #{provider}\n          AND i.\"type\" = #{type}\n    </select>\n\n    <select id=\"getLanguageList\" resultType=\"cn.hutool.core.lang.Dict\">\n        select * FROM \"language\"\n    </select>\n</mapper>"
  }
]